@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.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,
|