1 | #!/usr/bin/env node
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const fs = require('fs');
|
6 | const path = require('path');
|
7 | const Module = require('module');
|
8 | const program = require('commander');
|
9 | const options = program.opts();
|
10 | const differenceWith = require('lodash.differencewith');
|
11 | const flatten = require('lodash.flatten');
|
12 | const markdownlint = require('markdownlint');
|
13 | const rc = require('rc');
|
14 | const glob = require('glob');
|
15 | const minimatch = require('minimatch');
|
16 | const minimist = require('minimist');
|
17 | const pkg = require('./package');
|
18 | const os = require('os');
|
19 |
|
20 | function jsoncParse(text) {
|
21 | return JSON.parse(require('jsonc-parser').stripComments(text));
|
22 | }
|
23 |
|
24 | function jsYamlSafeLoad(text) {
|
25 | return require('js-yaml').load(text);
|
26 | }
|
27 |
|
28 | const projectConfigFiles = [
|
29 | '.markdownlint.json',
|
30 | '.markdownlint.yaml',
|
31 | '.markdownlint.yml'
|
32 | ];
|
33 | const configFileParsers = [jsoncParse, jsYamlSafeLoad];
|
34 | const fsOptions = {encoding: 'utf8'};
|
35 | const processCwd = process.cwd();
|
36 |
|
37 | function readConfiguration(userConfigFile) {
|
38 | const jsConfigFile = /\.js$/i.test(userConfigFile);
|
39 | const rcArgv = minimist(process.argv.slice(2));
|
40 | if (jsConfigFile) {
|
41 |
|
42 | delete rcArgv.config;
|
43 | }
|
44 |
|
45 |
|
46 | let config = rc('markdownlint', {}, rcArgv);
|
47 | for (const projectConfigFile of projectConfigFiles) {
|
48 | try {
|
49 | fs.accessSync(projectConfigFile, fs.R_OK);
|
50 | const projectConfig = markdownlint.readConfigSync(projectConfigFile, configFileParsers);
|
51 | config = require('deep-extend')(config, projectConfig);
|
52 | break;
|
53 | } catch {
|
54 |
|
55 | }
|
56 | }
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | if (userConfigFile) {
|
63 | try {
|
64 | const userConfig = jsConfigFile ?
|
65 |
|
66 | require(path.resolve(processCwd, userConfigFile)) :
|
67 |
|
68 | markdownlint.readConfigSync(userConfigFile, configFileParsers);
|
69 | config = require('deep-extend')(config, userConfig);
|
70 | } catch (error) {
|
71 | console.warn('Cannot read or parse config file ' + userConfigFile + ': ' + error.message);
|
72 | }
|
73 | }
|
74 |
|
75 | return config;
|
76 | }
|
77 |
|
78 | function prepareFileList(files, fileExtensions, previousResults) {
|
79 | const globOptions = {
|
80 | dot: Boolean(options.dot),
|
81 | nodir: true
|
82 | };
|
83 | let extensionGlobPart = '*.';
|
84 | if (fileExtensions.length === 1) {
|
85 |
|
86 | extensionGlobPart += fileExtensions[0];
|
87 | } else {
|
88 | extensionGlobPart += '{' + fileExtensions.join(',') + '}';
|
89 | }
|
90 |
|
91 | files = files.map(function (file) {
|
92 | try {
|
93 | if (fs.lstatSync(file).isDirectory()) {
|
94 |
|
95 | if (previousResults) {
|
96 | const matcher = new minimatch.Minimatch(
|
97 | path.resolve(processCwd, path.join(file, '**', extensionGlobPart)), globOptions);
|
98 | return previousResults.filter(function (fileInfo) {
|
99 | return matcher.match(fileInfo.absolute);
|
100 | }).map(function (fileInfo) {
|
101 | return fileInfo.original;
|
102 | });
|
103 | }
|
104 |
|
105 | return glob.sync(path.join(file, '**', extensionGlobPart), globOptions);
|
106 | }
|
107 | } catch {
|
108 |
|
109 | if (previousResults) {
|
110 | const matcher = new minimatch.Minimatch(path.resolve(processCwd, file), globOptions);
|
111 | return previousResults.filter(function (fileInfo) {
|
112 | return matcher.match(fileInfo.absolute);
|
113 | }).map(function (fileInfo) {
|
114 | return fileInfo.original;
|
115 | });
|
116 | }
|
117 |
|
118 | return glob.sync(file, globOptions);
|
119 | }
|
120 |
|
121 |
|
122 | return file;
|
123 | });
|
124 | return flatten(files).map(function (file) {
|
125 | return {
|
126 | original: file,
|
127 | relative: path.relative(processCwd, file),
|
128 | absolute: path.resolve(file)
|
129 | };
|
130 | });
|
131 | }
|
132 |
|
133 | function printResult(lintResult) {
|
134 | const results = flatten(Object.keys(lintResult).map(file => {
|
135 | return lintResult[file].map(result => {
|
136 | return {
|
137 | file: file,
|
138 | lineNumber: result.lineNumber,
|
139 | column: (result.errorRange && result.errorRange[0]) || 0,
|
140 | names: result.ruleNames.join('/'),
|
141 | description: result.ruleDescription +
|
142 | (result.errorDetail ? ' [' + result.errorDetail + ']' : '') +
|
143 | (result.errorContext ? ' [Context: "' + result.errorContext + '"]' : '')
|
144 | };
|
145 | });
|
146 | }));
|
147 | let lintResultString = '';
|
148 | if (results.length > 0) {
|
149 | results.sort((a, b) => {
|
150 | return a.file.localeCompare(b.file) || a.lineNumber - b.lineNumber ||
|
151 | a.names.localeCompare(b.names) || a.description.localeCompare(b.description);
|
152 | });
|
153 | lintResultString = results.map(result => {
|
154 | const {file, lineNumber, column, names, description} = result;
|
155 | const columnText = column ? `:${column}` : '';
|
156 | return `${file}:${lineNumber}${columnText} ${names} ${description}`;
|
157 | }).join('\n');
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | process.exitCode = 1;
|
164 | }
|
165 |
|
166 | if (options.output) {
|
167 | lintResultString = lintResultString.length > 0 ?
|
168 | lintResultString + os.EOL :
|
169 | lintResultString;
|
170 | try {
|
171 | fs.writeFileSync(options.output, lintResultString);
|
172 | } catch (error) {
|
173 | console.warn('Cannot write to output file ' + options.output + ': ' + error.message);
|
174 | process.exitCode = 2;
|
175 | }
|
176 | } else if (lintResultString) {
|
177 | console.error(lintResultString);
|
178 | }
|
179 | }
|
180 |
|
181 | function concatArray(item, array) {
|
182 | array.push(item);
|
183 | return array;
|
184 | }
|
185 |
|
186 | program
|
187 | .version(pkg.version)
|
188 | .description(pkg.description)
|
189 | .usage('[options] <files|directories|globs>')
|
190 | .option('-c, --config [configFile]', 'configuration file (JSON, JSONC, JS, or YAML)')
|
191 | .option('-d, --dot', 'include files/folders with a dot (for example `.github`)')
|
192 | .option('-f, --fix', 'fix basic errors (does not work with STDIN)')
|
193 | .option('-i, --ignore [file|directory|glob]', 'file(s) to ignore/exclude', concatArray, [])
|
194 | .option('-o, --output [outputFile]', 'write issues to file (no console)')
|
195 | .option('-p, --ignore-path [file]', 'path to file with ignore pattern(s)')
|
196 | .option('-r, --rules [file|directory|glob|package]', 'custom rule files', concatArray, [])
|
197 | .option('-s, --stdin', 'read from STDIN (does not work with files)');
|
198 |
|
199 | program.parse(process.argv);
|
200 |
|
201 | function tryResolvePath(filepath) {
|
202 | try {
|
203 | if (path.basename(filepath) === filepath && path.extname(filepath) === '') {
|
204 |
|
205 |
|
206 | let paths = Module._nodeModulePaths(processCwd);
|
207 |
|
208 | paths = paths.concat(Module.globalPaths);
|
209 | if (require.resolve.paths) {
|
210 |
|
211 | return require.resolve(filepath, {paths: paths});
|
212 | }
|
213 |
|
214 | return Module._resolveFilename(filepath, {paths: paths});
|
215 | }
|
216 |
|
217 |
|
218 | return require.resolve(path.join(processCwd, filepath));
|
219 | } catch {
|
220 | return filepath;
|
221 | }
|
222 | }
|
223 |
|
224 | function loadCustomRules(rules) {
|
225 | return flatten(rules.map(function (rule) {
|
226 | try {
|
227 | const resolvedPath = [tryResolvePath(rule)];
|
228 | const fileList = flatten(prepareFileList(resolvedPath, ['js']).map(function (filepath) {
|
229 | return require(filepath.absolute);
|
230 | }));
|
231 | if (fileList.length === 0) {
|
232 | throw new Error('No such rule');
|
233 | }
|
234 |
|
235 | return fileList;
|
236 | } catch (error) {
|
237 | console.error('Cannot load custom rule ' + rule + ': ' + error.message);
|
238 | process.exit(3);
|
239 | }
|
240 | }));
|
241 | }
|
242 |
|
243 | let ignorePath = '.markdownlintignore';
|
244 | let {existsSync} = fs;
|
245 | if (options.ignorePath) {
|
246 | ignorePath = options.ignorePath;
|
247 | existsSync = () => true;
|
248 | }
|
249 |
|
250 | let ignoreFilter = () => true;
|
251 | if (existsSync(ignorePath)) {
|
252 | const ignoreText = fs.readFileSync(ignorePath, fsOptions);
|
253 | const ignore = require('ignore');
|
254 | const ignoreInstance = ignore().add(ignoreText);
|
255 | ignoreFilter = fileInfo => !ignoreInstance.ignores(fileInfo.relative);
|
256 | }
|
257 |
|
258 | const files = prepareFileList(program.args, ['md', 'markdown'])
|
259 | .filter(value => ignoreFilter(value));
|
260 | const ignores = prepareFileList(options.ignore, ['md', 'markdown'], files);
|
261 | const customRules = loadCustomRules(options.rules);
|
262 | const diff = differenceWith(files, ignores, function (a, b) {
|
263 | return a.absolute === b.absolute;
|
264 | }).map(function (paths) {
|
265 | return paths.original;
|
266 | });
|
267 |
|
268 | function lintAndPrint(stdin, files) {
|
269 | files = files || [];
|
270 | const config = readConfiguration(options.config);
|
271 | const lintOptions = {
|
272 | config,
|
273 | customRules,
|
274 | files
|
275 | };
|
276 | if (stdin) {
|
277 | lintOptions.strings = {
|
278 | stdin
|
279 | };
|
280 | }
|
281 |
|
282 | if (options.fix) {
|
283 | const fixOptions = {
|
284 | ...lintOptions,
|
285 | resultVersion: 3
|
286 | };
|
287 | const markdownlintRuleHelpers = require('markdownlint-rule-helpers');
|
288 | for (const file of files) {
|
289 | fixOptions.files = [file];
|
290 | const fixResult = markdownlint.sync(fixOptions);
|
291 | const fixes = fixResult[file].filter(error => error.fixInfo);
|
292 | if (fixes.length > 0) {
|
293 | const originalText = fs.readFileSync(file, fsOptions);
|
294 | const fixedText = markdownlintRuleHelpers.applyFixes(originalText, fixes);
|
295 | if (originalText !== fixedText) {
|
296 | fs.writeFileSync(file, fixedText, fsOptions);
|
297 | }
|
298 | }
|
299 | }
|
300 | }
|
301 |
|
302 | const lintResult = markdownlint.sync(lintOptions);
|
303 | printResult(lintResult);
|
304 | }
|
305 |
|
306 | if ((files.length > 0) && !options.stdin) {
|
307 | lintAndPrint(null, diff);
|
308 | } else if ((files.length === 0) && options.stdin && !options.fix) {
|
309 | const getStdin = require('get-stdin');
|
310 | getStdin().then(lintAndPrint);
|
311 | } else {
|
312 | program.help();
|
313 | }
|