@mathscan/math-exercise-engine 1.2.0 → 1.2.1
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 +169 -164
- package/dist/index.js +66 -87
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +66 -87
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +44 -0
- package/package.json +20 -19
package/dist/index.js
CHANGED
|
@@ -394,13 +394,9 @@ const MATCHER = chevrotain.createToken({
|
|
|
394
394
|
name: "MATCHER",
|
|
395
395
|
pattern: /MATCHER/
|
|
396
396
|
});
|
|
397
|
-
const
|
|
398
|
-
name: "
|
|
399
|
-
pattern: /
|
|
400
|
-
});
|
|
401
|
-
const BOTTOM_ROW = chevrotain.createToken({
|
|
402
|
-
name: "BOTTOM_ROW",
|
|
403
|
-
pattern: /BOTTOM_ROW/
|
|
397
|
+
const GROUP = chevrotain.createToken({
|
|
398
|
+
name: "GROUP",
|
|
399
|
+
pattern: /GROUP/
|
|
404
400
|
});
|
|
405
401
|
const ITEM = chevrotain.createToken({
|
|
406
402
|
name: "ITEM",
|
|
@@ -556,8 +552,7 @@ const allTokens = [
|
|
|
556
552
|
ARROW,
|
|
557
553
|
TABLE,
|
|
558
554
|
ROW,
|
|
559
|
-
|
|
560
|
-
BOTTOM_ROW,
|
|
555
|
+
GROUP,
|
|
561
556
|
CELL,
|
|
562
557
|
COUNTER,
|
|
563
558
|
CLOCK,
|
|
@@ -1065,39 +1060,19 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1065
1060
|
this.MANY3(() => {
|
|
1066
1061
|
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1067
1062
|
});
|
|
1068
|
-
this.SUBRULE(this.
|
|
1069
|
-
this.
|
|
1063
|
+
this.SUBRULE(this.pairMatcherGroupSection);
|
|
1064
|
+
this.SUBRULE2(this.pairMatcherGroupSection);
|
|
1070
1065
|
this.SUBRULE(this.matcherAnswers);
|
|
1071
1066
|
this.CONSUME(RBrace);
|
|
1072
1067
|
this.MANY4(() => {
|
|
1073
1068
|
this.OR4([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
1074
1069
|
});
|
|
1075
1070
|
});
|
|
1076
|
-
this.
|
|
1077
|
-
this.MANY(() => {
|
|
1078
|
-
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1079
|
-
});
|
|
1080
|
-
this.CONSUME(TOP_ROW);
|
|
1081
|
-
this.MANY2(() => {
|
|
1082
|
-
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1083
|
-
});
|
|
1084
|
-
this.CONSUME(LBrace);
|
|
1085
|
-
this.MANY3(() => {
|
|
1086
|
-
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1087
|
-
});
|
|
1088
|
-
this.AT_LEAST_ONE(() => {
|
|
1089
|
-
this.SUBRULE(this.matcherItem);
|
|
1090
|
-
});
|
|
1091
|
-
this.CONSUME(RBrace);
|
|
1092
|
-
this.MANY4(() => {
|
|
1093
|
-
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
this.pairMatcherBottomSection = this.RULE("pairMatcherBottomSection", () => {
|
|
1071
|
+
this.pairMatcherGroupSection = this.RULE("pairMatcherGroupSection", () => {
|
|
1097
1072
|
this.MANY(() => {
|
|
1098
1073
|
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1099
1074
|
});
|
|
1100
|
-
this.CONSUME(
|
|
1075
|
+
this.CONSUME(GROUP);
|
|
1101
1076
|
this.MANY2(() => {
|
|
1102
1077
|
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1103
1078
|
});
|
|
@@ -2590,20 +2565,24 @@ function isMatcherBlock(value) {
|
|
|
2590
2565
|
const PairMatcherBlockSchema = zod.z.object({
|
|
2591
2566
|
type: zod.z.literal(AST_TYPES.PAIR_MATCHER_BLOCK),
|
|
2592
2567
|
name: zod.z.string().min(1),
|
|
2593
|
-
/**
|
|
2594
|
-
|
|
2595
|
-
/**
|
|
2596
|
-
|
|
2568
|
+
/** Row layout (top/bottom, default) or column layout (left/right). */
|
|
2569
|
+
layout: zod.z.enum(["rows", "columns"]).optional(),
|
|
2570
|
+
/** Exactly two groups, in declaration order — neither more nor fewer. */
|
|
2571
|
+
groups: zod.z.tuple([
|
|
2572
|
+
zod.z.array(MatcherItemSchema).min(1),
|
|
2573
|
+
zod.z.array(MatcherItemSchema).min(1)
|
|
2574
|
+
]),
|
|
2597
2575
|
/**
|
|
2598
|
-
* Each answer pair is [
|
|
2576
|
+
* Each answer pair is [groups[0] id, groups[1] id].
|
|
2599
2577
|
* At least one pair is required.
|
|
2600
2578
|
*/
|
|
2601
2579
|
answers: zod.z.array(zod.z.tuple([zod.z.string().min(1), zod.z.string().min(1)])).min(1)
|
|
2602
2580
|
}).superRefine((data, ctx) => {
|
|
2581
|
+
const [groupAItems, groupBItems] = data.groups;
|
|
2603
2582
|
const idRegistry = /* @__PURE__ */ new Map();
|
|
2604
2583
|
const sections = [
|
|
2605
|
-
{ key:
|
|
2606
|
-
{ key:
|
|
2584
|
+
{ key: 0, items: groupAItems },
|
|
2585
|
+
{ key: 1, items: groupBItems }
|
|
2607
2586
|
];
|
|
2608
2587
|
for (const { key: key2, items } of sections) {
|
|
2609
2588
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -2613,59 +2592,59 @@ const PairMatcherBlockSchema = zod.z.object({
|
|
|
2613
2592
|
const isCrossSection = existing.section !== key2;
|
|
2614
2593
|
ctx.addIssue({
|
|
2615
2594
|
code: zod.z.ZodIssueCode.custom,
|
|
2616
|
-
message: isCrossSection ? `itemId "${itemId}" appears in both
|
|
2617
|
-
path: [key2, i, "itemId"]
|
|
2595
|
+
message: isCrossSection ? `itemId "${itemId}" appears in both groups — ids must be globally unique` : `Duplicate itemId "${itemId}" in group ${key2}`,
|
|
2596
|
+
path: ["groups", key2, i, "itemId"]
|
|
2618
2597
|
});
|
|
2619
2598
|
} else {
|
|
2620
2599
|
idRegistry.set(itemId, { section: key2, index: i });
|
|
2621
2600
|
}
|
|
2622
2601
|
}
|
|
2623
2602
|
}
|
|
2624
|
-
const
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
const
|
|
2628
|
-
data.answers.forEach(([
|
|
2629
|
-
if (!
|
|
2603
|
+
const groupAIds = new Set(groupAItems.map((it) => it.itemId));
|
|
2604
|
+
const groupBIds = new Set(groupBItems.map((it) => it.itemId));
|
|
2605
|
+
const usedGroupAIds = /* @__PURE__ */ new Set();
|
|
2606
|
+
const usedGroupBIds = /* @__PURE__ */ new Set();
|
|
2607
|
+
data.answers.forEach(([aId, bId], i) => {
|
|
2608
|
+
if (!groupAIds.has(aId)) {
|
|
2630
2609
|
ctx.addIssue({
|
|
2631
2610
|
code: zod.z.ZodIssueCode.custom,
|
|
2632
|
-
message: `Answer pair [${
|
|
2611
|
+
message: `Answer pair [${aId}, ${bId}]: "${aId}" is not declared in groups[0]`,
|
|
2633
2612
|
path: ["answers", i, 0]
|
|
2634
2613
|
});
|
|
2635
2614
|
}
|
|
2636
|
-
if (!
|
|
2615
|
+
if (!groupBIds.has(bId)) {
|
|
2637
2616
|
ctx.addIssue({
|
|
2638
2617
|
code: zod.z.ZodIssueCode.custom,
|
|
2639
|
-
message: `Answer pair [${
|
|
2618
|
+
message: `Answer pair [${aId}, ${bId}]: "${bId}" is not declared in groups[1]`,
|
|
2640
2619
|
path: ["answers", i, 1]
|
|
2641
2620
|
});
|
|
2642
2621
|
}
|
|
2643
|
-
if (
|
|
2622
|
+
if (usedGroupAIds.has(aId)) {
|
|
2644
2623
|
ctx.addIssue({
|
|
2645
2624
|
code: zod.z.ZodIssueCode.custom,
|
|
2646
|
-
message: `
|
|
2625
|
+
message: `id "${aId}" appears in more than one answer pair`,
|
|
2647
2626
|
path: ["answers", i, 0]
|
|
2648
2627
|
});
|
|
2649
2628
|
} else {
|
|
2650
|
-
|
|
2629
|
+
usedGroupAIds.add(aId);
|
|
2651
2630
|
}
|
|
2652
|
-
if (
|
|
2631
|
+
if (usedGroupBIds.has(bId)) {
|
|
2653
2632
|
ctx.addIssue({
|
|
2654
2633
|
code: zod.z.ZodIssueCode.custom,
|
|
2655
|
-
message: `
|
|
2634
|
+
message: `id "${bId}" appears in more than one answer pair`,
|
|
2656
2635
|
path: ["answers", i, 1]
|
|
2657
2636
|
});
|
|
2658
2637
|
} else {
|
|
2659
|
-
|
|
2638
|
+
usedGroupBIds.add(bId);
|
|
2660
2639
|
}
|
|
2661
2640
|
});
|
|
2662
2641
|
});
|
|
2663
|
-
function createPairMatcherBlock(name,
|
|
2642
|
+
function createPairMatcherBlock(name, groups, answers, layout) {
|
|
2664
2643
|
return PairMatcherBlockSchema.parse({
|
|
2665
2644
|
type: AST_TYPES.PAIR_MATCHER_BLOCK,
|
|
2666
2645
|
name,
|
|
2667
|
-
|
|
2668
|
-
|
|
2646
|
+
layout,
|
|
2647
|
+
groups,
|
|
2669
2648
|
answers
|
|
2670
2649
|
});
|
|
2671
2650
|
}
|
|
@@ -2852,7 +2831,7 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
2852
2831
|
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
2853
2832
|
const pm = block;
|
|
2854
2833
|
record(pm.name, ["lines", bi, "name"]);
|
|
2855
|
-
|
|
2834
|
+
pm.groups.flat().forEach((item, ii) => {
|
|
2856
2835
|
visitInlines(item.inlines, ["lines", bi, "items", ii, "inlines"]);
|
|
2857
2836
|
});
|
|
2858
2837
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
@@ -3065,24 +3044,14 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3065
3044
|
pairMatcher(ctx) {
|
|
3066
3045
|
const params = this.visit(ctx.parameterList);
|
|
3067
3046
|
const name = params.name;
|
|
3068
|
-
const
|
|
3069
|
-
const
|
|
3047
|
+
const layout = params.layout;
|
|
3048
|
+
const [firstGroup, secondGroup] = ctx.pairMatcherGroupSection;
|
|
3049
|
+
const groupAItems = this.visit(firstGroup);
|
|
3050
|
+
const groupBItems = this.visit(secondGroup);
|
|
3070
3051
|
const rawAnswers = this.visit(ctx.matcherAnswers);
|
|
3071
|
-
return createPairMatcherBlock(name,
|
|
3052
|
+
return createPairMatcherBlock(name, [groupAItems, groupBItems], rawAnswers, layout);
|
|
3072
3053
|
}
|
|
3073
|
-
|
|
3074
|
-
return this.visitMatcherItemList(ctx);
|
|
3075
|
-
}
|
|
3076
|
-
pairMatcherBottomSection(ctx) {
|
|
3077
|
-
return this.visitMatcherItemList(ctx);
|
|
3078
|
-
}
|
|
3079
|
-
/**
|
|
3080
|
-
* Maps `ctx.matcherItem` CST nodes to MatcherItem AST nodes.
|
|
3081
|
-
* Shared by pairMatcherTopSection and pairMatcherBottomSection because their
|
|
3082
|
-
* grammar rules are structurally identical — both are `matcherItem+` lists
|
|
3083
|
-
* wrapped in a labelled section keyword.
|
|
3084
|
-
*/
|
|
3085
|
-
visitMatcherItemList(ctx) {
|
|
3054
|
+
pairMatcherGroupSection(ctx) {
|
|
3086
3055
|
return ctx.matcherItem ? ctx.matcherItem.map((i) => this.visit(i)) : [];
|
|
3087
3056
|
}
|
|
3088
3057
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -3874,8 +3843,12 @@ const MatcherWidgetJSONSchema = zod.z.object({
|
|
|
3874
3843
|
});
|
|
3875
3844
|
const PairMatcherWidgetJSONSchema = zod.z.object({
|
|
3876
3845
|
type: zod.z.literal(JSON_TYPES.PAIR_MATCHER),
|
|
3877
|
-
|
|
3878
|
-
|
|
3846
|
+
/** Row layout (top/bottom, default) or column layout (left/right). */
|
|
3847
|
+
layout: zod.z.enum(["rows", "columns"]).optional(),
|
|
3848
|
+
groups: zod.z.tuple([
|
|
3849
|
+
zod.z.array(MatcherItemContentSchema).min(1),
|
|
3850
|
+
zod.z.array(MatcherItemContentSchema).min(1)
|
|
3851
|
+
])
|
|
3879
3852
|
});
|
|
3880
3853
|
const VisualFractionJSONSchema = zod.z.object({
|
|
3881
3854
|
type: zod.z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
@@ -9835,7 +9808,7 @@ function collectAllInlines(blocks) {
|
|
|
9835
9808
|
}
|
|
9836
9809
|
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
9837
9810
|
const pm = block;
|
|
9838
|
-
for (const item of
|
|
9811
|
+
for (const item of pm.groups.flat()) {
|
|
9839
9812
|
result.push(...item.inlines);
|
|
9840
9813
|
}
|
|
9841
9814
|
}
|
|
@@ -9942,10 +9915,14 @@ function buildMatcherWidget(matcher, idGenerator, inlineDragIds) {
|
|
|
9942
9915
|
return result;
|
|
9943
9916
|
}
|
|
9944
9917
|
function buildPairMatcherWidget(pm, idGenerator, inlineDragIds) {
|
|
9918
|
+
const [groupAItems, groupBItems] = pm.groups;
|
|
9945
9919
|
return {
|
|
9946
9920
|
type: JSON_TYPES.PAIR_MATCHER,
|
|
9947
|
-
|
|
9948
|
-
|
|
9921
|
+
...pm.layout !== void 0 && { layout: pm.layout },
|
|
9922
|
+
groups: [
|
|
9923
|
+
mapMatcherItemList(groupAItems, idGenerator, inlineDragIds),
|
|
9924
|
+
mapMatcherItemList(groupBItems, idGenerator, inlineDragIds)
|
|
9925
|
+
]
|
|
9949
9926
|
};
|
|
9950
9927
|
}
|
|
9951
9928
|
function transformVerticalOperation(vop, idGenerator) {
|
|
@@ -16740,7 +16717,9 @@ const PairMatcherWidget = ({
|
|
|
16740
16717
|
widgets,
|
|
16741
16718
|
shuffleItems = true
|
|
16742
16719
|
}) => {
|
|
16743
|
-
const {
|
|
16720
|
+
const { groups, layout } = widget;
|
|
16721
|
+
const [topItems, bottomItems] = groups;
|
|
16722
|
+
const isColumns = layout === "columns";
|
|
16744
16723
|
const exerciseLocked = useExerciseLocked();
|
|
16745
16724
|
const [shuffledTop] = React.useState(
|
|
16746
16725
|
() => shuffleItems ? lodashExports.shuffle([...topItems]) : [...topItems]
|
|
@@ -16917,8 +16896,8 @@ const PairMatcherWidget = ({
|
|
|
16917
16896
|
pairStatusByTopId
|
|
16918
16897
|
});
|
|
16919
16898
|
const cols = React.useMemo(
|
|
16920
|
-
() => Math.max(topItems.length, bottomItems.length, 1),
|
|
16921
|
-
[topItems.length, bottomItems.length]
|
|
16899
|
+
() => isColumns ? null : Math.max(topItems.length, bottomItems.length, 1),
|
|
16900
|
+
[isColumns, topItems.length, bottomItems.length]
|
|
16922
16901
|
);
|
|
16923
16902
|
const renderCard = (item, isTop) => {
|
|
16924
16903
|
const isRtl = shouldItemsUseRTL(item.content);
|
|
@@ -16964,8 +16943,8 @@ const PairMatcherWidget = ({
|
|
|
16964
16943
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
16965
16944
|
"div",
|
|
16966
16945
|
{
|
|
16967
|
-
className: `pair-matcher-widget${validationStatus ? ` pair-matcher-widget--${validationStatus}` : ""}`,
|
|
16968
|
-
style: { "--pm-cols": cols },
|
|
16946
|
+
className: `pair-matcher-widget${isColumns ? " pair-matcher-widget--columns" : ""}${validationStatus ? ` pair-matcher-widget--${validationStatus}` : ""}`,
|
|
16947
|
+
style: cols === null ? {} : { "--pm-cols": cols },
|
|
16969
16948
|
ref: containerRef,
|
|
16970
16949
|
role: "group",
|
|
16971
16950
|
"aria-label": "Pair matcher exercise",
|