npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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