The bplus-parser library is a set of parsers and combinators inspired by the FParsec library. The goal was to create an easy to use, TypeScript friendly version of those libraries.
This project is maintained over at GitLab: https://gitlab.com/adleatherwood/bplus-parser
https://www.npmjs.com/package/bplus-parser
npm i bplus-parser
Here's a basic example setup to parse the name and parameters of some function. This is a basic example of the patterns that are used to build a parser.
/* typescript */
// some structures to hold our parsed data in
type FunctionInfo = {
name: string
params: ParamInfo[]
}
type ParamInfo = {
name: string
type: string
}
// a function for creating space insensitive parsers
let syntax = (value: string) => |
Parser.combine(many(space))
.take(exact(value)) // 'take' discards previous and keeps the current value
.skip(many(space)) // 'skip' parses but ignores values
.build() // 'build' returns a single unified parser
// a parser for finding an unknown name
let id =
Parser.combine(many1(char("ID", /[a-z]/)))
.map(a => a.join("")) // 'map' restructures parsed values
.build("id") // 'build' takes a "label" for the parser
// a parser that returns a ParamInfo
let param =
Parser.combine(id)
.skip(syntax(":"))
.and(id) // 'and' keeps previous values
.map(args => {
return {
name: args[0],
type: args[1]
} as ParamInfo
})
.build()
// a parser that returns a FunctionInfo
let parser =
Parser.combine(many(space)) // 'many' is a zero to (n) parser
.skip(syntax("function"))
.take(id)
.skip(syntax("("))
.and(separated(syntax(","), param))// 'separated' parses delimited values
.map(args => {
return {
name: args[0],
params: args[1]
} as FunctionInfo
})
.build()
// a "stream" of input to be parsed
let stream = StringStream.create(`
function create(name: string, value: int) {
...
}`)
// parsing the stream
let result = parser.parse(stream)
// debugging parsers is sometimes challenging, printing the result can help
console.log(result)
// deconstructing the result
let value = Result.match(result,
success => success.value,
failure => { throw failure.error })
expect(value.name).toBe("create")
expect(value.params[0].name).toBe("name")
expect(value.params[0].type).toBe("string")
expect(value.params[1].name).toBe("value")
expect(value.params[1].type).toBe("int")
It's basically all about combining small parsers together to build bigger parsers.
In the following signatures, we'll assume that 'a', 'b', 'c', etc. are parsers.
Name | Signature | Function |
---|---|---|
exact | (a) => a | Returns the exact value if matched |
between | (a,o,c) => a | Returns the result of 'a' from in between 'o' and 'c' |
separated | (a, d) => | Returns an 'a' array that is delimited by 'd' |
Name | Signature | Function |
---|---|---|
maybe | (a) => a | If the parser succeeds it returns the value, otherwise undefined |
many | (a) => a[] | Returns 0 to (n) values depending on many times the parser succeeds |
many1 | (a) => a[] | Like 'many', but returns 1 to (n) values and fails if no values are found |
any | (...a) => a | Returns the result of the first successful parser of the array |
attempt | (a) => a | For ensuring you can walk back the stream index if the parser fails |
map | (a) => b | For transforming input to a different type |
labeled | (a) => a | Adds a label to the current parser |
Name | Signature | Function |
---|---|---|
flatten3-8 | ((a,b),c) => [a,b,c] | Takes nested tuples and flattens them into a single tuple |
Name | Signature | Function |
---|---|---|
char | (ex) => c | Like 'exact', but takes a regular expression for a single character |
Name | Function |
---|---|
space | Matches a single space |
letter | Matches any letter |
digit | Matches any digit |
number | Matches a number (e.g. "12", "1.2", "-12") |
The parser builder is intended to give you an intuitive way to combine smaller parsers into larger parsers. Here is a very simple example.
let testParser: Parser<string,string[]> =
Parser.combine(exact("this"))
.skip(space)
.and(exact("is"))
.skip(space)
.and(exact("a"))
.skip(space)
.and(exact("test"))
// the result before 'flattening' is [[["this","is"],"a"],"test"]
.map(flatten4)
// the result after 'flattening' is ["this","is","a","test"]
.build()
Here's a more complex example for the parsing of a number with a line by line breakdown.
const numberParser: Parser<string,number> =
// Parser.combine() starts the builder workflow. It takes a single parser.
// Our first parser might return a "+" or a "-" or nothing as "-"
Parser.combine(maybe(any(exact("+"), exact("-"))))
// 'and' takes the previous sign and also returns 1 to (n) digits as ["-", [1,2,3]]
.and(many1(digit))
// this parser will return a decimal or undefined as [["-", [1,2,3]], "."]
.and(maybe(exact(".")))
// this parser will return 0 to (n) digits as [[["-", [1,2,3]], "."], [4,5,6]]
.and(many(digit))
// this helper will flatten out our nested tuples to ["-", [1,2,3], ".", [4,5,6]]
.map(flatten4)
// now we can easily put those parts together and return an actual number
.map((a) => {
const text = (a[0] || "") + (a[1].join("")) + (a[2] || "") + (a[3].join(""))
return Number.parseFloat(text)
})
// build returns you a single parser for the parsers above
.build()
The available builder functions are as follows:
Name | Signature | Function |
---|---|---|
and | (a, b) => [a,b] | Takes two parsers and returns a tuple of their results |
take | (a, b) => b | Discards and previously accumulated values and takes the value of 'b' |
skip | (a, b) => a | Skips the value of 'b' and keeps the value of 'a' |
Skip and take seem backwards, but in the context of a '.'-chained builder it fairly intuitive.
Streams conform to a specific interface.
export interface Stream<T> {
tryTake(value: T): Option<[T, Stream<T>]>
eof(): boolean
}
Below are the existing stream already defined in the library.
A general purpose stream for parsing strings.
let stream - StringStream.create('this is a test')
A case insensitive stream can be created with the following.
let stream - StringStream.create('this is a test', { caseInsensitive: true })
Icons made by freepik