@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.mjs CHANGED
@@ -23,6 +23,8 @@ const AST_TYPES = {
23
23
  INLINE_INPUT: "InlineInput",
24
24
  /** Inline fraction widget (numerator/denominator with optional inputs) */
25
25
  FRACTION: "Fraction",
26
+ /** Inline image element */
27
+ IMAGE: "Image",
26
28
  // ═══════════════════════════════════════════════════════════════════════════
27
29
  // Blocks
28
30
  // ═══════════════════════════════════════════════════════════════════════════
@@ -88,6 +90,8 @@ const JSON_TYPES = {
88
90
  COLOR_TEXT: "color-text",
89
91
  /** Bold text */
90
92
  BOLD_TEXT: "bold-text",
93
+ /** Inline image */
94
+ IMAGE: "image",
91
95
  /** Reference to a widget */
92
96
  WIDGET_REF: "widget-ref",
93
97
  /** Line break element */
@@ -278,6 +282,10 @@ const ANSWERS = createToken({
278
282
  name: "ANSWERS",
279
283
  pattern: /ANSWERS/
280
284
  });
285
+ const IMAGE = createToken({
286
+ name: "IMAGE",
287
+ pattern: /IMAGE/
288
+ });
281
289
  const NumberLiteral = createToken({
282
290
  name: "NumberLiteral",
283
291
  pattern: /-?\d+(\.\d+)?/
@@ -391,6 +399,7 @@ const allTokens = [
391
399
  MATCHER,
392
400
  ITEM,
393
401
  ANSWERS,
402
+ IMAGE,
394
403
  // Literals
395
404
  NumberLiteral,
396
405
  StringLiteral,
@@ -849,6 +858,8 @@ class MathExerciseParser extends CstParser {
849
858
  { ALT: () => this.SUBRULE(this.boldText) },
850
859
  // FRACTION (inline fraction widget)
851
860
  { ALT: () => this.SUBRULE(this.fraction) },
861
+ // IMAGE (inline image element)
862
+ { ALT: () => this.SUBRULE(this.image) },
852
863
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
853
864
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
854
865
  ]);
@@ -880,6 +891,12 @@ class MathExerciseParser extends CstParser {
880
891
  this.SUBRULE(this.parameterList);
881
892
  this.CONSUME(RBracket);
882
893
  });
894
+ this.image = this.RULE("image", () => {
895
+ this.CONSUME(IMAGE);
896
+ this.CONSUME(LBracket);
897
+ this.SUBRULE(this.parameterList);
898
+ this.CONSUME(RBracket);
899
+ });
883
900
  this.inlineInput = this.RULE("inlineInput", () => {
884
901
  this.OR([
885
902
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
@@ -1070,6 +1087,17 @@ const FractionInlineSchema = z.object({
1070
1087
  answer: z.string().optional(),
1071
1088
  simplified: z.boolean().default(true)
1072
1089
  });
1090
+ const IMAGE_SRC_SCHEME_RE$1 = /^(https?:\/\/|data:image\/)/i;
1091
+ const ImageInlineSchema = z.object({
1092
+ type: z.literal(AST_TYPES.IMAGE),
1093
+ name: z.string().min(1),
1094
+ src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE$1.test(s), {
1095
+ message: "src must use http(s) or data:image scheme"
1096
+ }),
1097
+ alt: z.string(),
1098
+ width: z.number().positive().optional(),
1099
+ height: z.number().positive().optional()
1100
+ });
1073
1101
  const InlineNodeSchema = z.union([
1074
1102
  FreeTextInlineSchema,
1075
1103
  SpaceInlineSchema,
@@ -1077,7 +1105,8 @@ const InlineNodeSchema = z.union([
1077
1105
  ColorTextInlineSchema,
1078
1106
  BoldTextInlineSchema,
1079
1107
  InlineInputWidgetSchema,
1080
- FractionInlineSchema
1108
+ FractionInlineSchema,
1109
+ ImageInlineSchema
1081
1110
  ]);
1082
1111
  function createFreeText(value) {
1083
1112
  return FreeTextInlineSchema.parse({
@@ -1130,6 +1159,15 @@ function createNumericInput(name, digits, answer) {
1130
1159
  answer
1131
1160
  });
1132
1161
  }
1162
+ function createImage(name, src, alt, options = {}) {
1163
+ return ImageInlineSchema.parse({
1164
+ type: AST_TYPES.IMAGE,
1165
+ name,
1166
+ src,
1167
+ alt,
1168
+ ...options
1169
+ });
1170
+ }
1133
1171
  function createFraction(name, options = {}) {
1134
1172
  return FractionInlineSchema.parse({
1135
1173
  type: AST_TYPES.FRACTION,
@@ -1149,6 +1187,9 @@ function isInlineInput(value) {
1149
1187
  function isFraction(value) {
1150
1188
  return FractionInlineSchema.safeParse(value).success;
1151
1189
  }
1190
+ function isImage(value) {
1191
+ return ImageInlineSchema.safeParse(value).success;
1192
+ }
1152
1193
  function isInlineNode(value) {
1153
1194
  return InlineNodeSchema.safeParse(value).success;
1154
1195
  }
@@ -1353,7 +1394,8 @@ const DisplayInlineNodeSchema = z.union([
1353
1394
  HighlightedTextInlineSchema,
1354
1395
  ColorTextInlineSchema,
1355
1396
  BoldTextInlineSchema,
1356
- DisplayFractionInlineSchema
1397
+ DisplayFractionInlineSchema,
1398
+ ImageInlineSchema
1357
1399
  ]);
1358
1400
  const MatcherItemSchema = z.object({
1359
1401
  type: z.literal(AST_TYPES.MATCHER_ITEM),
@@ -1529,6 +1571,51 @@ const ExerciseDocumentSchema = z.object({
1529
1571
  lines: z.array(ContentBlockSchema),
1530
1572
  tools: z.array(ToolNodeSchema).optional(),
1531
1573
  validation: ValidationNodeSchema.optional()
1574
+ }).superRefine((doc, ctx) => {
1575
+ const seen = /* @__PURE__ */ new Set();
1576
+ const record = (name, path) => {
1577
+ if (seen.has(name)) {
1578
+ ctx.addIssue({
1579
+ code: z.ZodIssueCode.custom,
1580
+ message: `Duplicate name "${name}" — widget/image names must be unique across the document`,
1581
+ path
1582
+ });
1583
+ } else {
1584
+ seen.add(name);
1585
+ }
1586
+ };
1587
+ const visitInlines = (inlines, basePath) => {
1588
+ inlines.forEach((inline, i) => {
1589
+ if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE) {
1590
+ record(inline.name, [...basePath, i, "name"]);
1591
+ }
1592
+ });
1593
+ };
1594
+ doc.lines.forEach((block, bi) => {
1595
+ if (block.type === AST_TYPES.LINE_BLOCK) {
1596
+ visitInlines(block.inlines, ["lines", bi, "inlines"]);
1597
+ } else if (block.type === AST_TYPES.TABLE_BLOCK) {
1598
+ block.rows.forEach((row, ri) => {
1599
+ row.cells.forEach((cell, ci) => {
1600
+ visitInlines(cell.inlines, ["lines", bi, "rows", ri, "cells", ci, "inlines"]);
1601
+ });
1602
+ });
1603
+ } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
1604
+ block.positions.forEach((pos, pi) => {
1605
+ visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
1606
+ });
1607
+ } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
1608
+ const m = block;
1609
+ record(m.name, ["lines", bi, "name"]);
1610
+ m.rows.forEach((row, ri) => {
1611
+ row.items.forEach((item, ii) => {
1612
+ visitInlines(item.inlines, ["lines", bi, "rows", ri, "items", ii, "inlines"]);
1613
+ });
1614
+ });
1615
+ } else if (block.type === AST_TYPES.RADIO_OPTIONS) {
1616
+ record(block.name, ["lines", bi, "name"]);
1617
+ }
1618
+ });
1532
1619
  });
1533
1620
  function createExerciseDocument(question, lines, validation, tools) {
1534
1621
  return ExerciseDocumentSchema.parse({
@@ -1771,6 +1858,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1771
1858
  if (ctx.fraction) {
1772
1859
  return this.visit(ctx.fraction);
1773
1860
  }
1861
+ if (ctx.image) {
1862
+ return this.visit(ctx.image);
1863
+ }
1774
1864
  if (ctx.inlineInput) {
1775
1865
  return this.visit(ctx.inlineInput);
1776
1866
  }
@@ -1813,6 +1903,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1813
1903
  const simplified = params.simplified !== void 0 ? params.simplified : void 0;
1814
1904
  return createFraction(name, { numerator, denominator, answer, simplified });
1815
1905
  }
1906
+ image(ctx) {
1907
+ const params = this.visit(ctx.parameterList);
1908
+ const name = params.name;
1909
+ const src = params.src;
1910
+ const alt = params.alt;
1911
+ const width = params.width;
1912
+ const height = params.height;
1913
+ return createImage(name, src, alt, { width, height });
1914
+ }
1816
1915
  inlineInput(ctx) {
1817
1916
  const params = this.visit(ctx.parameterList);
1818
1917
  const name = params.name;
@@ -1971,6 +2070,18 @@ const BoldTextSchema = z.object({
1971
2070
  value: z.string(),
1972
2071
  highlight: z.tuple([z.number(), z.number()]).optional()
1973
2072
  });
2073
+ const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
2074
+ const ImageItemSchema = z.object({
2075
+ id: z.string(),
2076
+ type: z.literal(JSON_TYPES.IMAGE),
2077
+ name: z.string().min(1),
2078
+ src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE.test(s), {
2079
+ message: "src must use http(s) or data:image scheme"
2080
+ }),
2081
+ alt: z.string(),
2082
+ width: z.number().positive().optional(),
2083
+ height: z.number().positive().optional()
2084
+ });
1974
2085
  const SpaceItemSchema = z.object({
1975
2086
  id: z.string(),
1976
2087
  type: z.literal(JSON_TYPES.SPACE)
@@ -1993,6 +2104,7 @@ const LayoutItemSchema = z.lazy(
1993
2104
  HighlightedTextSchema,
1994
2105
  ColorTextSchema,
1995
2106
  BoldTextSchema,
2107
+ ImageItemSchema,
1996
2108
  SpaceItemSchema,
1997
2109
  LinebreakSchema,
1998
2110
  GroupItemSchema,
@@ -2179,6 +2291,9 @@ function isColorText(item) {
2179
2291
  function isBoldText(item) {
2180
2292
  return item.type === JSON_TYPES.BOLD_TEXT;
2181
2293
  }
2294
+ function isImageItem(item) {
2295
+ return item.type === JSON_TYPES.IMAGE;
2296
+ }
2182
2297
  function isSpaceItem(item) {
2183
2298
  return item.type === JSON_TYPES.SPACE;
2184
2299
  }
@@ -2531,6 +2646,16 @@ function transformInline(inline, idGenerator) {
2531
2646
  type: JSON_TYPES.WIDGET_REF,
2532
2647
  widgetId: inline.name
2533
2648
  };
2649
+ case AST_TYPES.IMAGE:
2650
+ return {
2651
+ id: idGenerator.generate(JSON_TYPES.IMAGE),
2652
+ type: JSON_TYPES.IMAGE,
2653
+ name: inline.name,
2654
+ src: inline.src,
2655
+ alt: inline.alt,
2656
+ ...inline.width !== void 0 && { width: inline.width },
2657
+ ...inline.height !== void 0 && { height: inline.height }
2658
+ };
2534
2659
  default:
2535
2660
  throw new Error(`Unknown inline type: ${inline.type}`);
2536
2661
  }
@@ -13754,6 +13879,18 @@ const LayoutRenderer = ({
13754
13879
  }
13755
13880
  return /* @__PURE__ */ jsx("strong", { className: "bold-text text-item", dir: dirAttr, children: item.value });
13756
13881
  }
13882
+ case JSON_TYPES.IMAGE:
13883
+ return /* @__PURE__ */ jsx(
13884
+ "img",
13885
+ {
13886
+ className: "image-item text-item",
13887
+ src: item.src,
13888
+ alt: item.alt,
13889
+ ...item.width !== void 0 && { width: item.width },
13890
+ ...item.height !== void 0 && { height: item.height },
13891
+ loading: "lazy"
13892
+ }
13893
+ );
13757
13894
  case JSON_TYPES.WIDGET_REF: {
13758
13895
  const widget = widgets[item.widgetId];
13759
13896
  if (!widget) return null;
@@ -14129,6 +14266,8 @@ export {
14129
14266
  GroupItemSchema,
14130
14267
  HighlightedTextInlineSchema,
14131
14268
  HighlightedTextSchema,
14269
+ ImageInlineSchema,
14270
+ ImageItemSchema,
14132
14271
  InlineInputJSONSchema,
14133
14272
  InlineInputWidgetSchema,
14134
14273
  InlineNodeSchema,
@@ -14178,6 +14317,7 @@ export {
14178
14317
  createFraction,
14179
14318
  createFreeText,
14180
14319
  createHighlightedText,
14320
+ createImage,
14181
14321
  createLineBlock,
14182
14322
  createMatcherBlock,
14183
14323
  createMatcherItem,
@@ -14205,6 +14345,8 @@ export {
14205
14345
  isFreeText,
14206
14346
  isGroupItem,
14207
14347
  isHighlightedText,
14348
+ isImage,
14349
+ isImageItem,
14208
14350
  isInlineInput,
14209
14351
  isInlineInputJSON,
14210
14352
  isInlineNode,