print-tree.mjs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // from https://gist.github.com/msteen/e4828fbf25d6efef73576fc43ac479d2
  2. // https://discuss.codemirror.net/t/whats-the-best-to-test-and-debug-grammars/2542/5
  3. // MIT License
  4. //
  5. // Copyright (c) 2021 Matthijs Steen
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in all
  15. // copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. // SOFTWARE.
  24. import { Text } from '@codemirror/state'
  25. import { Tree, TreeCursor } from '@lezer/common'
  26. class StringInput {
  27. constructor(input) {
  28. this.input = input
  29. this.lineChunks = false
  30. }
  31. get length() {
  32. return this.input.length
  33. }
  34. chunk(from) {
  35. return this.input.slice(from)
  36. }
  37. read(from, to) {
  38. return this.input.slice(from, to)
  39. }
  40. }
  41. function cursorNode({ type, from, to }, isLeaf = false) {
  42. return { type, from, to, isLeaf }
  43. }
  44. function traverseTree(
  45. cursor,
  46. {
  47. from = -Infinity,
  48. to = Infinity,
  49. includeParents = false,
  50. beforeEnter,
  51. onEnter,
  52. onLeave,
  53. }
  54. ) {
  55. if (!(cursor instanceof TreeCursor))
  56. cursor = cursor instanceof Tree ? cursor.cursor() : cursor.cursor()
  57. for (;;) {
  58. let node = cursorNode(cursor)
  59. let leave = false
  60. if (node.from <= to && node.to >= from) {
  61. const enter =
  62. !node.type.isAnonymous &&
  63. (includeParents || (node.from >= from && node.to <= to))
  64. if (enter && beforeEnter) beforeEnter(cursor)
  65. node.isLeaf = !cursor.firstChild()
  66. if (enter) {
  67. leave = true
  68. if (onEnter(node) === false) return
  69. }
  70. if (!node.isLeaf) continue
  71. }
  72. for (;;) {
  73. node = cursorNode(cursor, node.isLeaf)
  74. if (leave && onLeave) if (onLeave(node) === false) return
  75. leave = cursor.type.isAnonymous
  76. node.isLeaf = false
  77. if (cursor.nextSibling()) break
  78. if (!cursor.parent()) return
  79. leave = true
  80. }
  81. }
  82. }
  83. function isChildOf(child, parent) {
  84. return (
  85. child.from >= parent.from &&
  86. child.from <= parent.to &&
  87. child.to <= parent.to &&
  88. child.to >= parent.from
  89. )
  90. }
  91. function validatorTraversal(input, { fullMatch = true } = {}) {
  92. if (typeof input === 'string') input = new StringInput(input)
  93. const state = {
  94. valid: true,
  95. parentNodes: [],
  96. lastLeafTo: 0,
  97. }
  98. return {
  99. state,
  100. traversal: {
  101. onEnter(node) {
  102. state.valid = true
  103. if (!node.isLeaf) state.parentNodes.unshift(node)
  104. if (node.from > node.to || node.from < state.lastLeafTo) {
  105. state.valid = false
  106. } else if (node.isLeaf) {
  107. if (
  108. state.parentNodes.length &&
  109. !isChildOf(node, state.parentNodes[0])
  110. )
  111. state.valid = false
  112. state.lastLeafTo = node.to
  113. } else {
  114. if (state.parentNodes.length) {
  115. if (!isChildOf(node, state.parentNodes[0])) state.valid = false
  116. } else if (
  117. fullMatch &&
  118. (node.from !== 0 || node.to !== input.length)
  119. ) {
  120. state.valid = false
  121. }
  122. }
  123. },
  124. onLeave(node) {
  125. if (!node.isLeaf) state.parentNodes.shift()
  126. },
  127. },
  128. }
  129. }
  130. let Color
  131. ;(function (Color) {
  132. Color[(Color.Red = 31)] = 'Red'
  133. Color[(Color.Green = 32)] = 'Green'
  134. Color[(Color.Yellow = 33)] = 'Yellow'
  135. })(Color || (Color = {}))
  136. function colorize(value, color) {
  137. return '\u001b[' + color + 'm' + String(value) + '\u001b[39m'
  138. }
  139. function printTree(
  140. cursor,
  141. input,
  142. { from, to, start = 0, includeParents } = {}
  143. ) {
  144. const inp = typeof input === 'string' ? new StringInput(input) : input
  145. const text = Text.of(inp.read(0, inp.length).split('\n'))
  146. const state = {
  147. output: '',
  148. prefixes: [],
  149. hasNextSibling: false,
  150. }
  151. const validator = validatorTraversal(inp)
  152. traverseTree(cursor, {
  153. from,
  154. to,
  155. includeParents,
  156. beforeEnter(cursor) {
  157. state.hasNextSibling = cursor.nextSibling() && cursor.prevSibling()
  158. },
  159. onEnter(node) {
  160. validator.traversal.onEnter(node)
  161. const isTop = state.output === ''
  162. const hasPrefix = !isTop || node.from > 0
  163. if (hasPrefix) {
  164. state.output += (!isTop ? '\n' : '') + state.prefixes.join('')
  165. if (state.hasNextSibling) {
  166. state.output += ' ├─ '
  167. state.prefixes.push(' │ ')
  168. } else {
  169. state.output += ' └─ '
  170. state.prefixes.push(' ')
  171. }
  172. }
  173. const hasRange = node.from !== node.to
  174. state.output +=
  175. (node.type.isError || !validator.state.valid
  176. ? colorize('ERROR ' + node.type.name, Color.Red)
  177. : node.type.name) +
  178. ' ' +
  179. (hasRange
  180. ? '[' +
  181. colorize(locAt(text, start + node.from), Color.Yellow) +
  182. '..' +
  183. colorize(locAt(text, start + node.to), Color.Yellow) +
  184. ']'
  185. : colorize(locAt(text, start + node.from), Color.Yellow))
  186. if (hasRange && node.isLeaf) {
  187. state.output +=
  188. ': ' +
  189. colorize(JSON.stringify(inp.read(node.from, node.to)), Color.Green)
  190. }
  191. },
  192. onLeave(node) {
  193. validator.traversal.onLeave(node)
  194. state.prefixes.pop()
  195. },
  196. })
  197. return state.output
  198. }
  199. function locAt(text, pos) {
  200. const line = text.lineAt(pos)
  201. return line.number + ':' + (pos - line.from)
  202. }
  203. export function logTree(tree, input, options) {
  204. console.warn(printTree(tree, input, options))
  205. }