nl3 parses short Subject · Predicate · Object phrases into validated triples. Describe your domain with a grammar and a vocabulary — nl3 handles the synonyms, tenses, reversed phrasings, and missing types.
You teach nl3 your domain once, then throw natural phrasings at it.
Declare valid relations as
'Subject Predicate Object'. Words are
singularized, so a rule covers every number form.
Map word stems to predicates so synonyms and tenses —
msg,
messaged,
contacted — all collapse onto one.
Call parse() and get a validated
Triple — with reversed phrasings flipped back
into shape and missing types inferred.
TypeScript-first, ESM-only, dependency-light.
Written in strict TypeScript with bundled declarations —
Triple,
Nl3Options, and friends are all exported.
parse() throws a structured
Nl3ParseError with
.input and
.candidate;
tryParse() returns
null instead.
Omit an entity type and nl3 infers it from the grammar —
jack contacts jill resolves both ends. An
ambiguity policy controls the multi-match case.
Classic Porter stemming means sent,
sends, and
mailed fold to one predicate.
Reversed phrasings like
message 32 created user bob are detected
against the grammar and flipped into the correctly oriented triple.
A full POS tagger ships in-box (loaded lazily); pass your own
Tagger implementation to extend or replace
it.
~60–90k parses/sec with a bounded stem cache and ~10ms import. Triples are plain
objects — JSON.stringify just works.
Pure ES modules, sideEffects: false, Node
≥ 20. Three runtime dependencies, zero vulnerabilities.
Need the same grammar in a systems stack? The nl3-rs port mirrors this library feature-for-feature.
A grammar, a vocabulary, and a parse.
import nl3 from 'nl3';
const client = nl3({
grammar: ['users message users'],
vocabulary: {
msg: 'message', // user jack msgs user jill
messag: 'message', // user jack messaged user jill
contact: 'message', // user jack contacted user jill
},
});
client.parse('user jack contacts user jill');
// {
// subject: { type: 'user', value: 'jack' },
// predicate: { value: 'message' },
// object: { type: 'user', value: 'jill' }
// }
// 'message' only ever relates users, so bare
// phrases resolve both ends:
client.parse('jack contacts jill');
// subject: { type: 'user', value: 'jack' } ← inferred
// object: { type: 'user', value: 'jill' } ← inferred
// When two rules share a predicate, pick a policy:
const strict = nl3({
grammar: ['users message users', 'admins message users'],
vocabulary: { contact: 'message' },
ambiguity: 'error', // default: 'first-match'
});
try {
strict.parse('alice contacts bob');
} catch (error) {
error.predicate; // 'message'
error.candidates; // ['user', 'admin']
}
import nl3, { Nl3ParseError } from 'nl3';
try {
client.parse('dog jim hates cat sue');
} catch (error) {
if (error instanceof Nl3ParseError) {
error.input; // 'dog jim hates cat sue'
error.candidate; // the rejected triple
}
}
// …or skip the try/catch entirely:
const triple = client.tryParse('dog jim hates cat sue');
// => null
basic
One grammar rule; many phrasings → one triple
messenger
A fuller domain plus both error modes
ambiguity
first-match vs. error inference policies
custom_tagger
Supplying your own POS tagger
adventure
A playable text adventure — grammar as game affordances
node examples/basic.js
node examples/adventure.js
Node.js ≥ 20 · ES modules · TypeScript types included