@mathscan/math-exercise-engine
v1.0.4
Published
A DSL parser and validator for math exercises - transforms text-based exercise definitions into structured AST and JSON
Downloads
505
Readme
Math Exercise Engine
A TypeScript DSL parser for mathematical exercises. Parse exercise definitions into structured AST and JSON for use in educational apps.
✨ Features
- 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)
📦 Installation
npm install @mathscan/math-exercise-engine
# or
pnpm add @mathscan/math-exercise-engine
# or
yarn add @mathscan/math-exercise-engine🚀 Quick Start
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
<ExerciseRenderer
json={json}
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<ExerciseRendererRef>(null);
<ExerciseRenderer
ref={exerciseRef}
json={json}
onSubmit={(values) => {
const result = validate(json, values);
console.log(result.isCorrect);
}}
/>
// Custom buttons (fully controlled by your app)
<div>
<button
className="my-custom-submit-btn"
onClick={() => {
const result = exerciseRef.current?.submit();
console.log('Validation result:', result);
if (result?.isCorrect) {
alert('Correct! 🎉');
} else {
alert('Try again! ❌');
}
}}
>
Check My Answer
</button>
<button onClick={() => exerciseRef.current?.reset()}>
Reset Exercise
</button>
<button onClick={() => exerciseRef.current?.focusFirstInput()}>
Focus First Input
</button>
</div>🎛️ Ref API - Programmatic Control
When using ref for custom button styling, you get full programmatic control:
import type { ExerciseRendererRef } from '@mathscan/math-exercise-engine';
const exerciseRef = useRef<ExerciseRendererRef>(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 fieldMethod Details
submit(): Performs validation and returnsValidationResult(also triggersonSubmitcallback)getValues(): ReturnsRecord<string, string>of all current input valuesreset(): Clears all input fields to empty statefocusFirstInput(): Automatically focuses the first input element
📝 DSL Syntax
Input Widgets
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.
Text Highlighter
TEXT_HIGHLIGHTER[value="123456", start=2, end=3]
HIGHLIGHT_TEXT[value="123456", start=2, end=3] // AliasDisplays text with specific characters highlighted (useful for highlighting digits in numbers).
Both TEXT_HIGHLIGHTER and HIGHLIGHT_TEXT are supported (aliases).
Validators
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
Example 1: Math Expression (using readable aliases)
(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"]Example 2: Simple Addition
NUMERIC_INPUT[name=x, digits=2] + NUMERIC_INPUT[name=y, digits=2] = 100
WIDGET_ANSWER[type=expression, expr="x + y == 100"]Example 3: Text Highlighter
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
Parsing
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);Validation
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);Types
import type {
ExerciseJSON,
ExerciseNode,
ValidationResult,
WidgetJSON,
ExerciseRendererRef
} from '@mathscan/math-exercise-engine';
// ExerciseRendererRef interface:
interface ExerciseRendererRef {
submit: () => ValidationResult; // Perform validation & return result
getValues: () => Record<string, string>; // 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:
{
"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
pnpm test:run📄 License
mathscan
👥 Authors
mathscan
