@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.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/,
@@ -31,14 +112,18 @@ const QUESTION_TEXT = chevrotain.createToken({
31
112
  name: "QUESTION_TEXT",
32
113
  pattern: /QUESTION_TEXT/
33
114
  });
34
- const NUMBER_TEXT = chevrotain.createToken({
35
- name: "NUMBER_TEXT",
36
- pattern: /NUMBER_TEXT/
115
+ const TEXT_HIGHLIGHTER = chevrotain.createToken({
116
+ name: "TEXT_HIGHLIGHTER",
117
+ pattern: /TEXT_HIGHLIGHTER/
37
118
  });
38
119
  const W_S_I = chevrotain.createToken({
39
120
  name: "W_S_I",
40
121
  pattern: /W_S_I/
41
122
  });
123
+ const STRING_INPUT = chevrotain.createToken({
124
+ name: "STRING_INPUT",
125
+ pattern: /STRING_INPUT/
126
+ });
42
127
  const DRAGABLE_BUTTONS_OPTIONS = chevrotain.createToken({
43
128
  name: "DRAGABLE_BUTTONS_OPTIONS",
44
129
  pattern: /DRAGABLE_BUTTONS_OPTIONS/
@@ -55,6 +140,10 @@ const W_N_I = chevrotain.createToken({
55
140
  name: "W_N_I",
56
141
  pattern: /W_N_I/
57
142
  });
143
+ const NUMERIC_INPUT = chevrotain.createToken({
144
+ name: "NUMERIC_INPUT",
145
+ pattern: /NUMERIC_INPUT/
146
+ });
58
147
  const NumberLiteral = chevrotain.createToken({
59
148
  name: "NumberLiteral",
60
149
  pattern: /-?\d+(\.\d+)?/
@@ -124,7 +213,7 @@ const Comma = chevrotain.createToken({
124
213
  });
125
214
  const FreeText = chevrotain.createToken({
126
215
  name: "FreeText",
127
- pattern: /(?:[^A-Z\n\r]|(?![A-Z_]+\[)[A-Z])+/,
216
+ pattern: /(?:[^A-Z\n\r \t+\-−×*÷/=<>?()\[\],]|(?![A-Z_]+\[)[A-Z])+/,
128
217
  line_breaks: false
129
218
  });
130
219
  const allTokens = [
@@ -137,13 +226,14 @@ const allTokens = [
137
226
  Comment,
138
227
  // Component keywords (before Identifier!)
139
228
  QUESTION_TEXT,
140
- NUMBER_TEXT,
229
+ TEXT_HIGHLIGHTER,
141
230
  W_S_I,
231
+ STRING_INPUT,
142
232
  DRAGABLE_BUTTONS_OPTIONS,
143
233
  NUMERIC_KEYPAD,
144
234
  WIDGET_ANSWER,
145
- // Legacy keywords
146
235
  W_N_I,
236
+ NUMERIC_INPUT,
147
237
  // Literals
148
238
  NumberLiteral,
149
239
  StringLiteral,
@@ -321,14 +411,14 @@ class MathExerciseParser extends chevrotain.CstParser {
321
411
  // Parentheses (for grouping)
322
412
  { ALT: () => this.CONSUME(LParen) },
323
413
  { ALT: () => this.CONSUME(RParen) },
324
- // NUMBER_TEXT (for highlighting)
325
- { ALT: () => this.SUBRULE(this.numberText) },
414
+ // TEXT_HIGHLIGHTER (for highlighting)
415
+ { ALT: () => this.SUBRULE(this.textHighlighter) },
326
416
  // W_S_I (inline input widget)
327
417
  { ALT: () => this.SUBRULE(this.inlineInput) }
328
418
  ]);
329
419
  });
330
- this.numberText = this.RULE("numberText", () => {
331
- this.CONSUME(NUMBER_TEXT);
420
+ this.textHighlighter = this.RULE("textHighlighter", () => {
421
+ this.CONSUME(TEXT_HIGHLIGHTER);
332
422
  this.CONSUME(LBracket);
333
423
  this.SUBRULE(this.parameterList);
334
424
  this.CONSUME(RBracket);
@@ -336,7 +426,9 @@ class MathExerciseParser extends chevrotain.CstParser {
336
426
  this.inlineInput = this.RULE("inlineInput", () => {
337
427
  this.OR([
338
428
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
339
- { ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) }
429
+ { ALT: () => this.CONSUME(STRING_INPUT, { LABEL: "stringInput" }) },
430
+ { ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) },
431
+ { ALT: () => this.CONSUME(NUMERIC_INPUT, { LABEL: "numericInput" }) }
340
432
  ]);
341
433
  this.CONSUME(LBracket);
342
434
  this.SUBRULE(this.parameterList);
@@ -363,7 +455,9 @@ class MathExerciseParser extends chevrotain.CstParser {
363
455
  this.numericKeypad = this.RULE("numericKeypad", () => {
364
456
  this.CONSUME(NUMERIC_KEYPAD);
365
457
  this.CONSUME(LBracket);
366
- this.SUBRULE(this.parameterList);
458
+ this.OPTION(() => {
459
+ this.SUBRULE(this.parameterList);
460
+ });
367
461
  this.CONSUME(RBracket);
368
462
  this.MANY(() => {
369
463
  this.OR([
@@ -460,29 +554,29 @@ class MathExerciseParser extends chevrotain.CstParser {
460
554
  }
461
555
  const parser = new MathExerciseParser();
462
556
  const FreeTextInlineSchema = zod.z.object({
463
- type: zod.z.literal("FreeText"),
557
+ type: zod.z.literal(AST_TYPES.FREE_TEXT),
464
558
  value: zod.z.string()
465
559
  });
466
560
  const SpaceInlineSchema = zod.z.object({
467
- type: zod.z.literal("Space")
561
+ type: zod.z.literal(AST_TYPES.SPACE)
468
562
  });
469
- const NumberTextInlineSchema = zod.z.object({
470
- type: zod.z.literal("NumberText"),
563
+ const HighlightedTextInlineSchema = zod.z.object({
564
+ type: zod.z.literal(AST_TYPES.HIGHLIGHTED_TEXT),
471
565
  value: zod.z.string(),
472
566
  highlight: zod.z.tuple([zod.z.number(), zod.z.number()]).optional()
473
567
  });
474
568
  const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
475
569
  zod.z.object({
476
- type: zod.z.literal("InlineInput"),
570
+ type: zod.z.literal(AST_TYPES.INLINE_INPUT),
477
571
  name: zod.z.string(),
478
- inputType: zod.z.literal("string"),
572
+ inputType: zod.z.literal(INPUT_TYPES.STRING),
479
573
  length: zod.z.number().int().positive(),
480
574
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
481
575
  }),
482
576
  zod.z.object({
483
- type: zod.z.literal("InlineInput"),
577
+ type: zod.z.literal(AST_TYPES.INLINE_INPUT),
484
578
  name: zod.z.string(),
485
- inputType: zod.z.literal("numeric"),
579
+ inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
486
580
  digits: zod.z.number().int().positive(),
487
581
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
488
582
  })
@@ -490,52 +584,45 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
490
584
  const InlineNodeSchema = zod.z.union([
491
585
  FreeTextInlineSchema,
492
586
  SpaceInlineSchema,
493
- NumberTextInlineSchema,
587
+ HighlightedTextInlineSchema,
494
588
  InlineInputWidgetSchema
495
589
  ]);
496
590
  function createFreeText(value) {
497
591
  return FreeTextInlineSchema.parse({
498
- type: "FreeText",
592
+ type: AST_TYPES.FREE_TEXT,
499
593
  value
500
594
  });
501
595
  }
502
596
  function createSpace() {
503
597
  return SpaceInlineSchema.parse({
504
- type: "Space"
598
+ type: AST_TYPES.SPACE
505
599
  });
506
600
  }
507
- function createNumberText(value, highlight) {
508
- return NumberTextInlineSchema.parse({
509
- type: "NumberText",
601
+ function createHighlightedText(value, highlight) {
602
+ return HighlightedTextInlineSchema.parse({
603
+ type: AST_TYPES.HIGHLIGHTED_TEXT,
510
604
  value,
511
605
  highlight
512
606
  });
513
607
  }
514
608
  function createStringInput(name, length, answer) {
515
609
  return InlineInputWidgetSchema.parse({
516
- type: "InlineInput",
610
+ type: AST_TYPES.INLINE_INPUT,
517
611
  name,
518
- inputType: "string",
612
+ inputType: INPUT_TYPES.STRING,
519
613
  length,
520
614
  answer
521
615
  });
522
616
  }
523
617
  function createNumericInput(name, digits, answer) {
524
618
  return InlineInputWidgetSchema.parse({
525
- type: "InlineInput",
619
+ type: AST_TYPES.INLINE_INPUT,
526
620
  name,
527
- inputType: "numeric",
621
+ inputType: INPUT_TYPES.NUMERIC,
528
622
  digits,
529
623
  answer
530
624
  });
531
625
  }
532
- function createInlineInput(name, inputType, lengthOrDigits, answer) {
533
- if (inputType === "string") {
534
- return createStringInput(name, lengthOrDigits, answer);
535
- } else {
536
- return createNumericInput(name, lengthOrDigits, answer);
537
- }
538
- }
539
626
  function isFreeText(value) {
540
627
  return FreeTextInlineSchema.safeParse(value).success;
541
628
  }
@@ -549,22 +636,22 @@ function isInlineNode(value) {
549
636
  return InlineNodeSchema.safeParse(value).success;
550
637
  }
551
638
  const TextBlockSchema = zod.z.object({
552
- type: zod.z.literal("TextBlock"),
639
+ type: zod.z.literal(AST_TYPES.TEXT_BLOCK),
553
640
  value: zod.z.string()
554
641
  });
555
642
  const LineBlockSchema = zod.z.object({
556
- type: zod.z.literal("LineBlock"),
643
+ type: zod.z.literal(AST_TYPES.LINE_BLOCK),
557
644
  inlines: zod.z.array(InlineNodeSchema)
558
645
  });
559
646
  function createTextBlock(value) {
560
647
  return TextBlockSchema.parse({
561
- type: "TextBlock",
648
+ type: AST_TYPES.TEXT_BLOCK,
562
649
  value
563
650
  });
564
651
  }
565
652
  function createLineBlock(inlines) {
566
653
  return LineBlockSchema.parse({
567
- type: "LineBlock",
654
+ type: AST_TYPES.LINE_BLOCK,
568
655
  inlines
569
656
  });
570
657
  }
@@ -575,13 +662,13 @@ function isLineBlock(value) {
575
662
  return LineBlockSchema.safeParse(value).success;
576
663
  }
577
664
  const DragOptionsToolSchema = zod.z.object({
578
- type: zod.z.literal("DragOptions"),
665
+ type: zod.z.literal(AST_TYPES.DRAG_OPTIONS),
579
666
  options: zod.z.array(zod.z.string()).min(1)
580
667
  });
581
668
  const NumericKeypadToolSchema = zod.z.object({
582
- type: zod.z.literal("NumericKeypad"),
583
- includeDecimal: zod.z.boolean().default(false),
584
- 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()
585
672
  });
586
673
  const ToolNodeSchema = zod.z.discriminatedUnion("type", [
587
674
  DragOptionsToolSchema,
@@ -589,15 +676,15 @@ const ToolNodeSchema = zod.z.discriminatedUnion("type", [
589
676
  ]);
590
677
  function createDragOptions(options) {
591
678
  return DragOptionsToolSchema.parse({
592
- type: "DragOptions",
679
+ type: AST_TYPES.DRAG_OPTIONS,
593
680
  options
594
681
  });
595
682
  }
596
- function createNumericKeypad(includeDecimal = false, includeNegative = false) {
683
+ function createNumericKeypad(includeOperators, includeDecimal) {
597
684
  return NumericKeypadToolSchema.parse({
598
- type: "NumericKeypad",
599
- includeDecimal,
600
- includeNegative
685
+ type: AST_TYPES.NUMERIC_KEYPAD,
686
+ includeOperators,
687
+ includeDecimal
601
688
  });
602
689
  }
603
690
  function isDragOptions(value) {
@@ -610,10 +697,10 @@ function isToolNode(value) {
610
697
  return ToolNodeSchema.safeParse(value).success;
611
698
  }
612
699
  const SimpleValidationSchema = zod.z.object({
613
- type: zod.z.literal("SimpleValidation")
700
+ type: zod.z.literal(AST_TYPES.SIMPLE_VALIDATION)
614
701
  });
615
702
  const ExpressionValidationSchema = zod.z.object({
616
- type: zod.z.literal("ExpressionValidation"),
703
+ type: zod.z.literal(AST_TYPES.EXPRESSION_VALIDATION),
617
704
  expression: zod.z.string()
618
705
  });
619
706
  const ValidationNodeSchema = zod.z.discriminatedUnion("type", [
@@ -622,12 +709,12 @@ const ValidationNodeSchema = zod.z.discriminatedUnion("type", [
622
709
  ]);
623
710
  function createSimpleValidation() {
624
711
  return SimpleValidationSchema.parse({
625
- type: "SimpleValidation"
712
+ type: AST_TYPES.SIMPLE_VALIDATION
626
713
  });
627
714
  }
628
715
  function createExpressionValidation(expression) {
629
716
  return ExpressionValidationSchema.parse({
630
- type: "ExpressionValidation",
717
+ type: AST_TYPES.EXPRESSION_VALIDATION,
631
718
  expression
632
719
  });
633
720
  }
@@ -641,7 +728,7 @@ function isValidationNode(value) {
641
728
  return ValidationNodeSchema.safeParse(value).success;
642
729
  }
643
730
  const ExerciseDocumentSchema = zod.z.object({
644
- type: zod.z.literal("ExerciseDocument"),
731
+ type: zod.z.literal(AST_TYPES.EXERCISE_DOCUMENT),
645
732
  question: TextBlockSchema.optional(),
646
733
  lines: zod.z.array(LineBlockSchema),
647
734
  tools: zod.z.array(ToolNodeSchema).optional(),
@@ -649,7 +736,7 @@ const ExerciseDocumentSchema = zod.z.object({
649
736
  });
650
737
  function createExerciseDocument(question, lines, validation, tools) {
651
738
  return ExerciseDocumentSchema.parse({
652
- type: "ExerciseDocument",
739
+ type: AST_TYPES.EXERCISE_DOCUMENT,
653
740
  question,
654
741
  lines,
655
742
  tools,
@@ -754,19 +841,22 @@ class NaturalDSLVisitor extends BaseCstVisitor {
754
841
  const token = ctx.RParen[0];
755
842
  return createFreeText(token.image);
756
843
  }
757
- if (ctx.numberText) {
758
- return this.visit(ctx.numberText);
844
+ if (ctx.textHighlighter) {
845
+ return this.visit(ctx.textHighlighter);
759
846
  }
760
847
  if (ctx.inlineInput) {
761
848
  return this.visit(ctx.inlineInput);
762
849
  }
763
850
  throw new Error("Unknown inline element");
764
851
  }
765
- numberText(ctx) {
852
+ textHighlighter(ctx) {
766
853
  const params = this.visit(ctx.parameterList);
767
854
  const value = params.value;
768
- const highlight = params.highlight;
769
- return createNumberText(value, highlight);
855
+ let highlight = void 0;
856
+ if (params.start !== void 0 && params.end !== void 0) {
857
+ highlight = [params.start, params.end];
858
+ }
859
+ return createHighlightedText(value, highlight);
770
860
  }
771
861
  inlineInput(ctx) {
772
862
  const params = this.visit(ctx.parameterList);
@@ -796,9 +886,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
796
886
  return createDragOptions(strings);
797
887
  }
798
888
  numericKeypad(ctx) {
799
- const params = this.visit(ctx.parameterList);
889
+ const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
890
+ const includeOperators = params.includeOperators === true || params.includeOperators === "true";
800
891
  const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
801
- return createNumericKeypad(includeDecimal);
892
+ return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0);
802
893
  }
803
894
  // ═══════════════════════════════════════════════════════════════════════════
804
895
  // Validation
@@ -890,33 +981,33 @@ function toAST(cst) {
890
981
  }
891
982
  const TextItemSchema = zod.z.object({
892
983
  id: zod.z.string(),
893
- type: zod.z.literal("text"),
984
+ type: zod.z.literal(JSON_TYPES.TEXT),
894
985
  value: zod.z.string()
895
986
  });
896
987
  const QuestionTextSchema = zod.z.object({
897
988
  id: zod.z.string(),
898
- type: zod.z.literal("question-text"),
989
+ type: zod.z.literal(JSON_TYPES.QUESTION_TEXT),
899
990
  value: zod.z.string()
900
991
  });
901
992
  const WidgetRefSchema = zod.z.object({
902
993
  id: zod.z.string(),
903
- type: zod.z.literal("widget-ref"),
994
+ type: zod.z.literal(JSON_TYPES.WIDGET_REF),
904
995
  widgetId: zod.z.string()
905
996
  });
906
- const NumberTextSchema = zod.z.object({
997
+ const HighlightedTextSchema = zod.z.object({
907
998
  id: zod.z.string(),
908
- type: zod.z.literal("number-text"),
999
+ type: zod.z.literal(JSON_TYPES.HIGHLIGHTED_TEXT),
909
1000
  value: zod.z.string(),
910
1001
  highlight: zod.z.tuple([zod.z.number(), zod.z.number()]).optional()
911
1002
  });
912
1003
  const LinebreakSchema = zod.z.object({
913
1004
  id: zod.z.string(),
914
- type: zod.z.literal("linebreak")
1005
+ type: zod.z.literal(JSON_TYPES.LINEBREAK)
915
1006
  });
916
1007
  const OperatorSchema = zod.z.enum(["+", "-", "×", "÷", "=", "<", ">", "?"]);
917
1008
  const OperatorItemSchema = zod.z.object({
918
1009
  id: zod.z.string(),
919
- type: zod.z.literal("operator"),
1010
+ type: zod.z.literal(JSON_TYPES.OPERATOR),
920
1011
  operator: OperatorSchema
921
1012
  });
922
1013
  const LayoutItemSchema = zod.z.lazy(
@@ -924,7 +1015,7 @@ const LayoutItemSchema = zod.z.lazy(
924
1015
  QuestionTextSchema,
925
1016
  TextItemSchema,
926
1017
  WidgetRefSchema,
927
- NumberTextSchema,
1018
+ HighlightedTextSchema,
928
1019
  LinebreakSchema,
929
1020
  GroupItemSchema,
930
1021
  OperatorItemSchema
@@ -932,17 +1023,17 @@ const LayoutItemSchema = zod.z.lazy(
932
1023
  );
933
1024
  const GroupItemSchema = zod.z.object({
934
1025
  id: zod.z.string(),
935
- type: zod.z.literal("group"),
1026
+ type: zod.z.literal(JSON_TYPES.GROUP),
936
1027
  items: zod.z.array(LayoutItemSchema)
937
1028
  });
938
1029
  const StringInputJSONSchema = zod.z.object({
939
- type: zod.z.literal("inline-input"),
940
- inputType: zod.z.literal("string"),
1030
+ type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
1031
+ inputType: zod.z.literal(INPUT_TYPES.STRING),
941
1032
  length: zod.z.number().int().positive()
942
1033
  });
943
1034
  const NumericInputJSONSchema = zod.z.object({
944
- type: zod.z.literal("inline-input"),
945
- inputType: zod.z.literal("numeric"),
1035
+ type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
1036
+ inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
946
1037
  digits: zod.z.number().int().positive()
947
1038
  });
948
1039
  const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
@@ -950,12 +1041,13 @@ const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
950
1041
  NumericInputJSONSchema
951
1042
  ]);
952
1043
  const DragOptionsJSONSchema = zod.z.object({
953
- type: zod.z.literal("drag-options"),
1044
+ type: zod.z.literal(JSON_TYPES.DRAG_OPTIONS),
954
1045
  options: zod.z.array(zod.z.string())
955
1046
  });
956
1047
  const NumericKeypadJSONSchema = zod.z.object({
957
- type: zod.z.literal("numeric-keypad"),
958
- 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()
959
1051
  });
960
1052
  const WidgetJSONSchema = zod.z.union([
961
1053
  InlineInputJSONSchema,
@@ -963,7 +1055,7 @@ const WidgetJSONSchema = zod.z.union([
963
1055
  NumericKeypadJSONSchema
964
1056
  ]);
965
1057
  const ValidationJSONSchema = zod.z.object({
966
- mode: zod.z.enum(["simple", "expression"]),
1058
+ mode: zod.z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
967
1059
  answers: zod.z.record(zod.z.union([zod.z.string(), zod.z.number()])).optional(),
968
1060
  expression: zod.z.string().optional(),
969
1061
  variables: zod.z.array(zod.z.string()).optional()
@@ -995,40 +1087,34 @@ function validateWidgetJSONSafe(data) {
995
1087
  return WidgetJSONSchema.safeParse(data);
996
1088
  }
997
1089
  function isQuestionText(item) {
998
- return item.type === "question-text";
1090
+ return item.type === JSON_TYPES.QUESTION_TEXT;
999
1091
  }
1000
- function isTextItem(item) {
1001
- return item.type === "text";
1092
+ function isTextItem$1(item) {
1093
+ return item.type === JSON_TYPES.TEXT;
1002
1094
  }
1003
1095
  function isWidgetRef(item) {
1004
- return item.type === "widget-ref";
1096
+ return item.type === JSON_TYPES.WIDGET_REF;
1005
1097
  }
1006
- function isNumberText(item) {
1007
- return item.type === "number-text";
1098
+ function isHighlightedText(item) {
1099
+ return item.type === JSON_TYPES.HIGHLIGHTED_TEXT;
1008
1100
  }
1009
1101
  function isLinebreak(item) {
1010
- return item.type === "linebreak";
1102
+ return item.type === JSON_TYPES.LINEBREAK;
1011
1103
  }
1012
1104
  function isGroupItem(item) {
1013
- return item.type === "group";
1105
+ return item.type === JSON_TYPES.GROUP;
1014
1106
  }
1015
1107
  function isOperatorItem(item) {
1016
- return item.type === "operator";
1108
+ return item.type === JSON_TYPES.OPERATOR;
1017
1109
  }
1018
1110
  function isInlineInputJSON(widget) {
1019
- return widget.type === "inline-input";
1111
+ return widget.type === JSON_TYPES.INLINE_INPUT;
1020
1112
  }
1021
1113
  function isDragOptionsJSON(widget) {
1022
- return widget.type === "drag-options";
1114
+ return widget.type === JSON_TYPES.DRAG_OPTIONS;
1023
1115
  }
1024
1116
  function isNumericKeypadJSON(widget) {
1025
- return widget.type === "numeric-keypad";
1026
- }
1027
- function isWidgetPlaceholder(item) {
1028
- return item.type === "widget-ref";
1029
- }
1030
- function isNumericInputJSON(widget) {
1031
- return widget.type === "inline-input";
1117
+ return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
1032
1118
  }
1033
1119
  class LayoutIdGenerator {
1034
1120
  constructor() {
@@ -1046,8 +1132,8 @@ function toJSON(ast) {
1046
1132
  const layout = [];
1047
1133
  if (ast.question) {
1048
1134
  layout.push({
1049
- id: idGenerator.generate("question-text"),
1050
- type: "question-text",
1135
+ id: idGenerator.generate(JSON_TYPES.QUESTION_TEXT),
1136
+ type: JSON_TYPES.QUESTION_TEXT,
1051
1137
  value: ast.question.value
1052
1138
  });
1053
1139
  }
@@ -1059,17 +1145,17 @@ function toJSON(ast) {
1059
1145
  const answers = {};
1060
1146
  for (const line of ast.lines) {
1061
1147
  for (const inline of line.inlines) {
1062
- if (inline.type === "InlineInput") {
1063
- if (inline.inputType === "string") {
1148
+ if (inline.type === AST_TYPES.INLINE_INPUT) {
1149
+ if (inline.inputType === INPUT_TYPES.STRING) {
1064
1150
  widgets[inline.name] = {
1065
- type: "inline-input",
1066
- inputType: "string",
1151
+ type: JSON_TYPES.INLINE_INPUT,
1152
+ inputType: INPUT_TYPES.STRING,
1067
1153
  length: inline.length
1068
1154
  };
1069
1155
  } else {
1070
1156
  widgets[inline.name] = {
1071
- type: "inline-input",
1072
- inputType: "numeric",
1157
+ type: JSON_TYPES.INLINE_INPUT,
1158
+ inputType: INPUT_TYPES.NUMERIC,
1073
1159
  digits: inline.digits
1074
1160
  };
1075
1161
  }
@@ -1081,14 +1167,15 @@ function toJSON(ast) {
1081
1167
  }
1082
1168
  if (ast.tools) {
1083
1169
  for (const tool of ast.tools) {
1084
- if (tool.type === "DragOptions") {
1170
+ if (tool.type === AST_TYPES.DRAG_OPTIONS) {
1085
1171
  widgets["_drag_options"] = {
1086
- type: "drag-options",
1172
+ type: JSON_TYPES.DRAG_OPTIONS,
1087
1173
  options: tool.options
1088
1174
  };
1089
- } else if (tool.type === "NumericKeypad") {
1175
+ } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
1090
1176
  widgets["_numeric_keypad"] = {
1091
- type: "numeric-keypad",
1177
+ type: JSON_TYPES.NUMERIC_KEYPAD,
1178
+ includeOperators: tool.includeOperators,
1092
1179
  includeDecimal: tool.includeDecimal
1093
1180
  };
1094
1181
  }
@@ -1109,35 +1196,44 @@ function transformLine(line, idGenerator) {
1109
1196
  items.push(transformInline(inline, idGenerator));
1110
1197
  }
1111
1198
  items.push({
1112
- id: idGenerator.generate("linebreak"),
1113
- type: "linebreak"
1199
+ id: idGenerator.generate(JSON_TYPES.LINEBREAK),
1200
+ type: JSON_TYPES.LINEBREAK
1114
1201
  });
1115
1202
  return items;
1116
1203
  }
1117
1204
  function transformInline(inline, idGenerator) {
1118
1205
  switch (inline.type) {
1119
- 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
+ }
1120
1216
  return {
1121
- id: idGenerator.generate("text"),
1122
- type: "text",
1217
+ id: idGenerator.generate(JSON_TYPES.TEXT),
1218
+ type: JSON_TYPES.TEXT,
1123
1219
  value: inline.value
1124
1220
  };
1125
- case "Space":
1221
+ case AST_TYPES.SPACE:
1126
1222
  return {
1127
- id: idGenerator.generate("space"),
1128
- type: "space"
1223
+ id: idGenerator.generate(JSON_TYPES.SPACE),
1224
+ type: JSON_TYPES.SPACE
1129
1225
  };
1130
- case "NumberText":
1226
+ case AST_TYPES.HIGHLIGHTED_TEXT:
1131
1227
  return {
1132
- id: idGenerator.generate("number-text"),
1133
- type: "number-text",
1228
+ id: idGenerator.generate(JSON_TYPES.HIGHLIGHTED_TEXT),
1229
+ type: JSON_TYPES.HIGHLIGHTED_TEXT,
1134
1230
  value: inline.value,
1135
1231
  highlight: inline.highlight
1136
1232
  };
1137
- case "InlineInput":
1233
+ case AST_TYPES.INLINE_INPUT:
1138
1234
  return {
1139
- id: idGenerator.generate("widget-ref"),
1140
- type: "widget-ref",
1235
+ id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
1236
+ type: JSON_TYPES.WIDGET_REF,
1141
1237
  widgetId: inline.name
1142
1238
  };
1143
1239
  default:
@@ -1145,16 +1241,16 @@ function transformInline(inline, idGenerator) {
1145
1241
  }
1146
1242
  }
1147
1243
  function transformValidation(validation, answers, widgets) {
1148
- if (validation.type === "SimpleValidation") {
1244
+ if (validation.type === AST_TYPES.SIMPLE_VALIDATION) {
1149
1245
  return {
1150
- mode: "simple",
1246
+ mode: VALIDATION_MODES.SIMPLE,
1151
1247
  answers
1152
1248
  };
1153
- } else if (validation.type === "ExpressionValidation") {
1249
+ } else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
1154
1250
  const validVariableNames = Object.keys(widgets);
1155
1251
  const variables = extractVariables$1(validation.expression, validVariableNames);
1156
1252
  return {
1157
- mode: "expression",
1253
+ mode: VALIDATION_MODES.EXPRESSION,
1158
1254
  expression: validation.expression,
1159
1255
  variables
1160
1256
  };
@@ -1480,9 +1576,9 @@ function findValidCombination(expression, ranges) {
1480
1576
  }
1481
1577
  function validate(exercise, userInputs) {
1482
1578
  switch (exercise.validation.mode) {
1483
- case "simple":
1579
+ case VALIDATION_MODES.SIMPLE:
1484
1580
  return validateSimple(exercise, userInputs);
1485
- case "expression":
1581
+ case VALIDATION_MODES.EXPRESSION:
1486
1582
  return validateExpression(exercise, userInputs);
1487
1583
  default:
1488
1584
  return {
@@ -1494,13 +1590,13 @@ function validate(exercise, userInputs) {
1494
1590
  }
1495
1591
  function getMissingInputs(exercise, userInputs) {
1496
1592
  const missing = [];
1497
- if (exercise.validation.mode === "simple" && exercise.validation.answers) {
1593
+ if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
1498
1594
  for (const widgetId of Object.keys(exercise.validation.answers)) {
1499
1595
  if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
1500
1596
  missing.push(widgetId);
1501
1597
  }
1502
1598
  }
1503
- } else if (exercise.validation.mode === "expression" && exercise.validation.variables) {
1599
+ } else if (exercise.validation.mode === VALIDATION_MODES.EXPRESSION && exercise.validation.variables) {
1504
1600
  for (const varName of exercise.validation.variables) {
1505
1601
  if (!(varName in userInputs) || userInputs[varName] === "") {
1506
1602
  missing.push(varName);
@@ -1516,7 +1612,7 @@ function validateInputFormats(exercise, userInputs) {
1516
1612
  if (value === void 0 || value === "") {
1517
1613
  continue;
1518
1614
  }
1519
- if (widget.type === "inline-input" && widget.inputType === "numeric") {
1615
+ if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
1520
1616
  const numValue = typeof value === "number" ? value : parseFloat(String(value));
1521
1617
  if (!Number.isFinite(numValue)) {
1522
1618
  errors[widgetId] = "Must be a valid number";
@@ -5546,7 +5642,8 @@ const InlineInput = ({
5546
5642
  inputType,
5547
5643
  length,
5548
5644
  digits,
5549
- onChange
5645
+ onChange,
5646
+ onFocus
5550
5647
  }) => {
5551
5648
  const handleChange = React.useCallback((newValue, fromDrop = false) => {
5552
5649
  onChange(widgetId, newValue, fromDrop);
@@ -5587,6 +5684,7 @@ const InlineInput = ({
5587
5684
  className: "inline-input",
5588
5685
  value,
5589
5686
  onChange: (e) => handleChange(e.target.value, false),
5687
+ onFocus: () => onFocus?.(widgetId),
5590
5688
  maxLength: expectedLength,
5591
5689
  style: { width: dynamicWidth }
5592
5690
  }
@@ -5631,6 +5729,7 @@ const DragOptions = ({
5631
5729
  };
5632
5730
  const NumericKeypad = ({
5633
5731
  value,
5732
+ includeOperators = false,
5634
5733
  includeDecimal = false,
5635
5734
  onChange
5636
5735
  }) => {
@@ -5640,32 +5739,23 @@ const NumericKeypad = ({
5640
5739
  const handleBackspace = () => {
5641
5740
  onChange(value.slice(0, -1));
5642
5741
  };
5643
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "numeric-keypad", children: [
5644
- ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"].map((digit) => /* @__PURE__ */ jsxRuntime.jsx(
5645
- "button",
5646
- {
5647
- className: "keypad-btn",
5648
- onClick: () => handleDigit(digit),
5649
- children: digit
5650
- },
5651
- digit
5652
- )),
5653
- includeDecimal && /* @__PURE__ */ jsxRuntime.jsx(
5654
- "button",
5655
- {
5656
- className: "keypad-btn",
5657
- onClick: () => handleDigit("."),
5658
- children: "."
5659
- }
5660
- ),
5661
- /* @__PURE__ */ jsxRuntime.jsx(
5662
- "button",
5663
- {
5664
- className: "keypad-btn",
5665
- onClick: handleBackspace,
5666
- children: "⌫"
5667
- }
5668
- )
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: "÷" })
5669
5759
  ] });
5670
5760
  };
5671
5761
  function isInlineInputWidget(widget) {
@@ -5682,11 +5772,10 @@ const WidgetRenderer = ({
5682
5772
  widgetId,
5683
5773
  value,
5684
5774
  isDropped = false,
5685
- onChange
5775
+ onChange,
5776
+ onInputFocus,
5777
+ onKeypadChange
5686
5778
  }) => {
5687
- const handleChange = React.useCallback((newValue, fromDrop = false) => {
5688
- onChange(widgetId, newValue, fromDrop);
5689
- }, [onChange, widgetId]);
5690
5779
  if (isInlineInputWidget(widget)) {
5691
5780
  return /* @__PURE__ */ jsxRuntime.jsx(
5692
5781
  InlineInput,
@@ -5697,7 +5786,8 @@ const WidgetRenderer = ({
5697
5786
  inputType: widget.inputType,
5698
5787
  length: widget.inputType === "string" ? widget.length : void 0,
5699
5788
  digits: widget.inputType === "numeric" ? widget.digits : void 0,
5700
- onChange
5789
+ onChange,
5790
+ onFocus: onInputFocus
5701
5791
  }
5702
5792
  );
5703
5793
  }
@@ -5716,41 +5806,102 @@ const WidgetRenderer = ({
5716
5806
  NumericKeypad,
5717
5807
  {
5718
5808
  value,
5809
+ includeOperators: widget.includeOperators,
5719
5810
  includeDecimal: widget.includeDecimal,
5720
- onChange: handleChange
5811
+ onChange: onKeypadChange || (() => {
5812
+ })
5721
5813
  }
5722
5814
  );
5723
5815
  }
5724
5816
  return null;
5725
5817
  };
5726
- function isArabic$1(text) {
5818
+ function isArabic(text) {
5727
5819
  return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
5728
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
+ }
5729
5876
  const LayoutRenderer = ({
5730
5877
  item,
5731
5878
  widgets,
5732
5879
  values,
5733
5880
  droppedInputs,
5734
- onChange
5881
+ onChange,
5882
+ onInputFocus
5735
5883
  }) => {
5736
5884
  switch (item.type) {
5737
- case "question-text": {
5738
- const hasArabic = isArabic$1(item.value);
5885
+ case JSON_TYPES.QUESTION_TEXT: {
5886
+ const hasArabic = isArabic(item.value);
5739
5887
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `question-text ${hasArabic ? "question-text-arabic" : ""}`, children: item.value });
5740
5888
  }
5741
- case "text":
5742
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-item ${isArabic$1(item.value) ? "text-item-arabic" : "text-item-not-arabic"}`, children: item.value });
5743
- 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;
5744
5894
  if (item.highlight) {
5745
5895
  const [start, end] = item.highlight;
5746
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "number-text", children: [
5896
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "highlighted-text", dir: dirAttr, children: [
5747
5897
  item.value.slice(0, start),
5748
- /* @__PURE__ */ jsxRuntime.jsx("mark", { className: "highlight", children: item.value.slice(start, end) }),
5898
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
5749
5899
  item.value.slice(end)
5750
5900
  ] });
5751
5901
  }
5752
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-text", children: item.value });
5753
- case "widget-ref": {
5902
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlighted-text", dir: dirAttr, children: item.value });
5903
+ }
5904
+ case JSON_TYPES.WIDGET_REF: {
5754
5905
  const widget = widgets[item.widgetId];
5755
5906
  if (!widget) return null;
5756
5907
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -5760,15 +5911,18 @@ const LayoutRenderer = ({
5760
5911
  widgetId: item.widgetId,
5761
5912
  value: values[item.widgetId] || "",
5762
5913
  isDropped: droppedInputs.has(item.widgetId),
5763
- onChange
5914
+ onChange,
5915
+ onInputFocus
5764
5916
  }
5765
5917
  );
5766
5918
  }
5767
- case "linebreak":
5919
+ case JSON_TYPES.SPACE:
5920
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "space", children: " " });
5921
+ case JSON_TYPES.LINEBREAK:
5768
5922
  return null;
5769
- case "operator":
5923
+ case JSON_TYPES.OPERATOR:
5770
5924
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "operator", children: item.operator });
5771
- case "group":
5925
+ case JSON_TYPES.GROUP:
5772
5926
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "group", children: item.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
5773
5927
  LayoutRenderer,
5774
5928
  {
@@ -5776,7 +5930,8 @@ const LayoutRenderer = ({
5776
5930
  widgets,
5777
5931
  values,
5778
5932
  droppedInputs,
5779
- onChange
5933
+ onChange,
5934
+ onInputFocus
5780
5935
  },
5781
5936
  subItem.id
5782
5937
  )) });
@@ -5793,21 +5948,6 @@ const DEFAULT_RULER_TOOL = {
5793
5948
  width: 300,
5794
5949
  allowRotate: true
5795
5950
  };
5796
- function isArabic(text) {
5797
- return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
5798
- }
5799
- function itemContainsArabic(item) {
5800
- if (item.type === "text" && "value" in item) {
5801
- return isArabic(item.value);
5802
- }
5803
- if (item.type === "number-text" && "value" in item) {
5804
- return isArabic(item.value);
5805
- }
5806
- if (item.type === "group" && "items" in item) {
5807
- return item.items.some((subItem) => itemContainsArabic(subItem));
5808
- }
5809
- return false;
5810
- }
5811
5951
  const ExerciseRenderer = React.forwardRef(({
5812
5952
  json,
5813
5953
  onSubmit,
@@ -5817,6 +5957,7 @@ const ExerciseRenderer = React.forwardRef(({
5817
5957
  }, ref) => {
5818
5958
  const [values, setValues] = React.useState({});
5819
5959
  const [droppedInputs, setDroppedInputs] = React.useState(/* @__PURE__ */ new Set());
5960
+ const [focusedInputId, setFocusedInputId] = React.useState(null);
5820
5961
  React.useImperativeHandle(ref, () => ({
5821
5962
  submit: () => {
5822
5963
  const result = validate(json, values);
@@ -5827,6 +5968,7 @@ const ExerciseRenderer = React.forwardRef(({
5827
5968
  reset: () => {
5828
5969
  setValues({});
5829
5970
  setDroppedInputs(/* @__PURE__ */ new Set());
5971
+ setFocusedInputId(null);
5830
5972
  },
5831
5973
  focusFirstInput: () => {
5832
5974
  const container = document.querySelector(".math-exercise-renderer");
@@ -5857,10 +5999,20 @@ const ExerciseRenderer = React.forwardRef(({
5857
5999
  const groups = [];
5858
6000
  let currentGroup = [];
5859
6001
  for (const item of json.layout) {
5860
- 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) {
5861
6012
  if (currentGroup.length > 0) {
5862
6013
  const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
5863
- groups.push({ items: currentGroup, hasArabic });
6014
+ const subGroups = groupLineItems(currentGroup);
6015
+ groups.push({ items: currentGroup, hasArabic, subGroups });
5864
6016
  currentGroup = [];
5865
6017
  }
5866
6018
  } else {
@@ -5869,7 +6021,8 @@ const ExerciseRenderer = React.forwardRef(({
5869
6021
  }
5870
6022
  if (currentGroup.length > 0) {
5871
6023
  const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
5872
- groups.push({ items: currentGroup, hasArabic });
6024
+ const subGroups = groupLineItems(currentGroup);
6025
+ groups.push({ items: currentGroup, hasArabic, subGroups });
5873
6026
  }
5874
6027
  return groups;
5875
6028
  }, [json.layout]);
@@ -5890,17 +6043,52 @@ const ExerciseRenderer = React.forwardRef(({
5890
6043
  "div",
5891
6044
  {
5892
6045
  className: `exercise-line ${group.hasArabic ? "exercise-line-arabic" : ""}`,
5893
- children: group.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
5894
- LayoutRenderer,
5895
- {
5896
- item,
5897
- widgets: json.widgets,
5898
- values,
5899
- droppedInputs,
5900
- onChange: handleChange
5901
- },
5902
- item.id
5903
- ))
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
+ )
5904
6092
  },
5905
6093
  groupIdx
5906
6094
  )) }),
@@ -5909,9 +6097,13 @@ const ExerciseRenderer = React.forwardRef(({
5909
6097
  {
5910
6098
  widget: numericKeypadWidget,
5911
6099
  widgetId: "_numeric_keypad",
5912
- value: "",
5913
- onChange: (v, w, f) => {
5914
- 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
+ }
5915
6107
  }
5916
6108
  }
5917
6109
  ) })
@@ -5981,6 +6173,7 @@ function compileWithAST(dsl) {
5981
6173
  const json = toJSON(ast);
5982
6174
  return { ast, json };
5983
6175
  }
6176
+ exports.AST_TYPES = AST_TYPES;
5984
6177
  exports.DragOptionsJSONSchema = DragOptionsJSONSchema;
5985
6178
  exports.DragOptionsToolSchema = DragOptionsToolSchema;
5986
6179
  exports.EngineError = EngineError;
@@ -5991,16 +6184,17 @@ exports.ExerciseRenderer = ExerciseRenderer;
5991
6184
  exports.ExpressionValidationSchema = ExpressionValidationSchema;
5992
6185
  exports.FreeTextInlineSchema = FreeTextInlineSchema;
5993
6186
  exports.GroupItemSchema = GroupItemSchema;
6187
+ exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
6188
+ exports.HighlightedTextSchema = HighlightedTextSchema;
5994
6189
  exports.InlineInputJSONSchema = InlineInputJSONSchema;
5995
6190
  exports.InlineInputWidgetSchema = InlineInputWidgetSchema;
5996
6191
  exports.InlineNodeSchema = InlineNodeSchema;
6192
+ exports.JSON_TYPES = JSON_TYPES;
5997
6193
  exports.LayoutItemSchema = LayoutItemSchema;
5998
6194
  exports.LayoutRenderer = LayoutRenderer;
5999
6195
  exports.LexerError = LexerError;
6000
6196
  exports.LineBlockSchema = LineBlockSchema;
6001
6197
  exports.LinebreakSchema = LinebreakSchema;
6002
- exports.NumberTextInlineSchema = NumberTextInlineSchema;
6003
- exports.NumberTextSchema = NumberTextSchema;
6004
6198
  exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
6005
6199
  exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
6006
6200
  exports.OperatorItemSchema = OperatorItemSchema;
@@ -6025,9 +6219,8 @@ exports.createDragOptions = createDragOptions;
6025
6219
  exports.createExerciseDocument = createExerciseDocument;
6026
6220
  exports.createExpressionValidation = createExpressionValidation;
6027
6221
  exports.createFreeText = createFreeText;
6028
- exports.createInlineInput = createInlineInput;
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,13 +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
- exports.isNumericInputJSON = isNumericInputJSON;
6054
6246
  exports.isNumericKeypad = isNumericKeypad;
6055
6247
  exports.isNumericKeypadJSON = isNumericKeypadJSON;
6056
6248
  exports.isOperatorItem = isOperatorItem;
@@ -6058,11 +6250,10 @@ exports.isQuestionText = isQuestionText;
6058
6250
  exports.isSimpleValidation = isSimpleValidation;
6059
6251
  exports.isSpace = isSpace;
6060
6252
  exports.isTextBlock = isTextBlock;
6061
- exports.isTextItem = isTextItem;
6253
+ exports.isTextItem = isTextItem$1;
6062
6254
  exports.isToolNode = isToolNode;
6063
6255
  exports.isValidExpression = isValidExpression;
6064
6256
  exports.isValidationNode = isValidationNode;
6065
- exports.isWidgetPlaceholder = isWidgetPlaceholder;
6066
6257
  exports.isWidgetRef = isWidgetRef;
6067
6258
  exports.parse = parse;
6068
6259
  exports.parseSafe = parseSafe;