@mathscan/math-exercise-engine 1.1.2 → 1.1.4

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
@@ -21,6 +21,8 @@ const AST_TYPES = {
21
21
  BOLD_TEXT: "BoldText",
22
22
  /** Inline input widget (string or numeric) */
23
23
  INLINE_INPUT: "InlineInput",
24
+ /** Single inline draggable option (appears in-flow with text) */
25
+ INLINE_DRAG_OPTION: "InlineDragOption",
24
26
  /** Inline fraction widget (numerator/denominator with optional inputs) */
25
27
  FRACTION: "Fraction",
26
28
  /** Inline image element */
@@ -38,6 +40,16 @@ const AST_TYPES = {
38
40
  TABLE_ROW: "TableRow",
39
41
  /** Cell within a table row */
40
42
  TABLE_CELL: "TableCell",
43
+ /** Vertical operation (column arithmetic) block */
44
+ VERTICAL_OPERATION_BLOCK: "VerticalOperationBlock",
45
+ /** Row within a vertical operation */
46
+ VOP_ROW: "VopRow",
47
+ /** Cell within a vertical-operation row */
48
+ VOP_CELL: "VopCell",
49
+ /** Separator line within a vertical operation */
50
+ VOP_SEPARATOR: "VopSeparator",
51
+ /** Carry row within a vertical operation */
52
+ VOP_CARRY_ROW: "VopCarryRow",
41
53
  /** Number line layout block */
42
54
  NUMBER_LINE_BLOCK: "NumberLineBlock",
43
55
  /** A position marker on the number line */
@@ -112,6 +124,16 @@ const JSON_TYPES = {
112
124
  TABLE_ROW: "table-row",
113
125
  /** Cell within a table */
114
126
  TABLE_CELL: "table-cell",
127
+ /** Vertical operation (column arithmetic) block */
128
+ VERTICAL_OPERATION: "vertical-operation",
129
+ /** Row within a vertical operation */
130
+ VOP_ROW: "vop-row",
131
+ /** Cell within a vertical-operation row */
132
+ VOP_CELL: "vop-cell",
133
+ /** Separator within a vertical operation */
134
+ VOP_SEPARATOR: "vop-separator",
135
+ /** Carry row within a vertical operation */
136
+ VOP_CARRY_ROW: "vop-carry-row",
115
137
  /** Number line layout block */
116
138
  NUMBER_LINE: "number-line",
117
139
  /** Position marker within a number line */
@@ -135,6 +157,8 @@ const JSON_TYPES = {
135
157
  INLINE_INPUT: "inline-input",
136
158
  /** Drag-and-drop options widget */
137
159
  DRAG_OPTIONS: "drag-options",
160
+ /** Single inline draggable option (appears in-flow with text) */
161
+ INLINE_DRAG_OPTION: "inline-drag-option",
138
162
  /** Radio button options widget */
139
163
  RADIO_OPTIONS: "radio-options",
140
164
  /** Numeric keypad widget */
@@ -150,6 +174,13 @@ const STATEFUL_WIDGET_TYPES = /* @__PURE__ */ new Set([
150
174
  JSON_TYPES.PAIR_MATCHER,
151
175
  JSON_TYPES.VISUAL_FRACTION
152
176
  ]);
177
+ const INLINE_DRAG_OPT_PREFIX = "_inline_drag_opt_";
178
+ const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
179
+ JSON_TYPES.INLINE_INPUT,
180
+ JSON_TYPES.INLINE_DRAG_OPTION,
181
+ JSON_TYPES.FRACTION,
182
+ JSON_TYPES.VISUAL_FRACTION
183
+ ]);
153
184
  const VALIDATION_MODES = {
154
185
  /** Simple answer matching validation */
155
186
  SIMPLE: "simple",
@@ -232,6 +263,10 @@ const STRING_INPUT = createToken({
232
263
  name: "STRING_INPUT",
233
264
  pattern: /STRING_INPUT/
234
265
  });
266
+ const INLINE_DRAG_OPTION = createToken({
267
+ name: "INLINE_DRAG_OPTION",
268
+ pattern: /INLINE_DRAG_OPTION/
269
+ });
235
270
  const DRAGABLE_BUTTONS_OPTIONS = createToken({
236
271
  name: "DRAGABLE_BUTTONS_OPTIONS",
237
272
  pattern: /DRAGABLE_BUTTONS_OPTIONS/
@@ -320,6 +355,26 @@ const IMAGE = createToken({
320
355
  name: "IMAGE",
321
356
  pattern: /IMAGE/
322
357
  });
358
+ const VERTICAL_OPERATION = createToken({
359
+ name: "VERTICAL_OPERATION",
360
+ pattern: /VERTICAL_OPERATION/
361
+ });
362
+ const SEP = createToken({
363
+ name: "SEP",
364
+ pattern: /SEP(?![a-zA-Z0-9_])/
365
+ });
366
+ const CARRY = createToken({
367
+ name: "CARRY",
368
+ pattern: /CARRY(?![a-zA-Z0-9_])/
369
+ });
370
+ const INPUT = createToken({
371
+ name: "INPUT",
372
+ pattern: /INPUT(?=\s*\[)/
373
+ });
374
+ const Underscore = createToken({
375
+ name: "Underscore",
376
+ pattern: /_(?![a-zA-Z0-9_])/
377
+ });
323
378
  const NumberLiteral = createToken({
324
379
  name: "NumberLiteral",
325
380
  pattern: /-?\d+(\.\d+)?/
@@ -416,6 +471,7 @@ const allTokens = [
416
471
  HIGHLIGHT_TEXT,
417
472
  W_S_I,
418
473
  STRING_INPUT,
474
+ INLINE_DRAG_OPTION,
419
475
  DRAGABLE_BUTTONS_OPTIONS,
420
476
  RADIO_BUTTONS_OPTIONS,
421
477
  NUMERIC_KEYPAD,
@@ -438,6 +494,11 @@ const allTokens = [
438
494
  ITEM,
439
495
  ANSWERS,
440
496
  IMAGE,
497
+ VERTICAL_OPERATION,
498
+ SEP,
499
+ CARRY,
500
+ INPUT,
501
+ Underscore,
441
502
  // Literals
442
503
  NumberLiteral,
443
504
  StringLiteral,
@@ -595,6 +656,10 @@ class MathExerciseParser extends CstParser {
595
656
  GATE: this.BACKTRACK(this.matcher),
596
657
  ALT: () => this.SUBRULE(this.matcher)
597
658
  },
659
+ {
660
+ GATE: this.BACKTRACK(this.verticalOperation),
661
+ ALT: () => this.SUBRULE(this.verticalOperation)
662
+ },
598
663
  { ALT: () => this.SUBRULE(this.line) }
599
664
  ]);
600
665
  });
@@ -915,6 +980,130 @@ class MathExerciseParser extends CstParser {
915
980
  this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
916
981
  });
917
982
  });
983
+ this.verticalOperation = this.RULE("verticalOperation", () => {
984
+ this.MANY(() => {
985
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
986
+ });
987
+ this.CONSUME(VERTICAL_OPERATION);
988
+ this.OPTION(() => {
989
+ this.CONSUME(LBracket);
990
+ this.SUBRULE(this.parameterList);
991
+ this.CONSUME(RBracket);
992
+ });
993
+ this.MANY2(() => {
994
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
995
+ });
996
+ this.CONSUME(LBrace);
997
+ this.MANY3(() => {
998
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
999
+ });
1000
+ this.AT_LEAST_ONE(() => {
1001
+ this.OR4([
1002
+ { ALT: () => this.SUBRULE(this.vopRow) },
1003
+ { ALT: () => this.SUBRULE(this.vopCarry) },
1004
+ { ALT: () => this.SUBRULE(this.vopSep) }
1005
+ ]);
1006
+ });
1007
+ this.CONSUME(RBrace);
1008
+ this.MANY4(() => {
1009
+ this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
1010
+ });
1011
+ });
1012
+ this.vopRow = this.RULE("vopRow", () => {
1013
+ this.CONSUME(ROW);
1014
+ this.OPTION(() => {
1015
+ this.CONSUME(LBracket);
1016
+ this.SUBRULE(this.parameterList);
1017
+ this.CONSUME(RBracket);
1018
+ });
1019
+ this.MANY(() => {
1020
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1021
+ });
1022
+ this.CONSUME(LBrace);
1023
+ this.MANY2(() => {
1024
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1025
+ });
1026
+ this.SUBRULE(this.vopCell);
1027
+ this.MANY3(() => {
1028
+ this.MANY4(() => {
1029
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
1030
+ });
1031
+ this.CONSUME(Comma);
1032
+ this.MANY5(() => {
1033
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
1034
+ });
1035
+ this.SUBRULE2(this.vopCell);
1036
+ });
1037
+ this.MANY6(() => {
1038
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1039
+ });
1040
+ this.CONSUME(RBrace);
1041
+ this.MANY7(() => {
1042
+ this.OR6([{ ALT: () => this.CONSUME6(Space) }, { ALT: () => this.CONSUME6(Newline) }]);
1043
+ });
1044
+ });
1045
+ this.vopCell = this.RULE("vopCell", () => {
1046
+ this.OR([
1047
+ { ALT: () => this.CONSUME(NumberLiteral) },
1048
+ { ALT: () => this.CONSUME(StringLiteral) },
1049
+ { ALT: () => this.CONSUME(Underscore) },
1050
+ {
1051
+ ALT: () => {
1052
+ this.CONSUME(INPUT);
1053
+ this.CONSUME(LBracket);
1054
+ this.SUBRULE(this.parameterList);
1055
+ this.CONSUME(RBracket);
1056
+ }
1057
+ },
1058
+ { ALT: () => this.SUBRULE(this.colorText) },
1059
+ { ALT: () => this.SUBRULE(this.boldText) },
1060
+ { ALT: () => this.SUBRULE(this.textHighlighter) }
1061
+ ]);
1062
+ });
1063
+ this.vopCarry = this.RULE("vopCarry", () => {
1064
+ this.CONSUME(CARRY);
1065
+ this.MANY(() => {
1066
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1067
+ });
1068
+ this.CONSUME(LBrace);
1069
+ this.MANY2(() => {
1070
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1071
+ });
1072
+ this.SUBRULE(this.vopCell);
1073
+ this.MANY3(() => {
1074
+ this.MANY4(() => {
1075
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
1076
+ });
1077
+ this.CONSUME(Comma);
1078
+ this.MANY5(() => {
1079
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
1080
+ });
1081
+ this.SUBRULE2(this.vopCell);
1082
+ });
1083
+ this.MANY6(() => {
1084
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1085
+ });
1086
+ this.CONSUME(RBrace);
1087
+ this.MANY7(() => {
1088
+ this.OR6([{ ALT: () => this.CONSUME6(Newline) }, { ALT: () => this.CONSUME6(Space) }]);
1089
+ });
1090
+ });
1091
+ this.vopSep = this.RULE("vopSep", () => {
1092
+ this.CONSUME(SEP);
1093
+ this.OPTION(() => {
1094
+ this.MANY(() => {
1095
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1096
+ });
1097
+ this.CONSUME(LBrace);
1098
+ this.MANY2(() => {
1099
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1100
+ });
1101
+ this.CONSUME(RBrace);
1102
+ });
1103
+ this.MANY3(() => {
1104
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
1105
+ });
1106
+ });
918
1107
  this.line = this.RULE("line", () => {
919
1108
  this.MANY(() => {
920
1109
  this.CONSUME(Space);
@@ -968,7 +1157,9 @@ class MathExerciseParser extends CstParser {
968
1157
  // VISUAL_FRACTION — always allowed; display-only when answer=[] or absence of answer parameter, interactive when answer=[...] with values.
969
1158
  { ALT: () => this.SUBRULE(this.visualFraction) },
970
1159
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
971
- { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
1160
+ { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
1161
+ // INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
1162
+ { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) }
972
1163
  ]);
973
1164
  });
974
1165
  this.textHighlighter = this.RULE("textHighlighter", () => {
@@ -1050,6 +1241,12 @@ class MathExerciseParser extends CstParser {
1050
1241
  this.SUBRULE(this.parameterList);
1051
1242
  this.CONSUME(RBracket);
1052
1243
  });
1244
+ this.inlineDragOption = this.RULE("inlineDragOption", () => {
1245
+ this.CONSUME(INLINE_DRAG_OPTION);
1246
+ this.CONSUME(LBracket);
1247
+ this.CONSUME(StringLiteral);
1248
+ this.CONSUME(RBracket);
1249
+ });
1053
1250
  this.tool = this.RULE("tool", () => {
1054
1251
  this.OR([
1055
1252
  { ALT: () => this.SUBRULE(this.dragableButtons) },
@@ -1318,6 +1515,10 @@ function isPerfectSquare(value) {
1318
1515
  const root = Math.sqrt(value);
1319
1516
  return Number.isInteger(root);
1320
1517
  }
1518
+ const InlineDragOptionSchema = z.object({
1519
+ type: z.literal(AST_TYPES.INLINE_DRAG_OPTION),
1520
+ value: z.string().min(1)
1521
+ });
1321
1522
  const InlineNodeSchema = z.union([
1322
1523
  FreeTextInlineSchema,
1323
1524
  SpaceInlineSchema,
@@ -1327,7 +1528,8 @@ const InlineNodeSchema = z.union([
1327
1528
  InlineInputWidgetSchema,
1328
1529
  FractionInlineSchema,
1329
1530
  ImageInlineSchema,
1330
- VisualFractionInlineSchema
1531
+ VisualFractionInlineSchema,
1532
+ InlineDragOptionSchema
1331
1533
  ]);
1332
1534
  function createFreeText(value) {
1333
1535
  return FreeTextInlineSchema.parse({
@@ -1363,24 +1565,32 @@ function createBoldText(value, highlight) {
1363
1565
  });
1364
1566
  }
1365
1567
  function createStringInput(name, length, answer, placeholder) {
1366
- return parseOrThrow(InlineInputWidgetSchema, {
1367
- type: AST_TYPES.INLINE_INPUT,
1368
- name,
1369
- inputType: INPUT_TYPES.STRING,
1370
- length,
1371
- placeholder,
1372
- answer
1373
- }, "STRING_INPUT");
1568
+ return parseOrThrow(
1569
+ InlineInputWidgetSchema,
1570
+ {
1571
+ type: AST_TYPES.INLINE_INPUT,
1572
+ name,
1573
+ inputType: INPUT_TYPES.STRING,
1574
+ length,
1575
+ placeholder,
1576
+ answer
1577
+ },
1578
+ "STRING_INPUT"
1579
+ );
1374
1580
  }
1375
1581
  function createNumericInput(name, digits, answer, placeholder) {
1376
- return parseOrThrow(InlineInputWidgetSchema, {
1377
- type: AST_TYPES.INLINE_INPUT,
1378
- name,
1379
- inputType: INPUT_TYPES.NUMERIC,
1380
- digits,
1381
- placeholder,
1382
- answer
1383
- }, "NUMERIC_INPUT");
1582
+ return parseOrThrow(
1583
+ InlineInputWidgetSchema,
1584
+ {
1585
+ type: AST_TYPES.INLINE_INPUT,
1586
+ name,
1587
+ inputType: INPUT_TYPES.NUMERIC,
1588
+ digits,
1589
+ placeholder,
1590
+ answer
1591
+ },
1592
+ "NUMERIC_INPUT"
1593
+ );
1384
1594
  }
1385
1595
  function createImage(name, src, alt, options = {}) {
1386
1596
  return ImageInlineSchema.parse({
@@ -1406,6 +1616,12 @@ function createVisualFraction(name, divisions, options = {}) {
1406
1616
  ...options
1407
1617
  });
1408
1618
  }
1619
+ function createInlineDragOption(value) {
1620
+ return InlineDragOptionSchema.parse({
1621
+ type: AST_TYPES.INLINE_DRAG_OPTION,
1622
+ value
1623
+ });
1624
+ }
1409
1625
  function isFreeText(value) {
1410
1626
  return FreeTextInlineSchema.safeParse(value).success;
1411
1627
  }
@@ -1427,6 +1643,9 @@ function isInlineNode(value) {
1427
1643
  function isVisualFractionInline(value) {
1428
1644
  return VisualFractionInlineSchema.safeParse(value).success;
1429
1645
  }
1646
+ function isInlineDragOption(value) {
1647
+ return InlineDragOptionSchema.safeParse(value).success;
1648
+ }
1430
1649
  const TextBlockSchema = z.object({
1431
1650
  type: z.literal(AST_TYPES.TEXT_BLOCK),
1432
1651
  value: z.string()
@@ -1480,17 +1699,19 @@ const NumberLineCounterSchema = z.object({
1480
1699
  min: z.number().int().min(1, { message: "COUNTER: min must be >= 1" }),
1481
1700
  max: z.number().int().positive({ message: "COUNTER: max must be > 0" }),
1482
1701
  default: z.number().int().nonnegative().optional(),
1483
- name: z.string().optional()
1702
+ name: z.string().optional(),
1703
+ answer: z.number().int().positive({ message: "COUNTER: answer must be a positive integer" }).optional()
1484
1704
  }).refine((d) => d.min <= d.max, {
1485
1705
  message: "COUNTER: min must be <= max"
1486
1706
  });
1487
- function createNumberLineCounter(max, min, defaultValue2, name) {
1707
+ function createNumberLineCounter(max, min, defaultValue2, name, answer) {
1488
1708
  return NumberLineCounterSchema.parse({
1489
1709
  type: AST_TYPES.NUMBER_LINE_COUNTER,
1490
1710
  min: min ?? 1,
1491
1711
  max,
1492
1712
  ...defaultValue2 !== void 0 && { default: defaultValue2 },
1493
- ...name !== void 0 && { name }
1713
+ ...name !== void 0 && { name },
1714
+ ...answer !== void 0 && { answer }
1494
1715
  });
1495
1716
  }
1496
1717
  const NumberLineHighlightSchema = z.object({
@@ -1624,6 +1845,146 @@ function createTableBlock(cols, rows) {
1624
1845
  rows
1625
1846
  });
1626
1847
  }
1848
+ const VOP_OPERATORS = ["+", "-", "×", "÷", "="];
1849
+ function normalizeVopOperator(raw) {
1850
+ const map = {
1851
+ "+": "+",
1852
+ "-": "-",
1853
+ "−": "-",
1854
+ "*": "×",
1855
+ "x": "×",
1856
+ "X": "×",
1857
+ "×": "×",
1858
+ "/": "÷",
1859
+ "÷": "÷",
1860
+ "=": "="
1861
+ };
1862
+ const op = map[raw];
1863
+ if (!op) throw new Error(`VERTICAL_OPERATION: unsupported op "${raw}"`);
1864
+ return op;
1865
+ }
1866
+ const VopDigitCellSchema = z.object({
1867
+ type: z.literal(AST_TYPES.VOP_CELL),
1868
+ kind: z.literal("digit"),
1869
+ value: z.string()
1870
+ });
1871
+ const VopLabelCellSchema = z.object({
1872
+ type: z.literal(AST_TYPES.VOP_CELL),
1873
+ kind: z.literal("label"),
1874
+ value: z.string()
1875
+ });
1876
+ const VopInputCellSchema = z.object({
1877
+ type: z.literal(AST_TYPES.VOP_CELL),
1878
+ kind: z.literal("input"),
1879
+ name: z.string().min(1),
1880
+ digits: z.number().int().positive().default(1),
1881
+ answer: z.union([z.string(), z.number()]).optional()
1882
+ });
1883
+ const VopEmptyCellSchema = z.object({
1884
+ type: z.literal(AST_TYPES.VOP_CELL),
1885
+ kind: z.literal("empty")
1886
+ });
1887
+ const VopColorCellSchema = z.object({
1888
+ type: z.literal(AST_TYPES.VOP_CELL),
1889
+ kind: z.literal("color"),
1890
+ value: z.string(),
1891
+ color: z.string()
1892
+ });
1893
+ const VopBoldCellSchema = z.object({
1894
+ type: z.literal(AST_TYPES.VOP_CELL),
1895
+ kind: z.literal("bold"),
1896
+ value: z.string()
1897
+ });
1898
+ const VopHighlightCellSchema = z.object({
1899
+ type: z.literal(AST_TYPES.VOP_CELL),
1900
+ kind: z.literal("highlight"),
1901
+ value: z.string()
1902
+ });
1903
+ const VopCellSchema = z.discriminatedUnion("kind", [
1904
+ VopDigitCellSchema,
1905
+ VopLabelCellSchema,
1906
+ VopColorCellSchema,
1907
+ VopBoldCellSchema,
1908
+ VopHighlightCellSchema,
1909
+ VopInputCellSchema,
1910
+ VopEmptyCellSchema
1911
+ ]);
1912
+ function createVopDigitCell(value) {
1913
+ return VopDigitCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "digit", value });
1914
+ }
1915
+ function createVopInputCell(name, digits = 1, answer) {
1916
+ return VopInputCellSchema.parse({
1917
+ type: AST_TYPES.VOP_CELL,
1918
+ kind: "input",
1919
+ name,
1920
+ digits,
1921
+ answer
1922
+ });
1923
+ }
1924
+ function createVopLabelCell(value) {
1925
+ return VopLabelCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "label", value });
1926
+ }
1927
+ function createVopEmptyCell() {
1928
+ return VopEmptyCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "empty" });
1929
+ }
1930
+ function createVopColorCell(value, color) {
1931
+ return VopColorCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "color", value, color });
1932
+ }
1933
+ function createVopBoldCell(value) {
1934
+ return VopBoldCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "bold", value });
1935
+ }
1936
+ function createVopHighlightCell(value) {
1937
+ return VopHighlightCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "highlight", value });
1938
+ }
1939
+ const VopRowSchema = z.object({
1940
+ type: z.literal(AST_TYPES.VOP_ROW),
1941
+ cells: z.array(VopCellSchema).min(1),
1942
+ op: z.enum(VOP_OPERATORS).optional()
1943
+ });
1944
+ function createVopRow(cells, op) {
1945
+ return VopRowSchema.parse({ type: AST_TYPES.VOP_ROW, cells, op });
1946
+ }
1947
+ const VopCarryRowSchema = z.object({
1948
+ type: z.literal(AST_TYPES.VOP_CARRY_ROW),
1949
+ cells: z.array(VopCellSchema).min(1)
1950
+ });
1951
+ function createVopCarryRow(cells) {
1952
+ return VopCarryRowSchema.parse({ type: AST_TYPES.VOP_CARRY_ROW, cells });
1953
+ }
1954
+ const VopSeparatorSchema = z.object({
1955
+ type: z.literal(AST_TYPES.VOP_SEPARATOR)
1956
+ });
1957
+ function createVopSeparator() {
1958
+ return VopSeparatorSchema.parse({ type: AST_TYPES.VOP_SEPARATOR });
1959
+ }
1960
+ const VopBlockItemSchema = z.union([VopRowSchema, VopCarryRowSchema, VopSeparatorSchema]);
1961
+ const VerticalOperationBlockSchema = z.object({
1962
+ type: z.literal(AST_TYPES.VERTICAL_OPERATION_BLOCK),
1963
+ op: z.enum(VOP_OPERATORS).optional(),
1964
+ items: z.array(VopBlockItemSchema).min(1)
1965
+ });
1966
+ function createVerticalOperationBlock(op, items) {
1967
+ return VerticalOperationBlockSchema.parse({
1968
+ type: AST_TYPES.VERTICAL_OPERATION_BLOCK,
1969
+ op,
1970
+ items
1971
+ });
1972
+ }
1973
+ function isVopCell(value) {
1974
+ return VopCellSchema.safeParse(value).success;
1975
+ }
1976
+ function isVopRow(value) {
1977
+ return VopRowSchema.safeParse(value).success;
1978
+ }
1979
+ function isVopCarryRow(value) {
1980
+ return VopCarryRowSchema.safeParse(value).success;
1981
+ }
1982
+ function isVopSeparator(value) {
1983
+ return VopSeparatorSchema.safeParse(value).success;
1984
+ }
1985
+ function isVerticalOperationBlock(value) {
1986
+ return VerticalOperationBlockSchema.safeParse(value).success;
1987
+ }
1627
1988
  const DisplayFractionInlineSchema = FractionInlineSchema.refine(
1628
1989
  (f) => f.numerator !== void 0 && f.denominator !== void 0,
1629
1990
  { message: "FRACTION inside MATCHER ITEM must be in display mode (both numerator and denominator required)" }
@@ -1910,7 +2271,7 @@ function isMixedValidation(value) {
1910
2271
  function isValidationNode(value) {
1911
2272
  return ValidationNodeSchema.safeParse(value).success;
1912
2273
  }
1913
- const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema]);
2274
+ const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema]);
1914
2275
  const ExerciseDocumentSchema = z.object({
1915
2276
  type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
1916
2277
  question: TextBlockSchema.optional(),
@@ -2014,6 +2375,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2014
2375
  if (ctx.numberLine) return this.visit(ctx.numberLine);
2015
2376
  if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
2016
2377
  if (ctx.matcher) return this.visit(ctx.matcher);
2378
+ if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
2017
2379
  if (ctx.line) return this.visit(ctx.line);
2018
2380
  throw new Error("Unknown content block");
2019
2381
  }
@@ -2085,7 +2447,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2085
2447
  const min = params.min !== void 0 ? Number(params.min) : void 0;
2086
2448
  const defaultValue2 = params.default !== void 0 ? Number(params.default) : void 0;
2087
2449
  const name = params.name;
2088
- return createNumberLineCounter(max, min, defaultValue2, name);
2450
+ const answer = params.answer !== void 0 ? Number(params.answer) : void 0;
2451
+ return createNumberLineCounter(max, min, defaultValue2, name, answer);
2089
2452
  }
2090
2453
  highlight(ctx) {
2091
2454
  const params = this.visit(ctx.parameterList);
@@ -2152,6 +2515,76 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2152
2515
  return ctx.matcherItem ? ctx.matcherItem.map((i) => this.visit(i)) : [];
2153
2516
  }
2154
2517
  // ═══════════════════════════════════════════════════════════════════════════
2518
+ // Vertical Operation Block
2519
+ // ═══════════════════════════════════════════════════════════════════════════
2520
+ verticalOperation(ctx) {
2521
+ const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
2522
+ const op = params.op !== void 0 ? normalizeVopOperator(String(params.op)) : void 0;
2523
+ const tagged = [];
2524
+ ctx.vopRow?.forEach((cstNode, i) => {
2525
+ tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
2526
+ });
2527
+ ctx.vopCarry?.forEach((cstNode, i) => {
2528
+ tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
2529
+ });
2530
+ ctx.vopSep?.forEach((cstNode, i) => {
2531
+ tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
2532
+ });
2533
+ tagged.sort((a, b) => a.offset - b.offset);
2534
+ const items = tagged.map((t) => t.node);
2535
+ return createVerticalOperationBlock(op, items);
2536
+ }
2537
+ vopRow(ctx) {
2538
+ const cells = ctx.vopCell ? ctx.vopCell.map((c) => this.visit(c)) : [];
2539
+ const rowParams = ctx.parameterList ? this.visit(ctx.parameterList) : {};
2540
+ const op = rowParams.op !== void 0 ? normalizeVopOperator(String(rowParams.op)) : void 0;
2541
+ return createVopRow(cells, op);
2542
+ }
2543
+ vopCarry(ctx) {
2544
+ const cells = ctx.vopCell ? ctx.vopCell.map((c) => this.visit(c)) : [];
2545
+ return createVopCarryRow(cells);
2546
+ }
2547
+ vopSep(_ctx) {
2548
+ return createVopSeparator();
2549
+ }
2550
+ vopCell(ctx) {
2551
+ if (ctx.NumberLiteral) {
2552
+ const token = ctx.NumberLiteral[0];
2553
+ return createVopDigitCell(token.image);
2554
+ }
2555
+ if (ctx.StringLiteral) {
2556
+ const token = ctx.StringLiteral[0];
2557
+ const value = token.image.slice(1, -1);
2558
+ return createVopLabelCell(value);
2559
+ }
2560
+ if (ctx.Underscore) {
2561
+ return createVopEmptyCell();
2562
+ }
2563
+ if (ctx.INPUT) {
2564
+ const params = this.visit(ctx.parameterList);
2565
+ const name = params.name;
2566
+ if (!name) {
2567
+ throw new Error("INPUT inside VERTICAL_OPERATION requires parameter: name");
2568
+ }
2569
+ const digits = params.digits !== void 0 ? Number(params.digits) : 1;
2570
+ const answer = params.answer;
2571
+ return createVopInputCell(name, digits, answer);
2572
+ }
2573
+ if (ctx.colorText) {
2574
+ const inline = this.visit(ctx.colorText);
2575
+ return createVopColorCell(inline.value, inline.color);
2576
+ }
2577
+ if (ctx.boldText) {
2578
+ const inline = this.visit(ctx.boldText);
2579
+ return createVopBoldCell(inline.value);
2580
+ }
2581
+ if (ctx.textHighlighter) {
2582
+ const inline = this.visit(ctx.textHighlighter);
2583
+ return createVopHighlightCell(inline.value);
2584
+ }
2585
+ throw new Error("Unknown vopCell");
2586
+ }
2587
+ // ═══════════════════════════════════════════════════════════════════════════
2155
2588
  // Line Block
2156
2589
  // ═══════════════════════════════════════════════════════════════════════════
2157
2590
  line(ctx) {
@@ -2247,6 +2680,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2247
2680
  if (ctx.inlineInput) {
2248
2681
  return this.visit(ctx.inlineInput);
2249
2682
  }
2683
+ if (ctx.inlineDragOption) {
2684
+ return this.visit(ctx.inlineDragOption);
2685
+ }
2250
2686
  throw new Error("Unknown inline element");
2251
2687
  }
2252
2688
  textHighlighter(ctx) {
@@ -2323,6 +2759,11 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2323
2759
  const digits = params.digits;
2324
2760
  return createNumericInput(name, digits, answer, placeholder);
2325
2761
  }
2762
+ inlineDragOption(ctx) {
2763
+ const valueToken = ctx.StringLiteral[0];
2764
+ const value = this.extractString(valueToken.image);
2765
+ return createInlineDragOption(value);
2766
+ }
2326
2767
  // ═══════════════════════════════════════════════════════════════════════════
2327
2768
  // Tools
2328
2769
  // ═══════════════════════════════════════════════════════════════════════════
@@ -2517,7 +2958,8 @@ const LayoutItemSchema = z.lazy(
2517
2958
  GroupItemSchema,
2518
2959
  OperatorItemSchema,
2519
2960
  TableItemSchema,
2520
- NumberLineItemSchema
2961
+ NumberLineItemSchema,
2962
+ VerticalOperationItemSchema
2521
2963
  ])
2522
2964
  );
2523
2965
  const GroupItemSchema = z.object({
@@ -2554,7 +2996,8 @@ const NumberLineCounterItemSchema = z.object({
2554
2996
  min: z.number().int().min(1),
2555
2997
  max: z.number().int().positive(),
2556
2998
  default: z.number().int().nonnegative().optional(),
2557
- name: z.string().optional()
2999
+ name: z.string().optional(),
3000
+ answer: z.number().int().positive().optional()
2558
3001
  });
2559
3002
  const NumberLineHighlightItemSchema = z.object({
2560
3003
  id: z.string(),
@@ -2591,6 +3034,80 @@ const MatcherRowItemSchema = z.object({
2591
3034
  type: z.literal(JSON_TYPES.MATCHER_ROW),
2592
3035
  items: z.array(MatcherItemContentSchema).min(1)
2593
3036
  });
3037
+ const VopOperatorJSONSchema = z.enum(["+", "-", "×", "÷", "="]);
3038
+ const VopDigitCellItemSchema = z.object({
3039
+ id: z.string(),
3040
+ type: z.literal(JSON_TYPES.VOP_CELL),
3041
+ kind: z.literal("digit"),
3042
+ value: z.string()
3043
+ });
3044
+ const VopInputCellItemSchema = z.object({
3045
+ id: z.string(),
3046
+ type: z.literal(JSON_TYPES.VOP_CELL),
3047
+ kind: z.literal("input"),
3048
+ widgetId: z.string()
3049
+ });
3050
+ const VopLabelCellItemSchema = z.object({
3051
+ id: z.string(),
3052
+ type: z.literal(JSON_TYPES.VOP_CELL),
3053
+ kind: z.literal("label"),
3054
+ value: z.string()
3055
+ });
3056
+ const VopEmptyCellItemSchema = z.object({
3057
+ id: z.string(),
3058
+ type: z.literal(JSON_TYPES.VOP_CELL),
3059
+ kind: z.literal("empty")
3060
+ });
3061
+ const VopColorCellItemSchema = z.object({
3062
+ id: z.string(),
3063
+ type: z.literal(JSON_TYPES.VOP_CELL),
3064
+ kind: z.literal("color"),
3065
+ value: z.string(),
3066
+ color: z.string()
3067
+ });
3068
+ const VopBoldCellItemSchema = z.object({
3069
+ id: z.string(),
3070
+ type: z.literal(JSON_TYPES.VOP_CELL),
3071
+ kind: z.literal("bold"),
3072
+ value: z.string()
3073
+ });
3074
+ const VopHighlightCellItemSchema = z.object({
3075
+ id: z.string(),
3076
+ type: z.literal(JSON_TYPES.VOP_CELL),
3077
+ kind: z.literal("highlight"),
3078
+ value: z.string()
3079
+ });
3080
+ const VopCellItemSchema = z.discriminatedUnion("kind", [
3081
+ VopDigitCellItemSchema,
3082
+ VopLabelCellItemSchema,
3083
+ VopColorCellItemSchema,
3084
+ VopBoldCellItemSchema,
3085
+ VopHighlightCellItemSchema,
3086
+ VopInputCellItemSchema,
3087
+ VopEmptyCellItemSchema
3088
+ ]);
3089
+ const VopRowItemSchema = z.object({
3090
+ id: z.string(),
3091
+ type: z.literal(JSON_TYPES.VOP_ROW),
3092
+ cells: z.array(VopCellItemSchema),
3093
+ op: VopOperatorJSONSchema.optional()
3094
+ });
3095
+ const VopCarryRowItemSchema = z.object({
3096
+ id: z.string(),
3097
+ type: z.literal(JSON_TYPES.VOP_CARRY_ROW),
3098
+ cells: z.array(VopCellItemSchema)
3099
+ });
3100
+ const VopSeparatorItemSchema = z.object({
3101
+ id: z.string(),
3102
+ type: z.literal(JSON_TYPES.VOP_SEPARATOR)
3103
+ });
3104
+ const VerticalOperationItemSchema = z.object({
3105
+ id: z.string(),
3106
+ type: z.literal(JSON_TYPES.VERTICAL_OPERATION),
3107
+ op: VopOperatorJSONSchema.optional(),
3108
+ cols: z.number().int().positive(),
3109
+ items: z.array(z.union([VopRowItemSchema, VopCarryRowItemSchema, VopSeparatorItemSchema]))
3110
+ });
2594
3111
  const StringInputJSONSchema = z.object({
2595
3112
  type: z.literal(JSON_TYPES.INLINE_INPUT),
2596
3113
  inputType: z.literal(INPUT_TYPES.STRING),
@@ -2611,6 +3128,10 @@ const DragOptionsJSONSchema = z.object({
2611
3128
  type: z.literal(JSON_TYPES.DRAG_OPTIONS),
2612
3129
  options: z.array(z.string())
2613
3130
  });
3131
+ const InlineDragOptionJSONSchema = z.object({
3132
+ type: z.literal(JSON_TYPES.INLINE_DRAG_OPTION),
3133
+ value: z.string().min(1)
3134
+ });
2614
3135
  const RadioOptionsJSONSchema = z.object({
2615
3136
  type: z.literal(JSON_TYPES.RADIO_OPTIONS),
2616
3137
  options: z.array(z.string())
@@ -2661,6 +3182,7 @@ const VisualFractionJSONSchema = z.object({
2661
3182
  const WidgetJSONSchema = z.union([
2662
3183
  InlineInputJSONSchema,
2663
3184
  DragOptionsJSONSchema,
3185
+ InlineDragOptionJSONSchema,
2664
3186
  RadioOptionsJSONSchema,
2665
3187
  NumericKeypadJSONSchema,
2666
3188
  FractionJSONSchema,
@@ -2753,12 +3275,18 @@ function isTableRowItem(item) {
2753
3275
  function isTableCellItem(item) {
2754
3276
  return item.type === JSON_TYPES.TABLE_CELL;
2755
3277
  }
3278
+ function isVerticalOperationItem(item) {
3279
+ return item.type === JSON_TYPES.VERTICAL_OPERATION;
3280
+ }
2756
3281
  function isInlineInputJSON(widget) {
2757
3282
  return widget.type === JSON_TYPES.INLINE_INPUT;
2758
3283
  }
2759
3284
  function isDragOptionsJSON(widget) {
2760
3285
  return widget.type === JSON_TYPES.DRAG_OPTIONS;
2761
3286
  }
3287
+ function isInlineDragOptionJSON(widget) {
3288
+ return widget.type === JSON_TYPES.INLINE_DRAG_OPTION;
3289
+ }
2762
3290
  function isRadioOptionsJSON(widget) {
2763
3291
  return widget.type === JSON_TYPES.RADIO_OPTIONS;
2764
3292
  }
@@ -2781,6 +3309,18 @@ class LayoutIdGenerator {
2781
3309
  }
2782
3310
  function toJSON(ast) {
2783
3311
  const idGenerator = new LayoutIdGenerator();
3312
+ const widgets = {};
3313
+ const answers = {};
3314
+ const allInlines = collectAllInlines(ast.lines);
3315
+ const inlineDragIds = /* @__PURE__ */ new Map();
3316
+ {
3317
+ let counter = 0;
3318
+ for (const inline of allInlines) {
3319
+ if (inline.type === AST_TYPES.INLINE_DRAG_OPTION) {
3320
+ inlineDragIds.set(inline, `${INLINE_DRAG_OPT_PREFIX}${counter++}`);
3321
+ }
3322
+ }
3323
+ }
2784
3324
  const layout = [];
2785
3325
  if (ast.question) {
2786
3326
  layout.push({
@@ -2791,9 +3331,13 @@ function toJSON(ast) {
2791
3331
  }
2792
3332
  for (const block of ast.lines) {
2793
3333
  if (block.type === AST_TYPES.TABLE_BLOCK) {
2794
- layout.push(transformTable(block, idGenerator));
3334
+ layout.push(transformTable(block, idGenerator, inlineDragIds));
2795
3335
  } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
2796
- layout.push(transformNumberLine(block, idGenerator));
3336
+ const nlItem = transformNumberLine(block, idGenerator, inlineDragIds);
3337
+ layout.push(nlItem);
3338
+ if (nlItem.counter?.answer !== void 0) {
3339
+ answers[nlItem.counter.id] = nlItem.counter.answer;
3340
+ }
2797
3341
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
2798
3342
  const matcherBlock = block;
2799
3343
  layout.push({
@@ -2816,6 +3360,8 @@ function toJSON(ast) {
2816
3360
  id: idGenerator.generate(JSON_TYPES.LINEBREAK),
2817
3361
  type: JSON_TYPES.LINEBREAK
2818
3362
  });
3363
+ } else if (block.type === AST_TYPES.VERTICAL_OPERATION_BLOCK) {
3364
+ layout.push(transformVerticalOperation(block, idGenerator));
2819
3365
  } else if (block.type === AST_TYPES.RADIO_OPTIONS) {
2820
3366
  const radioBlock = block;
2821
3367
  layout.push({
@@ -2828,13 +3374,10 @@ function toJSON(ast) {
2828
3374
  type: JSON_TYPES.LINEBREAK
2829
3375
  });
2830
3376
  } else {
2831
- const lineItems = transformLine(block, idGenerator);
3377
+ const lineItems = transformLine(block, idGenerator, inlineDragIds);
2832
3378
  layout.push(...lineItems);
2833
3379
  }
2834
3380
  }
2835
- const widgets = {};
2836
- const answers = {};
2837
- const allInlines = collectAllInlines(ast.lines);
2838
3381
  for (const inline of allInlines) {
2839
3382
  if (inline.type === AST_TYPES.INLINE_INPUT) {
2840
3383
  if (inline.inputType === INPUT_TYPES.STRING) {
@@ -2868,6 +3411,13 @@ function toJSON(ast) {
2868
3411
  answers[inline.name] = inline.answer;
2869
3412
  }
2870
3413
  }
3414
+ if (inline.type === AST_TYPES.INLINE_DRAG_OPTION) {
3415
+ const widgetId = inlineDragIds.get(inline);
3416
+ widgets[widgetId] = {
3417
+ type: JSON_TYPES.INLINE_DRAG_OPTION,
3418
+ value: inline.value
3419
+ };
3420
+ }
2871
3421
  if (inline.type === AST_TYPES.VISUAL_FRACTION) {
2872
3422
  const answerArr = inline.answer ?? [];
2873
3423
  widgets[inline.name] = {
@@ -2885,6 +3435,27 @@ function toJSON(ast) {
2885
3435
  }
2886
3436
  }
2887
3437
  }
3438
+ for (const block of ast.lines) {
3439
+ if (block.type !== AST_TYPES.VERTICAL_OPERATION_BLOCK) continue;
3440
+ const vop = block;
3441
+ for (const item of vop.items) {
3442
+ if (item.type !== AST_TYPES.VOP_ROW && item.type !== AST_TYPES.VOP_CARRY_ROW) continue;
3443
+ for (const cell of item.cells) {
3444
+ if (cell.kind !== "input") continue;
3445
+ if (widgets[cell.name]) {
3446
+ throw new Error(`VERTICAL_OPERATION: duplicate INPUT name "${cell.name}"`);
3447
+ }
3448
+ widgets[cell.name] = {
3449
+ type: JSON_TYPES.INLINE_INPUT,
3450
+ inputType: INPUT_TYPES.NUMERIC,
3451
+ digits: cell.digits
3452
+ };
3453
+ if (cell.answer !== void 0) {
3454
+ answers[cell.name] = cell.answer;
3455
+ }
3456
+ }
3457
+ }
3458
+ }
2888
3459
  for (const block of ast.lines) {
2889
3460
  if (block.type === AST_TYPES.RADIO_OPTIONS) {
2890
3461
  const radioBlock = block;
@@ -2895,11 +3466,11 @@ function toJSON(ast) {
2895
3466
  answers[radioBlock.name] = radioBlock.answer;
2896
3467
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
2897
3468
  const matcherBlock = block;
2898
- widgets[matcherBlock.name] = buildMatcherWidget(matcherBlock, idGenerator);
3469
+ widgets[matcherBlock.name] = buildMatcherWidget(matcherBlock, idGenerator, inlineDragIds);
2899
3470
  answers[matcherBlock.name] = matcherBlock.answers;
2900
3471
  } else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
2901
3472
  const pmBlock = block;
2902
- widgets[pmBlock.name] = buildPairMatcherWidget(pmBlock, idGenerator);
3473
+ widgets[pmBlock.name] = buildPairMatcherWidget(pmBlock, idGenerator, inlineDragIds);
2903
3474
  answers[pmBlock.name] = pmBlock.answers;
2904
3475
  }
2905
3476
  }
@@ -2958,9 +3529,9 @@ function collectAllInlines(blocks) {
2958
3529
  }
2959
3530
  return result;
2960
3531
  }
2961
- function transformNumberLine(numberLine, idGenerator) {
3532
+ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
2962
3533
  const positions = numberLine.positions.map((pos) => {
2963
- const content = pos.inlines.map((inline) => transformInline(inline, idGenerator));
3534
+ const content = pos.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
2964
3535
  return {
2965
3536
  id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_POSITION),
2966
3537
  type: JSON_TYPES.NUMBER_LINE_POSITION,
@@ -2977,7 +3548,8 @@ function transformNumberLine(numberLine, idGenerator) {
2977
3548
  min: c.min,
2978
3549
  max: c.max,
2979
3550
  ...c.default !== void 0 && { default: c.default },
2980
- ...c.name !== void 0 && { name: c.name }
3551
+ ...c.name !== void 0 && { name: c.name },
3552
+ ...c.answer !== void 0 && { answer: c.answer }
2981
3553
  };
2982
3554
  }
2983
3555
  let highlights;
@@ -3007,19 +3579,19 @@ function transformNumberLine(numberLine, idGenerator) {
3007
3579
  };
3008
3580
  return item;
3009
3581
  }
3010
- function mapMatcherItemList(items, idGenerator) {
3582
+ function mapMatcherItemList(items, idGenerator, inlineDragIds) {
3011
3583
  return items.map((item) => ({
3012
3584
  id: idGenerator.generate(JSON_TYPES.MATCHER_ITEM),
3013
3585
  type: JSON_TYPES.MATCHER_ITEM,
3014
3586
  itemId: item.itemId,
3015
- content: item.inlines.map((inline) => transformInline(inline, idGenerator))
3587
+ content: item.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
3016
3588
  }));
3017
3589
  }
3018
- function buildMatcherWidget(matcher, idGenerator) {
3590
+ function buildMatcherWidget(matcher, idGenerator, inlineDragIds) {
3019
3591
  const rows = matcher.rows.map((row) => ({
3020
3592
  id: idGenerator.generate(JSON_TYPES.MATCHER_ROW),
3021
3593
  type: JSON_TYPES.MATCHER_ROW,
3022
- items: mapMatcherItemList(row.items, idGenerator)
3594
+ items: mapMatcherItemList(row.items, idGenerator, inlineDragIds)
3023
3595
  }));
3024
3596
  const result = {
3025
3597
  type: JSON_TYPES.MATCHER,
@@ -3031,17 +3603,72 @@ function buildMatcherWidget(matcher, idGenerator) {
3031
3603
  }
3032
3604
  return result;
3033
3605
  }
3034
- function buildPairMatcherWidget(pm, idGenerator) {
3606
+ function buildPairMatcherWidget(pm, idGenerator, inlineDragIds) {
3035
3607
  return {
3036
3608
  type: JSON_TYPES.PAIR_MATCHER,
3037
- topItems: mapMatcherItemList(pm.topItems, idGenerator),
3038
- bottomItems: mapMatcherItemList(pm.bottomItems, idGenerator)
3609
+ topItems: mapMatcherItemList(pm.topItems, idGenerator, inlineDragIds),
3610
+ bottomItems: mapMatcherItemList(pm.bottomItems, idGenerator, inlineDragIds)
3039
3611
  };
3040
3612
  }
3041
- function transformTable(table, idGenerator) {
3613
+ function transformVerticalOperation(vop, idGenerator) {
3614
+ const anyRowHasOp = vop.items.some(
3615
+ (item) => item.type === AST_TYPES.VOP_ROW && item.op !== void 0
3616
+ );
3617
+ if (!anyRowHasOp && vop.op === void 0) {
3618
+ throw new Error("VERTICAL_OPERATION: at least one op is required (set op on the block or on individual ROWs)");
3619
+ }
3620
+ let cols = 1;
3621
+ for (const item of vop.items) {
3622
+ if (item.type === AST_TYPES.VOP_ROW || item.type === AST_TYPES.VOP_CARRY_ROW) {
3623
+ cols = Math.max(cols, item.cells.length);
3624
+ }
3625
+ }
3626
+ function buildCells(row) {
3627
+ return row.cells.map((cell) => {
3628
+ const id = idGenerator.generate(JSON_TYPES.VOP_CELL);
3629
+ if (cell.kind === "digit") return { id, type: JSON_TYPES.VOP_CELL, kind: "digit", value: cell.value };
3630
+ if (cell.kind === "label") return { id, type: JSON_TYPES.VOP_CELL, kind: "label", value: cell.value };
3631
+ if (cell.kind === "color") return { id, type: JSON_TYPES.VOP_CELL, kind: "color", value: cell.value, color: cell.color };
3632
+ if (cell.kind === "bold") return { id, type: JSON_TYPES.VOP_CELL, kind: "bold", value: cell.value };
3633
+ if (cell.kind === "highlight") return { id, type: JSON_TYPES.VOP_CELL, kind: "highlight", value: cell.value };
3634
+ if (cell.kind === "input") return { id, type: JSON_TYPES.VOP_CELL, kind: "input", widgetId: cell.name };
3635
+ return { id, type: JSON_TYPES.VOP_CELL, kind: "empty" };
3636
+ });
3637
+ }
3638
+ const items = vop.items.map((item) => {
3639
+ if (item.type === AST_TYPES.VOP_SEPARATOR) {
3640
+ return {
3641
+ id: idGenerator.generate(JSON_TYPES.VOP_SEPARATOR),
3642
+ type: JSON_TYPES.VOP_SEPARATOR
3643
+ };
3644
+ }
3645
+ if (item.type === AST_TYPES.VOP_CARRY_ROW) {
3646
+ return {
3647
+ id: idGenerator.generate(JSON_TYPES.VOP_CARRY_ROW),
3648
+ type: JSON_TYPES.VOP_CARRY_ROW,
3649
+ cells: buildCells(item)
3650
+ };
3651
+ }
3652
+ const row = item;
3653
+ return {
3654
+ id: idGenerator.generate(JSON_TYPES.VOP_ROW),
3655
+ type: JSON_TYPES.VOP_ROW,
3656
+ cells: buildCells(row),
3657
+ ...row.op !== void 0 && { op: row.op }
3658
+ };
3659
+ });
3660
+ return {
3661
+ id: idGenerator.generate(JSON_TYPES.VERTICAL_OPERATION),
3662
+ type: JSON_TYPES.VERTICAL_OPERATION,
3663
+ op: vop.op,
3664
+ cols,
3665
+ items
3666
+ };
3667
+ }
3668
+ function transformTable(table, idGenerator, inlineDragIds) {
3042
3669
  const rows = table.rows.map((row) => {
3043
3670
  const cells = row.cells.map((cell) => {
3044
- const items = cell.inlines.map((inline) => transformInline(inline, idGenerator));
3671
+ const items = cell.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
3045
3672
  const cellItem = {
3046
3673
  id: idGenerator.generate(JSON_TYPES.TABLE_CELL),
3047
3674
  type: JSON_TYPES.TABLE_CELL,
@@ -3065,10 +3692,10 @@ function transformTable(table, idGenerator) {
3065
3692
  rows
3066
3693
  };
3067
3694
  }
3068
- function transformLine(line, idGenerator) {
3695
+ function transformLine(line, idGenerator, inlineDragIds) {
3069
3696
  const items = [];
3070
3697
  for (const inline of line.inlines) {
3071
- items.push(transformInline(inline, idGenerator));
3698
+ items.push(transformInline(inline, idGenerator, inlineDragIds));
3072
3699
  }
3073
3700
  items.push({
3074
3701
  id: idGenerator.generate(JSON_TYPES.LINEBREAK),
@@ -3076,7 +3703,7 @@ function transformLine(line, idGenerator) {
3076
3703
  });
3077
3704
  return items;
3078
3705
  }
3079
- function transformInline(inline, idGenerator) {
3706
+ function transformInline(inline, idGenerator, inlineDragIds) {
3080
3707
  switch (inline.type) {
3081
3708
  case AST_TYPES.FREE_TEXT:
3082
3709
  const trimmed = inline.value.trim();
@@ -3148,6 +3775,12 @@ function transformInline(inline, idGenerator) {
3148
3775
  type: JSON_TYPES.WIDGET_REF,
3149
3776
  widgetId: inline.name
3150
3777
  };
3778
+ case AST_TYPES.INLINE_DRAG_OPTION:
3779
+ return {
3780
+ id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
3781
+ type: JSON_TYPES.WIDGET_REF,
3782
+ widgetId: inlineDragIds.get(inline)
3783
+ };
3151
3784
  default:
3152
3785
  throw new Error(`Unknown inline type: ${inline.type}`);
3153
3786
  }
@@ -13312,7 +13945,7 @@ const InlineInput = ({
13312
13945
  id: widgetId,
13313
13946
  accepts: ["drag-option"],
13314
13947
  onDrop: handleDrop,
13315
- className: "inline-input-droppable",
13948
+ className: "inline-widget inline-input-droppable",
13316
13949
  children: isDropped && value !== "" ? (
13317
13950
  // Show as chip only when value came from drag-drop
13318
13951
  /* @__PURE__ */ jsx(
@@ -13344,6 +13977,32 @@ const InlineInput = ({
13344
13977
  }
13345
13978
  );
13346
13979
  };
13980
+ const SingleDragOption = ({
13981
+ optionId,
13982
+ value,
13983
+ onOptionClick,
13984
+ className = ""
13985
+ }) => {
13986
+ const { usedOptionIds } = useExerciseDnd();
13987
+ const isUsed = usedOptionIds.has(optionId);
13988
+ return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
13989
+ Draggable,
13990
+ {
13991
+ id: optionId,
13992
+ type: "drag-option",
13993
+ data: { value, label: value },
13994
+ className: "drag-option-wrapper",
13995
+ children: /* @__PURE__ */ jsx(
13996
+ "button",
13997
+ {
13998
+ className: "drag-option",
13999
+ onClick: () => onOptionClick?.(value),
14000
+ children: value
14001
+ }
14002
+ )
14003
+ }
14004
+ ) });
14005
+ };
13347
14006
  const PillBackground = ({
13348
14007
  width = "100%",
13349
14008
  minHeight = "72px",
@@ -13416,36 +14075,32 @@ const DragOptions = ({
13416
14075
  options,
13417
14076
  onOptionClick
13418
14077
  }) => {
13419
- const { usedOptionIds } = useExerciseDnd();
13420
14078
  return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
13421
14079
  const optionId = `drag-option-${idx}`;
13422
14080
  const optionValue = String(option);
13423
- const isUsed = usedOptionIds.has(optionId);
13424
- return /* @__PURE__ */ jsx("div", { className: "drag-option-slot", children: isUsed ? (
13425
- // Empty slot - this specific option is in an input
13426
- /* @__PURE__ */ jsx("div", { className: "drag-option-empty" })
13427
- ) : (
13428
- // Draggable option
13429
- /* @__PURE__ */ jsx(
13430
- Draggable,
13431
- {
13432
- id: optionId,
13433
- type: "drag-option",
13434
- data: { value: optionValue, label: optionValue },
13435
- className: "drag-option-wrapper",
13436
- children: /* @__PURE__ */ jsx(
13437
- "button",
13438
- {
13439
- className: "drag-option",
13440
- onClick: () => onOptionClick?.(optionValue),
13441
- children: optionValue
13442
- }
13443
- )
13444
- }
13445
- )
13446
- ) }, optionId);
14081
+ return /* @__PURE__ */ jsx(
14082
+ SingleDragOption,
14083
+ {
14084
+ optionId,
14085
+ value: optionValue,
14086
+ onOptionClick
14087
+ },
14088
+ optionId
14089
+ );
13447
14090
  }) }) });
13448
14091
  };
14092
+ const InlineDragOption = ({
14093
+ widgetId,
14094
+ widget
14095
+ }) => {
14096
+ return /* @__PURE__ */ jsx("div", { className: "inline-widget", children: /* @__PURE__ */ jsx(
14097
+ SingleDragOption,
14098
+ {
14099
+ optionId: widgetId,
14100
+ value: widget.value
14101
+ }
14102
+ ) });
14103
+ };
13449
14104
  const NumericKeypad = ({
13450
14105
  value,
13451
14106
  includeOperators = false,
@@ -13540,7 +14195,7 @@ const FractionInput = ({
13540
14195
  }
13541
14196
  );
13542
14197
  };
13543
- return /* @__PURE__ */ jsxs("span", { className: "fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
14198
+ return /* @__PURE__ */ jsxs("span", { className: "inline-widget fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
13544
14199
  /* @__PURE__ */ jsx("span", { className: "fraction-numerator", children: renderSlot(numKey, numerator, numValue, "numerator", numValidationStatus) }),
13545
14200
  /* @__PURE__ */ jsx("span", { className: "fraction-bar", "aria-hidden": "true" }),
13546
14201
  /* @__PURE__ */ jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator", denValidationStatus) })
@@ -13972,7 +14627,7 @@ const VisualFraction = ({
13972
14627
  return /* @__PURE__ */ jsxs(
13973
14628
  "div",
13974
14629
  {
13975
- className: `visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
14630
+ className: `inline-widget visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
13976
14631
  style: rootStyle,
13977
14632
  role: "group",
13978
14633
  "aria-label": `${divisions} part ${shape} fraction`,
@@ -14593,6 +15248,9 @@ function isInlineInputWidget(widget) {
14593
15248
  function isDragOptionsWidget(widget) {
14594
15249
  return widget.type === "drag-options";
14595
15250
  }
15251
+ function isInlineDragOptionWidget(widget) {
15252
+ return widget.type === "inline-drag-option";
15253
+ }
14596
15254
  function isRadioOptionsWidget(widget) {
14597
15255
  return widget.type === "radio-options";
14598
15256
  }
@@ -14655,6 +15313,15 @@ const WidgetRenderer = ({
14655
15313
  }
14656
15314
  );
14657
15315
  }
15316
+ if (isInlineDragOptionWidget(widget)) {
15317
+ return /* @__PURE__ */ jsx(
15318
+ InlineDragOption,
15319
+ {
15320
+ widgetId,
15321
+ widget
15322
+ }
15323
+ );
15324
+ }
14658
15325
  if (isRadioOptionsWidget(widget)) {
14659
15326
  return /* @__PURE__ */ jsx(
14660
15327
  RadioOptions,
@@ -15371,6 +16038,13 @@ const NumberLineRenderer = ({
15371
16038
  useEffect(() => {
15372
16039
  setCounterCount(defaultCounterCount);
15373
16040
  }, [counter]);
16041
+ const onChangeRef = useRef(onChange);
16042
+ onChangeRef.current = onChange;
16043
+ useEffect(() => {
16044
+ if (counter !== void 0) {
16045
+ onChangeRef.current(counter.id, String(counterCount));
16046
+ }
16047
+ }, [counterCount, counter?.id]);
15374
16048
  const effectiveStep = useMemo(() => {
15375
16049
  if (counter !== void 0 && counterCount > 0) {
15376
16050
  return cleanFloat((max - min) / counterCount);
@@ -15404,10 +16078,17 @@ const NumberLineRenderer = ({
15404
16078
  [visiblePositions]
15405
16079
  );
15406
16080
  const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
16081
+ const nlValidationStatus = useMemo(() => {
16082
+ if (!validationDetails || counter === void 0) return null;
16083
+ if (counter.answer === void 0) return null;
16084
+ const counterValidation = validationDetails[counter.id];
16085
+ if (counterValidation === void 0) return null;
16086
+ return counterValidation.isCorrect ? "correct" : "incorrect";
16087
+ }, [validationDetails, counter]);
15407
16088
  return /* @__PURE__ */ jsxs(
15408
16089
  "div",
15409
16090
  {
15410
- className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
16091
+ className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
15411
16092
  style: {
15412
16093
  [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
15413
16094
  ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
@@ -15674,6 +16355,126 @@ const LayoutRenderer = ({
15674
16355
  },
15675
16356
  resetKey
15676
16357
  );
16358
+ case JSON_TYPES.VERTICAL_OPERATION: {
16359
+ const vop = item;
16360
+ const anyRowHasOp = vop.items.some(
16361
+ (x) => x.type === JSON_TYPES.VOP_ROW && x.op !== void 0
16362
+ );
16363
+ let operatorRowIdx = -1;
16364
+ if (!anyRowHasOp && vop.op !== void 0) {
16365
+ const firstSepIdx = vop.items.findIndex((x) => x.type === JSON_TYPES.VOP_SEPARATOR);
16366
+ for (let i = (firstSepIdx === -1 ? vop.items.length : firstSepIdx) - 1; i >= 0; i--) {
16367
+ const curr = vop.items[i];
16368
+ if (curr && curr.type === JSON_TYPES.VOP_ROW) {
16369
+ operatorRowIdx = i;
16370
+ break;
16371
+ }
16372
+ }
16373
+ }
16374
+ const totalCols = vop.cols + 1;
16375
+ return /* @__PURE__ */ jsx(
16376
+ "div",
16377
+ {
16378
+ className: "vop",
16379
+ style: { "--vop-cols": totalCols },
16380
+ dir: "ltr",
16381
+ children: vop.items.map((child, idx) => {
16382
+ if (child.type === JSON_TYPES.VOP_SEPARATOR) {
16383
+ const sep = child;
16384
+ return /* @__PURE__ */ jsx("div", { className: "vop-sep" }, sep.id);
16385
+ }
16386
+ if (child.type === JSON_TYPES.VOP_CARRY_ROW) {
16387
+ const carry = child;
16388
+ const carryLeadingEmpty = Math.max(0, vop.cols - carry.cells.length);
16389
+ return /* @__PURE__ */ jsxs("div", { className: "vop-row vop-carry-row", children: [
16390
+ /* @__PURE__ */ jsx("span", { className: "vop-op-cell" }),
16391
+ Array.from({ length: carryLeadingEmpty }).map((_, i) => /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-empty" }, `cpad-${i}`)),
16392
+ carry.cells.map((cell) => {
16393
+ if (cell.kind === "digit") {
16394
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-digit", children: cell.value }, cell.id);
16395
+ }
16396
+ if (cell.kind === "label") {
16397
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-label", children: cell.value }, cell.id);
16398
+ }
16399
+ if (cell.kind === "color") {
16400
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-color", style: { color: cell.color }, children: cell.value }, cell.id);
16401
+ }
16402
+ if (cell.kind === "bold") {
16403
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-bold", children: cell.value }, cell.id);
16404
+ }
16405
+ if (cell.kind === "highlight") {
16406
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-highlight", children: /* @__PURE__ */ jsx("mark", { children: cell.value }) }, cell.id);
16407
+ }
16408
+ if (cell.kind === "empty") {
16409
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-empty" }, cell.id);
16410
+ }
16411
+ const widget = widgets[cell.widgetId];
16412
+ if (!widget) return /* @__PURE__ */ jsx("span", { className: "vop-cell" }, cell.id);
16413
+ const isCorrect = validationDetails?.[cell.widgetId]?.isCorrect;
16414
+ const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
16415
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-input", children: /* @__PURE__ */ jsx(
16416
+ WidgetRenderer,
16417
+ {
16418
+ widget,
16419
+ widgetId: cell.widgetId,
16420
+ value: values[cell.widgetId] || "",
16421
+ isDropped: droppedInputs.has(cell.widgetId),
16422
+ validationStatus,
16423
+ onChange,
16424
+ onInputFocus
16425
+ }
16426
+ ) }, cell.id);
16427
+ })
16428
+ ] }, carry.id);
16429
+ }
16430
+ const row = child;
16431
+ const showOperator = row.op !== void 0 || idx === operatorRowIdx;
16432
+ const displayOp = row.op ?? vop.op;
16433
+ const leadingEmpty = vop.cols - row.cells.length;
16434
+ return /* @__PURE__ */ jsxs("div", { className: "vop-row", children: [
16435
+ /* @__PURE__ */ jsx("span", { className: "vop-op-cell", children: showOperator ? /* @__PURE__ */ jsx("span", { className: "operator vop-op", children: displayOp }) : null }),
16436
+ Array.from({ length: leadingEmpty }).map((_, i) => /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-empty" }, `pad-${i}`)),
16437
+ row.cells.map((cell) => {
16438
+ if (cell.kind === "digit") {
16439
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-digit", children: cell.value }, cell.id);
16440
+ }
16441
+ if (cell.kind === "label") {
16442
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-label", children: cell.value }, cell.id);
16443
+ }
16444
+ if (cell.kind === "color") {
16445
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-color", style: { color: cell.color }, children: cell.value }, cell.id);
16446
+ }
16447
+ if (cell.kind === "bold") {
16448
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-bold", children: cell.value }, cell.id);
16449
+ }
16450
+ if (cell.kind === "highlight") {
16451
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-highlight", children: /* @__PURE__ */ jsx("mark", { children: cell.value }) }, cell.id);
16452
+ }
16453
+ if (cell.kind === "empty") {
16454
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-empty" }, cell.id);
16455
+ }
16456
+ const widget = widgets[cell.widgetId];
16457
+ if (!widget) return /* @__PURE__ */ jsx("span", { className: "vop-cell" }, cell.id);
16458
+ const isCorrect = validationDetails?.[cell.widgetId]?.isCorrect;
16459
+ const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
16460
+ return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-input", children: /* @__PURE__ */ jsx(
16461
+ WidgetRenderer,
16462
+ {
16463
+ widget,
16464
+ widgetId: cell.widgetId,
16465
+ value: values[cell.widgetId] || "",
16466
+ isDropped: droppedInputs.has(cell.widgetId),
16467
+ validationStatus,
16468
+ onChange,
16469
+ onInputFocus
16470
+ }
16471
+ ) }, cell.id);
16472
+ })
16473
+ ] }, row.id);
16474
+ })
16475
+ }
16476
+ );
16477
+ }
15677
16478
  default:
15678
16479
  return null;
15679
16480
  }
@@ -15767,6 +16568,13 @@ const ExerciseRenderer = forwardRef(({
15767
16568
  onSubmit?.(values);
15768
16569
  };
15769
16570
  const lineGroups = useMemo(() => {
16571
+ const isRTLInlineWidget = (item) => {
16572
+ if (item.type === JSON_TYPES.WIDGET_REF) {
16573
+ const widget = json.widgets[item.widgetId];
16574
+ return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
16575
+ }
16576
+ return RTL_INLINE_WIDGET_TYPES.has(item.type);
16577
+ };
15770
16578
  const groups = [];
15771
16579
  let currentGroup = [];
15772
16580
  for (const item of json.layout) {
@@ -15774,6 +16582,7 @@ const ExerciseRenderer = forwardRef(({
15774
16582
  groups.push({
15775
16583
  items: [item],
15776
16584
  hasArabic: itemContainsArabic(item),
16585
+ hasRTLInlineWidget: false,
15777
16586
  subGroups: []
15778
16587
  // Question text doesn't need sub-grouping
15779
16588
  });
@@ -15782,13 +16591,15 @@ const ExerciseRenderer = forwardRef(({
15782
16591
  if (item.type === JSON_TYPES.TABLE) {
15783
16592
  if (currentGroup.length > 0) {
15784
16593
  const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
16594
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
15785
16595
  const subGroups = groupLineItems(currentGroup);
15786
- groups.push({ items: currentGroup, hasArabic, subGroups });
16596
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
15787
16597
  currentGroup = [];
15788
16598
  }
15789
16599
  groups.push({
15790
16600
  items: [item],
15791
16601
  hasArabic: false,
16602
+ hasRTLInlineWidget: false,
15792
16603
  subGroups: []
15793
16604
  });
15794
16605
  continue;
@@ -15796,13 +16607,31 @@ const ExerciseRenderer = forwardRef(({
15796
16607
  if (item.type === JSON_TYPES.NUMBER_LINE) {
15797
16608
  if (currentGroup.length > 0) {
15798
16609
  const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
16610
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
16611
+ const subGroups = groupLineItems(currentGroup);
16612
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
16613
+ currentGroup = [];
16614
+ }
16615
+ groups.push({
16616
+ items: [item],
16617
+ hasArabic: false,
16618
+ hasRTLInlineWidget: false,
16619
+ subGroups: []
16620
+ });
16621
+ continue;
16622
+ }
16623
+ if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
16624
+ if (currentGroup.length > 0) {
16625
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
16626
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
15799
16627
  const subGroups = groupLineItems(currentGroup);
15800
- groups.push({ items: currentGroup, hasArabic, subGroups });
16628
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
15801
16629
  currentGroup = [];
15802
16630
  }
15803
16631
  groups.push({
15804
16632
  items: [item],
15805
16633
  hasArabic: false,
16634
+ hasRTLInlineWidget: false,
15806
16635
  subGroups: []
15807
16636
  });
15808
16637
  continue;
@@ -15810,8 +16639,9 @@ const ExerciseRenderer = forwardRef(({
15810
16639
  if (item.type === JSON_TYPES.LINEBREAK) {
15811
16640
  if (currentGroup.length > 0) {
15812
16641
  const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
16642
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
15813
16643
  const subGroups = groupLineItems(currentGroup);
15814
- groups.push({ items: currentGroup, hasArabic, subGroups });
16644
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
15815
16645
  currentGroup = [];
15816
16646
  }
15817
16647
  } else {
@@ -15820,11 +16650,12 @@ const ExerciseRenderer = forwardRef(({
15820
16650
  }
15821
16651
  if (currentGroup.length > 0) {
15822
16652
  const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
16653
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
15823
16654
  const subGroups = groupLineItems(currentGroup);
15824
- groups.push({ items: currentGroup, hasArabic, subGroups });
16655
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
15825
16656
  }
15826
16657
  return groups;
15827
- }, [json.layout]);
16658
+ }, [json.layout, json.widgets]);
15828
16659
  const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
15829
16660
  const dragOptionsWidget = json.widgets["_drag_options"];
15830
16661
  const rulerWidget = json.widgets["_ruler"];
@@ -15842,7 +16673,8 @@ const ExerciseRenderer = forwardRef(({
15842
16673
  /* @__PURE__ */ jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsx(
15843
16674
  "div",
15844
16675
  {
15845
- className: `exercise-line ${group.hasArabic ? "exercise-line-arabic" : ""}`,
16676
+ className: "exercise-line",
16677
+ dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
15846
16678
  children: group.subGroups.length > 0 ? (
15847
16679
  // Render sub-groups for regular lines
15848
16680
  group.subGroups.map((subGroup, subIdx) => subGroup.isTextGroup ? (
@@ -15987,6 +16819,7 @@ export {
15987
16819
  HighlightedTextSchema,
15988
16820
  ImageInlineSchema,
15989
16821
  ImageItemSchema,
16822
+ InlineDragOptionSchema,
15990
16823
  InlineInputJSONSchema,
15991
16824
  InlineInputWidgetSchema,
15992
16825
  InlineNodeSchema,
@@ -16028,10 +16861,31 @@ export {
16028
16861
  VISUAL_FRACTION_MAX_DIVISIONS,
16029
16862
  VISUAL_FRACTION_SHAPE,
16030
16863
  VISUAL_FRACTION_SHAPES,
16864
+ VOP_OPERATORS,
16031
16865
  ValidationError,
16032
16866
  ValidationJSONSchema,
16033
16867
  ValidationNodeSchema,
16868
+ VerticalOperationBlockSchema,
16869
+ VerticalOperationItemSchema,
16034
16870
  VisualFractionInlineSchema,
16871
+ VopBoldCellItemSchema,
16872
+ VopBoldCellSchema,
16873
+ VopCarryRowItemSchema,
16874
+ VopCarryRowSchema,
16875
+ VopCellItemSchema,
16876
+ VopCellSchema,
16877
+ VopColorCellItemSchema,
16878
+ VopColorCellSchema,
16879
+ VopDigitCellSchema,
16880
+ VopEmptyCellSchema,
16881
+ VopHighlightCellItemSchema,
16882
+ VopHighlightCellSchema,
16883
+ VopInputCellSchema,
16884
+ VopLabelCellSchema,
16885
+ VopRowItemSchema,
16886
+ VopRowSchema,
16887
+ VopSeparatorItemSchema,
16888
+ VopSeparatorSchema,
16035
16889
  WidgetJSONSchema,
16036
16890
  WidgetRefSchema,
16037
16891
  WidgetRenderer,
@@ -16046,6 +16900,7 @@ export {
16046
16900
  createFreeText,
16047
16901
  createHighlightedText,
16048
16902
  createImage,
16903
+ createInlineDragOption,
16049
16904
  createLineBlock,
16050
16905
  createMatcherBlock,
16051
16906
  createMatcherItem,
@@ -16058,7 +16913,18 @@ export {
16058
16913
  createSpace,
16059
16914
  createStringInput,
16060
16915
  createTextBlock,
16916
+ createVerticalOperationBlock,
16061
16917
  createVisualFraction,
16918
+ createVopBoldCell,
16919
+ createVopCarryRow,
16920
+ createVopColorCell,
16921
+ createVopDigitCell,
16922
+ createVopEmptyCell,
16923
+ createVopHighlightCell,
16924
+ createVopInputCell,
16925
+ createVopLabelCell,
16926
+ createVopRow,
16927
+ createVopSeparator,
16062
16928
  evaluate,
16063
16929
  extractVariables as extractExpressionVariables,
16064
16930
  findValidCombination,
@@ -16078,6 +16944,8 @@ export {
16078
16944
  isHighlightedText,
16079
16945
  isImage,
16080
16946
  isImageItem,
16947
+ isInlineDragOption,
16948
+ isInlineDragOptionJSON,
16081
16949
  isInlineInput,
16082
16950
  isInlineInputJSON,
16083
16951
  isInlineNode,
@@ -16106,8 +16974,15 @@ export {
16106
16974
  isToolNode,
16107
16975
  isValidExpression,
16108
16976
  isValidationNode,
16977
+ isVerticalOperationBlock,
16978
+ isVerticalOperationItem,
16109
16979
  isVisualFractionInline,
16980
+ isVopCarryRow,
16981
+ isVopCell,
16982
+ isVopRow,
16983
+ isVopSeparator,
16110
16984
  isWidgetRef,
16985
+ normalizeVopOperator,
16111
16986
  parse,
16112
16987
  parseFractionKey,
16113
16988
  parseSafe,