73 lines · 3.3 KB
/** `.gitattributes` parsing shared by the editor's LFS guard and the language breakdown. */
const REGEX_SPECIAL = /[.+^${}()|\\]/;

/** gitattributes/fnmatch globs: `*` and `?` stay within a segment, `**` spans them, `[...]` is a class. */
export function attributeGlob(pattern: string): RegExp {
  let source = "";
  for (let index = 0; index < pattern.length; index++) {
    const char = pattern[index];
    if (char === "*" && pattern[index + 1] === "*") {
      const slash = pattern[index + 2] === "/";
      source += slash ? "(?:.*/)?" : ".*";
      index += slash ? 2 : 1;
    } else if (char === "*") source += "[^/]*";
    else if (char === "?") source += "[^/]";
    else if (char === "[") {
      const close = pattern.indexOf("]", index + 2);
      if (close === -1) {
        source += "\\[";
        continue;
      }
      const body = pattern
        .slice(index + 1, close)
        .replace(/^!/, "^")
        .replace(/\\/g, "\\\\");
      source += `[${body}]`;
      index = close;
    } else if (char === "\\" && index + 1 < pattern.length) {
      index++;
      source += pattern[index].replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
    } else source += REGEX_SPECIAL.test(char) || char === "]" ? `\\${char}` : char;
  }
  return new RegExp(`^${source}$`);
}

/** A value set by a rule: `attr` is true, `-attr` false, `!attr` unset (null), `attr=value` the value. */
export type AttributeValue = string | boolean | null;
export type AttributeRule = { regex: RegExp; anchored: boolean; attributes: Map<string, AttributeValue> };

/** Rules in file order (later ones win, as in git); macros and nested `.gitattributes` files are not read. */
export function parseAttributes(text: string, maxRules: number): AttributeRule[] {