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.2.3

Published

A DSL parser and validator for math exercises - transforms text-based exercise definitions into structured AST and JSON

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 — LOCKS the exercise
exerciseRef.current?.getValues();        // Get current input values
exerciseRef.current?.getCompletion();    // How many answerable inputs are filled
exerciseRef.current?.revealAnswers();    // Fill in the correct answers — also LOCKS
exerciseRef.current?.reset();            // Clear all inputs and unlock
exerciseRef.current?.focusFirstInput();  // Focus first editable input field

Method Details

  • submit(): Performs validation, returns ValidationResult, triggers onSubmit, and locks the exercise (see below). Calling it again while locked is a no-op that replays the same ValidationResult — no sound, no onSubmit
  • getValues(): Returns Record<string, UserAnswerValue> of all current input values
  • getCompletion(): Returns { total, filled, missing, filledIds, isComplete, isAnyFilled } — use it to decide when to enable your submit button
  • revealAnswers(): Fills every widget with its correct answer and locks the exercise. No-op for display-only exercises
  • reset(): Clears all input fields and unlocks the exercise
  • focusFirstInput(): Focuses the first editable input of this renderer. Skips read-only/disabled fields, so it does nothing while the exercise is locked

🔒 Locking After a Check

Once the answer is checked, the exercise becomes read-only until it is reset. This is the default and cannot be turned off.

What locks it: submit(), the built-in showSubmitButton button, and revealAnswers(). What unlocks it: reset() — nothing else.

While locked, every widget refuses input: text inputs go readOnly, option/card widgets get aria-disabled, keypad and counter buttons get disabled, the built-in Check Answer button is disabled, drag-and-drop is disabled, and clock hands / number-line cursors stop responding. The root element gets a .locked class so you can style the state:

.math-exercise-renderer.locked { /* your read-only styling */ }

Floating geometry tools (ruler, protractor, set square) stay draggable — they are instruments, not answers.

If your app shows an exercise in review mode (correction, teacher preview) and wants it read-only from the start, wrap it in your own provider:

import { ExerciseLockProvider } from '@mathscan/math-exercise-engine';

<ExerciseLockProvider locked>
  <ExerciseRenderer json={json} />
</ExerciseLockProvider>

The renderer ORs your value with its own state, so an outer lock is never overridden by an un-submitted exercise. Custom widgets of your own can read the same state with the useExerciseLocked() hook.

📝 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;        // Validate, return result, lock the exercise
  getValues: () => Record<string, UserAnswerValue>; // Get current values
  getCompletion: () => InputCompletion;  // Fill status of the answerable inputs
  revealAnswers: () => void;             // Fill in correct answers, lock the exercise
  reset: () => void;                     // Clear all inputs and unlock
  focusFirstInput: () => void;           // Focus first editable 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