linguist-js 2.6.0 → 2.7.0-pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,389 +1,429 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- const fs_1 = __importDefault(require("fs"));
6
- const path_1 = __importDefault(require("path"));
7
- const js_yaml_1 = __importDefault(require("js-yaml"));
8
- const ignore_1 = __importDefault(require("ignore"));
9
- const common_path_prefix_1 = __importDefault(require("common-path-prefix"));
10
- const binary_extensions_1 = __importDefault(require("binary-extensions"));
11
- const isbinaryfile_1 = require("isbinaryfile");
12
- const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
13
- const load_data_1 = __importDefault(require("./helpers/load-data"));
14
- const read_file_1 = __importDefault(require("./helpers/read-file"));
15
- const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
16
- async function analyse(input, opts = {}) {
17
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
18
- var _t, _u;
19
- const useRawContent = opts.fileContent !== undefined;
20
- input = [input !== null && input !== void 0 ? input : []].flat();
21
- opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
22
- // Load data from github-linguist web repo
23
- const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
24
- const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
25
- const docData = await (0, load_data_1.default)('documentation.yml', opts.offline).then(js_yaml_1.default.load);
26
- const heuristicsData = await (0, load_data_1.default)('heuristics.yml', opts.offline).then(js_yaml_1.default.load);
27
- const generatedData = await (0, load_data_1.default)('generated.rb', opts.offline).then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []; });
28
- const vendorPaths = [...vendorData, ...docData, ...generatedData];
29
- // Setup main variables
30
- const fileAssociations = {};
31
- const extensions = {};
32
- const overrides = {};
33
- const results = {
34
- files: { count: 0, bytes: 0, results: {}, alternatives: {} },
35
- languages: { count: 0, bytes: 0, results: {} },
36
- unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
37
- };
38
- // Prepare list of ignored files
39
- const gitignores = (0, ignore_1.default)();
40
- const regexIgnores = [];
41
- gitignores.add('/.git');
42
- if (!opts.keepVendored)
43
- regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
44
- if (opts.ignoredFiles)
45
- gitignores.add(opts.ignoredFiles);
46
- // Set a common root path so that vendor paths do not incorrectly match parent folders
47
- const resolvedInput = input.map(path => path_1.default.resolve(path).replace(/\\/g, '/'));
48
- const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
49
- const relPath = (file) => path_1.default.relative(commonRoot, file).replace(/\\/g, '/');
50
- const unRelPath = (file) => path_1.default.resolve(commonRoot, file).replace(/\\/g, '/');
51
- // Load file paths and folders
52
- let files, folders;
53
- if (useRawContent) {
54
- // Uses raw file content
55
- files = input;
56
- folders = [''];
57
- }
58
- else {
59
- // Uses directory on disc
60
- const data = (0, walk_tree_1.default)(true, commonRoot, input, gitignores, regexIgnores);
61
- files = data.files;
62
- folders = data.folders;
63
- }
64
- // Apply aliases
65
- opts = {
66
- checkIgnored: !opts.quick,
67
- checkAttributes: !opts.quick,
68
- checkHeuristics: !opts.quick,
69
- checkShebang: !opts.quick,
70
- checkModeline: !opts.quick,
71
- ...opts
72
- };
73
- // Ignore specific languages
74
- for (const lang of (_b = opts.ignoredLanguages) !== null && _b !== void 0 ? _b : []) {
75
- for (const key in langData) {
76
- if (lang.toLowerCase() === key.toLowerCase()) {
77
- delete langData[key];
78
- break;
79
- }
80
- }
81
- }
82
- // Load gitignores and gitattributes
83
- const customBinary = (0, ignore_1.default)();
84
- const customText = (0, ignore_1.default)();
85
- if (!useRawContent && opts.checkAttributes) {
86
- for (const folder of folders) {
87
- // Skip if folder is marked in gitattributes
88
- if (relPath(folder) && gitignores.ignores(relPath(folder))) {
89
- continue;
90
- }
91
- // Parse gitignores
92
- const ignoresFile = path_1.default.join(folder, '.gitignore');
93
- if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
94
- const ignoresData = await (0, read_file_1.default)(ignoresFile);
95
- gitignores.add(ignoresData);
96
- }
97
- // Parse gitattributes
98
- const attributesFile = path_1.default.join(folder, '.gitattributes');
99
- if (opts.checkAttributes && fs_1.default.existsSync(attributesFile)) {
100
- const attributesData = await (0, read_file_1.default)(attributesFile);
101
- // Explicit text/binary associations
102
- const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
103
- for (const [_line, path, type] of contentTypeMatches) {
104
- if (['text', '-binary'].includes(type)) {
105
- customText.add(path);
106
- }
107
- if (['-text', 'binary'].includes(type)) {
108
- customBinary.add(path);
109
- }
110
- }
111
- // Custom vendor options
112
- const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
113
- for (const [_line, path] of vendorMatches) {
114
- gitignores.add(path);
115
- }
116
- // Custom file associations
117
- const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm);
118
- for (let [_line, path, forcedLang] of customLangMatches) {
119
- // If specified language is an alias, associate it with its full name
120
- if (!langData[forcedLang]) {
121
- const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
122
- if (overrideLang) {
123
- forcedLang = overrideLang[0];
124
- }
125
- }
126
- const fullPath = path_1.default.join(relPath(folder), path);
127
- overrides[fullPath] = forcedLang;
128
- }
129
- }
130
- }
131
- }
132
- // Check vendored files
133
- if (!opts.keepVendored) {
134
- // Filter out any files that match a vendor file path
135
- if (useRawContent) {
136
- files = gitignores.filter(files);
137
- files = files.filter(file => !regexIgnores.find(match => match.test(file)));
138
- }
139
- else {
140
- files = gitignores.filter(files.map(relPath)).map(unRelPath);
141
- }
142
- }
143
- // Load all files and parse languages
144
- const addResult = (file, result) => {
145
- if (!fileAssociations[file]) {
146
- fileAssociations[file] = [];
147
- extensions[file] = '';
148
- }
149
- // Set parent to result group if it is present
150
- // Is nullish if either `opts.childLanguages` is set or if there is no group
151
- const finalResult = !opts.childLanguages && result && langData[result].group || result;
152
- if (!fileAssociations[file].includes(finalResult))
153
- fileAssociations[file].push(finalResult);
154
- extensions[file] = path_1.default.extname(file).toLowerCase();
155
- };
156
- const overridesArray = Object.entries(overrides);
157
- // List all languages that could be associated with a given file
158
- const definiteness = {};
159
- const fromShebang = {};
160
- for (const file of files) {
161
- let firstLine;
162
- if (useRawContent) {
163
- firstLine = (_e = (_d = (_c = opts.fileContent) === null || _c === void 0 ? void 0 : _c[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
164
- }
165
- else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
166
- firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
167
- }
168
- else
169
- continue;
170
- // Skip if file is unreadable
171
- if (firstLine === null)
172
- continue;
173
- // Check first line for explicit classification
174
- const hasShebang = opts.checkShebang && /^#!/.test(firstLine);
175
- const hasModeline = opts.checkModeline && /-\*-|(syntax|filetype|ft)\s*=/.test(firstLine);
176
- if (!opts.quick && (hasShebang || hasModeline)) {
177
- const matches = [];
178
- for (const [lang, data] of Object.entries(langData)) {
179
- const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
180
- // Check for interpreter match
181
- if (opts.checkShebang && hasShebang) {
182
- const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
183
- if (matchesInterpretor)
184
- matches.push(lang);
185
- }
186
- // Check modeline declaration
187
- if (opts.checkModeline && hasModeline) {
188
- const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
189
- const matchesLang = modelineText.match(langMatcher(lang));
190
- const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
191
- if (matchesLang || matchesAlias)
192
- matches.push(lang);
193
- }
194
- }
195
- // Add identified language(s)
196
- if (matches.length) {
197
- for (const match of matches)
198
- addResult(file, match);
199
- if (matches.length === 1)
200
- definiteness[file] = true;
201
- fromShebang[file] = true;
202
- continue;
203
- }
204
- }
205
- // Check override for manual language classification
206
- if (!useRawContent && !opts.quick && opts.checkAttributes) {
207
- const isOverridden = (path) => (0, ignore_1.default)().add(path).ignores(relPath(file));
208
- const match = overridesArray.find(item => isOverridden(item[0]));
209
- if (match) {
210
- const forcedLang = match[1];
211
- addResult(file, forcedLang);
212
- definiteness[file] = true;
213
- continue;
214
- }
215
- }
216
- // Search each language
217
- let skipExts = false;
218
- // Check if filename is a match
219
- for (const lang in langData) {
220
- const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
221
- if (matchesName) {
222
- addResult(file, lang);
223
- skipExts = true;
224
- }
225
- }
226
- // Check if extension is a match
227
- const possibleExts = [];
228
- if (!skipExts)
229
- for (const lang in langData) {
230
- const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
231
- if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
232
- for (const ext of extMatches)
233
- possibleExts.push({ ext, lang });
234
- }
235
- }
236
- // Apply more specific extension if available
237
- const isComplexExt = (ext) => /\..+\./.test(ext);
238
- const hasComplexExt = possibleExts.some(data => isComplexExt(data.ext));
239
- for (const { ext, lang } of possibleExts) {
240
- if (hasComplexExt && !isComplexExt(ext))
241
- continue;
242
- if (!hasComplexExt && isComplexExt(ext))
243
- continue;
244
- addResult(file, lang);
245
- }
246
- // Fallback to null if no language matches
247
- if (!fileAssociations[file]) {
248
- addResult(file, null);
249
- }
250
- }
251
- // Narrow down file associations to the best fit
252
- for (const file in fileAssociations) {
253
- // Skip if file has explicit association
254
- if (definiteness[file]) {
255
- results.files.results[file] = fileAssociations[file][0];
256
- continue;
257
- }
258
- // Skip binary files
259
- if (!useRawContent && !opts.keepBinary) {
260
- const isCustomText = customText.ignores(relPath(file));
261
- const isCustomBinary = customBinary.ignores(relPath(file));
262
- const isBinaryExt = binary_extensions_1.default.some(ext => file.endsWith('.' + ext));
263
- if (!isCustomText && (isCustomBinary || isBinaryExt || await (0, isbinaryfile_1.isBinaryFile)(file))) {
264
- continue;
265
- }
266
- }
267
- // Parse heuristics if applicable
268
- if (opts.checkHeuristics)
269
- for (const heuristics of heuristicsData.disambiguations) {
270
- // Make sure the extension matches the current file
271
- if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
272
- continue;
273
- // Load heuristic rules
274
- for (const heuristic of heuristics.rules) {
275
- // Make sure the language is not an array
276
- if (Array.isArray(heuristic.language)) {
277
- heuristic.language = heuristic.language[0];
278
- }
279
- // Make sure the results includes this language
280
- const languageGroup = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
281
- const matchesLang = fileAssociations[file].includes(heuristic.language);
282
- const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
283
- if (!matchesLang && !matchesParent)
284
- continue;
285
- // Normalise heuristic data
286
- const patterns = [];
287
- const normalise = (contents) => patterns.push(...[contents].flat());
288
- if (heuristic.pattern)
289
- normalise(heuristic.pattern);
290
- if (heuristic.named_pattern)
291
- normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
292
- if (heuristic.and) {
293
- for (const data of heuristic.and) {
294
- if (data.pattern)
295
- normalise(data.pattern);
296
- if (data.named_pattern)
297
- normalise(heuristicsData.named_patterns[data.named_pattern]);
298
- }
299
- }
300
- // Check file contents and apply heuristic patterns
301
- const fileContent = ((_l = opts.fileContent) === null || _l === void 0 ? void 0 : _l.length) ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
302
- // Skip if file read errors
303
- if (fileContent === null)
304
- continue;
305
- // Apply heuristics
306
- if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
307
- results.files.results[file] = heuristic.language;
308
- break;
309
- }
310
- }
311
- }
312
- // If no heuristics, assign a language
313
- if (!results.files.results[file]) {
314
- const possibleLangs = fileAssociations[file];
315
- // Assign first language as a default option
316
- const defaultLang = possibleLangs[0];
317
- const alternativeLangs = possibleLangs.slice(1);
318
- results.files.results[file] = defaultLang;
319
- // List alternative languages if there are any
320
- if (alternativeLangs.length > 0)
321
- results.files.alternatives[file] = alternativeLangs;
322
- }
323
- }
324
- // Skip specified categories
325
- if ((_m = opts.categories) === null || _m === void 0 ? void 0 : _m.length) {
326
- const categories = ['data', 'markup', 'programming', 'prose'];
327
- const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
328
- for (const [file, lang] of Object.entries(results.files.results)) {
329
- if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
330
- continue;
331
- }
332
- delete results.files.results[file];
333
- if (lang) {
334
- delete results.languages.results[lang];
335
- }
336
- }
337
- for (const category of hiddenCategories) {
338
- for (const [lang, { type }] of Object.entries(results.languages.results)) {
339
- if (type === category) {
340
- delete results.languages.results[lang];
341
- }
342
- }
343
- }
344
- }
345
- // Convert paths to relative
346
- if (!useRawContent && opts.relativePaths) {
347
- const newMap = {};
348
- for (const [file, lang] of Object.entries(results.files.results)) {
349
- let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
350
- if (!relPath.startsWith('../')) {
351
- relPath = './' + relPath;
352
- }
353
- newMap[relPath] = lang;
354
- }
355
- results.files.results = newMap;
356
- }
357
- // Load language bytes size
358
- for (const [file, lang] of Object.entries(results.files.results)) {
359
- if (lang && !langData[lang])
360
- continue;
361
- const fileSize = (_q = (_p = (_o = opts.fileContent) === null || _o === void 0 ? void 0 : _o[files.indexOf(file)]) === null || _p === void 0 ? void 0 : _p.length) !== null && _q !== void 0 ? _q : fs_1.default.statSync(file).size;
362
- results.files.bytes += fileSize;
363
- // If no language found, add extension in other section
364
- if (!lang) {
365
- const ext = path_1.default.extname(file);
366
- const unknownType = ext === '' ? 'filenames' : 'extensions';
367
- const name = ext === '' ? path_1.default.basename(file) : ext;
368
- (_r = (_t = results.unknown[unknownType])[name]) !== null && _r !== void 0 ? _r : (_t[name] = 0);
369
- results.unknown[unknownType][name] += fileSize;
370
- results.unknown.bytes += fileSize;
371
- continue;
372
- }
373
- // Add language and bytes data to corresponding section
374
- const { type } = langData[lang];
375
- (_s = (_u = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_u[lang] = { type, bytes: 0, color: langData[lang].color });
376
- if (opts.childLanguages) {
377
- results.languages.results[lang].parent = langData[lang].group;
378
- }
379
- results.languages.results[lang].bytes += fileSize;
380
- results.languages.bytes += fileSize;
381
- }
382
- // Set counts
383
- results.files.count = Object.keys(results.files.results).length;
384
- results.languages.count = Object.keys(results.languages.results).length;
385
- results.unknown.count = Object.keys({ ...results.unknown.extensions, ...results.unknown.filenames }).length;
386
- // Return
387
- return results;
388
- }
389
- module.exports = analyse;
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ const fs_1 = __importDefault(require("fs"));
29
+ const path_1 = __importDefault(require("path"));
30
+ const js_yaml_1 = __importDefault(require("js-yaml"));
31
+ const ignore_1 = __importDefault(require("ignore"));
32
+ const common_path_prefix_1 = __importDefault(require("common-path-prefix"));
33
+ const binary_extensions_1 = __importDefault(require("binary-extensions"));
34
+ const isbinaryfile_1 = require("isbinaryfile");
35
+ const walk_tree_1 = __importDefault(require("./helpers/walk-tree"));
36
+ const load_data_1 = __importStar(require("./helpers/load-data"));
37
+ const read_file_1 = __importDefault(require("./helpers/read-file"));
38
+ const parse_gitattributes_1 = __importDefault(require("./helpers/parse-gitattributes"));
39
+ const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
40
+ async function analyse(rawInput, opts = {}) {
41
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
42
+ var _r, _s;
43
+ const useRawContent = opts.fileContent !== undefined;
44
+ const input = [rawInput !== null && rawInput !== void 0 ? rawInput : []].flat();
45
+ const manualFileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
46
+ // Normalise input option arguments
47
+ opts = {
48
+ checkIgnored: !opts.quick,
49
+ checkAttributes: !opts.quick,
50
+ checkHeuristics: !opts.quick,
51
+ checkShebang: !opts.quick,
52
+ checkModeline: !opts.quick,
53
+ ...opts,
54
+ };
55
+ // Load data from github-linguist web repo
56
+ const langData = await (0, load_data_1.default)('languages.yml', opts.offline).then(js_yaml_1.default.load);
57
+ const vendorData = await (0, load_data_1.default)('vendor.yml', opts.offline).then(js_yaml_1.default.load);
58
+ const docData = await (0, load_data_1.default)('documentation.yml', opts.offline).then(js_yaml_1.default.load);
59
+ const heuristicsData = await (0, load_data_1.default)('heuristics.yml', opts.offline).then(js_yaml_1.default.load);
60
+ const generatedData = await (0, load_data_1.default)('generated.rb', opts.offline).then(load_data_1.parseGeneratedDataFile);
61
+ const vendorPaths = [...vendorData, ...docData, ...generatedData];
62
+ // Setup main variables
63
+ const fileAssociations = {};
64
+ const extensions = {};
65
+ const globOverrides = {};
66
+ const results = {
67
+ files: { count: 0, bytes: 0, results: {}, alternatives: {} },
68
+ languages: { count: 0, bytes: 0, results: {} },
69
+ unknown: { count: 0, bytes: 0, extensions: {}, filenames: {} },
70
+ };
71
+ // Set a common root path so that vendor paths do not incorrectly match parent folders
72
+ const normPath = (file) => file.replace(/\\/g, '/');
73
+ const resolvedInput = input.map(path => normPath(path_1.default.resolve(path)));
74
+ const commonRoot = (input.length > 1 ? (0, common_path_prefix_1.default)(resolvedInput) : resolvedInput[0]).replace(/\/?$/, '');
75
+ const localRoot = (folder) => folder.replace(commonRoot, '').replace(/^\//, '');
76
+ const relPath = (file) => useRawContent ? file : normPath(path_1.default.relative(commonRoot, file));
77
+ const unRelPath = (file) => useRawContent ? file : normPath(path_1.default.resolve(commonRoot, file));
78
+ // Other helper functions
79
+ const fileMatchesGlobs = (file, ...globs) => (0, ignore_1.default)().add(globs).ignores(relPath(file));
80
+ const filterOutIgnored = (files, ignored) => ignored.filter(files.map(relPath)).map(unRelPath);
81
+ //*PREPARE FILES AND DATA*//
82
+ // Prepare list of ignored files
83
+ const ignored = (0, ignore_1.default)();
84
+ ignored.add('.git/');
85
+ ignored.add((_b = opts.ignoredFiles) !== null && _b !== void 0 ? _b : []);
86
+ const regexIgnores = [];
87
+ if (!opts.keepVendored)
88
+ regexIgnores.push(...vendorPaths.map(path => RegExp(path, 'i')));
89
+ // Load file paths and folders
90
+ let files;
91
+ let folders;
92
+ if (useRawContent) {
93
+ // Uses raw file content
94
+ files = input;
95
+ folders = [''];
96
+ }
97
+ else {
98
+ // Uses directory on disc
99
+ const data = (0, walk_tree_1.default)({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, ignored });
100
+ files = data.files;
101
+ folders = data.folders;
102
+ }
103
+ // Load gitignore data and apply ignores rules
104
+ if (!useRawContent) {
105
+ // TODO switch to be like the gitattributes code
106
+ for (const folder of folders) {
107
+ // Parse gitignores
108
+ const ignoresFile = path_1.default.join(folder, '.gitignore');
109
+ if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
110
+ const ignoresData = await (0, read_file_1.default)(ignoresFile);
111
+ const localIgnoresData = ignoresData.replace(/^[\/\\]/g, localRoot(folder) + '/');
112
+ ignored.add(localIgnoresData);
113
+ files = filterOutIgnored(files, ignored);
114
+ }
115
+ }
116
+ }
117
+ // Fetch and normalise gitattributes data of all subfolders and save to metadata
118
+ const manualAttributes = {}; // Maps file globs to gitattribute boolean flags
119
+ const getFlaggedGlobs = (attr, val) => {
120
+ return Object.entries(manualAttributes).filter(([, attrs]) => attrs[attr] === val).map(([glob,]) => glob);
121
+ };
122
+ if (!useRawContent && opts.checkAttributes) {
123
+ const nestedAttrFiles = files.filter(file => file.endsWith('.gitattributes'));
124
+ for (const attrFile of nestedAttrFiles) {
125
+ const relAttrFile = relPath(attrFile);
126
+ const relAttrFolder = path_1.default.dirname(relAttrFile);
127
+ const contents = await (0, read_file_1.default)(attrFile);
128
+ const parsed = (0, parse_gitattributes_1.default)(contents, relAttrFolder);
129
+ for (const { glob, attrs } of parsed) {
130
+ manualAttributes[glob] = attrs;
131
+ }
132
+ }
133
+ }
134
+ // Apply vendor file path matches and filter out vendored files
135
+ if (!opts.keepVendored) {
136
+ // Get data of files that have been manually marked with metadata
137
+ const vendorTrueGlobs = [...getFlaggedGlobs('vendored', true), ...getFlaggedGlobs('generated', true), ...getFlaggedGlobs('documentation', true)];
138
+ const vendorFalseGlobs = [...getFlaggedGlobs('vendored', false), ...getFlaggedGlobs('generated', false), ...getFlaggedGlobs('documentation', false)];
139
+ // Set up glob ignore object to use for expanding globs to match files
140
+ const vendorOverrides = (0, ignore_1.default)().add(vendorFalseGlobs);
141
+ // Remove all files marked as vendored by default
142
+ const excludedFiles = files.filter(file => vendorPaths.some(pathPtn => RegExp(pathPtn, 'i').test(relPath(file))));
143
+ files = files.filter(file => !excludedFiles.includes(file));
144
+ // Re-add removed files that are overridden manually in gitattributes
145
+ const overriddenExcludedFiles = excludedFiles.filter(file => vendorOverrides.ignores(relPath(file)));
146
+ files.push(...overriddenExcludedFiles);
147
+ // Remove files explicitly marked as vendored in gitattributes
148
+ // TODO change globs.includes(file) to parse the glob using ignore()
149
+ files = files.filter(file => !vendorTrueGlobs.includes(relPath(file)));
150
+ }
151
+ // Filter out binary files
152
+ if (!opts.keepBinary) {
153
+ // Filter out files that are binary by default
154
+ files = files.filter(file => !binary_extensions_1.default.some(ext => file.endsWith('.' + ext)));
155
+ // Filter out manually specified binary files
156
+ const binaryIgnored = (0, ignore_1.default)().add(getFlaggedGlobs('binary', true));
157
+ files = filterOutIgnored(files, binaryIgnored);
158
+ // Re-add files manually marked not as binary
159
+ const binaryUnignored = (0, ignore_1.default)().add(getFlaggedGlobs('binary', false));
160
+ // TODO parse the globs using ignore()
161
+ const unignoredList = filterOutIgnored(files, binaryUnignored);
162
+ files.push(...unignoredList);
163
+ }
164
+ // Ignore specific languages
165
+ for (const lang of (_c = opts.ignoredLanguages) !== null && _c !== void 0 ? _c : []) {
166
+ for (const key in langData) {
167
+ if (lang.toLowerCase() === key.toLowerCase()) {
168
+ delete langData[key];
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ // Establish language overrides taken from gitattributes
174
+ const forcedLangs = Object.entries(manualAttributes).filter(([, attrs]) => attrs.language);
175
+ for (const [globPath, attrs] of forcedLangs) {
176
+ let forcedLang = attrs.language;
177
+ if (!forcedLang)
178
+ continue;
179
+ // If specified language is an alias, associate it with its full name
180
+ if (!langData[forcedLang]) {
181
+ const overrideLang = Object.entries(langData).find(entry => { var _a; return (_a = entry[1].aliases) === null || _a === void 0 ? void 0 : _a.includes(forcedLang.toLowerCase()); });
182
+ if (overrideLang) {
183
+ forcedLang = overrideLang[0];
184
+ }
185
+ }
186
+ globOverrides[globPath] = forcedLang;
187
+ }
188
+ //*PARSE LANGUAGES*//
189
+ const addResult = (file, result) => {
190
+ if (!fileAssociations[file]) {
191
+ fileAssociations[file] = [];
192
+ extensions[file] = '';
193
+ }
194
+ // Set parent to result group if it is present
195
+ // Is nullish if either `opts.childLanguages` is set or if there is no group
196
+ const finalResult = !opts.childLanguages && result && langData[result] && langData[result].group || result;
197
+ if (!fileAssociations[file].includes(finalResult)) {
198
+ fileAssociations[file].push(finalResult);
199
+ }
200
+ extensions[file] = path_1.default.extname(file).toLowerCase();
201
+ };
202
+ const definiteness = {};
203
+ const fromShebang = {};
204
+ fileLoop: for (const file of files) {
205
+ // Check manual override
206
+ for (const globMatch in globOverrides) {
207
+ if (!fileMatchesGlobs(file, globMatch))
208
+ continue;
209
+ // If the given file matches the glob, apply the override to the file
210
+ const forcedLang = globOverrides[globMatch];
211
+ addResult(file, forcedLang);
212
+ definiteness[file] = true;
213
+ continue fileLoop; // no need to check other heuristics, the classified language has been found
214
+ }
215
+ // Check first line for readability
216
+ let firstLine;
217
+ if (useRawContent) {
218
+ firstLine = (_e = (_d = manualFileContent[files.indexOf(file)]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) !== null && _e !== void 0 ? _e : null;
219
+ }
220
+ else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
221
+ firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
222
+ }
223
+ else
224
+ continue;
225
+ // Skip if file is unreadable or blank
226
+ if (firstLine === null)
227
+ continue;
228
+ // Check first line for explicit classification
229
+ const hasShebang = opts.checkShebang && /^#!/.test(firstLine);
230
+ const hasModeline = opts.checkModeline && /-\*-|(syntax|filetype|ft)\s*=/.test(firstLine);
231
+ if (!opts.quick && (hasShebang || hasModeline)) {
232
+ const matches = [];
233
+ for (const [lang, data] of Object.entries(langData)) {
234
+ const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
235
+ // Check for interpreter match
236
+ if (opts.checkShebang && hasShebang) {
237
+ const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
238
+ if (matchesInterpretor)
239
+ matches.push(lang);
240
+ }
241
+ // Check modeline declaration
242
+ if (opts.checkModeline && hasModeline) {
243
+ const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
244
+ const matchesLang = modelineText.match(langMatcher(lang));
245
+ const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
246
+ if (matchesLang || matchesAlias)
247
+ matches.push(lang);
248
+ }
249
+ }
250
+ // Add identified language(s)
251
+ if (matches.length) {
252
+ for (const match of matches)
253
+ addResult(file, match);
254
+ if (matches.length === 1)
255
+ definiteness[file] = true;
256
+ fromShebang[file] = true;
257
+ continue;
258
+ }
259
+ }
260
+ // Search each language
261
+ let skipExts = false;
262
+ // Check if filename is a match
263
+ for (const lang in langData) {
264
+ const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
265
+ if (matchesName) {
266
+ addResult(file, lang);
267
+ skipExts = true;
268
+ }
269
+ }
270
+ // Check if extension is a match
271
+ const possibleExts = [];
272
+ if (!skipExts)
273
+ for (const lang in langData) {
274
+ const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
275
+ if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
276
+ for (const ext of extMatches)
277
+ possibleExts.push({ ext, lang });
278
+ }
279
+ }
280
+ // Apply more specific extension if available
281
+ const isComplexExt = (ext) => /\..+\./.test(ext);
282
+ const hasComplexExt = possibleExts.some(data => isComplexExt(data.ext));
283
+ for (const { ext, lang } of possibleExts) {
284
+ if (hasComplexExt && !isComplexExt(ext))
285
+ continue;
286
+ if (!hasComplexExt && isComplexExt(ext))
287
+ continue;
288
+ addResult(file, lang);
289
+ }
290
+ // Fallback to null if no language matches
291
+ if (!fileAssociations[file]) {
292
+ addResult(file, null);
293
+ }
294
+ }
295
+ // Narrow down file associations to the best fit
296
+ for (const file in fileAssociations) {
297
+ // Skip if file has explicit association
298
+ if (definiteness[file]) {
299
+ results.files.results[file] = fileAssociations[file][0];
300
+ continue;
301
+ }
302
+ // Skip binary files
303
+ if (!useRawContent && !opts.keepBinary) {
304
+ if (await (0, isbinaryfile_1.isBinaryFile)(file))
305
+ continue;
306
+ }
307
+ // Parse heuristics if applicable
308
+ if (opts.checkHeuristics)
309
+ for (const heuristics of heuristicsData.disambiguations) {
310
+ // Make sure the extension matches the current file
311
+ if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
312
+ continue;
313
+ // Load heuristic rules
314
+ for (const heuristic of heuristics.rules) {
315
+ // Make sure the language is not an array
316
+ if (Array.isArray(heuristic.language)) {
317
+ heuristic.language = heuristic.language[0];
318
+ }
319
+ // Make sure the results includes this language
320
+ const languageGroup = (_k = langData[heuristic.language]) === null || _k === void 0 ? void 0 : _k.group;
321
+ const matchesLang = fileAssociations[file].includes(heuristic.language);
322
+ const matchesParent = languageGroup && fileAssociations[file].includes(languageGroup);
323
+ if (!matchesLang && !matchesParent)
324
+ continue;
325
+ // Normalise heuristic data
326
+ const patterns = [];
327
+ const normalise = (contents) => patterns.push(...[contents].flat());
328
+ if (heuristic.pattern)
329
+ normalise(heuristic.pattern);
330
+ if (heuristic.named_pattern)
331
+ normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
332
+ if (heuristic.and) {
333
+ for (const data of heuristic.and) {
334
+ if (data.pattern)
335
+ normalise(data.pattern);
336
+ if (data.named_pattern)
337
+ normalise(heuristicsData.named_patterns[data.named_pattern]);
338
+ }
339
+ }
340
+ // Check file contents and apply heuristic patterns
341
+ const fileContent = opts.fileContent ? manualFileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
342
+ // Skip if file read errors
343
+ if (fileContent === null)
344
+ continue;
345
+ // Apply heuristics
346
+ if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
347
+ results.files.results[file] = heuristic.language;
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ // If no heuristics, assign a language
353
+ if (!results.files.results[file]) {
354
+ const possibleLangs = fileAssociations[file];
355
+ // Assign first language as a default option
356
+ const defaultLang = possibleLangs[0];
357
+ const alternativeLangs = possibleLangs.slice(1);
358
+ results.files.results[file] = defaultLang;
359
+ // List alternative languages if there are any
360
+ if (alternativeLangs.length > 0)
361
+ results.files.alternatives[file] = alternativeLangs;
362
+ }
363
+ }
364
+ // Skip specified categories
365
+ if ((_l = opts.categories) === null || _l === void 0 ? void 0 : _l.length) {
366
+ const categories = ['data', 'markup', 'programming', 'prose'];
367
+ const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
368
+ for (const [file, lang] of Object.entries(results.files.results)) {
369
+ if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
370
+ continue;
371
+ }
372
+ delete results.files.results[file];
373
+ if (lang) {
374
+ delete results.languages.results[lang];
375
+ }
376
+ }
377
+ for (const category of hiddenCategories) {
378
+ for (const [lang, { type }] of Object.entries(results.languages.results)) {
379
+ if (type === category) {
380
+ delete results.languages.results[lang];
381
+ }
382
+ }
383
+ }
384
+ }
385
+ // Convert paths to relative
386
+ if (!useRawContent && opts.relativePaths) {
387
+ const newMap = {};
388
+ for (const [file, lang] of Object.entries(results.files.results)) {
389
+ let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
390
+ if (!relPath.startsWith('../')) {
391
+ relPath = './' + relPath;
392
+ }
393
+ newMap[relPath] = lang;
394
+ }
395
+ results.files.results = newMap;
396
+ }
397
+ // Load language bytes size
398
+ for (const [file, lang] of Object.entries(results.files.results)) {
399
+ if (lang && !langData[lang])
400
+ continue;
401
+ const fileSize = (_o = (_m = manualFileContent[files.indexOf(file)]) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : fs_1.default.statSync(file).size;
402
+ results.files.bytes += fileSize;
403
+ // If no language found, add extension in other section
404
+ if (!lang) {
405
+ const ext = path_1.default.extname(file);
406
+ const unknownType = ext === '' ? 'filenames' : 'extensions';
407
+ const name = ext === '' ? path_1.default.basename(file) : ext;
408
+ (_p = (_r = results.unknown[unknownType])[name]) !== null && _p !== void 0 ? _p : (_r[name] = 0);
409
+ results.unknown[unknownType][name] += fileSize;
410
+ results.unknown.bytes += fileSize;
411
+ continue;
412
+ }
413
+ // Add language and bytes data to corresponding section
414
+ const { type } = langData[lang];
415
+ (_q = (_s = results.languages.results)[lang]) !== null && _q !== void 0 ? _q : (_s[lang] = { type, bytes: 0, color: langData[lang].color });
416
+ if (opts.childLanguages) {
417
+ results.languages.results[lang].parent = langData[lang].group;
418
+ }
419
+ results.languages.results[lang].bytes += fileSize;
420
+ results.languages.bytes += fileSize;
421
+ }
422
+ // Set counts
423
+ results.files.count = Object.keys(results.files.results).length;
424
+ results.languages.count = Object.keys(results.languages.results).length;
425
+ results.unknown.count = Object.keys({ ...results.unknown.extensions, ...results.unknown.filenames }).length;
426
+ // Return
427
+ return results;
428
+ }
429
+ module.exports = analyse;