Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Options:
-c, --config <configFile> configuration file (JSON, JSONC, JS, YAML, or TOML)
--configPointer <pointer> 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
Comment thread
DavidAnson marked this conversation as resolved.
-i, --ignore <file|directory|glob> file(s) to ignore/exclude (default: [])
-j, --json write issues in json format
-o, --output <outputFile> write issues to file (no console)
Expand Down
59 changes: 51 additions & 8 deletions markdownlint.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {});
Expand Down Expand Up @@ -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);
}
Expand All @@ -208,7 +212,7 @@ program
.option('-c, --config <configFile>', 'configuration file (JSON, JSONC, JS, YAML, or TOML)')
.option('--configPointer <pointer>', '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')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Please update README.md with the same change.

.option('-i, --ignore <file|directory|glob>', 'file(s) to ignore/exclude', concatArray, [])
.option('-j, --json', 'write issues in json format')
.option('-o, --output <outputFile>', 'write issues to file (no console)')
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to filter here, I think - pass the full list and it will ignore what it cannot do.

let outputText = stdin;
outputText = applyFixes(stdin, fixes);

if (options.output) {
writeOutputFile(options.output, outputText);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README says this file contains the issues, not the file content - I do not think this should be used for both purposes?

// 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)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only one input just like line 321, so it can be used directly here.

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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above, only one key will be present.


process.stdout.write(outputText);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this line above the previous lines setting remainingErrors/hasErrors.


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);
Expand All @@ -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();
Expand Down
70 changes: 70 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,76 @@ test('--output with invalid path fails', async t => {
}
});

test('--stdin --fix with fixable content outputs fixed content', async t => {
Comment thread
DavidAnson marked this conversation as resolved.
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 => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for why I think this is testing the wrong behavior.

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, '');
Expand Down