| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- // from https://gist.github.com/msteen/e4828fbf25d6efef73576fc43ac479d2
- // https://discuss.codemirror.net/t/whats-the-best-to-test-and-debug-grammars/2542/5
- // MIT License
- //
- // Copyright (c) 2021 Matthijs Steen
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in all
- // copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- // SOFTWARE.
- import { Text } from '@codemirror/state'
- import { Input, NodeType, SyntaxNode, Tree, TreeCursor } from '@lezer/common'
- class StringInput implements Input {
- private input: string
- constructor(input: string) {
- this.input = input
- }
- get length() {
- return this.input.length
- }
- chunk(from: number): string {
- return this.input.slice(from)
- }
- lineChunks = false
- read(from: number, to: number): string {
- return this.input.slice(from, to)
- }
- }
- export function sliceType(
- cursor: TreeCursor,
- input: Input,
- type: number
- ): string | null {
- if (cursor.type.id === type) {
- const s = input.read(cursor.from, cursor.to)
- cursor.nextSibling()
- return s
- }
- return null
- }
- export function isType(cursor: TreeCursor, type: number): boolean {
- const cond = cursor.type.id === type
- if (cond) cursor.nextSibling()
- return cond
- }
- export type CursorNode = {
- type: NodeType
- from: number
- to: number
- isLeaf: boolean
- }
- function cursorNode(
- { type, from, to }: TreeCursor,
- isLeaf = false
- ): CursorNode {
- return { type, from, to, isLeaf }
- }
- export type TreeTraversal = {
- beforeEnter?: (cursor: TreeCursor) => void
- onEnter: (node: CursorNode) => false | void
- onLeave?: (node: CursorNode) => false | void
- }
- type TreeTraversalOptions = {
- from?: number
- to?: number
- includeParents?: boolean
- } & TreeTraversal
- export function traverseTree(
- cursor: TreeCursor | Tree | SyntaxNode,
- {
- from = -Infinity,
- to = Infinity,
- includeParents = false,
- beforeEnter,
- onEnter,
- onLeave,
- }: TreeTraversalOptions
- ): void {
- if (!(cursor instanceof TreeCursor))
- cursor = cursor instanceof Tree ? cursor.cursor() : cursor.cursor()
- for (;;) {
- let node = cursorNode(cursor)
- let leave = false
- if (node.from <= to && node.to >= from) {
- const enter =
- !node.type.isAnonymous &&
- (includeParents || (node.from >= from && node.to <= to))
- if (enter && beforeEnter) beforeEnter(cursor)
- node.isLeaf = !cursor.firstChild()
- if (enter) {
- leave = true
- if (onEnter(node) === false) return
- }
- if (!node.isLeaf) continue
- }
- for (;;) {
- node = cursorNode(cursor, node.isLeaf)
- if (leave && onLeave) if (onLeave(node) === false) return
- leave = cursor.type.isAnonymous
- node.isLeaf = false
- if (cursor.nextSibling()) break
- if (!cursor.parent()) return
- leave = true
- }
- }
- }
- function isChildOf(child: CursorNode, parent: CursorNode): boolean {
- return (
- child.from >= parent.from &&
- child.from <= parent.to &&
- child.to <= parent.to &&
- child.to >= parent.from
- )
- }
- export function validatorTraversal(
- input: Input | string,
- { fullMatch = true }: { fullMatch?: boolean } = {}
- ) {
- if (typeof input === 'string') input = new StringInput(input)
- const state = {
- valid: true,
- parentNodes: [] as CursorNode[],
- lastLeafTo: 0,
- }
- return {
- state,
- traversal: {
- onEnter(node) {
- state.valid = true
- if (!node.isLeaf) state.parentNodes.unshift(node)
- if (node.from > node.to || node.from < state.lastLeafTo) {
- state.valid = false
- } else if (node.isLeaf) {
- if (
- state.parentNodes.length &&
- !isChildOf(node, state.parentNodes[0])
- )
- state.valid = false
- state.lastLeafTo = node.to
- } else {
- if (state.parentNodes.length) {
- if (!isChildOf(node, state.parentNodes[0])) state.valid = false
- } else if (
- fullMatch &&
- (node.from !== 0 || node.to !== input.length)
- ) {
- state.valid = false
- }
- }
- },
- onLeave(node) {
- if (!node.isLeaf) state.parentNodes.shift()
- },
- } as TreeTraversal,
- }
- }
- export function validateTree(
- tree: TreeCursor | Tree | SyntaxNode,
- input: Input | string,
- options?: { fullMatch?: boolean }
- ): boolean {
- const { state, traversal } = validatorTraversal(input, options)
- traverseTree(tree, traversal)
- return state.valid
- }
- function colorize(value: any): string {
- return '' + value
- }
- type PrintTreeOptions = {
- from?: number
- to?: number
- start?: number
- includeParents?: boolean
- }
- export function printTree(
- cursor: TreeCursor | Tree | SyntaxNode,
- input: Input | string,
- { from, to, start = 0, includeParents }: PrintTreeOptions = {}
- ): string {
- const inp = typeof input === 'string' ? new StringInput(input) : input
- const text = Text.of(inp.read(0, inp.length).split('\n'))
- const state = {
- output: '',
- prefixes: [] as string[],
- hasNextSibling: false,
- }
- const validator = validatorTraversal(inp)
- traverseTree(cursor, {
- from,
- to,
- includeParents,
- beforeEnter(cursor) {
- state.hasNextSibling = cursor.nextSibling() && cursor.prevSibling()
- },
- onEnter(node) {
- validator.traversal.onEnter(node)
- const isTop = state.output === ''
- const hasPrefix = !isTop || node.from > 0
- if (hasPrefix) {
- state.output += (!isTop ? '\n' : '') + state.prefixes.join('')
- if (state.hasNextSibling) {
- state.output += ' ├─ '
- state.prefixes.push(' │ ')
- } else {
- state.output += ' └─ '
- state.prefixes.push(' ')
- }
- }
- const hasRange = node.from !== node.to
- state.output +=
- (node.type.isError || !validator.state.valid
- ? colorize('ERROR ' + node.type.name)
- : node.type.name) +
- ' ' +
- (hasRange
- ? '[' +
- colorize(locAt(text, start + node.from)) +
- '..' +
- colorize(locAt(text, start + node.to)) +
- ']'
- : colorize(locAt(text, start + node.from)))
- if (hasRange && node.isLeaf) {
- state.output +=
- ': ' + colorize(JSON.stringify(inp.read(node.from, node.to)))
- }
- },
- onLeave(node) {
- if (validator.traversal.onLeave) {
- validator.traversal.onLeave(node)
- }
- state.prefixes.pop()
- },
- })
- return state.output
- }
- function locAt(text: Text, pos: number): string {
- const line = text.lineAt(pos)
- return line.number + ':' + (pos - line.from)
- }
- export function logTree(
- tree: TreeCursor | Tree | SyntaxNode,
- input: string,
- options?: PrintTreeOptions
- ): void {
- console.log(printTree(tree, input, options))
- }
|