tell-me-when
Version:
human relative date and time parser
282 lines (248 loc) • 7.23 kB
text/typescript
import { ParseNode } from './ParseNode'
import { ParseState } from './ParseState'
export abstract class GrammarNode {
abstract parse(state: ParseState): ParseNode
parseAs(parseAs: new (node: ParseNode) => ParseNode) {
return new ParseAsNode(this, parseAs)
}
/**
* Matches this node once or zero times
*/
maybe() {
return new MaybeNode(this)
}
/**
* Matches this node or the alternate node
*/
or(alternate: GrammarNode) {
return new OrNode(this, alternate)
}
/**
* Matches one of the given nodes. The first option to parse successfully wins
*/
static oneOf(
...options: (string | RegExp | GrammarNode | (() => GrammarNode))[]
) {
return new OrNode(...options.map(GrammarNode.toGrammarNode))
}
/**
* Matches one of the given nodes. Tries all of the options, and the one that
* successfully parses the farthest in the input wins
*/
static longestOf(
...options: (string | RegExp | GrammarNode | (() => GrammarNode))[]
) {
return new LongestOfNode(...options.map(GrammarNode.toGrammarNode))
}
/**
* Matches this node repeated the given number of times
*/
repeat(count: number): RepeatNode
/**
* Matches this node repeated between min and max (inclusive) times
*/
repeat(min: number, max: number): RepeatNode
repeat(countOrMin: number, max?: number): RepeatNode {
return new RepeatNode(this, max != null ? [countOrMin, max] : countOrMin)
}
static toGrammarNode(
factor: string | RegExp | GrammarNode | (() => GrammarNode)
) {
return typeof factor === 'function'
? new GrammarNodeRef(factor)
: factor instanceof GrammarNode
? factor
: GrammarNode.token(factor)
}
/**
* Matches the given string or regular expression
*/
static token(token: string | RegExp) {
return token instanceof RegExp
? new RegExpNode(token)
: new StringTokenNode(token)
}
/**
* Matches the given nodes in sequence
*/
static group(
...sequence: (string | RegExp | GrammarNode | (() => GrammarNode))[]
) {
return new GroupNode(undefined, ...sequence.map(GrammarNode.toGrammarNode))
}
/**
* Creates a named group that matches the given nodes in sequence.
* Same as {@link group} but the {@link ParseNode} returned by {@link parse}
* will have the given name.
*/
static named(
name: string,
...sequence: (string | RegExp | GrammarNode | (() => GrammarNode))[]
) {
return new GroupNode(name, ...sequence.map(GrammarNode.toGrammarNode))
}
static negativeLookahead(
...sequence: (string | RegExp | GrammarNode | (() => GrammarNode))[]
) {
return new NegativeLookaheadNode(
sequence.length === 1
? GrammarNode.toGrammarNode(sequence[0])
: GrammarNode.group(...sequence)
)
}
}
export class GrammarNodeRef extends GrammarNode {
constructor(public ref: () => GrammarNode) {
super()
}
parse(state: ParseState): ParseNode {
return this.ref().parse(state)
}
}
export class StringTokenNode extends GrammarNode {
constructor(public token: string) {
super()
this.token = this.token.toLowerCase()
}
parse(state: ParseState): ParseNode {
const start = state.index
if (state.testLowerCase(this.token)) {
return new ParseNode(undefined, start, state.index)
}
return ParseNode.error(start)
}
}
export class RegExpNode extends GrammarNode {
constructor(public token: RegExp) {
super()
if (!token.sticky) {
this.token = new RegExp(token.source, `${token.flags}y`)
}
}
parse(state: ParseState): ParseNode {
const start = state.index
if (state.testRegex(this.token)) {
return new ParseNode(undefined, start, state.index)
}
return ParseNode.error(start)
}
}
export class MaybeNode extends GrammarNode {
constructor(public node: GrammarNode) {
super()
}
parse(state: ParseState): ParseNode {
const parsed = this.node.parse(state)
return parsed.isError ? ParseNode.empty(state.index) : parsed
}
}
export class RepeatNode extends GrammarNode {
constructor(
public node: GrammarNode,
public count: number | [number, number]
) {
super()
}
parse(state: ParseState): ParseNode {
const startIndex = state.index
const children: ParseNode[] = []
for (
let i = 0;
i < (Array.isArray(this.count) ? this.count[1] : this.count);
i++
) {
const parsed = this.node.parse(state)
if (parsed.isError) {
if (i < (Array.isArray(this.count) ? this.count[0] : this.count)) {
state.index = startIndex
return parsed
}
break
}
if (!parsed.isEmpty) children.push(parsed)
}
return new ParseNode(undefined, startIndex, state.index, children)
}
}
export class OrNode extends GrammarNode {
public options: GrammarNode[]
constructor(...options: GrammarNode[]) {
super()
this.options = options
}
parse(state: ParseState): ParseNode {
const startIndex = state.index
for (const option of this.options) {
const parsed = option.parse(state)
if (!parsed.isError) return parsed
}
state.index = startIndex
return ParseNode.error(startIndex)
}
}
export class LongestOfNode extends GrammarNode {
public options: GrammarNode[]
constructor(...options: GrammarNode[]) {
super()
this.options = options
}
parse(state: ParseState): ParseNode {
const startIndex = state.index
let best: ParseNode = ParseNode.error(startIndex)
for (const option of this.options) {
state.index = startIndex
const parsed = option.parse(state)
if (!parsed.isError && (best.isError || parsed.to > best.to)) {
best = parsed
if (parsed.to === state.end) break
}
}
state.index = best.isError ? startIndex : best.to
return best as any
}
}
export class GroupNode extends GrammarNode {
public factors: GrammarNode[]
constructor(public name: string | undefined, ...factors: GrammarNode[]) {
super()
this.factors = factors
}
parse(state: ParseState): ParseNode {
const startIndex = state.index
const children: ParseNode[] = []
for (const factor of this.factors) {
const parsed = factor.parse(state)
if (parsed.isError) {
state.index = startIndex
return parsed
}
if (!parsed.isEmpty) children.push(parsed)
}
return new ParseNode(this.name, startIndex, state.index, children)
}
}
export class NegativeLookaheadNode extends GrammarNode {
constructor(public node: GrammarNode) {
super()
}
parse(state: ParseState): ParseNode {
const { index } = state
const parsed = this.node.parse(state)
if (!parsed.isError) return ParseNode.error(index)
state.index = index
return ParseNode.empty(index)
}
}
export class ParseAsNode extends GrammarNode {
constructor(
public node: { parse: (state: ParseState) => ParseNode },
private parseAsClass: new (node: ParseNode) => ParseNode
) {
super()
}
parse(state: ParseState): ParseNode {
const parsed = this.node.parse(state)
if (parsed.isError) return parsed
return new this.parseAsClass(parsed)
}
}