@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.d.ts +1688 -41
- package/dist/index.js +144 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +144 -2
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +21 -7
- package/package.json +1 -1
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;
|