Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multi inputs #179

Merged
merged 6 commits into from
Jan 21, 2022
Merged
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
255 changes: 148 additions & 107 deletions markdown-link-check
Original file line number Diff line number Diff line change
Expand Up @@ -18,85 +18,108 @@ const statusLabels = {
error: chalk.yellow('⚠'),
};

const opts = {};
let filenameForOutput = '';
let stream = process.stdin; // read from stdin unless a filename is given
class Input {
constructor(filenameForOutput, stream) {
this.filenameForOutput = filenameForOutput;
this.stream = stream;
}
};

function commaSeparatedCodesList(value, dummyPrevious) {
return value.split(',').map(function(item) {
return parseInt(item, 10);
return parseInt(item, 10);
});
}

program
.version(pkg.version)
.option('-p, --progress', 'show progress bar')
.option('-c, --config [config]', 'apply a config file (JSON), holding e.g. url specific header configuration')
.option('-q, --quiet', 'displays errors only')
.option('-v, --verbose', 'displays detailed error information')
.option('-a, --alive <code>', 'comma separated list of HTTP codes to be considered as alive', commaSeparatedCodesList)
.option('-r, --retry', 'retry after the duration indicated in \'retry-after\' header when HTTP code is 429')
.arguments('[filenameOrUrl]')
.action(function (filenameOrUrl) {
filenameForOutput = filenameOrUrl;
if (/https?:/.test(filenameOrUrl)) {
stream = needle.get(filenameOrUrl);
stream.on('error', function (error) {
console.error(chalk.red('\nERROR: Unable to connect! Please provide a valid URL as an argument.'));
process.exit(1);
});
stream.on('response', function (response) {
if (response.statusCode === 404) {
console.error(chalk.red('\nERROR: 404 - File not found! Please provide a valid URL as an argument.'));
process.exit(1);
}
});
try { // extract baseUrl from supplied URL
const parsed = url.parse(filenameOrUrl);
delete parsed.search;
delete parsed.hash;
if (parsed.pathname.lastIndexOf('/') !== -1) {
parsed.pathname = parsed.pathname.substr(0, parsed.pathname.lastIndexOf('/') + 1);
}
opts.baseUrl = url.format(parsed);
} catch (err) { /* ignore error */
function getInputs() {
const opts = {};
const inputs = [];

program
.version(pkg.version)
.option('-p, --progress', 'show progress bar')
.option('-c, --config [config]', 'apply a config file (JSON), holding e.g. url specific header configuration')
.option('-q, --quiet', 'displays errors only')
.option('-v, --verbose', 'displays detailed error information')
.option('-a, --alive <code>', 'comma separated list of HTTP codes to be considered as alive', commaSeparatedCodesList)
.option('-r, --retry', 'retry after the duration indicated in \'retry-after\' header when HTTP code is 429')
.arguments('[filenamesOrUrls...]')
.action(function (filenamesOrUrls) {
let filenameForOutput;
let stream;

if (!filenamesOrUrls.length) {
// read from stdin unless a filename is given
inputs.push(new Input(filenameForOutput, process.stdin))
}
} else {
fs.stat(filenameOrUrl, function(error , stats){
if (!error && stats.isDirectory()){
console.error(chalk.red('\nERROR: ' + filenameOrUrl + ' is a directory! Please provide a valid filename as an argument.'));
process.exit(1);

for (const filenameOrUrl of filenamesOrUrls) {
filenameForOutput = filenameOrUrl;
if (/https?:/.test(filenameOrUrl)) {
stream = needle.get(filenameOrUrl);
stream.on('error', function (error) {
console.error(chalk.red('\nERROR: Unable to connect! Please provide a valid URL as an argument.'));
process.exit(1);
});
stream.on('response', function (response) {
if (response.statusCode === 404) {
console.error(chalk.red('\nERROR: 404 - File not found! Please provide a valid URL as an argument.'));
process.exit(1);
}
});
try { // extract baseUrl from supplied URL
const parsed = url.parse(filenameOrUrl);
delete parsed.search;
delete parsed.hash;
if (parsed.pathname.lastIndexOf('/') !== -1) {
parsed.pathname = parsed.pathname.substr(0, parsed.pathname.lastIndexOf('/') + 1);
}
opts.baseUrl = url.format(parsed);
} catch (err) { /* ignore error */
}
} else {
fs.stat(filenameOrUrl, function(error , stats){
if (!error && stats.isDirectory()){
console.error(chalk.red('\nERROR: ' + filenameOrUrl + ' is a directory! Please provide a valid filename as an argument.'));
process.exit(1);
}
});
opts.baseUrl = 'file://' + path.dirname(path.resolve(filenameOrUrl));
stream = fs.createReadStream(filenameOrUrl);
}

inputs.push(new Input(filenameForOutput, stream))
}
});
opts.baseUrl = 'file://' + path.dirname(path.resolve(filenameOrUrl));
stream = fs.createReadStream(filenameOrUrl);
}
}
).parse(process.argv);

opts.showProgressBar = (program.progress === true); // force true or undefined to be true or false.
opts.quiet = (program.quiet === true);
opts.verbose = (program.verbose === true);
opts.retryOn429 = (program.retry === true);
opts.aliveStatusCodes = program.alive;
// set the projectBaseUrl to the current working directory, so that `{{BASEURL}}` can be resolved to the project root.
opts.projectBaseUrl = `file://${process.cwd()}`;

return [inputs, opts];
}

async function processInput(filenameForOutput, stream, opts) {
let markdown = ''; // collect the markdown data, then process it

}).parse(process.argv);

opts.showProgressBar = (program.progress === true); // force true or undefined to be true or false.
opts.quiet = (program.quiet === true);
opts.verbose = (program.verbose === true);
opts.retryOn429 = (program.retry === true);
opts.aliveStatusCodes = program.alive;
// set the projectBaseUrl to the current working directory, so that `{{BASEURL}}` can be resolved to the project root.
opts.projectBaseUrl = `file://${process.cwd()}`;

let markdown = ''; // collect the markdown data, then process it

stream
.on('data', function (chunk) {
markdown += chunk.toString();
})
.on('error', function(error) {
if (error.code === 'ENOENT') {
console.error(chalk.red('\nERROR: File not found! Please provide a valid filename as an argument.'));
} else {
console.error(chalk.red(error));
stream.on('error', function(error) {
if (error.code === 'ENOENT') {
console.error(chalk.red('\nERROR: File not found! Please provide a valid filename as an argument.'));
} else {
console.error(chalk.red(error));
}
return process.exit(1);
})

for await (const chunk of stream) {
markdown += chunk.toString();
}
return process.exit(1);
})
.on('end', function () {

if (filenameForOutput) {
console.log(chalk.cyan('\nFILE: ' + filenameForOutput));
}
Expand All @@ -121,8 +144,6 @@ stream
opts.retryCount = config.retryCount;
opts.fallbackRetryDelay = config.fallbackRetryDelay;
opts.aliveStatusCodes = config.aliveStatusCodes;

runMarkdownLinkCheck(markdown, opts);
});
}
else {
Expand All @@ -131,47 +152,67 @@ stream
}
});
}
else {
runMarkdownLinkCheck(markdown, opts);
}
});

function runMarkdownLinkCheck(markdown, opts) {
markdownLinkCheck(markdown, opts, function (err, results) {
if (err) {
console.error(chalk.red('\n ERROR: something went wrong!'));
console.error(err.stack);
process.exit(1);
}

if (results.length === 0 && !opts.quiet) {
console.log(chalk.yellow(' No hyperlinks found!'));
}
results.forEach(function (result) {
// Skip messages for non-deadlinks in quiet mode.
if (opts.quiet && result.status !== 'dead') {
return;
await runMarkdownLinkCheck(markdown, opts).catch(() => reject());
resolve();
}

async function runMarkdownLinkCheck(markdown, opts) {
return new Promise((resolve, reject) => {
markdownLinkCheck(markdown, opts, function (err, results) {
if (err) {
console.error(chalk.red('\n ERROR: something went wrong!'));
console.error(err.stack);
reject();
}

if (opts.verbose) {
if (result.err) {
console.log(' [%s] %s → Status: %s %s', statusLabels[result.status], result.link, result.statusCode, result.err);
} else {
console.log(' [%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
}
if (results.length === 0 && !opts.quiet) {
console.log(chalk.yellow(' No hyperlinks found!'));
}
else {
console.log(' [%s] %s', statusLabels[result.status], result.link);
results.forEach(function (result) {
// Skip messages for non-deadlinks in quiet mode.
if (opts.quiet && result.status !== 'dead') {
return;
}

if (opts.verbose) {
if (result.err) {
console.log(' [%s] %s → Status: %s %s', statusLabels[result.status], result.link, result.statusCode, result.err);
} else {
console.log(' [%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
}
}
else {
console.log(' [%s] %s', statusLabels[result.status], result.link);
}
});
console.log('\n %s links checked.', results.length);
if (results.some((result) => result.status === 'dead')) {
let deadLinks = results.filter(result => { return result.status === 'dead'; });
console.error(chalk.red('\n ERROR: %s dead links found!'), deadLinks.length);
deadLinks.forEach(function (result) {
console.log(' [%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
});
reject();
}

resolve();
});
console.log('\n %s links checked.', results.length);
if (results.some((result) => result.status === 'dead')) {
let deadLinks = results.filter(result => { return result.status === 'dead'; });
console.error(chalk.red('\n ERROR: %s dead links found!'), deadLinks.length);
deadLinks.forEach(function (result) {
console.log(' [%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
});
process.exit(1);
}
});
}

async function main() {
const [inputs, opts] = getInputs();

let isOk = true;
for (const input of inputs) {
await processInput(input.filenameForOutput, input.stream, opts)
.catch(() => { isOk = false; });
}

if (!isOk) {
process.exit(1);
}
}

main();