mdbadges-cli
Version:
An extensive CLI tool to find over 400+ Shields.io badges for your projects.
517 lines (435 loc) • 17.6 kB
JavaScript
import badges from './badges.mjs';
import c from 'chalk';
import cliSpinners from 'cli-spinners';
import { consola } from 'consola';
import { confirm, select, group, text, log, outro } from '@clack/prompts';
import clipboardy from 'clipboardy';
import fs from 'fs';
import Fuse from 'fuse.js';
import open from 'open';
import ora from 'ora';
import path from 'path';
import { program } from 'commander';
import * as utils from './utils.mjs';
const packageName = 'mdbadges-cli';
// Main Command
program
.name('mdb')
.arguments('[category] [badgeNames...]')
.usage('[category] [badgeName] [--option]')
.option('--html', 'toggle HTML version of a badge')
.option('-s, --style [badgeStyle]', 'toggle style of a badge')
.option('--link', 'toggle links in a badge')
.action(async (category, badgeNames = [], options) => {
const categoryData = badges[utils.searchCategory(category)];
const styles = ['flat', 'flat-square', 'plastic', 'social', 'for-the-badge'];
if (categoryData) {
console.log();
const links = [];
let badgesFound = false;
for (let index = 0; index < badgeNames.length; index++) {
const badgeName = badgeNames[index];
const formattedBadgeName = badgeName.toLowerCase();
const foundBadge = Object.keys(categoryData).find(
(key) => key.toLowerCase() === formattedBadgeName,
);
const badge = categoryData[foundBadge];
if (badge) {
badgesFound = true;
let link = '';
// --link
if (options.link) {
let linkMessage = '';
if (badgeNames.length === 1) {
linkMessage = c.cyan.bold('Enter your link here:');
} else if (index === 0) {
linkMessage = c.cyan.bold(`Enter the link for ${c.magenta(utils.formatBadgeName(badgeName))} here:`);
} else {
linkMessage = c.cyan.bold(`Enter the link for ${c.magenta(utils.formatBadgeName(badgeName))} (Badge ${index + 1}) here:`);
}
const linkResponse = await text({
message: linkMessage,
validate: input => utils.isValidURL(input) ? undefined : c.dim('Enter a valid link.'),
});
// Slightly space out badge and prompt
console.log();
await utils.checkCancellation(linkResponse);
link = linkResponse;
links.push(link);
}
} else {
consola.error(c.red(`'${utils.formatBadgeName(badgeName)}' is not a valid badge.`));
console.log(`Try running ${c.magenta.bold('mdb search')} to look for a badge.\n`);
const fuseOptions = {
keys: ['name'],
threshold: 0.3,
};
const fuse = new Fuse(Object.keys(categoryData).map(key => ({
name: key,
value: categoryData[key],
})), fuseOptions);
const similarBadges = fuse.search(badgeName).map(result => ({
label: utils.formatBadgeName(result.item.name),
value: result.item.value,
}));
if (similarBadges.length > 0) {
const selectedBadge = await select({
message: c.cyan.bold('Did you mean one of these badges instead?'),
options: [
...similarBadges,
{ label: 'None of these', value: 'none' },
],
});
await utils.checkCancellation(selectedBadge);
if (selectedBadge !== 'none') {
const styleOption = options.style && styles.includes(options.style) ? options.style : '';
const { badgeMarkdown, htmlBadge } = utils.formatBadge(selectedBadge, styleOption, options.link ? links[badgeNames.indexOf(selectedBadge)] : '');
if (options.html) {
outro(`${htmlBadge}`);
} else {
outro(`${badgeMarkdown}`);
}
} else {
process.exit(0);
}
}
}
}
// Show warning if user gives an invalid style
if (options.style && !styles.includes(options.style)) {
consola.warn(c.yellow(`An invalid style was detected. It will not be added to the badge(s).\n Available styles are: ${c.magenta.bold(styles.join(', '))}`));
// Remove invalid style
delete options.style;
}
for (let index = 0; index < badgeNames.length; index++) {
const badgeName = badgeNames[index];
const formattedBadgeName = badgeName.toLowerCase();
const foundBadge = Object.keys(categoryData).find(
(key) => key.toLowerCase() === formattedBadgeName,
);
const badge = categoryData[foundBadge];
if (badge) {
let styleOption = options.style || '';
if (badgesFound) {
console.log(c.green.bold('Badge found:'));
}
const { badgeMarkdown, htmlBadge } = utils.formatBadge(badge, styleOption, options.link ? links[index] : '');
if (options.html) {
// HTML
console.log(`${htmlBadge}\n`);
} else {
// Markdown
console.log(`${badgeMarkdown}\n`);
}
}
}
} else {
consola.error(c.red('The specified badge or category could not be found. '));
console.log(
`Check the available categories: ${c.magenta('https://github.com/inttter/mdbadges-cli#categories')}\n` +
`Or directly search badges with: ${c.magenta('mdb search')}\n\n` +
`Looking for commands? See here: ${c.magenta('https://github.com/inttter/mdbadges-cli#commands')}`
);
}
});
// Search Command
program
.command('search')
.alias('s')
.alias('find')
.alias('lookup')
.description('search for badges across any category')
.action(async () => {
const fuse = utils.getFuseInstance(badges);
let continueSearch = true;
while (continueSearch) {
const keyword = await text({
message: c.cyan.bold('Enter a keyword to search for:'),
validate: (value) => {
if (!value.trim()) {
return c.dim('You must enter a keyword.');
}
return;
},
});
await utils.checkCancellation(keyword);
const results = utils.searchBadges(fuse, keyword);
if (results.length === 0) {
log.error(c.red(`No badges containing '${keyword}' could be found.`));
} else {
const badgeChoices = results.map(({ item }) => ({
name: `${(item.formattedBadge)} ${(`(${item.formattedCategory})`)}`,
value: item.badgeCode,
}));
const selectedBadge = await select({
message: c.cyan.bold('Select a badge:'),
options: badgeChoices.map(badge => ({
label: badge.name,
value: badge.value,
})),
});
await utils.checkCancellation(selectedBadge);
// Fixes extra space in front of the selected badge output
const { badgeMarkdown } = utils.formatBadge(selectedBadge);
outro(badgeMarkdown);
}
continueSearch = await confirm({
message: c.cyan.bold('Would you like to search for another badge?'),
initial: true,
});
await utils.checkCancellation(continueSearch);
}
});
// Badge Creation Command
program
.command('create')
.alias('generate')
.description('display prompts to create your own badge')
.action(async () => {
try {
const responses = await group(
{
alt: () =>
text({
message: c.cyan.bold('Enter the alt text for the badge:'),
}),
name: () =>
text({
message: c.cyan.bold('Enter the text you would like to display on the badge:'),
validate: (value) => {
if (!value.trim()) return c.dim('This field is required.');
},
}),
color: () =>
text({
message: c.cyan.bold('Enter a hexadecimal value for the color of the badge:'),
validate: (value) => {
const hexColorRegex = /^#?(?:[0-9a-fA-F]{3}){1,2}$/;
if (!hexColorRegex.test(value.trim())) {
return c.dim('Enter a valid hexadecimal color. Valid ones include #d8e, #96f732');
}
},
}),
logo: () =>
text({
message: c.cyan.bold('Enter the logo to be displayed on the badge:'),
validate: (value) => {
if (!value.trim()) return c.dim('This field is required.');
},
}),
logoColor: () =>
text({
message: c.cyan.bold('Enter the color for the logo:'),
validate: (value) => {
if (!value.trim()) return c.dim('This field is required.');
},
}),
style: () =>
select({
message: c.cyan.bold('Choose the style of the badge:'),
options: [
{ value: 'flat', label: 'Flat', hint: 'Popular' },
{ value: 'flat-square', label: 'Flat Square' },
{ value: 'plastic', label: 'Plastic' },
{ value: 'social', label: 'Social' },
{ value: 'for-the-badge', label: 'For The Badge', hint: 'Popular' },
],
}),
link: () =>
text({
message: c.cyan.bold('[Optional] Enter the URL to redirect to when clicked:'),
}),
},
{
onCancel: () => {
log.error(c.yellow('Exiting because `CTRL+C` was pressed.\n'));
process.exit(0);
},
}
);
// Escape spaces, dashes, and underscores
let name = responses.name
.replace(/-/g, '--')
.replace(/\s/g, '_')
.replace(/_/g, '__');
const badgeLink =
`https://img.shields.io/badge/${name}-${encodeURIComponent(responses.color)}?logo=${encodeURIComponent(responses.logo)}&style=${encodeURIComponent(responses.style)}` +
(responses.logoColor ? `&logoColor=${encodeURIComponent(responses.logoColor)}` : '');
const badgeMarkdown = responses.link
? `[](${responses.link})`
: `[](#)`;
const badgeHtml = responses.link
? `<a href="${utils.escapeHtml(responses.link)}">\n <img src="${badgeLink}" alt="${utils.escapeHtml(responses.alt)}" />\n</a>`
: `<img src="${badgeLink}" alt="${utils.escapeHtml(responses.alt)}" />`;
log.success(c.green.bold('Custom badge created successfully!'));
// Markdown
log.info(c.green.bold('Markdown:'));
log.message(badgeMarkdown);
// HTML
log.info(c.green.bold('HTML:'));
log.message(badgeHtml + '\n');
} catch (error) {
consola.error(new Error(c.red(`An error occurred when trying to make the badge: ${error.message}`)));
}
});
// Add Command
program
.command('add [category] [badgeName] [filePath]')
.option('--html', 'add HTML version of a badge to a file')
.description('add a badge to a file')
.action((category, badgeName, filePath) => {
if (!filePath) {
consola.error(c.red('No file or file path was specified.'));
return;
}
const resolvedPath = path.resolve(filePath);
// Validate that the file (path) exists and is a regular file
if (!fs.existsSync(resolvedPath) || !fs.lstatSync(resolvedPath).isFile()) {
consola.error(c.red(`The path '${filePath}' is invalid or does not point to a valid file.`));
return;
}
// * This makes options work, do not remove.
const options = program.opts();
const formattedBadgeName = badgeName?.toLowerCase() || '';
const formattedCategory = category?.toLowerCase() || '';
const categoryData = badges[formattedCategory];
const foundBadge = categoryData?.[formattedBadgeName];
if (!foundBadge) {
consola.error(c.red('The specified badge or category could not be found. '));
console.log(
`Check the available categories: ${c.magenta('https://github.com/inttter/mdbadges-cli#categories')}\n` +
`Or directly search badges with: ${c.magenta('mdb search')}`
);
return;
}
const { badgeMarkdown, htmlBadge } = utils.formatBadge(foundBadge);
const badgeToAdd = options.html ? htmlBadge : badgeMarkdown;
try {
// Append the badge to the specified file
fs.appendFileSync(filePath, `\n${badgeToAdd}`, 'utf8');
console.log(c.green.bold(`\nBadge has been added to ${filePath} successfully.\n`));
} catch (error) {
consola.error(new Error(c.red(`Could not write to the file: ${error.message}`)));
}
});
// Copy Command
program
.command('copy [category] [badgeName]')
.alias('c')
.option('--html', 'copy HTML version of a badge')
.description('copy a badge\'s code to the clipboard')
.action((category, badgeName) => {
// * This makes options work, do not remove.
const options = program.opts();
const formattedCategory = category.toLowerCase();
const formattedBadgeName = badgeName.toLowerCase();
// Check if the badge exists in the specified category
const selectedBadge = badges[formattedCategory]?.[formattedBadgeName];
if (selectedBadge) {
const { badgeMarkdown, htmlBadge } = utils.formatBadge(selectedBadge);
clipboardy.writeSync(options.html ? htmlBadge : badgeMarkdown);
console.log(c.green.bold(`\n${options.html ? 'HTML' : 'Markdown'} version of badge was copied to the clipboard successfully.\n`));
} else {
consola.error(c.red(`The specified badge or category could not be found.`));
console.log(
`Check the available categories: ${c.magenta('https://github.com/inttter/mdbadges-cli#categories')}\n` +
`Or directly search badges with: ${c.magenta('mdb search')}`
);
}
});
// Random Badge Command
program
.command('random')
.alias('r')
.description('display a random badge')
.action(async () => {
const categories = Object.keys(badges);
const randomCategory = categories[Math.floor(Math.random() * categories.length)];
const badgesInCategory = Object.keys(badges[randomCategory]);
const randomBadgeName = badgesInCategory[Math.floor(Math.random() * badgesInCategory.length)];
const badge = badges[randomCategory][randomBadgeName];
const badgeLink = badge.match(/\(([^)]+)\)/)[1];
const badgeAlt = badge.match(/\[([^)]+)\]/)[1];
const markdownBadge = `[${badgeAlt}](${badgeLink})`;
const htmlBadgeAlt = badgeAlt.replace(/^!\[/, ''); // strips the '![' from the alt text
const htmlBadge = `<img src="${badgeLink}" alt="${htmlBadgeAlt}" />`;
// outputs both versions, Markdown and HTML
console.log(c.green.bold('\nMarkdown:'));
console.log(markdownBadge + '\n');
console.log(c.green.bold('HTML:'));
console.log(htmlBadge + '\n');
});
// Badge List Command
program
.command('badges')
.alias('list')
.description('open a link to the badge list in your browser')
.action(async () => {
console.log();
const spinner = ora({
text: c.blue('Opening in browser...'),
spinner: cliSpinners.arc,
color: 'magenta',
}).start();
const listLink = 'https://inttter.github.io/md-badges/';
try {
await open(listLink);
spinner.succeed(c.green.bold('Opened in your browser!'));
} catch (error) {
consola.error(new Error(c.red(`An error occurred when trying to open the link in your browser: ${error.message}`)));
console.log(` Follow the link here instead: ${c.magenta.bold(listLink)}`);
spinner.stop();
}
});
// Documentation Command
program
.command('docs')
.description('open a link to the documentation in your browser')
.action(async () => {
console.log();
const spinner = ora({
text: c.blue('Opening the documentation in your browser...'),
spinner: cliSpinners.arc,
color: 'yellow',
}).start();
const docsLink = 'https://inttter.gitbook.io/mdbcli'
try {
await open(docsLink);
spinner.succeed(c.green.bold('Opened in your browser!'));
} catch (error) {
consola.error(new Error(c.red(`An error occurred when trying to open the link in your browser: ${error.message}`)));
console.log(`\n Follow the link here instead: ${c.magenta.bold(docsLink)}`);
spinner.stop();
}
});
// Changelog/Releases Command
program
.command('changelog')
.alias('release')
.description('open a link to the latest release and it\'s changelog in your browser')
.action(async () => {
console.log()
const spinner = ora({
text: c.blue('Opening the latest release...'),
spinner: cliSpinners.arc,
color: 'magenta',
}).start();
const changelogLink = `https://github.com/inttter/${packageName}/releases/latest`
try {
await open(changelogLink);
spinner.succeed(c.green.bold('Opened in your browser!'));
} catch (error) {
consola.error(new Error(c.red(`Could not open the link in your browser: ${error.message}`)));
console.log(`\n Follow the link here instead: ${c.magenta.bold(changelogLink)}`);
spinner.stop();
}
});
// Help Command
program
.command('help')
.description('display help information')
.action(() => {
program.outputHelp();
});
program.parse(process.argv);