A DSL parser and validator for math exercises - transforms text-based exercise definitions into structured AST and JSON
npm install @mathscan/math-exercise-engineA TypeScript DSL parser for mathematical exercises. Parse exercise definitions into structured AST and JSON for use in educational apps.
- Text-first DSL — Write exercises in readable text format
- Chevrotain parser — Robust tokenization and parsing
- AST-based — Clean separation between parsing and rendering
- Dual validation — Simple (predetermined answers) and expression-based
- TypeScript-first — Full type definitions included
- React renderer — Production-ready components included
- Ref-based API — Full programmatic control (submit, reset, focus, get values)
``bash`
npm install @mathscan/math-exercise-engineor
pnpm add @mathscan/math-exercise-engineor
yarn add @mathscan/math-exercise-engine
`typescript
import { compile, validate, ExerciseRenderer } from '@mathscan/math-exercise-engine';
// Compile a DSL exercise to JSON
const json = compile(
QUESTION_TEXT["What is 25 + 5?"]
25 + NUMERIC_INPUT[name="x", digits=2, answer="5"] = 30
WIDGET_ANSWER[type=simple]);
// Render in React
import '@mathscan/math-exercise-engine/style.css'; // Opt-in: import minimal default styles
// Option 1: Use built-in submit button
onSubmit={(values) => {
const result = validate(json, values);
console.log(result.isCorrect);
}}
showSubmitButton={true}
/>
// Option 2: Control submit with ref (recommended for custom styling)
const exerciseRef = useRef
json={json}
onSubmit={(values) => {
const result = validate(json, values);
console.log(result.isCorrect);
}}
/>
// Custom buttons (fully controlled by your app)
🎛️ Ref API - Programmatic Control
When using
ref for custom button styling, you get full programmatic control:`typescript
import type { ExerciseRendererRef } from '@mathscan/math-exercise-engine';const exerciseRef = useRef(null);
// Available methods:
const result = exerciseRef.current?.submit(); // Returns ValidationResult
exerciseRef.current?.getValues(); // Get current input values
exerciseRef.current?.reset(); // Clear all inputs
exerciseRef.current?.focusFirstInput(); // Focus first input field
`$3
-
submit(): Performs validation and returns ValidationResult (also triggers onSubmit callback)
- getValues(): Returns Record of all current input values
- reset(): Clears all input fields to empty state
- focusFirstInput(): Automatically focuses the first input element📝 DSL Syntax
$3
The DSL supports both legacy short names and modern readable aliases:
#### Numeric Input Widget
`
W_N_I[name=a, digits=2]
// or use the readable alias
NUMERIC_INPUT[name=a, digits=2]
`Creates an input field for numbers with up to 2 digits.
#### String Input Widget
`
W_S_I[name=x, length=5]
// or use the readable alias
STRING_INPUT[name=x, length=5]
`Creates an input field for text with up to 5 characters.
$3
`
TEXT_HIGHLIGHTER[value="123456", start=2, end=3]
`Displays text with specific characters highlighted (useful for highlighting digits in numbers).
$3
Simple mode — each widget has a predetermined answer:
`
WIDGET_ANSWER[type=simple]
`Expression mode — validate using a math expression:
`
WIDGET_ANSWER[type=expression, expr="(a b) + (c d) == 1200"]
`📚 Examples
$3
`
(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
WIDGET_ANSWER[type=expression, expr="(a b) + (c d) == 1200"]
`$3
`
NUMERIC_INPUT[name=x, digits=2] + NUMERIC_INPUT[name=y, digits=2] = 100
WIDGET_ANSWER[type=expression, expr="x + y == 100"]
`$3
`
QUESTION_TEXT["What digit is highlighted?"]
The number TEXT_HIGHLIGHTER[value="123456", start=2, end=3] has a highlighted digit
STRING_INPUT[name=answer, length=1, answer="3"]
WIDGET_ANSWER[type=simple]
`🔧 API Reference
$3
`typescript
import { compile, tokenize, parse, toAST, toJSON } from '@mathscan/math-exercise-engine';// Full pipeline (recommended)
const json = compile(dslText);
// Step by step
const tokens = tokenize(dslText);
const cst = parse(tokens);
const ast = toAST(cst);
const json = toJSON(ast);
// With both AST and JSON
const { ast, json } = compileWithAST(dslText);
`$3
`typescript
import { validate, validateSimple, validateExpression } from '@mathscan/math-exercise-engine';// Auto-detect validation type
const result = validate(exerciseJSON, userInputs);
// Manual validation
const simpleResult = validateSimple(exerciseJSON, userInputs);
const exprResult = validateExpression(exerciseJSON, userInputs);
`$3
`typescript
import type {
ExerciseJSON,
ExerciseNode,
ValidationResult,
WidgetJSON,
ExerciseRendererRef
} from '@mathscan/math-exercise-engine';// ExerciseRendererRef interface:
interface ExerciseRendererRef {
submit: () => ValidationResult; // Perform validation & return result
getValues: () => Record; // Get current values
reset: () => void; // Clear all inputs
focusFirstInput: () => void; // Focus first input
}
`📊 JSON Output Format
All layout items include unique
id fields for React/JSX rendering:`json
{
"version": "2.0",
"layout": [
{ "id": "text_1", "type": "text", "value": "25" },
{ "id": "operator_2", "type": "operator", "operator": "+" },
{ "id": "widget-ref_3", "type": "widget-ref", "widgetId": "x" },
{ "id": "operator_4", "type": "operator", "operator": "=" },
{ "id": "text_5", "type": "text", "value": "30" }
],
"widgets": {
"x": {
"type": "numeric-input",
"id": "x",
"config": {
"name": "x",
"digits": 2
}
}
},
"validation": {
"mode": "simple",
"answers": {
"x": "5"
}
}
}
`✅ Testing
`bash
pnpm test:run
``mathscan
mathscan
---