parsingUtils.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { SyntaxNode, TreeCursor } from '@lezer/common';
  2. import { QueryBuilderOperation } from './types';
  3. // This is used for error type for some reason
  4. export const ErrorName = '⚠';
  5. export function getLeftMostChild(cur: SyntaxNode): SyntaxNode {
  6. return cur.firstChild ? getLeftMostChild(cur.firstChild) : cur;
  7. }
  8. export function makeError(expr: string, node: SyntaxNode) {
  9. return {
  10. text: getString(expr, node),
  11. // TODO: this are positions in the string with the replaced variables. Means it cannot be used to show exact
  12. // placement of the error for the user. We need some translation table to positions before the variable
  13. // replace.
  14. from: node.from,
  15. to: node.to,
  16. parentType: node.parent?.name,
  17. };
  18. }
  19. // Taken from template_srv, but copied so to not mess with the regex.index which is manipulated in the service
  20. /*
  21. * This regex matches 3 types of variable reference with an optional format specifier
  22. * \$(\w+) $var1
  23. * \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
  24. * \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
  25. */
  26. const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
  27. /**
  28. * As variables with $ are creating parsing errors, we first replace them with magic string that is parsable and at
  29. * the same time we can get the variable and it's format back from it.
  30. * @param expr
  31. */
  32. export function replaceVariables(expr: string) {
  33. return expr.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
  34. const fmt = fmt2 || fmt3;
  35. let variable = var1;
  36. let varType = '0';
  37. if (var2) {
  38. variable = var2;
  39. varType = '1';
  40. }
  41. if (var3) {
  42. variable = var3;
  43. varType = '2';
  44. }
  45. return `__V_${varType}__` + variable + '__V__' + (fmt ? '__F__' + fmt + '__F__' : '');
  46. });
  47. }
  48. const varTypeFunc = [
  49. (v: string, f?: string) => `\$${v}`,
  50. (v: string, f?: string) => `[[${v}${f ? `:${f}` : ''}]]`,
  51. (v: string, f?: string) => `\$\{${v}${f ? `:${f}` : ''}\}`,
  52. ];
  53. /**
  54. * Get back the text with variables in their original format.
  55. * @param expr
  56. */
  57. function returnVariables(expr: string) {
  58. return expr.replace(/__V_(\d)__(.+?)__V__(?:__F__(\w+)__F__)?/g, (match, type, v, f) => {
  59. return varTypeFunc[parseInt(type, 10)](v, f);
  60. });
  61. }
  62. /**
  63. * Get the actual string of the expression. That is not stored in the tree so we have to get the indexes from the node
  64. * and then based on that get it from the expression.
  65. * @param expr
  66. * @param node
  67. */
  68. export function getString(expr: string, node: SyntaxNode | TreeCursor | null | undefined) {
  69. if (!node) {
  70. return '';
  71. }
  72. return returnVariables(expr.substring(node.from, node.to));
  73. }
  74. /**
  75. * Create simple scalar binary op object.
  76. * @param opDef - definition of the op to be created
  77. * @param expr
  78. * @param numberNode - the node for the scalar
  79. * @param hasBool - whether operation has a bool modifier. Is used only for ops for which it makes sense.
  80. */
  81. export function makeBinOp(
  82. opDef: { id: string; comparison?: boolean },
  83. expr: string,
  84. numberNode: SyntaxNode,
  85. hasBool: boolean
  86. ): QueryBuilderOperation {
  87. const params: any[] = [parseFloat(getString(expr, numberNode))];
  88. if (opDef.comparison) {
  89. params.push(hasBool);
  90. }
  91. return {
  92. id: opDef.id,
  93. params,
  94. };
  95. }
  96. /**
  97. * Get all nodes with type in the tree. This traverses the tree so it is safe only when you know there shouldn't be
  98. * too much nesting but you just want to skip some of the wrappers. For example getting function args this way would
  99. * not be safe is it would also find arguments of nested functions.
  100. * @param expr
  101. * @param cur
  102. * @param type
  103. */
  104. export function getAllByType(expr: string, cur: SyntaxNode, type: string): string[] {
  105. if (cur.name === type) {
  106. return [getString(expr, cur)];
  107. }
  108. const values: string[] = [];
  109. let pos = 0;
  110. let child = cur.childAfter(pos);
  111. while (child) {
  112. values.push(...getAllByType(expr, child, type));
  113. pos = child.to;
  114. child = cur.childAfter(pos);
  115. }
  116. return values;
  117. }
  118. // Debugging function for convenience. Gives you nice output similar to linux tree util.
  119. // @ts-ignore
  120. export function log(expr: string, cur?: SyntaxNode) {
  121. if (!cur) {
  122. console.log('<empty>');
  123. return;
  124. }
  125. const json = toJson(expr, cur);
  126. const text = jsonToText(json);
  127. if (!text) {
  128. console.log('<empty>');
  129. return;
  130. }
  131. console.log(text);
  132. }
  133. function toJson(expr: string, cur: SyntaxNode) {
  134. const treeJson: any = {};
  135. const name = nodeToString(expr, cur);
  136. const children = [];
  137. let pos = 0;
  138. let child = cur.childAfter(pos);
  139. while (child) {
  140. children.push(toJson(expr, child));
  141. pos = child.to;
  142. child = cur.childAfter(pos);
  143. }
  144. treeJson.name = name;
  145. treeJson.children = children;
  146. return treeJson;
  147. }
  148. type JsonNode = {
  149. name: string;
  150. children: JsonNode[];
  151. };
  152. function jsonToText(
  153. node: JsonNode,
  154. context: { lastChild: boolean; indent: string } = {
  155. lastChild: true,
  156. indent: '',
  157. }
  158. ) {
  159. const name = node.name;
  160. const { lastChild, indent } = context;
  161. const newIndent = indent !== '' ? indent + (lastChild ? '└─' : '├─') : '';
  162. let text = newIndent + name;
  163. const children = node.children;
  164. children.forEach((child: any, index: number) => {
  165. const isLastChild = index === children.length - 1;
  166. text +=
  167. '\n' +
  168. jsonToText(child, {
  169. lastChild: isLastChild,
  170. indent: indent + (lastChild ? ' ' : '│ '),
  171. });
  172. });
  173. return text;
  174. }
  175. function nodeToString(expr: string, node: SyntaxNode) {
  176. return node.name + ': ' + getString(expr, node);
  177. }