diff --git a/README.md b/README.md index 83a4f164..d69472d6 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Options: -c, --config configuration file (JSON, JSONC, JS, YAML, or TOML) --configPointer JSON Pointer to object within configuration file (default: "") -d, --dot include files/folders with a dot (for example `.github`) - -f, --fix fix basic errors (does not work with STDIN) + -f, --fix fix basic errors -i, --ignore file(s) to ignore/exclude (default: []) -j, --json write issues in json format -o, --output write issues to file (no console) diff --git a/markdownlint.js b/markdownlint.js index 2c3c1c37..338faedc 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -60,6 +60,15 @@ const configParsers = [jsoncParse, tomlParse, yamlParse]; const fsOptions = {encoding: 'utf8'}; const processCwd = process.cwd(); +function writeOutputFile(path, content) { + try { + fs.writeFileSync(path, content, fsOptions); + } catch (error) { + console.warn('Cannot write to output file ' + path + ': ' + error.message); + process.exitCode = exitCodes.failedToWriteOutputFile; + } +} + function readConfiguration(userConfigFile) { // Load from well-known config files let config = rc('markdownlint', {}); @@ -186,12 +195,7 @@ function printResult(lintResult) { if (options.output) { lintResultString = lintResultString.length > 0 ? lintResultString + os.EOL : lintResultString; - try { - fs.writeFileSync(options.output, lintResultString); - } catch (error) { - console.warn('Cannot write to output file ' + options.output + ': ' + error.message); - process.exitCode = exitCodes.failedToWriteOutputFile; - } + writeOutputFile(options.output, lintResultString); } else if (lintResultString && !options.quiet) { console.error(lintResultString); } @@ -208,7 +212,7 @@ program .option('-c, --config ', 'configuration file (JSON, JSONC, JS, YAML, or TOML)') .option('--configPointer ', 'JSON Pointer to object within configuration file', '') .option('-d, --dot', 'include files/folders with a dot (for example `.github`)') - .option('-f, --fix', 'fix basic errors (does not work with STDIN)') + .option('-f, --fix', 'fix basic errors') .option('-i, --ignore ', 'file(s) to ignore/exclude', concatArray, []) .option('-j, --json', 'write issues in json format') .option('-o, --output ', 'write issues to file (no console)') @@ -311,6 +315,45 @@ function lintAndPrint(stdin, files) { if (options.fix) { const fixOptions = {...lintOptions}; + if (stdin) { + // Handle fix for stdin + const fixResult = lint(fixOptions); + const fixes = fixResult.stdin.filter(error => error.fixInfo); + let outputText = stdin; + outputText = applyFixes(stdin, fixes); + + if (options.output) { + writeOutputFile(options.output, outputText); + // Check for remaining errors after fix + const checkOptions = {...lintOptions}; + checkOptions.strings = {stdin: outputText}; + const checkResult = lint(checkOptions); + if (Object.keys(checkResult).some(file => checkResult[file].length > 0)) { + printResult(checkResult); + } + + return; // Exit early when output is specified + } + + // Update stdin with fixed content for subsequent linting + stdin = outputText; + lintOptions.strings.stdin = outputText; + + // Check for remaining errors after fix + const remainingErrors = lint(lintOptions); + const hasErrors = Object.keys(remainingErrors).some(file => remainingErrors[file].length > 0); + + process.stdout.write(outputText); + + if (hasErrors) { + // Print remaining errors to stderr + printResult(remainingErrors); + } + + return; // Exit after handling stdin with fix + } + + // Handle fix for files for (const file of files) { fixOptions.files = [file]; const fixResult = lint(fixOptions); @@ -332,7 +375,7 @@ function lintAndPrint(stdin, files) { try { if (files.length > 0 && !options.stdin) { lintAndPrint(null, diff); - } else if (files.length === 0 && options.stdin && !options.fix) { + } else if (files.length === 0 && options.stdin) { import('node:stream/consumers').then(module => module.text(process.stdin)).then(lintAndPrint); } else { program.help(); diff --git a/test/test.js b/test/test.js index 7ed579a7..24f3f69d 100644 --- a/test/test.js +++ b/test/test.js @@ -411,6 +411,76 @@ test('--output with invalid path fails', async t => { } }); +test('--stdin --fix with fixable content outputs fixed content', async t => { + const stdin = {string: ['# Heading', '', 'Text with trailing spaces ', '', '- List item', ''].join('\n')}; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix'], {stdin}); + const expected = ['# Heading', '', 'Text with trailing spaces', '', '- List item'].join('\n'); + t.is(result.stdout, expected); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); +}); + +test('--stdin --fix with valid content outputs unchanged content', async t => { + const stdin = {string: ['# Heading', '', 'Text without issues', '', '- Clean list item'].join('\n')}; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix'], {stdin}); + t.is(result.stdout, stdin.string); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); +}); + +test('--stdin --fix with empty input outputs empty content', async t => { + const stdin = {string: ''}; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix'], {stdin}); + t.is(result.stdout, ''); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); +}); + +test('--stdin --fix --output writes fixed content to file', async t => { + const stdin = {string: ['# Heading', '', 'Text with trailing spaces ', ''].join('\n')}; + const output = '../outputStdinFix.md'; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--output', output], {stdin}); + t.is(result.stdout, ''); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); + const fileContent = fs.readFileSync(output, 'utf8'); + const expected = ['# Heading', '', 'Text with trailing spaces', ''].join('\n'); + t.is(fileContent, expected); + fs.unlinkSync(output); +}); + +test('--stdin --fix with unfixable errors still outputs original content', async t => { + // MD041 first-line-heading is not automatically fixable + const stdin = {string: ['## Not a first level heading', '', 'Text content'].join('\n')}; + try { + await spawn('../markdownlint.js', ['--stdin', '--fix', '--config', 'test-config.json'], {stdin}); + t.fail(); + } catch (error) { + t.is(error.stdout, stdin.string); + t.is(error.stderr.match(errorPattern).length, 1); + t.true(error.stderr.includes('stdin:1')); + t.true(error.stderr.includes('MD041')); + t.is(error.exitCode, 1); + } +}); + +test('--stdin --fix with mixed fixable and unfixable errors', async t => { + // MD041 (first-line-heading) is not fixable, trailing spaces should be fixable + const stdin = {string: ['## Not a first level heading ', '', 'Text content '].join('\n')}; + const expected = ['## Not a first level heading', '', 'Text content'].join('\n'); + try { + await spawn('../markdownlint.js', ['--stdin', '--fix'], {stdin}); + t.fail(); + } catch (error) { + t.is(error.stdout, expected); + t.is(error.stderr.match(errorPattern).length, 1); + t.true(error.stderr.includes('stdin:1')); + t.true(error.stderr.includes('MD041')); + t.false(error.stderr.includes('MD009')); // Trailing spaces should be fixed + t.is(error.exitCode, 1); + } +}); + test('configuration file can be YAML', async t => { const result = await spawn('../markdownlint.js', ['--config', 'md043-config.yaml', 'md043-config.md']); t.is(result.stdout, '');