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