# 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

```bash
npm install @mathscan/math-exercise-engine
# or
pnpm add @mathscan/math-exercise-engine
# or
yarn add @mathscan/math-exercise-engine
```

## 🚀 Quick Start

```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
<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:

```typescript
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 field
```

### Method Details

- **`submit()`**: Performs validation and returns `ValidationResult` (also triggers `onSubmit` callback)
- **`getValues()`**: Returns `Record<string, string>` of all current input values
- **`reset()`**: Clears all input fields to empty state
- **`focusFirstInput()`**: 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]  // Alias
```

Displays 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

```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);
```

### Validation

```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);
```

### Types

```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<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:

```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
```

## 📄 License

mathscan

## 👥 Authors

mathscan

---
