linguist-js 2.4.2 → 2.5.2

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/cli.js CHANGED
@@ -19,6 +19,7 @@ commander_1.program
19
19
  .option('-j|--json [bool]', 'Display the output as JSON', false)
20
20
  .option('-t|--tree <traversal>', 'Which part of the output JSON to display (dot-delimited)')
21
21
  .option('-q|--quick [bool]', 'Skip complex language analysis (alias for -{A|I|H|S}=false)', false)
22
+ .option('-o|--offline [bool]', 'Use packaged data files instead of fetching latest from GitHub', false)
22
23
  .option('-V|--keepVendored [bool]', 'Prevent skipping over vendored/generated files', false)
23
24
  .option('-B|--keepBinary [bool]', 'Prevent skipping over binary files', false)
24
25
  .option('-r|--relativePaths [bool]', 'Convert absolute file paths to relative', false)
@@ -3,11 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
6
8
  const cross_fetch_1 = __importDefault(require("cross-fetch"));
7
9
  const node_cache_1 = __importDefault(require("node-cache"));
8
10
  const cache = new node_cache_1.default({});
9
- /** Load a data file from github-linguist. */
10
- async function loadFile(file) {
11
+ async function loadWebFile(file) {
11
12
  // Return cache if it exists
12
13
  const cachedContent = cache.get(file);
13
14
  if (cachedContent)
@@ -18,4 +19,12 @@ async function loadFile(file) {
18
19
  cache.set(file, fileContent);
19
20
  return fileContent;
20
21
  }
22
+ async function loadLocalFile(file) {
23
+ const filePath = path_1.default.resolve(__dirname, '../../ext', file);
24
+ return fs_1.default.promises.readFile(filePath).then(buffer => buffer.toString());
25
+ }
26
+ /** Load a data file from github-linguist. */
27
+ async function loadFile(file, offline = false) {
28
+ return offline ? loadLocalFile(file) : loadWebFile(file);
29
+ }
21
30
  exports.default = loadFile;
package/dist/index.js CHANGED
@@ -14,21 +14,20 @@ const load_data_1 = __importDefault(require("./helpers/load-data"));
14
14
  const read_file_1 = __importDefault(require("./helpers/read-file"));
15
15
  const convert_pcre_1 = __importDefault(require("./helpers/convert-pcre"));
16
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, _v;
17
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
18
+ var _u, _v, _w;
19
19
  const useRawContent = opts.fileContent !== undefined;
20
20
  input = [input !== null && input !== void 0 ? input : []].flat();
21
21
  opts.fileContent = [(_a = opts.fileContent) !== null && _a !== void 0 ? _a : []].flat();
22
22
  // Load data from github-linguist web repo
23
- const langData = await (0, load_data_1.default)('languages.yml').then(js_yaml_1.default.load);
24
- const vendorData = await (0, load_data_1.default)('vendor.yml').then(js_yaml_1.default.load);
25
- const docData = await (0, load_data_1.default)('documentation.yml').then(js_yaml_1.default.load);
26
- const heuristicsData = await (0, load_data_1.default)('heuristics.yml').then(js_yaml_1.default.load);
27
- const generatedData = await (0, load_data_1.default)('generated.rb').then(text => { var _a; return (_a = text.match(/(?<=name\.match\(\/).+?(?=(?<!\\)\/)/gm)) !== null && _a !== void 0 ? _a : []; });
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
28
  const vendorPaths = [...vendorData, ...docData, ...generatedData];
29
29
  // Setup main variables
30
30
  const fileAssociations = {};
31
- const definiteness = {};
32
31
  const extensions = {};
33
32
  const overrides = {};
34
33
  const results = {
@@ -86,8 +85,9 @@ async function analyse(input, opts = {}) {
86
85
  if (!useRawContent && opts.checkAttributes) {
87
86
  for (const folder of folders) {
88
87
  // Skip if folder is marked in gitattributes
89
- if (relPath(folder) && gitignores.ignores(relPath(folder)))
88
+ if (relPath(folder) && gitignores.ignores(relPath(folder))) {
90
89
  continue;
90
+ }
91
91
  // Parse gitignores
92
92
  const ignoresFile = path_1.default.join(folder, '.gitignore');
93
93
  if (opts.checkIgnored && fs_1.default.existsSync(ignoresFile)) {
@@ -101,10 +101,12 @@ async function analyse(input, opts = {}) {
101
101
  // Explicit text/binary associations
102
102
  const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm);
103
103
  for (const [_line, path, type] of contentTypeMatches) {
104
- if (['text', '-binary'].includes(type))
104
+ if (['text', '-binary'].includes(type)) {
105
105
  customText.add(path);
106
- if (['-text', 'binary'].includes(type))
106
+ }
107
+ if (['-text', 'binary'].includes(type)) {
107
108
  customBinary.add(path);
109
+ }
108
110
  }
109
111
  // Custom vendor options
110
112
  const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm);
@@ -117,8 +119,9 @@ async function analyse(input, opts = {}) {
117
119
  // If specified language is an alias, associate it with its full name
118
120
  if (!langData[forcedLang]) {
119
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()); });
120
- if (overrideLang)
122
+ if (overrideLang) {
121
123
  forcedLang = overrideLang[0];
124
+ }
122
125
  }
123
126
  const fullPath = relPath(folder) + '/' + path;
124
127
  overrides[fullPath] = forcedLang;
@@ -149,16 +152,18 @@ async function analyse(input, opts = {}) {
149
152
  };
150
153
  const overridesArray = Object.entries(overrides);
151
154
  // List all languages that could be associated with a given file
155
+ const definiteness = {};
156
+ const fromShebang = {};
152
157
  for (const file of files) {
153
158
  let firstLine;
154
159
  if (useRawContent) {
155
160
  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;
156
161
  }
157
- else {
158
- if (!fs_1.default.existsSync(file) || fs_1.default.lstatSync(file).isDirectory())
159
- continue;
162
+ else if (fs_1.default.existsSync(file) && !fs_1.default.lstatSync(file).isDirectory()) {
160
163
  firstLine = await (0, read_file_1.default)(file, true).catch(() => null);
161
164
  }
165
+ else
166
+ continue;
162
167
  // Skip if file is unreadable
163
168
  if (firstLine === null)
164
169
  continue;
@@ -170,21 +175,27 @@ async function analyse(input, opts = {}) {
170
175
  for (const [lang, data] of Object.entries(langData)) {
171
176
  const langMatcher = (lang) => `\\b${lang.toLowerCase().replace(/\W/g, '\\$&')}(?![\\w#+*]|-\*-)`;
172
177
  // Check for interpreter match
173
- const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
178
+ if (opts.checkShebang && hasShebang) {
179
+ const matchesInterpretor = (_f = data.interpreters) === null || _f === void 0 ? void 0 : _f.some(interpreter => firstLine.match(`\\b${interpreter}\\b`));
180
+ if (matchesInterpretor)
181
+ matches.push(lang);
182
+ }
174
183
  // Check modeline declaration
175
- const matchesLang = firstLine.toLowerCase().match(langMatcher(lang));
176
- const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => firstLine.toLowerCase().match(langMatcher(lang)));
177
- // Add language
178
- if (opts.checkShebang && matchesInterpretor)
179
- matches.push(lang);
180
- if (opts.checkModeline && (matchesLang || matchesAlias))
181
- matches.push(lang);
184
+ if (opts.checkModeline && hasModeline) {
185
+ const modelineText = firstLine.toLowerCase().replace(/^.*-\*-(.+)-\*-.*$/, '$1');
186
+ const matchesLang = modelineText.match(langMatcher(lang));
187
+ const matchesAlias = (_g = data.aliases) === null || _g === void 0 ? void 0 : _g.some(lang => modelineText.match(langMatcher(lang)));
188
+ if (matchesLang || matchesAlias)
189
+ matches.push(lang);
190
+ }
182
191
  }
192
+ // Add identified language(s)
183
193
  if (matches.length) {
184
- // Add explicitly-identified language
185
- const forcedLang = matches[0];
186
- addResult(file, forcedLang);
187
- definiteness[file] = true;
194
+ for (const match of matches)
195
+ addResult(file, match);
196
+ if (matches.length === 1)
197
+ definiteness[file] = true;
198
+ fromShebang[file] = true;
188
199
  continue;
189
200
  }
190
201
  }
@@ -201,24 +212,38 @@ async function analyse(input, opts = {}) {
201
212
  }
202
213
  // Search each language
203
214
  let skipExts = false;
215
+ // Check if filename is a match
204
216
  for (const lang in langData) {
205
- // Check if filename is a match
206
217
  const matchesName = (_h = langData[lang].filenames) === null || _h === void 0 ? void 0 : _h.some(name => path_1.default.basename(file.toLowerCase()) === name.toLowerCase());
207
218
  if (matchesName) {
208
219
  addResult(file, lang);
209
220
  skipExts = true;
210
221
  }
211
222
  }
223
+ // Check if extension is a match
224
+ const possibleExts = [];
212
225
  if (!skipExts)
213
226
  for (const lang in langData) {
214
- // Check if extension is a match
215
- const matchesExt = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.some(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
216
- if (matchesExt)
217
- addResult(file, lang);
227
+ const extMatches = (_j = langData[lang].extensions) === null || _j === void 0 ? void 0 : _j.filter(ext => file.toLowerCase().endsWith(ext.toLowerCase()));
228
+ if (extMatches === null || extMatches === void 0 ? void 0 : extMatches.length) {
229
+ for (const ext of extMatches)
230
+ possibleExts.push({ ext, lang });
231
+ }
218
232
  }
233
+ // Apply more specific extension if available
234
+ const isComplexExt = (ext) => /\..+\./.test(ext);
235
+ const hasComplexExt = possibleExts.some(data => isComplexExt(data.ext));
236
+ for (const { ext, lang } of possibleExts) {
237
+ if (hasComplexExt && !isComplexExt(ext))
238
+ continue;
239
+ if (!hasComplexExt && isComplexExt(ext))
240
+ continue;
241
+ addResult(file, lang);
242
+ }
219
243
  // Fallback to null if no language matches
220
- if (!fileAssociations[file])
244
+ if (!fileAssociations[file]) {
221
245
  addResult(file, null);
246
+ }
222
247
  }
223
248
  // Narrow down file associations to the best fit
224
249
  for (const file in fileAssociations) {
@@ -240,9 +265,8 @@ async function analyse(input, opts = {}) {
240
265
  if (opts.checkHeuristics)
241
266
  for (const heuristics of heuristicsData.disambiguations) {
242
267
  // Make sure the extension matches the current file
243
- if (!heuristics.extensions.includes(extensions[file])) {
268
+ if (!fromShebang[file] && !heuristics.extensions.includes(extensions[file]))
244
269
  continue;
245
- }
246
270
  // Load heuristic rules
247
271
  for (const heuristic of heuristics.rules) {
248
272
  // Make sure the language is not an array
@@ -262,8 +286,16 @@ async function analyse(input, opts = {}) {
262
286
  normalise(heuristic.pattern);
263
287
  if (heuristic.named_pattern)
264
288
  normalise(heuristicsData.named_patterns[heuristic.named_pattern]);
289
+ if (heuristic.and) {
290
+ for (const data of heuristic.and) {
291
+ if (data.pattern)
292
+ normalise(data.pattern);
293
+ if (data.named_pattern)
294
+ normalise(heuristicsData.named_patterns[data.named_pattern]);
295
+ }
296
+ }
265
297
  // Check file contents and apply heuristic patterns
266
- const fileContent = opts.fileContent ? opts.fileContent[files.indexOf(file)] : await (0, read_file_1.default)(file).catch(() => null);
298
+ 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);
267
299
  if (fileContent === null)
268
300
  continue;
269
301
  if (!patterns.length || patterns.some(pattern => (0, convert_pcre_1.default)(pattern).test(fileContent))) {
@@ -273,23 +305,26 @@ async function analyse(input, opts = {}) {
273
305
  }
274
306
  }
275
307
  // If no heuristics, assign a language
276
- (_l = (_t = results.files.results)[file]) !== null && _l !== void 0 ? _l : (_t[file] = fileAssociations[file][0]);
308
+ (_m = (_u = results.files.results)[file]) !== null && _m !== void 0 ? _m : (_u[file] = fileAssociations[file][0]);
277
309
  }
278
310
  // Skip specified categories
279
- if ((_m = opts.categories) === null || _m === void 0 ? void 0 : _m.length) {
311
+ if ((_o = opts.categories) === null || _o === void 0 ? void 0 : _o.length) {
280
312
  const categories = ['data', 'markup', 'programming', 'prose'];
281
313
  const hiddenCategories = categories.filter(cat => !opts.categories.includes(cat));
282
314
  for (const [file, lang] of Object.entries(results.files.results)) {
283
- if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; }))
315
+ if (!hiddenCategories.some(cat => { var _a; return lang && ((_a = langData[lang]) === null || _a === void 0 ? void 0 : _a.type) === cat; })) {
284
316
  continue;
317
+ }
285
318
  delete results.files.results[file];
286
- if (lang)
319
+ if (lang) {
287
320
  delete results.languages.results[lang];
321
+ }
288
322
  }
289
323
  for (const category of hiddenCategories) {
290
324
  for (const [lang, { type }] of Object.entries(results.languages.results)) {
291
- if (type === category)
325
+ if (type === category) {
292
326
  delete results.languages.results[lang];
327
+ }
293
328
  }
294
329
  }
295
330
  }
@@ -298,8 +333,9 @@ async function analyse(input, opts = {}) {
298
333
  const newMap = {};
299
334
  for (const [file, lang] of Object.entries(results.files.results)) {
300
335
  let relPath = path_1.default.relative(process.cwd(), file).replace(/\\/g, '/');
301
- if (!relPath.startsWith('../'))
336
+ if (!relPath.startsWith('../')) {
302
337
  relPath = './' + relPath;
338
+ }
303
339
  newMap[relPath] = lang;
304
340
  }
305
341
  results.files.results = newMap;
@@ -308,23 +344,24 @@ async function analyse(input, opts = {}) {
308
344
  for (const [file, lang] of Object.entries(results.files.results)) {
309
345
  if (lang && !langData[lang])
310
346
  continue;
311
- 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;
347
+ const fileSize = (_r = (_q = (_p = opts.fileContent) === null || _p === void 0 ? void 0 : _p[files.indexOf(file)]) === null || _q === void 0 ? void 0 : _q.length) !== null && _r !== void 0 ? _r : fs_1.default.statSync(file).size;
312
348
  results.files.bytes += fileSize;
313
349
  // If no language found, add extension in other section
314
350
  if (!lang) {
315
351
  const ext = path_1.default.extname(file);
316
352
  const unknownType = ext === '' ? 'filenames' : 'extensions';
317
353
  const name = ext === '' ? path_1.default.basename(file) : ext;
318
- (_r = (_u = results.unknown[unknownType])[name]) !== null && _r !== void 0 ? _r : (_u[name] = 0);
354
+ (_s = (_v = results.unknown[unknownType])[name]) !== null && _s !== void 0 ? _s : (_v[name] = 0);
319
355
  results.unknown[unknownType][name] += fileSize;
320
356
  results.unknown.bytes += fileSize;
321
357
  continue;
322
358
  }
323
359
  // Add language and bytes data to corresponding section
324
360
  const { type } = langData[lang];
325
- (_s = (_v = results.languages.results)[lang]) !== null && _s !== void 0 ? _s : (_v[lang] = { type, bytes: 0, color: langData[lang].color });
326
- if (opts.childLanguages)
361
+ (_t = (_w = results.languages.results)[lang]) !== null && _t !== void 0 ? _t : (_w[lang] = { type, bytes: 0, color: langData[lang].color });
362
+ if (opts.childLanguages) {
327
363
  results.languages.results[lang].parent = langData[lang].group;
364
+ }
328
365
  results.languages.results[lang].bytes += fileSize;
329
366
  results.languages.bytes += fileSize;
330
367
  }
package/dist/types.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface Options {
14
14
  relativePaths?: boolean;
15
15
  childLanguages?: boolean;
16
16
  quick?: boolean;
17
+ offline?: boolean;
17
18
  checkIgnored?: boolean;
18
19
  checkAttributes?: boolean;
19
20
  checkHeuristics?: boolean;
@@ -0,0 +1,17 @@
1
+ - ^[Dd]ocs?/
2
+ - (^|/)[Dd]ocumentation/
3
+ - (^|/)[Gg]roovydoc/
4
+ - (^|/)[Jj]avadoc/
5
+ - ^[Mm]an/
6
+ - ^[Ee]xamples/
7
+ - ^[Dd]emos?/
8
+ - (^|/)inst/doc/- (^|/)CITATION(\.cff|(S)?(\.(bib|md))?)$
9
+ - (^|/)CHANGE(S|LOG)?(\.|$)
10
+ - (^|/)CONTRIBUTING(\.|$)
11
+ - (^|/)COPYING(\.|$)
12
+ - (^|/)INSTALL(\.|$)
13
+ - (^|/)LICEN[CS]E(\.|$)
14
+ - (^|/)[Ll]icen[cs]e(\.|$)
15
+ - (^|/)README(\.|$)
16
+ - (^|/)[Rr]eadme(\.|$)
17
+ - ^[Ss]amples?/