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