@mathscan/math-exercise-engine 1.2.6 → 1.2.8

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
@@ -1886,6 +1886,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1886
1886
  inputType: z.literal(INPUT_TYPES.STRING),
1887
1887
  length: z.number().int().positive(),
1888
1888
  placeholder: z.string().max(32).optional(),
1889
+ disabled: z.boolean().optional(),
1889
1890
  answer: z.union([z.string(), z.number()]).optional()
1890
1891
  }),
1891
1892
  z.object({
@@ -1894,6 +1895,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1894
1895
  inputType: z.literal(INPUT_TYPES.NUMERIC),
1895
1896
  digits: z.number().int().positive(),
1896
1897
  placeholder: z.string().max(32).optional(),
1898
+ disabled: z.boolean().optional(),
1897
1899
  answer: z.union([z.string(), z.number()]).optional()
1898
1900
  })
1899
1901
  ]);
@@ -2049,7 +2051,7 @@ function createBoldText(value, highlight) {
2049
2051
  highlight
2050
2052
  });
2051
2053
  }
2052
- function createStringInput(name, length, answer, placeholder) {
2054
+ function createStringInput(name, length, answer, placeholder, disabled) {
2053
2055
  return parseOrThrow(
2054
2056
  InlineInputWidgetSchema,
2055
2057
  {
@@ -2058,12 +2060,14 @@ function createStringInput(name, length, answer, placeholder) {
2058
2060
  inputType: INPUT_TYPES.STRING,
2059
2061
  length,
2060
2062
  placeholder,
2061
- answer
2063
+ disabled,
2064
+ // A disabled input is never graded, so its answer is dropped.
2065
+ answer: disabled ? void 0 : answer
2062
2066
  },
2063
2067
  "STRING_INPUT"
2064
2068
  );
2065
2069
  }
2066
- function createNumericInput(name, digits, answer, placeholder) {
2070
+ function createNumericInput(name, digits, answer, placeholder, disabled) {
2067
2071
  return parseOrThrow(
2068
2072
  InlineInputWidgetSchema,
2069
2073
  {
@@ -2072,7 +2076,9 @@ function createNumericInput(name, digits, answer, placeholder) {
2072
2076
  inputType: INPUT_TYPES.NUMERIC,
2073
2077
  digits,
2074
2078
  placeholder,
2075
- answer
2079
+ disabled,
2080
+ // A disabled input is never graded, so its answer is dropped.
2081
+ answer: disabled ? void 0 : answer
2076
2082
  },
2077
2083
  "NUMERIC_INPUT"
2078
2084
  );
@@ -3685,6 +3691,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
3685
3691
  if (!name) {
3686
3692
  throw new Error("INPUT inside VERTICAL_OPERATION requires parameter: name");
3687
3693
  }
3694
+ if (params.disabled !== void 0) {
3695
+ throw new Error("INPUT inside VERTICAL_OPERATION does not support parameter: disabled");
3696
+ }
3688
3697
  const digits = params.digits !== void 0 ? Number(params.digits) : 1;
3689
3698
  const answer = params.answer;
3690
3699
  return createVopInputCell(name, digits, answer);
@@ -3930,12 +3939,20 @@ class NaturalDSLVisitor extends BaseCstVisitor {
3930
3939
  const name = params.name;
3931
3940
  const answer = params.answer;
3932
3941
  const placeholder = params.placeholder;
3942
+ let disabled;
3943
+ if (params.disabled === true || params.disabled === "true") disabled = true;
3944
+ else if (params.disabled === false || params.disabled === "false") disabled = false;
3945
+ else if (params.disabled !== void 0) {
3946
+ throw new Error(
3947
+ `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": disabled must be true or false, got ${JSON.stringify(params.disabled)}`
3948
+ );
3949
+ }
3933
3950
  if (ctx.stringInput) {
3934
3951
  const length = params.length;
3935
- return createStringInput(name, length, answer, placeholder);
3952
+ return createStringInput(name, length, answer, placeholder, disabled);
3936
3953
  }
3937
3954
  const digits = params.digits;
3938
- return createNumericInput(name, digits, answer, placeholder);
3955
+ return createNumericInput(name, digits, answer, placeholder, disabled);
3939
3956
  }
3940
3957
  inlineDragOption(ctx) {
3941
3958
  const valueToken = ctx.StringLiteral[0];
@@ -4531,13 +4548,17 @@ const StringInputJSONSchema = z.object({
4531
4548
  type: z.literal(JSON_TYPES.INLINE_INPUT),
4532
4549
  inputType: z.literal(INPUT_TYPES.STRING),
4533
4550
  length: z.number().int().positive(),
4534
- placeholder: z.string().max(32).optional()
4551
+ placeholder: z.string().max(32).optional(),
4552
+ /** Gray, non-interactive, never graded (any answer for it is ignored). */
4553
+ disabled: z.boolean().optional()
4535
4554
  });
4536
4555
  const NumericInputJSONSchema = z.object({
4537
4556
  type: z.literal(JSON_TYPES.INLINE_INPUT),
4538
4557
  inputType: z.literal(INPUT_TYPES.NUMERIC),
4539
4558
  digits: z.number().int().positive(),
4540
- placeholder: z.string().max(32).optional()
4559
+ placeholder: z.string().max(32).optional(),
4560
+ /** Gray, non-interactive, never graded (any answer for it is ignored). */
4561
+ disabled: z.boolean().optional()
4541
4562
  });
4542
4563
  const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
4543
4564
  StringInputJSONSchema,
@@ -4765,6 +4786,9 @@ function isChartItem(item) {
4765
4786
  function isInlineInputJSON(widget) {
4766
4787
  return widget.type === JSON_TYPES.INLINE_INPUT;
4767
4788
  }
4789
+ function isDisabledInputJSON(widget) {
4790
+ return isInlineInputJSON(widget) && widget.disabled === true;
4791
+ }
4768
4792
  function isDragOptionsJSON(widget) {
4769
4793
  return widget.type === JSON_TYPES.DRAG_OPTIONS;
4770
4794
  }
@@ -10411,14 +10435,16 @@ function toJSON(ast) {
10411
10435
  type: JSON_TYPES.INLINE_INPUT,
10412
10436
  inputType: INPUT_TYPES.STRING,
10413
10437
  length: inline.length,
10414
- placeholder: inline.placeholder
10438
+ placeholder: inline.placeholder,
10439
+ ...inline.disabled && { disabled: true }
10415
10440
  };
10416
10441
  } else {
10417
10442
  widgets[inline.name] = {
10418
10443
  type: JSON_TYPES.INLINE_INPUT,
10419
10444
  inputType: INPUT_TYPES.NUMERIC,
10420
10445
  digits: inline.digits,
10421
- placeholder: inline.placeholder
10446
+ placeholder: inline.placeholder,
10447
+ ...inline.disabled && { disabled: true }
10422
10448
  };
10423
10449
  }
10424
10450
  if (inline.answer !== void 0) {
@@ -10538,7 +10564,9 @@ function toJSON(ast) {
10538
10564
  }
10539
10565
  let validationSection = {};
10540
10566
  if (ast.validation) {
10541
- validationSection = { validation: transformValidation(ast.validation, answers, widgets) };
10567
+ const validation = transformValidation(ast.validation, answers, widgets);
10568
+ const onlyDisabledLeft = validation.mode === VALIDATION_MODES.SIMPLE && Object.keys(answers).length === 0 && Object.values(widgets).some(isDisabledInputJSON);
10569
+ if (!onlyDisabledLeft) validationSection = { validation };
10542
10570
  } else if (Object.keys(answers).length > 0) {
10543
10571
  validationSection = { validation: { mode: VALIDATION_MODES.SIMPLE, answers } };
10544
10572
  }
@@ -10994,7 +11022,8 @@ function transformValidation(validation, answers, widgets) {
10994
11022
  answers
10995
11023
  };
10996
11024
  } else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
10997
- const validVariableNames = Object.keys(widgets);
11025
+ assertNoDisabledInExpression(validation.expression, widgets);
11026
+ const validVariableNames = getEnabledWidgetNames(widgets);
10998
11027
  const variables = extractVariables$1(validation.expression, validVariableNames);
10999
11028
  return {
11000
11029
  mode: VALIDATION_MODES.EXPRESSION,
@@ -11002,13 +11031,15 @@ function transformValidation(validation, answers, widgets) {
11002
11031
  variables
11003
11032
  };
11004
11033
  } else if (validation.type === AST_TYPES.MIXED_VALIDATION) {
11005
- const validVariableNames = Object.keys(widgets);
11034
+ assertNoDisabledInExpression(validation.expression, widgets);
11035
+ const validVariableNames = getEnabledWidgetNames(widgets);
11006
11036
  const variables = extractVariables$1(validation.expression, validVariableNames);
11007
11037
  const inExpression = new Set(variables);
11008
11038
  for (const [name, widget] of Object.entries(widgets)) {
11009
11039
  if (name.startsWith("_")) continue;
11010
11040
  if (name in answers) continue;
11011
11041
  if (inExpression.has(name)) continue;
11042
+ if (isDisabledInputJSON(widget)) continue;
11012
11043
  if (widget.type === JSON_TYPES.FRACTION) {
11013
11044
  const w = widget;
11014
11045
  if (w.numerator !== null && w.denominator !== null) continue;
@@ -11030,6 +11061,17 @@ function transformValidation(validation, answers, widgets) {
11030
11061
  }
11031
11062
  throw new Error(`Unknown validation type: ${validation.type}`);
11032
11063
  }
11064
+ function assertNoDisabledInExpression(expression, widgets) {
11065
+ const disabled = extractVariables$1(expression, Object.keys(widgets)).filter((name) => widgets[name] && isDisabledInputJSON(widgets[name]));
11066
+ if (disabled.length > 0) {
11067
+ throw new Error(
11068
+ `WIDGET_ANSWER: expression references disabled input(s) ${disabled.map((n) => `"${n}"`).join(", ")} — a disabled input is never filled, so the expression could never be true`
11069
+ );
11070
+ }
11071
+ }
11072
+ function getEnabledWidgetNames(widgets) {
11073
+ return Object.entries(widgets).filter(([, widget]) => !isDisabledInputJSON(widget)).map(([name]) => name);
11074
+ }
11033
11075
  function extractVariables$1(expression, validVariables) {
11034
11076
  const matches = expression.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) || [];
11035
11077
  const variables = matches.filter(
@@ -11545,7 +11587,43 @@ function preprocessFractionValues(exercise, userInputs) {
11545
11587
  }
11546
11588
  return result;
11547
11589
  }
11590
+ function withoutDisabledWidgets(exercise) {
11591
+ const validation = exercise.validation;
11592
+ if (!validation) return exercise;
11593
+ const disabled = new Set(
11594
+ Object.entries(exercise.widgets).filter(([, widget]) => isDisabledInputJSON(widget)).map(([id]) => id)
11595
+ );
11596
+ if (disabled.size === 0) return exercise;
11597
+ return {
11598
+ ...exercise,
11599
+ validation: {
11600
+ ...validation,
11601
+ ...validation.answers && {
11602
+ answers: Object.fromEntries(
11603
+ Object.entries(validation.answers).filter(([id]) => !disabled.has(id))
11604
+ )
11605
+ },
11606
+ ...validation.variables && {
11607
+ variables: validation.variables.filter((v) => !disabled.has(v))
11608
+ }
11609
+ }
11610
+ };
11611
+ }
11612
+ function findDisabledInExpression(exercise) {
11613
+ const expression = exercise.validation?.expression;
11614
+ if (!expression) return [];
11615
+ return extractVariables(expression, Object.keys(exercise.widgets)).filter((name) => exercise.widgets[name] && isDisabledInputJSON(exercise.widgets[name]));
11616
+ }
11548
11617
  function validate(exercise, userInputs) {
11618
+ const disabledInExpression = findDisabledInExpression(exercise);
11619
+ if (disabledInExpression.length > 0) {
11620
+ return {
11621
+ isCorrect: false,
11622
+ details: {},
11623
+ error: `Expression references disabled input(s) ${disabledInExpression.map((n) => `"${n}"`).join(", ")}`
11624
+ };
11625
+ }
11626
+ exercise = withoutDisabledWidgets(exercise);
11549
11627
  if (!exercise.validation) {
11550
11628
  return {
11551
11629
  isCorrect: true,
@@ -11575,6 +11653,7 @@ function validate(exercise, userInputs) {
11575
11653
  }
11576
11654
  }
11577
11655
  function getAnswerableInputs(exercise) {
11656
+ exercise = withoutDisabledWidgets(exercise);
11578
11657
  if (!exercise.validation) return [];
11579
11658
  const keys = /* @__PURE__ */ new Set();
11580
11659
  if (exercise.validation.mode === VALIDATION_MODES.SIMPLE) {
@@ -11589,6 +11668,7 @@ function getAnswerableInputs(exercise) {
11589
11668
  }
11590
11669
  function getMissingInputs(exercise, userInputs) {
11591
11670
  const missing = [];
11671
+ exercise = withoutDisabledWidgets(exercise);
11592
11672
  if (!exercise.validation) {
11593
11673
  return missing;
11594
11674
  }
@@ -16326,6 +16406,7 @@ const InlineInput = ({
16326
16406
  length,
16327
16407
  digits,
16328
16408
  placeholder,
16409
+ disabled = false,
16329
16410
  onChange,
16330
16411
  onFocus
16331
16412
  }) => {
@@ -16340,13 +16421,14 @@ const InlineInput = ({
16340
16421
  const expectedLength = inputType === "numeric" ? digits : length;
16341
16422
  const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
16342
16423
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
16424
+ const disabledClass = disabled ? " inline-input--disabled" : "";
16343
16425
  return /* @__PURE__ */ jsx(
16344
16426
  Droppable,
16345
16427
  {
16346
16428
  id: widgetId,
16347
16429
  accepts: ["drag-option", "placed-chip"],
16348
16430
  onDrop: handleDrop,
16349
- disabled: locked,
16431
+ disabled: locked || disabled,
16350
16432
  className: "inline-widget inline-input-droppable",
16351
16433
  children: isDropped && value !== "" ? (
16352
16434
  // Show as chip only when value came from drag-drop
@@ -16366,7 +16448,7 @@ const InlineInput = ({
16366
16448
  {
16367
16449
  id: widgetId + "-input",
16368
16450
  type: "text",
16369
- className: `inline-input${statusClass}`,
16451
+ className: `inline-input${statusClass}${disabledClass}`,
16370
16452
  value,
16371
16453
  onChange: (e) => handleChange(e.target.value, false),
16372
16454
  onMouseDown: () => {
@@ -16374,6 +16456,7 @@ const InlineInput = ({
16374
16456
  },
16375
16457
  onFocus: () => onFocus?.(widgetId),
16376
16458
  readOnly: locked,
16459
+ disabled,
16377
16460
  maxLength: expectedLength,
16378
16461
  style: { width: dynamicWidth },
16379
16462
  placeholder
@@ -18622,6 +18705,7 @@ const WidgetRenderer = ({
18622
18705
  length: widget.inputType === "string" ? widget.length : void 0,
18623
18706
  digits: widget.inputType === "numeric" ? widget.digits : void 0,
18624
18707
  placeholder: widget.placeholder,
18708
+ disabled: widget.disabled,
18625
18709
  onChange,
18626
18710
  onFocus: onInputFocus
18627
18711
  }
@@ -20879,7 +20963,7 @@ function buildInitialValuesSignature(json) {
20879
20963
  }
20880
20964
  function buildRevealedValues(json) {
20881
20965
  const revealed = {};
20882
- const validation = json.validation;
20966
+ const validation = withoutDisabledWidgets(json).validation;
20883
20967
  if (!validation) return revealed;
20884
20968
  const answers = validation.answers ?? {};
20885
20969
  for (const [widgetId, expected] of Object.entries(answers)) {