@mathscan/math-exercise-engine 1.0.0
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 +268 -0
- package/dist/index.d.ts +2030 -0
- package/dist/index.js +6083 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6083 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +346 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Math Exercise Engine
|
|
2
|
+
|
|
3
|
+
A TypeScript DSL parser for mathematical exercises. Parse exercise definitions into structured AST and JSON for use in educational apps.
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- **Text-first DSL** — Write exercises in readable text format
|
|
8
|
+
- **Chevrotain parser** — Robust tokenization and parsing
|
|
9
|
+
- **AST-based** — Clean separation between parsing and rendering
|
|
10
|
+
- **Dual validation** — Simple (predetermined answers) and expression-based
|
|
11
|
+
- **TypeScript-first** — Full type definitions included
|
|
12
|
+
- **React renderer** — Production-ready components included
|
|
13
|
+
- **Ref-based API** — Full programmatic control (submit, reset, focus, get values)
|
|
14
|
+
|
|
15
|
+
## 📦 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install math-exercise-engine
|
|
19
|
+
# or
|
|
20
|
+
pnpm add math-exercise-engine
|
|
21
|
+
# or
|
|
22
|
+
yarn add math-exercise-engine
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 🚀 Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { compile, validate, ExerciseRenderer } from 'math-exercise-engine';
|
|
29
|
+
|
|
30
|
+
// Compile a DSL exercise to JSON
|
|
31
|
+
const json = compile(`
|
|
32
|
+
COMPARISON[350158 ? 2334443, answer="<"]
|
|
33
|
+
WIDGET_ANSWER[type=simple]
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
// Render in React
|
|
37
|
+
import 'math-exercise-engine/style.css'; // Opt-in: import minimal default styles
|
|
38
|
+
|
|
39
|
+
// Option 1: Use built-in submit button
|
|
40
|
+
<ExerciseRenderer
|
|
41
|
+
json={json}
|
|
42
|
+
onSubmit={(values) => {
|
|
43
|
+
const result = validate(json, values);
|
|
44
|
+
console.log(result.isCorrect);
|
|
45
|
+
}}
|
|
46
|
+
showSubmitButton={true}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
// Option 2: Control submit with ref (recommended for custom styling)
|
|
50
|
+
const exerciseRef = useRef<ExerciseRendererRef>(null);
|
|
51
|
+
|
|
52
|
+
<ExerciseRenderer
|
|
53
|
+
ref={exerciseRef}
|
|
54
|
+
json={json}
|
|
55
|
+
onSubmit={(values) => {
|
|
56
|
+
const result = validate(json, values);
|
|
57
|
+
console.log(result.isCorrect);
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
// Custom buttons (fully controlled by your app)
|
|
62
|
+
<div>
|
|
63
|
+
<button
|
|
64
|
+
className="my-custom-submit-btn"
|
|
65
|
+
onClick={() => {
|
|
66
|
+
const result = exerciseRef.current?.submit();
|
|
67
|
+
console.log('Validation result:', result);
|
|
68
|
+
if (result?.isCorrect) {
|
|
69
|
+
alert('Correct! 🎉');
|
|
70
|
+
} else {
|
|
71
|
+
alert('Try again! ❌');
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Check My Answer
|
|
76
|
+
</button>
|
|
77
|
+
<button onClick={() => exerciseRef.current?.reset()}>
|
|
78
|
+
Reset Exercise
|
|
79
|
+
</button>
|
|
80
|
+
<button onClick={() => exerciseRef.current?.focusFirstInput()}>
|
|
81
|
+
Focus First Input
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 🎛️ Ref API - Programmatic Control
|
|
87
|
+
|
|
88
|
+
When using `ref` for custom button styling, you get full programmatic control:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import type { ExerciseRendererRef } from 'math-exercise-engine';
|
|
92
|
+
|
|
93
|
+
const exerciseRef = useRef<ExerciseRendererRef>(null);
|
|
94
|
+
|
|
95
|
+
// Available methods:
|
|
96
|
+
const result = exerciseRef.current?.submit(); // Returns ValidationResult
|
|
97
|
+
exerciseRef.current?.getValues(); // Get current input values
|
|
98
|
+
exerciseRef.current?.reset(); // Clear all inputs
|
|
99
|
+
exerciseRef.current?.focusFirstInput(); // Focus first input field
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Method Details
|
|
103
|
+
|
|
104
|
+
- **`submit()`**: Performs validation and returns `ValidationResult` (also triggers `onSubmit` callback)
|
|
105
|
+
- **`getValues()`**: Returns `Record<string, string>` of all current input values
|
|
106
|
+
- **`reset()`**: Clears all input fields to empty state
|
|
107
|
+
- **`focusFirstInput()`**: Automatically focuses the first input element
|
|
108
|
+
|
|
109
|
+
## 📝 DSL Syntax
|
|
110
|
+
|
|
111
|
+
### Numeric Input Widget
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
W_N_I[name=a, digits=2]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Creates an input field for numbers with up to 2 digits.
|
|
118
|
+
|
|
119
|
+
### Comparison Widget
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
COMPARISON[350158 ? 2334443, answer="<"]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Student must choose `<`, `>`, or `=` to compare two numbers.
|
|
126
|
+
|
|
127
|
+
### Validators
|
|
128
|
+
|
|
129
|
+
**Simple mode** — each widget has a predetermined answer:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
WIDGET_ANSWER[type=simple]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Expression mode** — validate using a math expression:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 📚 Examples
|
|
142
|
+
|
|
143
|
+
### Example 1: Simple Comparison
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
COMPARISON[350158 ? 2334443, answer="<"]
|
|
147
|
+
WIDGET_ANSWER[type=simple]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Example 2: Math Expression
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
(W_N_I[name=a, digits=2] × W_N_I[name=b, digits=2]) + (W_N_I[name=c, digits=2] × W_N_I[name=d, digits=2]) = 1200
|
|
154
|
+
WIDGET_ANSWER[type=expression, expr="(a * b) + (c * d) == 1200"]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Example 3: Simple Addition
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
W_N_I[name=x, digits=2] + W_N_I[name=y, digits=2] = 100
|
|
161
|
+
WIDGET_ANSWER[type=expression, expr="x + y == 100"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🔧 API Reference
|
|
165
|
+
|
|
166
|
+
### Parsing
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { compile, tokenize, parse, toAST, toJSON } from 'math-exercise-engine';
|
|
170
|
+
|
|
171
|
+
// Full pipeline (recommended)
|
|
172
|
+
const json = compile(dslText);
|
|
173
|
+
|
|
174
|
+
// Step by step
|
|
175
|
+
const tokens = tokenize(dslText);
|
|
176
|
+
const cst = parse(tokens);
|
|
177
|
+
const ast = toAST(cst);
|
|
178
|
+
const json = toJSON(ast);
|
|
179
|
+
|
|
180
|
+
// With both AST and JSON
|
|
181
|
+
const { ast, json } = compileWithAST(dslText);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Validation
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { validate, validateSimple, validateExpression } from 'math-exercise-engine';
|
|
188
|
+
|
|
189
|
+
// Auto-detect validation type
|
|
190
|
+
const result = validate(exerciseJSON, userInputs);
|
|
191
|
+
|
|
192
|
+
// Manual validation
|
|
193
|
+
const simpleResult = validateSimple(exerciseJSON, userInputs);
|
|
194
|
+
const exprResult = validateExpression(exerciseJSON, userInputs);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Types
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import type {
|
|
201
|
+
ExerciseJSON,
|
|
202
|
+
ExerciseNode,
|
|
203
|
+
ValidationResult,
|
|
204
|
+
WidgetJSON,
|
|
205
|
+
ExerciseRendererRef
|
|
206
|
+
} from 'math-exercise-engine';
|
|
207
|
+
|
|
208
|
+
// ExerciseRendererRef interface:
|
|
209
|
+
interface ExerciseRendererRef {
|
|
210
|
+
submit: () => ValidationResult; // Perform validation & return result
|
|
211
|
+
getValues: () => Record<string, string>; // Get current values
|
|
212
|
+
reset: () => void; // Clear all inputs
|
|
213
|
+
focusFirstInput: () => void; // Focus first input
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 📊 JSON Output Format
|
|
218
|
+
|
|
219
|
+
All layout items include unique `id` fields for React/JSX rendering:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"version": "2.0",
|
|
224
|
+
"layout": [
|
|
225
|
+
{ "id": "text_1", "type": "text", "value": "350158" },
|
|
226
|
+
{ "id": "widget-ref_2", "type": "widget-ref", "widgetId": "comparison_1" },
|
|
227
|
+
{ "id": "text_3", "type": "text", "value": "2334443" }
|
|
228
|
+
],
|
|
229
|
+
"widgets": {
|
|
230
|
+
"comparison_1": {
|
|
231
|
+
"type": "comparison",
|
|
232
|
+
"id": "comparison_1",
|
|
233
|
+
"config": {
|
|
234
|
+
"left": 350158,
|
|
235
|
+
"right": 2334443
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"validation": {
|
|
240
|
+
"mode": "simple",
|
|
241
|
+
"answers": {
|
|
242
|
+
"comparison_1": "<"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
> 💡 **New:** Layout items now include unique IDs perfect for React keys. [Learn more](docs/LAYOUT_IDS.md)
|
|
249
|
+
|
|
250
|
+
## ✅ Testing
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
pnpm test
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
All 83 tests passing ✅
|
|
257
|
+
|
|
258
|
+
## 📄 License
|
|
259
|
+
|
|
260
|
+
[Your License]
|
|
261
|
+
|
|
262
|
+
## 👥 Authors
|
|
263
|
+
|
|
264
|
+
[Your Name]
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
**Status**: Production ready. All tests passing.
|