/** `.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 }; /** 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[] { const rules: AttributeRule[] = []; for (const line of text.split("\n")) { if (rules.length >= maxRules) break; const [pattern, ...fields] = line.trim().split(/\s+/); if (!pattern || pattern.startsWith("#") || fields.length === 0) continue; const attributes = new Map(); for (const field of fields) { if (field.startsWith("-")) attributes.set(field.slice(1), false); else if (field.startsWith("!")) attributes.set(field.slice(1), null); else if (field.includes("=")) attributes.set(field.slice(0, field.indexOf("=")), field.slice(field.indexOf("=") + 1)); else attributes.set(field, true); } // `dir/` (as linguist's docs write vendored directories) covers everything under dir at any depth. const directory = pattern.endsWith("/"); const body = pattern.replace(/^\//, "").replace(/\/$/, ""); const glob = directory ? `${body.includes("/") || pattern.startsWith("/") ? "" : "**/"}${body}/**` : body; rules.push({ regex: attributeGlob(glob), anchored: directory || pattern.includes("/"), attributes }); } return rules; } /** Every attribute the rules set for `path`, the last matching rule winning per attribute. */ export function attributesFor(rules: readonly AttributeRule[], path: string): Map { const out = new Map(); const basename = path.slice(path.lastIndexOf("/") + 1); for (const rule of rules) { if (!rule.regex.test(rule.anchored ? path : basename)) continue; for (const [name, value] of rule.attributes) out.set(name, value); } return out; }