From f8b12da048645929405f7422ea63ad67f5fdaa94 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 11 Jul 2025 16:58:54 +0800 Subject: [PATCH 1/5] feat: enable --fix option work with --stdin option --- README.md | 2 +- markdownlint.js | 30 +++++++++++++++++++++++++-- test/test.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) 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..c5fb2c7a 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -208,7 +208,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 +311,32 @@ 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; + if (fixes.length > 0) { + outputText = applyFixes(stdin, fixes); + } + + if (options.output) { + // Write content to output file + try { + fs.writeFileSync(options.output, outputText); + } catch (error) { + console.warn('Cannot write to output file ' + options.output + ': ' + error.message); + process.exitCode = exitCodes.failedToWriteOutputFile; + } + } else if (!options.quiet) { + // Output content to stdout + process.stdout.write(outputText); + } + + return; // Exit early when fixing stdin + } + + // Handle fix for files for (const file of files) { fixOptions.files = [file]; const fixResult = lint(fixOptions); @@ -332,7 +358,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..285e3c65 100644 --- a/test/test.js +++ b/test/test.js @@ -411,6 +411,61 @@ 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 --quiet suppresses stdout', async t => { + const stdin = {string: ['Heading', '', 'Text with trailing spaces '].join('\n')}; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--quiet'], {stdin}); + t.is(result.stdout, ''); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); +}); + +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')}; + const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--config', 'test-config.json'], {stdin}); + t.is(result.stdout, stdin.string); + t.is(result.stderr, ''); + t.is(result.exitCode, 0); +}); + 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, ''); From 96ef796a517581f332cb9fcfd98e8fb10c01fec9 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 13 Jul 2025 12:35:19 +0800 Subject: [PATCH 2/5] fix: remove unnecessary check --- markdownlint.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/markdownlint.js b/markdownlint.js index c5fb2c7a..e5ceb600 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -316,9 +316,7 @@ function lintAndPrint(stdin, files) { const fixResult = lint(fixOptions); const fixes = fixResult.stdin.filter(error => error.fixInfo); let outputText = stdin; - if (fixes.length > 0) { - outputText = applyFixes(stdin, fixes); - } + outputText = applyFixes(stdin, fixes); if (options.output) { // Write content to output file @@ -328,10 +326,8 @@ function lintAndPrint(stdin, files) { console.warn('Cannot write to output file ' + options.output + ': ' + error.message); process.exitCode = exitCodes.failedToWriteOutputFile; } - } else if (!options.quiet) { - // Output content to stdout - process.stdout.write(outputText); } + process.stdout.write(outputText); return; // Exit early when fixing stdin } From a66ffaa7e33d6c9e145e93e11a9c98e33ae70ed8 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 13 Jul 2025 15:08:16 +0800 Subject: [PATCH 3/5] refactor: eliminate duplicate file output handling code --- markdownlint.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/markdownlint.js b/markdownlint.js index e5ceb600..a0e2e138 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); + } 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); } @@ -319,13 +323,7 @@ function lintAndPrint(stdin, files) { outputText = applyFixes(stdin, fixes); if (options.output) { - // Write content to output file - try { - fs.writeFileSync(options.output, outputText); - } catch (error) { - console.warn('Cannot write to output file ' + options.output + ': ' + error.message); - process.exitCode = exitCodes.failedToWriteOutputFile; - } + writeOutputFile(options.output, outputText); } process.stdout.write(outputText); From 111fe3aa2763b760dd2e501e72b0cb10194137cc Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 13 Jul 2025 17:03:50 +0800 Subject: [PATCH 4/5] fix: process unfixable errors with --stdin & --fix --- markdownlint.js | 17 +++++++++++++---- test/test.js | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/markdownlint.js b/markdownlint.js index a0e2e138..cfdea7f2 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -62,7 +62,7 @@ const processCwd = process.cwd(); function writeOutputFile(path, content) { try { - fs.writeFileSync(path, content); + fs.writeFileSync(path, content, fsOptions); } catch (error) { console.warn('Cannot write to output file ' + path + ': ' + error.message); process.exitCode = exitCodes.failedToWriteOutputFile; @@ -195,7 +195,7 @@ function printResult(lintResult) { if (options.output) { lintResultString = lintResultString.length > 0 ? lintResultString + os.EOL : lintResultString; - writeOutputFile(options.output, lintResultString) + writeOutputFile(options.output, lintResultString); } else if (lintResultString && !options.quiet) { console.error(lintResultString); } @@ -324,10 +324,19 @@ function lintAndPrint(stdin, files) { 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 } - process.stdout.write(outputText); - return; // Exit early when fixing stdin + // Update stdin with fixed content for subsequent linting + stdin = outputText; + lintOptions.strings.stdin = outputText; } // Handle fix for files diff --git a/test/test.js b/test/test.js index 285e3c65..122ff70e 100644 --- a/test/test.js +++ b/test/test.js @@ -412,9 +412,9 @@ 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 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'); + 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); @@ -437,20 +437,20 @@ test('--stdin --fix with empty input outputs empty content', async t => { }); test('--stdin --fix --output writes fixed content to file', async t => { - const stdin = {string: ['Heading', '', 'Text with trailing spaces ', ''].join('\n')}; + 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'); + const expected = ['# Heading', '', 'Text with trailing spaces', ''].join('\n'); t.is(fileContent, expected); fs.unlinkSync(output); }); test('--stdin --fix --quiet suppresses stdout', async t => { - const stdin = {string: ['Heading', '', 'Text with trailing spaces '].join('\n')}; + const stdin = {string: ['# Heading', '', 'Text with trailing spaces '].join('\n')}; const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--quiet'], {stdin}); t.is(result.stdout, ''); t.is(result.stderr, ''); @@ -460,10 +460,33 @@ test('--stdin --fix --quiet suppresses stdout', async t => { 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')}; - const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--config', 'test-config.json'], {stdin}); - t.is(result.stdout, stdin.string); - t.is(result.stderr, ''); - t.is(result.exitCode, 0); + 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 => { From dc12b4c29e539bee05718b8749e1a1fd1145288f Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 14 Jul 2025 21:33:04 +0800 Subject: [PATCH 5/5] fix: format source code and remove useless test case --- markdownlint.js | 14 ++++++++++++++ test/test.js | 8 -------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/markdownlint.js b/markdownlint.js index cfdea7f2..338faedc 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -331,12 +331,26 @@ function lintAndPrint(stdin, files) { 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 diff --git a/test/test.js b/test/test.js index 122ff70e..24f3f69d 100644 --- a/test/test.js +++ b/test/test.js @@ -449,14 +449,6 @@ test('--stdin --fix --output writes fixed content to file', async t => { fs.unlinkSync(output); }); -test('--stdin --fix --quiet suppresses stdout', async t => { - const stdin = {string: ['# Heading', '', 'Text with trailing spaces '].join('\n')}; - const result = await spawn('../markdownlint.js', ['--stdin', '--fix', '--quiet'], {stdin}); - t.is(result.stdout, ''); - t.is(result.stderr, ''); - t.is(result.exitCode, 0); -}); - 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')};