clarity-pattern-parser
Version:
Parsing Library for Typescript and Javascript.
326 lines (272 loc) • 8.85 kB
text/typescript
import { Cursor } from "./Cursor";
import { Pattern } from "./Pattern";
import { Literal } from "./Literal";
import { Node } from "../ast/Node";
import { findPattern } from "./findPattern";
import { testPattern } from "./testPattern";
import { execPattern } from "./execPattern";
import { ParseResult } from "./ParseResult";
let idIndex = 0;
export class Block implements Pattern {
private _id: string;
private _type: string;
private _name: string;
private _parent: Pattern | null;
private _children: Pattern[];
private _openPattern: Pattern;
private _contentPattern: Pattern | null;
private _closePattern: Pattern;
private _firstIndex: number;
private _literalOpen: string;
private _literalClose: string;
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 startedOnIndex() {
return this._firstIndex;
}
constructor(
name: string,
openPattern: Literal,
contentPattern: Pattern | null,
closePattern: Literal
) {
const clonedOpen = openPattern.clone() as Literal;
const clonedContent = contentPattern != null ? contentPattern.clone() : null;
const clonedClose = closePattern.clone() as Literal;
this._id = `block-${idIndex++}`;
this._type = "block";
this._name = name;
this._parent = null;
this._openPattern = clonedOpen;
this._contentPattern = clonedContent;
this._closePattern = clonedClose;
this._children = clonedContent != null
? [clonedOpen, clonedContent, clonedClose]
: [clonedOpen, clonedClose];
this._firstIndex = -1;
this._literalOpen = clonedOpen.token;
this._literalClose = clonedClose.token;
for (const child of this._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;
// Phase 1: Match open delimiter
const openNode = this._openPattern.parse(cursor);
if (openNode === null || cursor.hasError) {
cursor.moveTo(this._firstIndex);
cursor.recordErrorAt(this._firstIndex, cursor.index, this);
return null;
}
// Phase 2: Scan ahead for matching close
const scanResult = this._scanForMatchingClose(cursor, openNode);
if (scanResult === null) {
cursor.moveTo(this._firstIndex);
cursor.recordErrorAt(this._firstIndex, cursor.index, this);
return null;
}
const { closeStartIndex, closeLastIndex } = scanResult;
// Phase 3: Parse content within boundaries
const contentResult = this._parseContent(cursor, openNode.endIndex, closeStartIndex);
if (contentResult.failed) {
cursor.moveTo(this._firstIndex);
cursor.recordErrorAt(this._firstIndex, cursor.index, this);
return null;
}
const contentNode = contentResult.node;
if (contentNode != null && contentNode.endIndex !== closeStartIndex) {
cursor.moveTo(this._firstIndex);
cursor.recordErrorAt(this._firstIndex, cursor.index, this);
return null;
}
// Phase 4: Parse close delimiter at its known position
cursor.moveTo(closeStartIndex);
const closeNode = this._closePattern.parse(cursor);
if (closeNode === null || cursor.hasError) {
cursor.moveTo(this._firstIndex);
cursor.recordErrorAt(this._firstIndex, cursor.index, this);
return null;
}
// Build AST — for wildcard blocks, capture inner text as a value node
let allChildren: Node[];
if (contentNode != null) {
allChildren = [openNode, contentNode, closeNode];
} else if (this._contentPattern == null && openNode.endIndex < closeStartIndex) {
const innerText = cursor.text.substring(openNode.endIndex, closeStartIndex);
const innerNode = new Node("block-content", `${this._name}-content`, openNode.endIndex, closeStartIndex - 1, [], innerText);
allChildren = [openNode, innerNode, closeNode];
} else {
allChildren = [openNode, closeNode];
}
const node = new Node(
"block",
this._name,
this._firstIndex,
closeLastIndex,
allChildren
);
cursor.moveTo(closeLastIndex);
cursor.recordMatch(this, node);
return node;
}
private _scanForMatchingClose(
cursor: Cursor,
openNode: Node
): { closeStartIndex: number; closeLastIndex: number } | null {
const text = cursor.text;
const openToken = this._literalOpen;
const closeToken = this._literalClose;
const openLen = openToken.length;
const closeLen = closeToken.length;
let from = openNode.endIndex;
let depth = 0;
let lastCloseIdx = -1;
while (true) {
const closeIdx = text.indexOf(closeToken, from);
if (closeIdx === -1) {
if (lastCloseIdx !== -1) {
return {
closeStartIndex: lastCloseIdx,
closeLastIndex: lastCloseIdx + closeLen - 1,
};
}
return null;
}
lastCloseIdx = closeIdx;
// Count any opens that appear before this close
let searchFrom = from;
while (true) {
const openIdx = text.indexOf(openToken, searchFrom);
if (openIdx === -1 || openIdx >= closeIdx) {
break;
}
depth++;
searchFrom = openIdx + openLen;
}
if (depth === 0) {
return {
closeStartIndex: closeIdx,
closeLastIndex: closeIdx + closeLen - 1,
};
}
depth--;
from = closeIdx + closeLen;
}
}
private _parseContent(
cursor: Cursor,
openEndIndex: number,
closeStartIndex: number
): { node: Node | null; failed: boolean } {
if (this._contentPattern == null) {
return { node: null, failed: false };
}
cursor.moveTo(openEndIndex);
if (openEndIndex >= closeStartIndex) {
// Empty block — delimiters are adjacent, so there's no content space.
// This is always valid: it's the natural base case for recursive blocks
// and structurally complete for any block with matched delimiters.
return { node: null, failed: false };
}
const node = this._contentPattern.parse(cursor);
if (cursor.hasError) {
return { node: null, failed: true };
}
if (node === null) {
// Content pattern didn't match — only fail if it's required
const isOptional = this._contentPattern.type === "optional";
return { node: null, failed: !isOptional };
}
if (node.endIndex !== closeStartIndex){
return { node: null, failed: true };
}
return { node, failed: false };
}
getTokens(): string[] {
return this._openPattern.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._openPattern.getPatterns();
}
getPatternsAfter(childReference: Pattern): Pattern[] {
const index = this._children.indexOf(childReference);
if (index === -1) {
return [];
}
const nextIndex = index + 1;
if (nextIndex >= this._children.length && this._parent !== null) {
return this._parent.getPatternsAfter(this);
}
const patterns: Pattern[] = [];
for (let i = nextIndex; i < this._children.length; i++) {
patterns.push(this._children[i]);
if (this._children[i].type !== "optional") {
break;
}
if (i === this._children.length - 1 && this._parent !== null) {
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 {
const clone = new Block(
name,
this._openPattern as Literal,
this._contentPattern,
this._closePattern as Literal
);
clone._id = this._id;
return clone;
}
isEqual(pattern: Block): boolean {
return (
pattern.type === this.type &&
this.children.every((c, index) => c.isEqual(pattern.children[index]))
);
}
}