SkillAgentSearch skills...

Parjs

JavaScript parser-combinator library

Install / Use

/learn @GregRos/Parjs

README

Parjs

build codecov npm Downloads Gzipped Size

🌍 ABOUT THIS REPOSITORY 🌍

Documentation:

Parjs a parser combinator library inspired by Parsec and FParsec (its F# adaptation), and written in TypeScript.

yarn add parjs

🍕 Lots of parsers!

⚙️ Lots of combinators!

💎 Lots of immutability!

🔍 Systematically documented!

🧐 Debugging features!

What is it?

PROTIP: 🍕 is the universal symbol for parser.

Parser combinator libraries let you construct parsers from small parsers and combinators that transform those parsers by, for example, applying a parser multiple times in a row.

For example, you could have a parser 🍕"fi" that parses the string fi and a combinator ⚙️exactly 2 that applies a parser exactly two times. Combining them lets you parse the string fifi!

// 🍕string "fi" ➜ ⚙️exactly 2
string("fi").pipe(exactly(2));

Here is an example that constructs a parser that parses n-tuples of numbers like (12.5, -1, 2), which is impossible using other parsing techniques<sup>citation needed</sup>.

import { float, string, whitespace } from "parjs";
import { between, manySepBy } from "parjs/combinators";

// 🍕float
//  Parses a floating point number
const tupleElement = float();

//  🍕float ➜ ⚙️between 🍕whitespace
//  Parses a float between whitespace
const paddedElement = tupleElement.pipe(between(whitespace()));

//  🍕float ➜ ⚙️between 🍕whitespace ➜
//  ⚙️until fails, separated by 🍕","
//  Parses many floats between whitespace, separated by commas.
const separated = paddedElement.pipe(manySepBy(","));

//  🍕float ➜ ⚙️between 🍕whitespace ➜
//  ⚙️until fails, separated by 🍕"," ➜ ⚙️between 🍕"(" and 🍕")"
//  Parses many floats separated by commas and surrounded by parens.
const surrounded = separated.pipe(between("(", ")"));

//  Parses the string and print [1, 2, 3]
console.log(surrounded.parse("(1,  2 , 3 )"));

Examples

Here are some more cool examples:

  1. tuple parser (tests)
  2. .ini parser (tests)
  3. JSON parser (tests)
  4. Math expression parser (tests)

How does it work?

Parsers are called on an input via the parse(input) method and return a result object.

Parsers that succeed return some kind of value. While basic parsers return the parsed input (always a string), combinators (such as map) let you change the returned value to pretty much anything. It’s normal to use this feature to return an AST, the result of a calculation, and so on.

If parsing succeeded, you can access the result.value property to get the return value.

const parser = string("hello world").pipe(map(text => text.length));
const result = parser.parse("hello world");
assert(result.value === 11);

However, doing this if parsing failed throws an exception. To check if parsing succeeded or not, use the isOkay property.

You can also use toString to get a textual description of the result.

const result2 = parser.parse("hello wrld");
if (result.isOkay) {
    console.log(result.value);
} else {
    console.log(result.toString());
    // Soft failure at Ln 1 Col 1
    // 1 | hello wrld
    //     ^expecting 'hello world'
    // Stack: string
}

Dealing with failure

parjs handles failure by using the SHF or 😕😬💀 system. It recognizes three kinds of failures:

  • 😕 Soft failures — A parser quickly says it’s not applicable to the input. Used to parse alternative inputs.
  • 😬 Hard failures — Parsing failed unexpectedly. Can only be handled by special combinators.
  • 💀 Fatal failure — Happen when you decide and tell the parser to halt and catch fire. They can’t be handled.

Parsing failures bubble up through combinators unless they’re handled, just like exceptions. Handling a failure always means backtracking to before it happened.

Some combinators can upgrade soft failures to hard ones (if it says so in their documentation).

Failing to parse something is a common occurrence and not exceptional in the slightest. As such, parjs won’t throw an exception when this happens. Instead, it will only throw exceptions if you used it incorrectly or there is a bug.

The result object mentioned earlier also gives the failure type via its kind property. It can be OK, Soft, Hard, or Fatal.

console.log(result.kind); // "Soft"

The reason field

The parsing result also includes the important reason field which says why parsing failed and usually what input was expected.

This text appears after the ^ character in the visualization, but can also be used elsewhere. It can be specified explicitly in some cases, but will usually come from the parser’s expecting property.

😕 Soft failures

A parser quickly says it’s not applicable to the input.

You can recover from soft failures by backtracking a constant amount. These failures are used to parse alternative inputs using lots of different combinators, like or:

// 🍕"hello" ➜ ⚙️or 🍕"goodbye" ➜ ⚙️or 🍕"blort"
// Parses any of the strings, "hello", "goodbye", or "blort"
const parser = string("hello").pipe(or("goodbye"), or("blort"));

😬 Hard failure

An unexpected failure that usually indicates a syntax error.

Hard failures usually indicate unexpected input, such as a syntax error. These failures bubble up through multiple parsers and recovering from them can involve backtracking any number of characters.

Most hard failures were soft failures in an internal parser that weren’t handled, and got upgraded by a combinator. After this happens, combinators like ⚙️or that recover from soft failures no longer work.

Sequential combinators tend to do this a lot if a parser fails late in the sequence. For example:

// 🍕"hello " ➜ ⚙️and then, 🍕"world" ➜ ⚙️or 🍕"whatever"
// Parses the string "hello " and then the string "world"
// or parses the string "hello kittie"
const helloParser = string("hello ").pipe(
    then(
        // If this parser fails, ⚙️then will upgrade
        // it to a 😬Hard failure.
        string("world")
    ),
    // The ⚙️or combinator can't recover from this:
    or("hello kittie")
);

console.log(helloParser.parse("whatever").toString());
// Hard failure at Ln 1 Col 6
// 1 | hello world
//           ^expecting "world"
// Stack: string < then < string

To avoid this situation, write parsers that quickly determine if the input is for them, and combinators like or that will immediately apply a fallback parser instead.

const helloParser2 = string("hello ").pipe(
    then(
        // The 😕Soft failure in the 🍕"world" parser
        // is handled immediately using ⚙️or
        // so it doesn't reach ⚙️then
        string("world").or("kittie")
    )
);

However, sometimes hard failures are inevitable or you can’t be bothered. In those cases, you can use ⚙️recover which lets you downgrade the failure or even pass it off as a success.

// Let's do the same thing as the first time:
const helloParser3 = string("hello ").pipe(
    // ⚙️then will fail 😬Hard, like we talked about:
    then(string("world")),
    // But then the ⚙️recover combinator will downgrade the failure:
    recover(() => ({ kind: "Soft" })),
    // So the ⚙️or combinator can be used:
    or("kittie")
);

However, code like this is the equivalent of using try .. catch for control flow and should be avoided.

The ⚙️must combinator, which validates the result of a parser, emits 😬 Hard failures by default.

💀 Fatal failures

A 💀 Fatal failure is the parsing equivalent of a Halt and Catch Fire instruction and can’t be recovered from – in other words, they cause the overall parsing operation to fail immediately and control to be returned to the caller.

They act kind of like thrown exceptions, except that parsers don’t throw exceptions for bad inputs.

parjs parsers will never fail this way unless you explicitly tell them to. One way to do this is using the fail basic parser. This parser fails immediately for any input and can emit any failure type.

const parser = fail({
    kind: "Fatal"
});

console.log(parse.parse("").toString());

Cool features

Immutability

In parjs, parsers are functionally immutable. Once a parjs parser is created, it will always do the same thing and can never change. I mean, you could do something like this:

// 🍕"hello world" ➜ predicate `() => Math.random() > 0.5`
string("hello world").pipe(must(() => Math.random() > 0.5));

But then it’s on you. And you know what you did.

Unicode support

JavaScript supports Unicode strings, including ”抱き枕”, ”כחול”, and ”tủ lạnh”. Those characters aren’t ASCII – most of them have character codes in the low thousands.

That doesn’t matter if you’re parsing a specific string, since it en

View on GitHub
GitHub Stars315
CategoryDevelopment
Updated12d ago
Forks20

Languages

TypeScript

Security Score

100/100

Audited on Mar 20, 2026

No findings