@mathscan/math-exercise-engine 1.0.1 → 1.0.2
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/README.md +56 -41
- package/dist/index.d.ts +1 -22
- package/dist/index.js +29 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -31
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +1 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -15,26 +15,27 @@ A TypeScript DSL parser for mathematical exercises. Parse exercise definitions i
|
|
|
15
15
|
## 📦 Installation
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install math-exercise-engine
|
|
18
|
+
npm install @mathscan/math-exercise-engine
|
|
19
19
|
# or
|
|
20
|
-
pnpm add math-exercise-engine
|
|
20
|
+
pnpm add @mathscan/math-exercise-engine
|
|
21
21
|
# or
|
|
22
|
-
yarn add math-exercise-engine
|
|
22
|
+
yarn add @mathscan/math-exercise-engine
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## 🚀 Quick Start
|
|
26
26
|
|
|
27
27
|
```typescript
|
|
28
|
-
import { compile, validate, ExerciseRenderer } from 'math-exercise-engine';
|
|
28
|
+
import { compile, validate, ExerciseRenderer } from '@mathscan/math-exercise-engine';
|
|
29
29
|
|
|
30
30
|
// Compile a DSL exercise to JSON
|
|
31
31
|
const json = compile(`
|
|
32
|
-
|
|
32
|
+
QUESTION_TEXT["What is 25 + 5?"]
|
|
33
|
+
25 + NUMERIC_INPUT[name="x", digits=2, answer="5"] = 30
|
|
33
34
|
WIDGET_ANSWER[type=simple]
|
|
34
35
|
`);
|
|
35
36
|
|
|
36
37
|
// Render in React
|
|
37
|
-
import 'math-exercise-engine/style.css'; // Opt-in: import minimal default styles
|
|
38
|
+
import '@mathscan/math-exercise-engine/style.css'; // Opt-in: import minimal default styles
|
|
38
39
|
|
|
39
40
|
// Option 1: Use built-in submit button
|
|
40
41
|
<ExerciseRenderer
|
|
@@ -88,7 +89,7 @@ const exerciseRef = useRef<ExerciseRendererRef>(null);
|
|
|
88
89
|
When using `ref` for custom button styling, you get full programmatic control:
|
|
89
90
|
|
|
90
91
|
```typescript
|
|
91
|
-
import type { ExerciseRendererRef } from 'math-exercise-engine';
|
|
92
|
+
import type { ExerciseRendererRef } from '@mathscan/math-exercise-engine';
|
|
92
93
|
|
|
93
94
|
const exerciseRef = useRef<ExerciseRendererRef>(null);
|
|
94
95
|
|
|
@@ -108,21 +109,37 @@ exerciseRef.current?.focusFirstInput(); // Focus first input field
|
|
|
108
109
|
|
|
109
110
|
## 📝 DSL Syntax
|
|
110
111
|
|
|
111
|
-
###
|
|
112
|
+
### Input Widgets
|
|
113
|
+
|
|
114
|
+
The DSL supports both legacy short names and modern readable aliases:
|
|
115
|
+
|
|
116
|
+
#### Numeric Input Widget
|
|
112
117
|
|
|
113
118
|
```
|
|
114
119
|
W_N_I[name=a, digits=2]
|
|
120
|
+
// or use the readable alias
|
|
121
|
+
NUMERIC_INPUT[name=a, digits=2]
|
|
115
122
|
```
|
|
116
123
|
|
|
117
124
|
Creates an input field for numbers with up to 2 digits.
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
#### String Input Widget
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
W_S_I[name=x, length=5]
|
|
130
|
+
// or use the readable alias
|
|
131
|
+
STRING_INPUT[name=x, length=5]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Creates an input field for text with up to 5 characters.
|
|
135
|
+
|
|
136
|
+
### Text Highlighter
|
|
120
137
|
|
|
121
138
|
```
|
|
122
|
-
|
|
139
|
+
TEXT_HIGHLIGHTER[value="123456", start=2, end=3]
|
|
123
140
|
```
|
|
124
141
|
|
|
125
|
-
|
|
142
|
+
Displays text with specific characters highlighted (useful for highlighting digits in numbers).
|
|
126
143
|
|
|
127
144
|
### Validators
|
|
128
145
|
|
|
@@ -140,25 +157,27 @@ WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
|
|
|
140
157
|
|
|
141
158
|
## 📚 Examples
|
|
142
159
|
|
|
143
|
-
### Example 1:
|
|
160
|
+
### Example 1: Math Expression (using readable aliases)
|
|
144
161
|
|
|
145
162
|
```
|
|
146
|
-
|
|
147
|
-
WIDGET_ANSWER[type=
|
|
163
|
+
(NUMERIC_INPUT[name=a, digits=2] × NUMERIC_INPUT[name=b, digits=2]) + (NUMERIC_INPUT[name=c, digits=2] × NUMERIC_INPUT[name=d, digits=2]) = 1200
|
|
164
|
+
WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
|
|
148
165
|
```
|
|
149
166
|
|
|
150
|
-
### Example 2:
|
|
167
|
+
### Example 2: Simple Addition
|
|
151
168
|
|
|
152
169
|
```
|
|
153
|
-
|
|
154
|
-
WIDGET_ANSWER[type=expression, expr="
|
|
170
|
+
NUMERIC_INPUT[name=x, digits=2] + NUMERIC_INPUT[name=y, digits=2] = 100
|
|
171
|
+
WIDGET_ANSWER[type=expression, expr="x + y == 100"]
|
|
155
172
|
```
|
|
156
173
|
|
|
157
|
-
### Example 3:
|
|
174
|
+
### Example 3: Text Highlighter
|
|
158
175
|
|
|
159
176
|
```
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
QUESTION_TEXT["What digit is highlighted?"]
|
|
178
|
+
The number TEXT_HIGHLIGHTER[value="123456", start=2, end=3] has a highlighted digit
|
|
179
|
+
STRING_INPUT[name=answer, length=1, answer="3"]
|
|
180
|
+
WIDGET_ANSWER[type=simple]
|
|
162
181
|
```
|
|
163
182
|
|
|
164
183
|
## 🔧 API Reference
|
|
@@ -166,7 +185,7 @@ WIDGET_ANSWER[type=expression, expr="x + y == 100"]
|
|
|
166
185
|
### Parsing
|
|
167
186
|
|
|
168
187
|
```typescript
|
|
169
|
-
import { compile, tokenize, parse, toAST, toJSON } from 'math-exercise-engine';
|
|
188
|
+
import { compile, tokenize, parse, toAST, toJSON } from '@mathscan/math-exercise-engine';
|
|
170
189
|
|
|
171
190
|
// Full pipeline (recommended)
|
|
172
191
|
const json = compile(dslText);
|
|
@@ -184,7 +203,7 @@ const { ast, json } = compileWithAST(dslText);
|
|
|
184
203
|
### Validation
|
|
185
204
|
|
|
186
205
|
```typescript
|
|
187
|
-
import { validate, validateSimple, validateExpression } from 'math-exercise-engine';
|
|
206
|
+
import { validate, validateSimple, validateExpression } from '@mathscan/math-exercise-engine';
|
|
188
207
|
|
|
189
208
|
// Auto-detect validation type
|
|
190
209
|
const result = validate(exerciseJSON, userInputs);
|
|
@@ -203,7 +222,7 @@ import type {
|
|
|
203
222
|
ValidationResult,
|
|
204
223
|
WidgetJSON,
|
|
205
224
|
ExerciseRendererRef
|
|
206
|
-
} from 'math-exercise-engine';
|
|
225
|
+
} from '@mathscan/math-exercise-engine';
|
|
207
226
|
|
|
208
227
|
// ExerciseRendererRef interface:
|
|
209
228
|
interface ExerciseRendererRef {
|
|
@@ -222,47 +241,43 @@ All layout items include unique `id` fields for React/JSX rendering:
|
|
|
222
241
|
{
|
|
223
242
|
"version": "2.0",
|
|
224
243
|
"layout": [
|
|
225
|
-
{ "id": "text_1", "type": "text", "value": "
|
|
226
|
-
{ "id": "
|
|
227
|
-
{ "id": "
|
|
244
|
+
{ "id": "text_1", "type": "text", "value": "25" },
|
|
245
|
+
{ "id": "operator_2", "type": "operator", "value": "+" },
|
|
246
|
+
{ "id": "widget-ref_3", "type": "widget-ref", "widgetId": "x" },
|
|
247
|
+
{ "id": "operator_4", "type": "operator", "value": "=" },
|
|
248
|
+
{ "id": "text_5", "type": "text", "value": "30" }
|
|
228
249
|
],
|
|
229
250
|
"widgets": {
|
|
230
|
-
"
|
|
231
|
-
"type": "
|
|
232
|
-
"id": "
|
|
251
|
+
"x": {
|
|
252
|
+
"type": "numeric-input",
|
|
253
|
+
"id": "x",
|
|
233
254
|
"config": {
|
|
234
|
-
"
|
|
235
|
-
"
|
|
255
|
+
"name": "x",
|
|
256
|
+
"digits": 2
|
|
236
257
|
}
|
|
237
258
|
}
|
|
238
259
|
},
|
|
239
260
|
"validation": {
|
|
240
261
|
"mode": "simple",
|
|
241
262
|
"answers": {
|
|
242
|
-
"
|
|
263
|
+
"x": "5"
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
}
|
|
246
267
|
```
|
|
247
268
|
|
|
248
|
-
> 💡 **New:** Layout items now include unique IDs perfect for React keys. [Learn more](docs/LAYOUT_IDS.md)
|
|
249
|
-
|
|
250
269
|
## ✅ Testing
|
|
251
270
|
|
|
252
271
|
```bash
|
|
253
|
-
pnpm test
|
|
272
|
+
pnpm test:run
|
|
254
273
|
```
|
|
255
274
|
|
|
256
|
-
All 83 tests passing ✅
|
|
257
|
-
|
|
258
275
|
## 📄 License
|
|
259
276
|
|
|
260
|
-
|
|
277
|
+
mathscan
|
|
261
278
|
|
|
262
279
|
## 👥 Authors
|
|
263
280
|
|
|
264
|
-
|
|
281
|
+
mathscan
|
|
265
282
|
|
|
266
283
|
---
|
|
267
|
-
|
|
268
|
-
**Status**: Production ready. All tests passing.
|
package/dist/index.d.ts
CHANGED
|
@@ -137,11 +137,6 @@ export declare function createExpressionValidation(expression: string): Expressi
|
|
|
137
137
|
*/
|
|
138
138
|
export declare function createFreeText(value: string): FreeTextInline;
|
|
139
139
|
|
|
140
|
-
/**
|
|
141
|
-
* @deprecated Use createStringInput or createNumericInput instead
|
|
142
|
-
*/
|
|
143
|
-
export declare function createInlineInput(name: string, inputType: "string" | "numeric", lengthOrDigits: number, answer?: string | number): InlineInputWidget;
|
|
144
|
-
|
|
145
140
|
/**
|
|
146
141
|
* Create a line block
|
|
147
142
|
*/
|
|
@@ -1025,13 +1020,6 @@ export declare function isNumberText(item: LayoutItem): item is Extract<LayoutIt
|
|
|
1025
1020
|
type: 'number-text';
|
|
1026
1021
|
}>;
|
|
1027
1022
|
|
|
1028
|
-
/**
|
|
1029
|
-
* @deprecated Use isInlineInputJSON instead
|
|
1030
|
-
*/
|
|
1031
|
-
export declare function isNumericInputJSON(widget: WidgetJSON): widget is Extract<WidgetJSON, {
|
|
1032
|
-
type: 'inline-input';
|
|
1033
|
-
}>;
|
|
1034
|
-
|
|
1035
1023
|
export declare function isNumericKeypad(value: unknown): value is NumericKeypadTool;
|
|
1036
1024
|
|
|
1037
1025
|
/**
|
|
@@ -1080,13 +1068,6 @@ export declare function isValidationNode(value: unknown): value is ValidationNod
|
|
|
1080
1068
|
*/
|
|
1081
1069
|
export declare function isValidExpression(expression: string): boolean;
|
|
1082
1070
|
|
|
1083
|
-
/**
|
|
1084
|
-
* @deprecated Use isWidgetRef instead
|
|
1085
|
-
*/
|
|
1086
|
-
export declare function isWidgetPlaceholder(item: LayoutItem): item is Extract<LayoutItem, {
|
|
1087
|
-
type: 'widget-ref';
|
|
1088
|
-
}>;
|
|
1089
|
-
|
|
1090
1071
|
/**
|
|
1091
1072
|
* Type guard for WidgetRef
|
|
1092
1073
|
*/
|
|
@@ -1975,9 +1956,7 @@ export declare const WidgetJSONSchema: z.ZodUnion<[z.ZodDiscriminatedUnion<"inpu
|
|
|
1975
1956
|
includeDecimal: boolean;
|
|
1976
1957
|
}>]>;
|
|
1977
1958
|
|
|
1978
|
-
declare type WidgetRef = z.infer<typeof WidgetRefSchema>;
|
|
1979
|
-
export { WidgetRef as WidgetPlaceholder }
|
|
1980
|
-
export { WidgetRef }
|
|
1959
|
+
export declare type WidgetRef = z.infer<typeof WidgetRefSchema>;
|
|
1981
1960
|
|
|
1982
1961
|
export declare const WidgetRefSchema: z.ZodObject<{
|
|
1983
1962
|
id: z.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -31,14 +31,18 @@ const QUESTION_TEXT = chevrotain.createToken({
|
|
|
31
31
|
name: "QUESTION_TEXT",
|
|
32
32
|
pattern: /QUESTION_TEXT/
|
|
33
33
|
});
|
|
34
|
-
const
|
|
35
|
-
name: "
|
|
36
|
-
pattern: /
|
|
34
|
+
const TEXT_HIGHLIGHTER = chevrotain.createToken({
|
|
35
|
+
name: "TEXT_HIGHLIGHTER",
|
|
36
|
+
pattern: /TEXT_HIGHLIGHTER/
|
|
37
37
|
});
|
|
38
38
|
const W_S_I = chevrotain.createToken({
|
|
39
39
|
name: "W_S_I",
|
|
40
40
|
pattern: /W_S_I/
|
|
41
41
|
});
|
|
42
|
+
const STRING_INPUT = chevrotain.createToken({
|
|
43
|
+
name: "STRING_INPUT",
|
|
44
|
+
pattern: /STRING_INPUT/
|
|
45
|
+
});
|
|
42
46
|
const DRAGABLE_BUTTONS_OPTIONS = chevrotain.createToken({
|
|
43
47
|
name: "DRAGABLE_BUTTONS_OPTIONS",
|
|
44
48
|
pattern: /DRAGABLE_BUTTONS_OPTIONS/
|
|
@@ -55,6 +59,10 @@ const W_N_I = chevrotain.createToken({
|
|
|
55
59
|
name: "W_N_I",
|
|
56
60
|
pattern: /W_N_I/
|
|
57
61
|
});
|
|
62
|
+
const NUMERIC_INPUT = chevrotain.createToken({
|
|
63
|
+
name: "NUMERIC_INPUT",
|
|
64
|
+
pattern: /NUMERIC_INPUT/
|
|
65
|
+
});
|
|
58
66
|
const NumberLiteral = chevrotain.createToken({
|
|
59
67
|
name: "NumberLiteral",
|
|
60
68
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -137,13 +145,14 @@ const allTokens = [
|
|
|
137
145
|
Comment,
|
|
138
146
|
// Component keywords (before Identifier!)
|
|
139
147
|
QUESTION_TEXT,
|
|
140
|
-
|
|
148
|
+
TEXT_HIGHLIGHTER,
|
|
141
149
|
W_S_I,
|
|
150
|
+
STRING_INPUT,
|
|
142
151
|
DRAGABLE_BUTTONS_OPTIONS,
|
|
143
152
|
NUMERIC_KEYPAD,
|
|
144
153
|
WIDGET_ANSWER,
|
|
145
|
-
// Legacy keywords
|
|
146
154
|
W_N_I,
|
|
155
|
+
NUMERIC_INPUT,
|
|
147
156
|
// Literals
|
|
148
157
|
NumberLiteral,
|
|
149
158
|
StringLiteral,
|
|
@@ -321,14 +330,14 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
321
330
|
// Parentheses (for grouping)
|
|
322
331
|
{ ALT: () => this.CONSUME(LParen) },
|
|
323
332
|
{ ALT: () => this.CONSUME(RParen) },
|
|
324
|
-
//
|
|
325
|
-
{ ALT: () => this.SUBRULE(this.
|
|
333
|
+
// TEXT_HIGHLIGHTER (for highlighting)
|
|
334
|
+
{ ALT: () => this.SUBRULE(this.textHighlighter) },
|
|
326
335
|
// W_S_I (inline input widget)
|
|
327
336
|
{ ALT: () => this.SUBRULE(this.inlineInput) }
|
|
328
337
|
]);
|
|
329
338
|
});
|
|
330
|
-
this.
|
|
331
|
-
this.CONSUME(
|
|
339
|
+
this.textHighlighter = this.RULE("textHighlighter", () => {
|
|
340
|
+
this.CONSUME(TEXT_HIGHLIGHTER);
|
|
332
341
|
this.CONSUME(LBracket);
|
|
333
342
|
this.SUBRULE(this.parameterList);
|
|
334
343
|
this.CONSUME(RBracket);
|
|
@@ -336,7 +345,9 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
336
345
|
this.inlineInput = this.RULE("inlineInput", () => {
|
|
337
346
|
this.OR([
|
|
338
347
|
{ ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
|
|
339
|
-
{ ALT: () => this.CONSUME(
|
|
348
|
+
{ ALT: () => this.CONSUME(STRING_INPUT, { LABEL: "stringInput" }) },
|
|
349
|
+
{ ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) },
|
|
350
|
+
{ ALT: () => this.CONSUME(NUMERIC_INPUT, { LABEL: "numericInput" }) }
|
|
340
351
|
]);
|
|
341
352
|
this.CONSUME(LBracket);
|
|
342
353
|
this.SUBRULE(this.parameterList);
|
|
@@ -529,13 +540,6 @@ function createNumericInput(name, digits, answer) {
|
|
|
529
540
|
answer
|
|
530
541
|
});
|
|
531
542
|
}
|
|
532
|
-
function createInlineInput(name, inputType, lengthOrDigits, answer) {
|
|
533
|
-
if (inputType === "string") {
|
|
534
|
-
return createStringInput(name, lengthOrDigits, answer);
|
|
535
|
-
} else {
|
|
536
|
-
return createNumericInput(name, lengthOrDigits, answer);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
543
|
function isFreeText(value) {
|
|
540
544
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
541
545
|
}
|
|
@@ -754,18 +758,21 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
754
758
|
const token = ctx.RParen[0];
|
|
755
759
|
return createFreeText(token.image);
|
|
756
760
|
}
|
|
757
|
-
if (ctx.
|
|
758
|
-
return this.visit(ctx.
|
|
761
|
+
if (ctx.textHighlighter) {
|
|
762
|
+
return this.visit(ctx.textHighlighter);
|
|
759
763
|
}
|
|
760
764
|
if (ctx.inlineInput) {
|
|
761
765
|
return this.visit(ctx.inlineInput);
|
|
762
766
|
}
|
|
763
767
|
throw new Error("Unknown inline element");
|
|
764
768
|
}
|
|
765
|
-
|
|
769
|
+
textHighlighter(ctx) {
|
|
766
770
|
const params = this.visit(ctx.parameterList);
|
|
767
771
|
const value = params.value;
|
|
768
|
-
|
|
772
|
+
let highlight = void 0;
|
|
773
|
+
if (params.start !== void 0 && params.end !== void 0) {
|
|
774
|
+
highlight = [params.start, params.end];
|
|
775
|
+
}
|
|
769
776
|
return createNumberText(value, highlight);
|
|
770
777
|
}
|
|
771
778
|
inlineInput(ctx) {
|
|
@@ -1024,12 +1031,6 @@ function isDragOptionsJSON(widget) {
|
|
|
1024
1031
|
function isNumericKeypadJSON(widget) {
|
|
1025
1032
|
return widget.type === "numeric-keypad";
|
|
1026
1033
|
}
|
|
1027
|
-
function isWidgetPlaceholder(item) {
|
|
1028
|
-
return item.type === "widget-ref";
|
|
1029
|
-
}
|
|
1030
|
-
function isNumericInputJSON(widget) {
|
|
1031
|
-
return widget.type === "inline-input";
|
|
1032
|
-
}
|
|
1033
1034
|
class LayoutIdGenerator {
|
|
1034
1035
|
constructor() {
|
|
1035
1036
|
this.counter = 0;
|
|
@@ -5745,7 +5746,7 @@ const LayoutRenderer = ({
|
|
|
5745
5746
|
const [start, end] = item.highlight;
|
|
5746
5747
|
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "number-text", children: [
|
|
5747
5748
|
item.value.slice(0, start),
|
|
5748
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
5749
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
|
|
5749
5750
|
item.value.slice(end)
|
|
5750
5751
|
] });
|
|
5751
5752
|
}
|
|
@@ -6025,7 +6026,6 @@ exports.createDragOptions = createDragOptions;
|
|
|
6025
6026
|
exports.createExerciseDocument = createExerciseDocument;
|
|
6026
6027
|
exports.createExpressionValidation = createExpressionValidation;
|
|
6027
6028
|
exports.createFreeText = createFreeText;
|
|
6028
|
-
exports.createInlineInput = createInlineInput;
|
|
6029
6029
|
exports.createLineBlock = createLineBlock;
|
|
6030
6030
|
exports.createNumberText = createNumberText;
|
|
6031
6031
|
exports.createNumericInput = createNumericInput;
|
|
@@ -6050,7 +6050,6 @@ exports.isInlineNode = isInlineNode;
|
|
|
6050
6050
|
exports.isLineBlock = isLineBlock;
|
|
6051
6051
|
exports.isLinebreak = isLinebreak;
|
|
6052
6052
|
exports.isNumberText = isNumberText;
|
|
6053
|
-
exports.isNumericInputJSON = isNumericInputJSON;
|
|
6054
6053
|
exports.isNumericKeypad = isNumericKeypad;
|
|
6055
6054
|
exports.isNumericKeypadJSON = isNumericKeypadJSON;
|
|
6056
6055
|
exports.isOperatorItem = isOperatorItem;
|
|
@@ -6062,7 +6061,6 @@ exports.isTextItem = isTextItem;
|
|
|
6062
6061
|
exports.isToolNode = isToolNode;
|
|
6063
6062
|
exports.isValidExpression = isValidExpression;
|
|
6064
6063
|
exports.isValidationNode = isValidationNode;
|
|
6065
|
-
exports.isWidgetPlaceholder = isWidgetPlaceholder;
|
|
6066
6064
|
exports.isWidgetRef = isWidgetRef;
|
|
6067
6065
|
exports.parse = parse;
|
|
6068
6066
|
exports.parseSafe = parseSafe;
|