All files / src/language spel-formatter.ts

100% Statements 65/65
96.42% Branches 27/28
100% Functions 5/5
100% Lines 65/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 821x                     1x 1x 1x 1x 1x   1x 12x 7x 7x 7x 7x 7x 7x 2x 2x 3x 7x 2x 2x 7x   12x 13x 13x 13x 86x 86x 86x 6x 3x 3x 3x 3x 6x 86x 31x 31x 31x 86x 26x 8x 8x 26x 26x 29x 29x 13x 13x   12x 5x 5x 5x 5x 2x 2x 5x   12x 2x 2x 2x 2x 2x   12x 12x 12x    
import { SpelExpressionParser } from '../spel-expression-parser.js';
 
/**
 * Options for SpEL expression formatting.
 */
export interface FormatOptions {
  indentSize?: number;
  spacing?: 'always' | 'compact';
  maxLineWidth?: number;
}
 
const DEFAULT_OPTIONS: Required<FormatOptions> = {
  indentSize: 2,
  spacing: 'always',
  maxLineWidth: 120,
};
 
export namespace SpelFormatter {
  export function format(expression: string, options?: FormatOptions): string {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    try {
      const parser = new SpelExpressionParser();
      const ast = parser.parseRaw(expression);
      const formatted = ast.toStringAST();
      if (opts.spacing === 'compact') {
        return compactSpaces(formatted);
      }
      return formatted;
    } catch {
      return SpelFormatter.minify(expression);
    }
  }
 
  export function minify(expression: string): string {
    let inString: string | null = null;
    let result = '';
    for (let i = 0; i < expression.length; i++) {
      const ch = expression[i] ?? '';
      const prev = result[result.length - 1];
      if ((ch === "'" || ch === '"') && (i === 0 || expression[i - 1] !== '\\')) {
        if (inString === null) {
          inString = ch;
        } else if (inString === ch) {
          inString = null;
        }
      }
      if (inString !== null) {
        result += ch;
        continue;
      }
      if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
        if (prev && prev !== ' ' && isTokenChar(prev)) {
          result += ' ';
        }
        continue;
      }
      result += ch;
    }
    return result.trim();
  }
 
  export function semanticallyEqual(a: string, b: string): boolean {
    try {
      const parser = new SpelExpressionParser();
      return parser.parseRaw(a).toStringAST() === parser.parseRaw(b).toStringAST();
    } catch {
      return SpelFormatter.minify(a) === SpelFormatter.minify(b);
    }
  }
 
  function compactSpaces(expr: string): string {
    return expr
      .replace(/\s*([+\-*/%=<>!&|^?:.,;()[\]{}])\s*/g, '$1')
      .replace(/\s+(and|or|not|eq|ne|lt|gt|le|ge|mod|matches|between|instanceof|new)\s+/gi, ' $1 ')
      .trim();
  }
 
  function isTokenChar(ch: string): boolean {
    return /[\w#@]/.test(ch);
  }
}