@mathscan/math-exercise-engine 1.0.14 → 1.0.15

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
@@ -25,6 +25,8 @@ const AST_TYPES = {
25
25
  INLINE_INPUT: "InlineInput",
26
26
  /** Inline fraction widget (numerator/denominator with optional inputs) */
27
27
  FRACTION: "Fraction",
28
+ /** Inline image element */
29
+ IMAGE: "Image",
28
30
  // ═══════════════════════════════════════════════════════════════════════════
29
31
  // Blocks
30
32
  // ═══════════════════════════════════════════════════════════════════════════
@@ -90,6 +92,8 @@ const JSON_TYPES = {
90
92
  COLOR_TEXT: "color-text",
91
93
  /** Bold text */
92
94
  BOLD_TEXT: "bold-text",
95
+ /** Inline image */
96
+ IMAGE: "image",
93
97
  /** Reference to a widget */
94
98
  WIDGET_REF: "widget-ref",
95
99
  /** Line break element */
@@ -280,6 +284,10 @@ const ANSWERS = chevrotain.createToken({
280
284
  name: "ANSWERS",
281
285
  pattern: /ANSWERS/
282
286
  });
287
+ const IMAGE = chevrotain.createToken({
288
+ name: "IMAGE",
289
+ pattern: /IMAGE/
290
+ });
283
291
  const NumberLiteral = chevrotain.createToken({
284
292
  name: "NumberLiteral",
285
293
  pattern: /-?\d+(\.\d+)?/
@@ -393,6 +401,7 @@ const allTokens = [
393
401
  MATCHER,
394
402
  ITEM,
395
403
  ANSWERS,
404
+ IMAGE,
396
405
  // Literals
397
406
  NumberLiteral,
398
407
  StringLiteral,
@@ -851,6 +860,8 @@ class MathExerciseParser extends chevrotain.CstParser {
851
860
  { ALT: () => this.SUBRULE(this.boldText) },
852
861
  // FRACTION (inline fraction widget)
853
862
  { ALT: () => this.SUBRULE(this.fraction) },
863
+ // IMAGE (inline image element)
864
+ { ALT: () => this.SUBRULE(this.image) },
854
865
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
855
866
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
856
867
  ]);
@@ -882,6 +893,12 @@ class MathExerciseParser extends chevrotain.CstParser {
882
893
  this.SUBRULE(this.parameterList);
883
894
  this.CONSUME(RBracket);
884
895
  });
896
+ this.image = this.RULE("image", () => {
897
+ this.CONSUME(IMAGE);
898
+ this.CONSUME(LBracket);
899
+ this.SUBRULE(this.parameterList);
900
+ this.CONSUME(RBracket);
901
+ });
885
902
  this.inlineInput = this.RULE("inlineInput", () => {
886
903
  this.OR([
887
904
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
@@ -1072,6 +1089,17 @@ const FractionInlineSchema = zod.z.object({
1072
1089
  answer: zod.z.string().optional(),
1073
1090
  simplified: zod.z.boolean().default(true)
1074
1091
  });
1092
+ const IMAGE_SRC_SCHEME_RE$1 = /^(https?:\/\/|data:image\/)/i;
1093
+ const ImageInlineSchema = zod.z.object({
1094
+ type: zod.z.literal(AST_TYPES.IMAGE),
1095
+ name: zod.z.string().min(1),
1096
+ src: zod.z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE$1.test(s), {
1097
+ message: "src must use http(s) or data:image scheme"
1098
+ }),
1099
+ alt: zod.z.string(),
1100
+ width: zod.z.number().positive().optional(),
1101
+ height: zod.z.number().positive().optional()
1102
+ });
1075
1103
  const InlineNodeSchema = zod.z.union([
1076
1104
  FreeTextInlineSchema,
1077
1105
  SpaceInlineSchema,
@@ -1079,7 +1107,8 @@ const InlineNodeSchema = zod.z.union([
1079
1107
  ColorTextInlineSchema,
1080
1108
  BoldTextInlineSchema,
1081
1109
  InlineInputWidgetSchema,
1082
- FractionInlineSchema
1110
+ FractionInlineSchema,
1111
+ ImageInlineSchema
1083
1112
  ]);
1084
1113
  function createFreeText(value) {
1085
1114
  return FreeTextInlineSchema.parse({
@@ -1132,6 +1161,15 @@ function createNumericInput(name, digits, answer) {
1132
1161
  answer
1133
1162
  });
1134
1163
  }
1164
+ function createImage(name, src, alt, options = {}) {
1165
+ return ImageInlineSchema.parse({
1166
+ type: AST_TYPES.IMAGE,
1167
+ name,
1168
+ src,
1169
+ alt,
1170
+ ...options
1171
+ });
1172
+ }
1135
1173
  function createFraction(name, options = {}) {
1136
1174
  return FractionInlineSchema.parse({
1137
1175
  type: AST_TYPES.FRACTION,
@@ -1151,6 +1189,9 @@ function isInlineInput(value) {
1151
1189
  function isFraction(value) {
1152
1190
  return FractionInlineSchema.safeParse(value).success;
1153
1191
  }
1192
+ function isImage(value) {
1193
+ return ImageInlineSchema.safeParse(value).success;
1194
+ }
1154
1195
  function isInlineNode(value) {
1155
1196
  return InlineNodeSchema.safeParse(value).success;
1156
1197
  }
@@ -1355,7 +1396,8 @@ const DisplayInlineNodeSchema = zod.z.union([
1355
1396
  HighlightedTextInlineSchema,
1356
1397
  ColorTextInlineSchema,
1357
1398
  BoldTextInlineSchema,
1358
- DisplayFractionInlineSchema
1399
+ DisplayFractionInlineSchema,
1400
+ ImageInlineSchema
1359
1401
  ]);
1360
1402
  const MatcherItemSchema = zod.z.object({
1361
1403
  type: zod.z.literal(AST_TYPES.MATCHER_ITEM),
@@ -1531,6 +1573,51 @@ const ExerciseDocumentSchema = zod.z.object({
1531
1573
  lines: zod.z.array(ContentBlockSchema),
1532
1574
  tools: zod.z.array(ToolNodeSchema).optional(),
1533
1575
  validation: ValidationNodeSchema.optional()
1576
+ }).superRefine((doc, ctx) => {
1577
+ const seen = /* @__PURE__ */ new Set();
1578
+ const record = (name, path) => {
1579
+ if (seen.has(name)) {
1580
+ ctx.addIssue({
1581
+ code: zod.z.ZodIssueCode.custom,
1582
+ message: `Duplicate name "${name}" — widget/image names must be unique across the document`,
1583
+ path
1584
+ });
1585
+ } else {
1586
+ seen.add(name);
1587
+ }
1588
+ };
1589
+ const visitInlines = (inlines, basePath) => {
1590
+ inlines.forEach((inline, i) => {
1591
+ if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE) {
1592
+ record(inline.name, [...basePath, i, "name"]);
1593
+ }
1594
+ });
1595
+ };
1596
+ doc.lines.forEach((block, bi) => {
1597
+ if (block.type === AST_TYPES.LINE_BLOCK) {
1598
+ visitInlines(block.inlines, ["lines", bi, "inlines"]);
1599
+ } else if (block.type === AST_TYPES.TABLE_BLOCK) {
1600
+ block.rows.forEach((row, ri) => {
1601
+ row.cells.forEach((cell, ci) => {
1602
+ visitInlines(cell.inlines, ["lines", bi, "rows", ri, "cells", ci, "inlines"]);
1603
+ });
1604
+ });
1605
+ } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
1606
+ block.positions.forEach((pos, pi) => {
1607
+ visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
1608
+ });
1609
+ } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
1610
+ const m = block;
1611
+ record(m.name, ["lines", bi, "name"]);
1612
+ m.rows.forEach((row, ri) => {
1613
+ row.items.forEach((item, ii) => {
1614
+ visitInlines(item.inlines, ["lines", bi, "rows", ri, "items", ii, "inlines"]);
1615
+ });
1616
+ });
1617
+ } else if (block.type === AST_TYPES.RADIO_OPTIONS) {
1618
+ record(block.name, ["lines", bi, "name"]);
1619
+ }
1620
+ });
1534
1621
  });
1535
1622
  function createExerciseDocument(question, lines, validation, tools) {
1536
1623
  return ExerciseDocumentSchema.parse({
@@ -1773,6 +1860,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1773
1860
  if (ctx.fraction) {
1774
1861
  return this.visit(ctx.fraction);
1775
1862
  }
1863
+ if (ctx.image) {
1864
+ return this.visit(ctx.image);
1865
+ }
1776
1866
  if (ctx.inlineInput) {
1777
1867
  return this.visit(ctx.inlineInput);
1778
1868
  }
@@ -1815,6 +1905,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1815
1905
  const simplified = params.simplified !== void 0 ? params.simplified : void 0;
1816
1906
  return createFraction(name, { numerator, denominator, answer, simplified });
1817
1907
  }
1908
+ image(ctx) {
1909
+ const params = this.visit(ctx.parameterList);
1910
+ const name = params.name;
1911
+ const src = params.src;
1912
+ const alt = params.alt;
1913
+ const width = params.width;
1914
+ const height = params.height;
1915
+ return createImage(name, src, alt, { width, height });
1916
+ }
1818
1917
  inlineInput(ctx) {
1819
1918
  const params = this.visit(ctx.parameterList);
1820
1919
  const name = params.name;
@@ -1973,6 +2072,18 @@ const BoldTextSchema = zod.z.object({
1973
2072
  value: zod.z.string(),
1974
2073
  highlight: zod.z.tuple([zod.z.number(), zod.z.number()]).optional()
1975
2074
  });
2075
+ const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
2076
+ const ImageItemSchema = zod.z.object({
2077
+ id: zod.z.string(),
2078
+ type: zod.z.literal(JSON_TYPES.IMAGE),
2079
+ name: zod.z.string().min(1),
2080
+ src: zod.z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE.test(s), {
2081
+ message: "src must use http(s) or data:image scheme"
2082
+ }),
2083
+ alt: zod.z.string(),
2084
+ width: zod.z.number().positive().optional(),
2085
+ height: zod.z.number().positive().optional()
2086
+ });
1976
2087
  const SpaceItemSchema = zod.z.object({
1977
2088
  id: zod.z.string(),
1978
2089
  type: zod.z.literal(JSON_TYPES.SPACE)
@@ -1995,6 +2106,7 @@ const LayoutItemSchema = zod.z.lazy(
1995
2106
  HighlightedTextSchema,
1996
2107
  ColorTextSchema,
1997
2108
  BoldTextSchema,
2109
+ ImageItemSchema,
1998
2110
  SpaceItemSchema,
1999
2111
  LinebreakSchema,
2000
2112
  GroupItemSchema,
@@ -2181,6 +2293,9 @@ function isColorText(item) {
2181
2293
  function isBoldText(item) {
2182
2294
  return item.type === JSON_TYPES.BOLD_TEXT;
2183
2295
  }
2296
+ function isImageItem(item) {
2297
+ return item.type === JSON_TYPES.IMAGE;
2298
+ }
2184
2299
  function isSpaceItem(item) {
2185
2300
  return item.type === JSON_TYPES.SPACE;
2186
2301
  }
@@ -2533,6 +2648,16 @@ function transformInline(inline, idGenerator) {
2533
2648
  type: JSON_TYPES.WIDGET_REF,
2534
2649
  widgetId: inline.name
2535
2650
  };
2651
+ case AST_TYPES.IMAGE:
2652
+ return {
2653
+ id: idGenerator.generate(JSON_TYPES.IMAGE),
2654
+ type: JSON_TYPES.IMAGE,
2655
+ name: inline.name,
2656
+ src: inline.src,
2657
+ alt: inline.alt,
2658
+ ...inline.width !== void 0 && { width: inline.width },
2659
+ ...inline.height !== void 0 && { height: inline.height }
2660
+ };
2536
2661
  default:
2537
2662
  throw new Error(`Unknown inline type: ${inline.type}`);
2538
2663
  }
@@ -13756,6 +13881,18 @@ const LayoutRenderer = ({
13756
13881
  }
13757
13882
  return /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "bold-text text-item", dir: dirAttr, children: item.value });
13758
13883
  }
13884
+ case JSON_TYPES.IMAGE:
13885
+ return /* @__PURE__ */ jsxRuntime.jsx(
13886
+ "img",
13887
+ {
13888
+ className: "image-item text-item",
13889
+ src: item.src,
13890
+ alt: item.alt,
13891
+ ...item.width !== void 0 && { width: item.width },
13892
+ ...item.height !== void 0 && { height: item.height },
13893
+ loading: "lazy"
13894
+ }
13895
+ );
13759
13896
  case JSON_TYPES.WIDGET_REF: {
13760
13897
  const widget = widgets[item.widgetId];
13761
13898
  if (!widget) return null;
@@ -14130,6 +14267,8 @@ exports.FreeTextInlineSchema = FreeTextInlineSchema;
14130
14267
  exports.GroupItemSchema = GroupItemSchema;
14131
14268
  exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
14132
14269
  exports.HighlightedTextSchema = HighlightedTextSchema;
14270
+ exports.ImageInlineSchema = ImageInlineSchema;
14271
+ exports.ImageItemSchema = ImageItemSchema;
14133
14272
  exports.InlineInputJSONSchema = InlineInputJSONSchema;
14134
14273
  exports.InlineInputWidgetSchema = InlineInputWidgetSchema;
14135
14274
  exports.InlineNodeSchema = InlineNodeSchema;
@@ -14179,6 +14318,7 @@ exports.createExpressionValidation = createExpressionValidation;
14179
14318
  exports.createFraction = createFraction;
14180
14319
  exports.createFreeText = createFreeText;
14181
14320
  exports.createHighlightedText = createHighlightedText;
14321
+ exports.createImage = createImage;
14182
14322
  exports.createLineBlock = createLineBlock;
14183
14323
  exports.createMatcherBlock = createMatcherBlock;
14184
14324
  exports.createMatcherItem = createMatcherItem;
@@ -14206,6 +14346,8 @@ exports.isFractionKey = isFractionKey;
14206
14346
  exports.isFreeText = isFreeText;
14207
14347
  exports.isGroupItem = isGroupItem;
14208
14348
  exports.isHighlightedText = isHighlightedText;
14349
+ exports.isImage = isImage;
14350
+ exports.isImageItem = isImageItem;
14209
14351
  exports.isInlineInput = isInlineInput;
14210
14352
  exports.isInlineInputJSON = isInlineInputJSON;
14211
14353
  exports.isInlineNode = isInlineNode;