npm package: @math-scan/exercise-renderer
Description: A flexible and scalable React package for rendering interactive math exercises
Author: MathScan Team
Latest version: 1.0.1

# @mathscan/exercise-renderer

A flexible and scalable React package for rendering interactive math exercises with TypeScript and Zod validation.

## Features

- 🎯 **Multiple Widget Types**: Numeric input, fractions, divisions, number lines, and more
- 🎨 **Beautiful UI**: Pre-styled with Tailwind CSS
- 🔄 **Flexible Modes**: Support for both typing and drag-and-drop interactions
- ✅ **Built-in Grading**: Automatic answer validation with customizable grading logic
- 📝 **Template-Based**: Parse math expressions from simple template strings
- 🔒 **Type-Safe**: Full TypeScript support with Zod schemas
- 🎭 **Extensible**: Easy to add custom widgets and grading logic
- 🌍 **RTL Support**: Built-in support for right-to-left languages

## Installation

```bash
npm install @mathscan/exercise-renderer react react-dom
# or
yarn add @mathscan/exercise-renderer react react-dom
# or
pnpm add @mathscan/exercise-renderer react react-dom
```

**Note:** This package requires Tailwind CSS to be configured in your project.

## Quick Start

```tsx
import { ExerciseRenderer, gradeExercise } from '@mathscan/exercise-renderer';
import { useState } from 'react';

function App() {
  const [answers, setAnswers] = useState({});

  const exercise = {
    type: "doc",
    content: [
      {
        type: "paragraph",
        attrs: { align: "center" },
        content: [
          {
            type: "widget",
            attrs: {
              _uid: "ex1",
              id: "numeric_input",
              label: "Simple Addition",
              description: "Basic math problem",
              content: {
                template: "15 + 5 = ?",
                answers: ["20"],
                options: {
                  mode: "type"
                }
              }
            }
          }
        ]
      }
    ]
  };

  const handleAnswerChange = (key: string, value: string) => {
    setAnswers(prev => ({ ...prev, [key]: value }));
  };

  const handleCheck = () => {
    const widget = exercise.content[0].content[0];
    const isCorrect = gradeExercise(widget, answers);
    alert(isCorrect ? 'Correct!' : 'Try again!');
  };

  return (
    <div className="p-8">
      <ExerciseRenderer
        exercise={exercise}
        answers={answers}
        onAnswerChange={handleAnswerChange}
      />
      <button
        onClick={handleCheck}
        className="mt-4 px-6 py-2 bg-purple-600 text-white rounded-lg"
      >
        Check Answer
      </button>
    </div>
  );
}
```

## Widget Types

### 1. Numeric Input

Simple numeric input with support for single or multiple blanks.

```tsx
{
  type: "widget",
  attrs: {
    id: "numeric_input",
    content: {
      template: "? + ? = 10",
      answers: ["5", "5"], // or multiple solutions: [["1","9"], ["2","8"], ...]
      options: {
        mode: "type", // or "drag"
        pool: ["1", "2", "3", "4", "5"], // for drag mode
      }
    }
  }
}
```

### 2. Fraction Input

Handle fraction expressions with automatic equivalence checking.

```tsx
{
  type: "widget",
  attrs: {
    id: "fraction_input",
    content: {
      template: "2 × 1/4 = ?/?",
      answers: ["2/4", "1/2"],
      options: {
        acceptEquivalent: true // Accept equivalent fractions
      }
    }
  }
}
```

### 3. Vertical Operations

Display operations vertically for traditional arithmetic format.

```tsx
{
  type: "widget",
  attrs: {
    id: "numeric_input",
    content: {
      template: "15 + 225 + 5 = ???",
      answers: ["245"],
      options: {
        mode: "type",
        verticalDisplay: true
      }
    }
  }
}
```

### 4. Division with Remainder

Handle division problems with quotient and remainder.

```tsx
{
  type: "widget",
  attrs: {
    id: "division_input",
    content: {
      template: "629 ÷ 4 = ? R ?",
      answers: ["157", "1"],
      options: {
        labels: ["Quotient", "Remainder"]
      }
    }
  }
}
```

### 5. Number Line

Interactive number line with positionable inputs.

```tsx
{
  type: "widget",
  attrs: {
    id: "number_line",
    content: {
      from: 0,
      to: 1,
      ticks: [
        { position: 0, label: "?", input: true },
        { position: 0.5, label: "?", input: true },
        { position: 1, label: "?", input: true }
      ],
      answers: ["0", "0.5", "1"],
      options: {
        mode: "drag",
        pool: ["0", "0.25", "0.5", "0.75", "1"]
      }
    }
  }
}
```

## Template Syntax

The template parser supports:

- **Numbers**: `15`, `3.14`, `-5`
- **Operators**: `+`, `-`, `×`, `÷`, `=`
- **Single input**: `?`
- **Multi-digit input**: `??`, `???`
- **Fractions**: `1/2`, `?/4`, `?/?`
- **Remainder**: `R` (with spaces around it)

## Grading

```tsx
import { gradeExercise, gradeWithFeedback } from '@mathscan/exercise-renderer';

// Simple grading
const isCorrect = gradeExercise(widget, userAnswers);

// Detailed feedback
const result = gradeWithFeedback(widget, userAnswers);
console.log(result.message); // "✓ Correct! Well done!" or "✗ Try again!"
```

## Advanced Usage

### Custom Hooks

```tsx
import { useExerciseState } from '@mathscan/exercise-renderer';

function CustomExercise() {
  const {
    answers,
    usedItems,
    validation,
    handleAnswerChange,
    handleUsedItemsChange,
    resetExercise,
    setValidationResult
  } = useExerciseState();

  // Use the state and handlers
}
```

### Validation with Zod

```tsx
import { ExerciseSchema } from '@mathscan/exercise-renderer';

const result = ExerciseSchema.safeParse(exerciseData);
if (result.success) {
  // Exercise data is valid
  const exercise = result.data;
} else {
  console.error(result.error);
}
```

### Custom Widget Components

```tsx
import { NumericInputWidget } from '@mathscan/exercise-renderer';

function CustomWidget() {
  return (
    <NumericInputWidget
      template="? + ? = 10"
      mode="type"
      answers={{}}
      onAnswerChange={(key, value) => console.log(key, value)}
    />
  );
}
```

## Styling

This package uses Tailwind CSS. Make sure Tailwind is configured in your project:

```js
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './node_modules/@mathscan/exercise-renderer/**/*.{js,jsx,ts,tsx}',
  ],
  // ... rest of config
};
```

The components don't include background colors by default, allowing you to customize the appearance in your app.

## TypeScript Support

Full TypeScript support with exported types:

```tsx
import type {
  Exercise,
  Widget,
  AnswerState,
  ValidationResult,
  NumericInputContent,
  FractionInputContent,
} from '@mathscan/exercise-renderer';
```

## API Reference

### Components

- `ExerciseRenderer` - Main component for rendering exercises
- `WidgetRenderer` - Renders individual widgets
- `NumericInputWidget` - Numeric input widget
- `VerticalOperationWidget` - Vertical arithmetic operations
- `NumberLineWidget` - Interactive number line
- `InputBox` - Shared input component
- `DragPool` - Drag-and-drop pool component
- `FractionDisplay` - Fraction display/input component

### Functions

- `parseTemplate(template)` - Parse template strings into tokens
- `parseVerticalOperation(template)` - Parse vertical operation templates
- `gradeExercise(widget, answers)` - Grade user answers
- `gradeWithFeedback(widget, answers)` - Grade with detailed feedback
- `areFractionsEquivalent(f1, f2)` - Check fraction equivalence

### Hooks

- `useExerciseState()` - Manage exercise state
- `useDragAndDrop()` - Manage drag-and-drop interactions

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.