Skip to content

Repository files navigation

dapter-engine

CI

A calculation engine for text. Give it a block of lines and it hands back a result for every line that reads like a calculation, plus highlight spans for colouring the source. Lines that read like prose stay prose.

rate = 45              45
hours = 12             12
rate * hours           540
200 usd in eur         €184.00
6 m / 2 s              3 m/s
today + 21 days        Sep 9, 2026
20% of 300             60

This is a real language implementation, not a pile of regexes. Lexer, Pratt parser, typed evaluator. It has no runtime dependencies and knows nothing about the DOM, React, or React Native, so it runs in a browser, in Node, or on a phone.

It powers däpter, a notes app for iOS, which is where the 356 tests come from.

Install

npm install dapter-engine

ESM only. Node 20 or newer.

Use

One call evaluates a whole document.

import { evaluateDocument } from "dapter-engine";

const result = evaluateDocument("100 usd in eur\n10 km + 5 m in miles", {
  rates: { USD: 1, EUR: 0.92 },
  now: Date.now(),
});

for (const line of result.lines) {
  console.log(line.source, "=>", line.display?.text ?? "(prose)");
}

result.lines is canonical. Each LineResult carries the source, the AST, the typed value, the formatted display string, the tokens, the highlight spans, and the error if the line has one. result.variables is the final environment, so you can see what the document defined.

For an editor, use a session instead. It re-folds only from the first changed line, which is what makes highlighting a note feel free on every keystroke.

import { createNotebookSession } from "dapter-engine";

const session = createNotebookSession({ now: Date.now() });
session.update(text);        // full result, same shape as evaluateDocument
session.ids();               // stable per-line ids, usable as React keys

The ids survive an insert or delete above them, so a renderer keyed on them remounts only the lines that actually changed.

Highlighting returns roles, never colours.

import { roleColor } from "dapter-engine";

roleColor("number"); // "syntaxNumber"

Your theme owns the nine token names roleColor can return. The engine never learns what syntaxNumber looks like, which is how the same spans serve a light theme, a dark theme, and a test in Node.

Rates are injected the same way. The package ships no rate table and makes no network calls. Pass rates if you want live currency conversion, pass now if you want today to resolve. Leave now out and relative dates read as ordinary words rather than reading a clock behind your back.

How it works

Four decisions carry the whole design. docs/DESIGN.md is the long version, 1,300 lines of it, and it is the document to read before changing anything here.

Every line is parsed alone. A document is a left fold over its lines. Each line parses on its own and evaluates against the environment accumulated above it. One malformed line cannot kill the rest of the document, and recompute starts at the first line that changed instead of the top.

A Pratt parser, so precedence is data. Binding powers live in a table. Adding an operator does not mean rewriting a recursive-descent chain.

Features are registry entries, not grammar. Units, currencies, functions, constants, date words, and keywords are all data under src/registry/. Adding furlong is a row. The parser does not change. This is also why Russian works: the registries carry the real inflected forms, including the prepositional plural that в километрах needs, rather than transliterating and hoping.

Values are typed, and units are exponent vectors. A quantity carries a dimension like { length: 1, time: -1 }. Multiplication adds exponents, division subtracts them, and that is why 6 m / 2 s knows it produced a speed instead of a number that happens to be 3.

What it deliberately does not do

Fetch exchange rates. Read the clock. Render anything. Persist anything. Each of those is the host's job, and keeping them out is what makes the engine testable in plain Node with no mocks.

Develop

pnpm install
pnpm test         # 356 tests, 15 files
pnpm typecheck
pnpm build        # tsc to dist/, ESM plus declarations

21 source files, about 3,600 lines. The tests are the specification for the language, so a new feature starts with a case in src/__tests__/.

License

MIT

About

A line-oriented natural-language calculation engine in TypeScript. Resolves lines like "200 usd in eur", "6 m / 2 s" and "today + 21 days". Lexer, Pratt parser, typed evaluator, zero runtime dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages