All files / src/util ascii.ts

100% Statements 21/21
100% Branches 14/14
100% Functions 2/2
100% Lines 21/21

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                                    25x 25x 25x 25x                 14259x 14259x   14259x 62720x 62720x 40x 40x   62680x 61716x       14259x 467x     13792x 13792x 62061x 62061x     13792x         5329x    
/**
 * ASCII-only case folding.
 *
 * Spring folds textual operator names with `toUpperCase(Locale.ROOT)`. We
 * deliberately implement an ASCII-only equivalent instead of calling
 * `String#toUpperCase`, for two reasons:
 *
 *  1. `toUpperCase` is locale-sensitive — under the Turkish locale `i` folds to
 *     `İ`, so `div` would stop matching. Spring pins `Locale.ROOT` for exactly
 *     this reason; folding ASCII ourselves removes the dependency entirely.
 *  2. `toUpperCase` can change string length (`ß` becomes `SS`), which would
 *     make a length-sensitive keyword lookup unreliable.
 *
 * Restricting the fold to ASCII also yields a useful property: non-ASCII input
 * short-circuits and can never be mistaken for a keyword. That is what lets
 * identifiers such as `年龄` or `café` coexist with the keyword table.
 */
 
const LOWER_A = 0x61;
const LOWER_Z = 0x7a;
const ASCII_MAX = 0x7f;
const CASE_DELTA = 32;
 
/**
 * Fold a string to upper case over ASCII letters only.
 *
 * Returns the input unchanged when it contains any non-ASCII character, since
 * such a string can never equal an ASCII keyword.
 */
export function foldAsciiUpper(text: string): string {
  let hasLowerAscii = false;
  let hasNonAscii = false;
 
  for (const character of text) {
    const code = character.charCodeAt(0);
    if (code > ASCII_MAX) {
      hasNonAscii = true;
      break;
    }
    if (code >= LOWER_A && code <= LOWER_Z) {
      hasLowerAscii = true;
    }
  }
 
  if (hasNonAscii || !hasLowerAscii) {
    return text;
  }
 
  let folded = '';
  for (const character of text) {
    const code = character.charCodeAt(0);
    folded +=
      code >= LOWER_A && code <= LOWER_Z ? String.fromCharCode(code - CASE_DELTA) : character;
  }
  return folded;
}
 
/** Case-insensitive equality over ASCII letters, matching Spring's fold. */
export function equalsIgnoreCaseAscii(left: string, right: string): boolean {
  return foldAsciiUpper(left) === foldAsciiUpper(right);
}