clarity-pattern-parser
Version:
Parsing Library for Typescript and Javascript.
363 lines (288 loc) • 8.63 kB
text/typescript
import { Node } from "../ast/Node";
import { Cursor } from "./Cursor";
import { Pattern } from "./Pattern";
import { findPattern } from "./findPattern";
import { ParseResult } from "./ParseResult";
import { execPattern } from "./execPattern";
import { testPattern } from "./testPattern";
let idIndex = 0;
export interface InfiniteRepeatOptions {
divider?: Pattern;
min?: number;
trimDivider?: boolean;
}
export class InfiniteRepeat implements Pattern {
private _id: string;
private _type: string;
private _name: string;
private _parent: Pattern | null;
private _children: Pattern[];
private _pattern: Pattern;
private _divider: Pattern | null;
private _nodes: Node[];
private _firstIndex: number;
private _min: number;
private _trimDivider: boolean;
private _patterns: Pattern[];
get id(): string {
return this._id;
}
get type(): string {
return this._type;
}
get name(): string {
return this._name;
}
get parent(): Pattern | null {
return this._parent;
}
set parent(pattern: Pattern | null) {
this._parent = pattern;
}
get children(): Pattern[] {
return this._children;
}
get min(): number {
return this._min;
}
get startedOnIndex() {
return this._firstIndex;
}
constructor(name: string, pattern: Pattern, options: InfiniteRepeatOptions = {}) {
const min = options.min != null ? Math.max(options.min, 1) : 1;
const divider = options.divider;
let children: Pattern[];
if (divider != null) {
children = [pattern.clone(), divider.clone()];
} else {
children = [pattern.clone()];
}
this._assignChildrenToParent(children);
this._id = `infinite-repeat-${idIndex++}`;
this._type = "infinite-repeat";
this._name = name;
this._min = min;
this._parent = null;
this._children = children;
this._pattern = children[0];
this._divider = children[1];
this._firstIndex = 0;
this._nodes = [];
this._trimDivider = options.trimDivider == null ? false : options.trimDivider;
this._patterns = [];
}
private _assignChildrenToParent(children: Pattern[]): void {
for (const child of children) {
child.parent = this;
}
}
test(text: string, record = false): boolean {
return testPattern(this, text, record);
}
exec(text: string, record = false): ParseResult {
return execPattern(this, text, record);
}
parse(cursor: Cursor): Node | null {
this._firstIndex = cursor.index;
this._nodes = [];
this._patterns = [];
const passed = this._tryToParse(cursor);
if (passed) {
cursor.resolveError();
const node = this._createNode(cursor);
if (node != null) {
cursor.moveTo(node.lastIndex);
cursor.recordMatch(this, node);
}
return node;
}
if (this._min > 0) {
return null;
}
cursor.resolveError();
return null;
}
private _meetsMin() {
if (this._divider != null) {
return Math.ceil(this._nodes.length / 2) >= this._min;
}
return this._nodes.length >= this._min;
}
private _tryToParse(cursor: Cursor): boolean {
const firstIndex = cursor.index;
let passed = false;
while (true) {
const runningCursorIndex = cursor.index;
const repeatNode = this._pattern.parse(cursor);
const hasError = cursor.hasError;
const hasNoErrorAndNoResult = !cursor.hasError && repeatNode == null;
const hasDivider = this._divider != null;
const hasNoDivider = !hasDivider;
if (hasError) {
const lastValidNode = this._getLastValidNode();
if (lastValidNode != null) {
passed = true;
} else {
cursor.moveTo(runningCursorIndex);
cursor.recordErrorAt(firstIndex, runningCursorIndex, this._pattern);
passed = false;
}
break;
} else {
if (hasNoErrorAndNoResult && hasNoDivider) {
// If we didn't match and didn't error we need to get out. Nothing different will happen.
break;
}
if (repeatNode != null) {
this._nodes.push(repeatNode);
this._patterns.push(this._pattern);
if (!cursor.hasNext()) {
passed = true;
break;
}
cursor.next();
}
if (this._divider != null) {
const dividerStartIndex = cursor.index;
const dividerNode = this._divider.parse(cursor);
if (cursor.hasError) {
passed = true;
break;
} else {
if (dividerNode == null) {
cursor.moveTo(dividerStartIndex);
if (repeatNode == null) {
// If neither the repeat pattern or divider pattern matched get out.
passed = true;
break;
}
} else {
this._nodes.push(dividerNode);
this._patterns.push(this._divider);
if (!cursor.hasNext()) {
passed = true;
break;
}
cursor.next();
}
}
}
}
}
const hasMinimum = this._meetsMin();
if (hasMinimum) {
return passed;
} else if (!hasMinimum && passed) {
cursor.recordErrorAt(firstIndex, cursor.index, this);
cursor.moveTo(this._firstIndex);
return false;
}
return passed;
}
private _createNode(cursor: Cursor): Node | null {
const hasDivider = this._divider != null;
const lastPattern = this._patterns[this._patterns.length - 1];
if (
hasDivider &&
this._trimDivider &&
lastPattern === this._divider
) {
const dividerNode = this._nodes.pop() as Node;
cursor.moveTo(dividerNode.firstIndex);
}
if (this._nodes.length === 0) {
cursor.moveTo(this._firstIndex);
return null;
}
const lastIndex = this._nodes[this._nodes.length - 1].lastIndex;
cursor.moveTo(lastIndex);
return new Node(
this._type,
this._name,
this._firstIndex,
lastIndex,
this._nodes
);
}
private _getLastValidNode(): Node | null {
const nodes = this._nodes.filter((node) => node !== null);
if (nodes.length === 0) {
return null;
}
return nodes[nodes.length - 1];
}
getTokens(): string[] {
return this._pattern.getTokens();
}
getTokensAfter(childReference: Pattern): string[] {
const patterns = this.getPatternsAfter(childReference);
const tokens: string[] = [];
patterns.forEach(p => tokens.push(...p.getTokens()));
return tokens;
}
getNextTokens(): string[] {
if (this._parent == null) {
return [];
}
return this._parent.getTokensAfter(this);
}
getPatterns(): Pattern[] {
return this._pattern.getPatterns();
}
getPatternsAfter(childReference: Pattern): Pattern[] {
let index = -1;
const patterns: Pattern[] = [];
for (let i = 0; i < this._children.length; i++) {
if (this._children[i] === childReference) {
index = i;
}
}
// If the last match isn't a child of this pattern.
if (index === -1) {
return [];
}
// If the last match was the repeated patterns, then suggest the divider.
if (index === 0 && this._divider) {
patterns.push(this._children[1]);
if (this._parent) {
patterns.push(...this._parent.getPatternsAfter(this));
}
}
// Suggest the pattern because the divider was the last match.
if (index === 1) {
patterns.push(this._children[0]);
}
// If there is no divider then suggest the repeating pattern and the next pattern after.
if (index === 0 && this._divider == null && this._parent) {
patterns.push(this._children[0]);
patterns.push(...this._parent.getPatternsAfter(this));
}
return patterns;
}
getNextPatterns(): Pattern[] {
if (this._parent == null) {
return [];
}
return this._parent.getPatternsAfter(this);
}
find(predicate: (p: Pattern) => boolean): Pattern | null {
return findPattern(this, predicate);
}
clone(name = this._name): Pattern {
let min = this._min;
const clone = new InfiniteRepeat(
name,
this._pattern,
{
divider: this._divider == null ? undefined : this._divider,
min: min,
trimDivider: this._trimDivider
}
);
clone._id = this._id;
return clone;
}
isEqual(pattern: InfiniteRepeat): boolean {
return pattern.type === this.type && this.children.every((c, index) => c.isEqual(pattern.children[index]));
}
}