UNPKG

clarity-pattern-parser

Version:

Parsing Library for Typescript and Javascript.

326 lines (272 loc) 8.85 kB
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])) ); } }