Skip to content

Commit

Permalink
Add add-icon-data script (#7849)
Browse files Browse the repository at this point in the history
Co-authored-by: Álvaro Mondéjar <mondejar1994@gmail.com>
  • Loading branch information
LitoMore and mondeja committed Sep 25, 2022
1 parent 62fcc79 commit 5958fc1
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -265,6 +265,8 @@ Here is the object of a fictional brand as an example:
}
```

You can use `npm run add-icon-data` to add metadata via a CLI prompt.

Make sure the icon is added in alphabetical order. If you're in doubt, you can always run `npm run our-lint` - this will tell you if any of the JSON data is in the wrong order.

#### Optional Data
Expand Down
6 changes: 5 additions & 1 deletion package.json
Expand Up @@ -37,10 +37,13 @@
"url": "https://opencollective.com/simple-icons"
},
"devDependencies": {
"chalk": "^5.0.1",
"editorconfig-checker": "4.0.2",
"esbuild": "0.15.6",
"fake-diff": "1.0.0",
"get-relative-luminance": "^1.0.0",
"husky": "8.0.1",
"inquirer": "^9.1.2",
"is-ci": "3.0.1",
"jsonschema": "1.4.1",
"mocha": "10.0.0",
Expand Down Expand Up @@ -71,7 +74,8 @@
"pretest": "npm run prepublishOnly",
"posttest": "npm run postpublish",
"svgo": "svgo --config svgo.config.js",
"get-filename": "node scripts/get-filename.js"
"get-filename": "node scripts/get-filename.js",
"add-icon-data": "node scripts/add-icon-data.js"
},
"engines": {
"node": ">=0.12.18"
Expand Down
137 changes: 137 additions & 0 deletions scripts/add-icon-data.js
@@ -0,0 +1,137 @@
import fs from 'node:fs/promises';
import inquirer from 'inquirer';
import chalk from 'chalk';
import getRelativeLuminance from 'get-relative-luminance';
import {
URL_REGEX,
collator,
getIconsDataString,
getIconDataPath,
writeIconsData,
titleToSlug,
normalizeColor,
} from './utils.js';

const hexPattern = /^#?[a-f0-9]{3,8}$/i;

const iconsData = JSON.parse(await getIconsDataString());

const titleValidator = (text) => {
if (!text) return 'This field is required';
if (
iconsData.icons.find(
(x) => x.title === text || titleToSlug(x.title) === titleToSlug(text),
)
)
return 'This icon title or slug already exist';
return true;
};

const hexValidator = (text) =>
hexPattern.test(text) ? true : 'This should be a valid hex code';

const sourceValidator = (text) =>
URL_REGEX.test(text) ? true : 'This should be a secure URL';

const hexTransformer = (text) => {
const color = normalizeColor(text);
const luminance = hexPattern.test(text)
? getRelativeLuminance.default(`#${color}`)
: -1;
if (luminance === -1) return text;
return chalk.bgHex(`#${color}`).hex(luminance < 0.4 ? '#fff' : '#000')(text);
};

const getIconDataFromAnswers = (answers) => ({
title: answers.title,
hex: answers.hex,
source: answers.source,
...(answers.hasGuidelines ? { guidelines: answers.guidelines } : {}),
...(answers.hasLicense
? {
license: {
type: answers.licenseType,
...(answers.licenseUrl ? { url: answers.licenseUrl } : {}),
},
}
: {}),
});

const dataPrompt = [
{
type: 'input',
name: 'title',
message: 'Title',
validate: titleValidator,
},
{
type: 'input',
name: 'hex',
message: 'Hex',
validate: hexValidator,
filter: (text) => normalizeColor(text),
transformer: hexTransformer,
},
{
type: 'input',
name: 'source',
message: 'Source',
validate: sourceValidator,
},
{
type: 'confirm',
name: 'hasGuidelines',
message: 'The icon has brand guidelines?',
},
{
type: 'input',
name: 'guidelines',
message: 'Guidelines',
validate: sourceValidator,
when: ({ hasGuidelines }) => hasGuidelines,
},
{
type: 'confirm',
name: 'hasLicense',
message: 'The icon has brand license?',
},
{
type: 'input',
name: 'licenseType',
message: 'License type',
validate: (text) => Boolean(text),
when: ({ hasLicense }) => hasLicense,
},
{
type: 'input',
name: 'licenseUrl',
message: 'License URL',
suffix: ' (optional)',
validate: (text) => !Boolean(text) || sourceValidator(text),
when: ({ hasLicense }) => hasLicense,
},
{
type: 'confirm',
name: 'confirm',
message: (answers) => {
const icon = getIconDataFromAnswers(answers);
return [
'About to write to simple-icons.json',
chalk.reset(JSON.stringify(icon, null, 4)),
chalk.reset('Is this OK?'),
].join('\n\n');
},
},
];

const answers = await inquirer.prompt(dataPrompt);
const icon = getIconDataFromAnswers(answers);

if (answers.confirm) {
iconsData.icons.push(icon);
iconsData.icons.sort((a, b) => collator.compare(a.title, b.title));
await writeIconsData(iconsData);
} else {
console.log('Aborted.');
process.exit(1);
}
2 changes: 1 addition & 1 deletion scripts/lint/ourlint.js
Expand Up @@ -48,7 +48,7 @@ const TESTS = {
/* Check the formatting of the data file */
prettified: async (data, dataString) => {
const normalizedDataString = normalizeNewlines(dataString);
const dataPretty = `${JSON.stringify(data, null, ' ')}\n`;
const dataPretty = `${JSON.stringify(data, null, 4)}\n`;

if (normalizedDataString !== dataPretty) {
const dataDiff = fakeDiff(normalizedDataString, dataPretty);
Expand Down
44 changes: 39 additions & 5 deletions scripts/utils.js
Expand Up @@ -4,7 +4,7 @@
*/

import path from 'node:path';
import { promises as fs } from 'node:fs';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const TITLE_TO_SLUG_REPLACEMENTS = {
Expand Down Expand Up @@ -96,15 +96,22 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
);

/**
* Get contents of _data/simple-icons.json.
* Get path of _data/simpe-icons.json.
* @param {String|undefined} rootDir Path to the root directory of the project.
*/
export const getIconsDataString = (rootDir) => {
export const getIconDataPath = (rootDir) => {
if (rootDir === undefined) {
rootDir = path.resolve(getDirnameFromImportMeta(import.meta.url), '..');
}
const iconDataPath = path.resolve(rootDir, '_data', 'simple-icons.json');
return fs.readFile(iconDataPath, 'utf8');
return path.resolve(rootDir, '_data', 'simple-icons.json');
};

/**
* Get contents of _data/simple-icons.json.
* @param {String|undefined} rootDir Path to the root directory of the project.
*/
export const getIconsDataString = (rootDir) => {
return fs.readFile(getIconDataPath(rootDir), 'utf8');
};

/**
Expand All @@ -116,6 +123,19 @@ export const getIconsData = async (rootDir) => {
return JSON.parse(fileContents).icons;
};

/**
* Write icons data to _data/simple-icons.json.
* @param {Object} iconsData Icons data object.
* @param {String|undefined} rootDir Path to the root directory of the project.
*/
export const writeIconsData = async (iconsData, rootDir) => {
return fs.writeFile(
getIconDataPath(rootDir),
`${JSON.stringify(iconsData, null, 4)}\n`,
'utf8',
);
};

/**
* Get the directory name where this file is located from `import.meta.url`,
* equivalent to the `__dirname` global variable in CommonJS.
Expand All @@ -132,6 +152,20 @@ export const normalizeNewlines = (text) => {
return text.replace(/\r\n/g, '\n');
};

/**
* Convert non-6-digit hex color to 6-digit.
* @param {String} text The color text
*/
export const normalizeColor = (text) => {
let color = text.replace('#', '').toUpperCase();
if (color.length < 6) {
color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join('');
} else if (color.length > 6) {
color = color.slice(0, 6);
}
return color;
};

/**
* Get information about third party extensions.
* @param {String} readmePath Path to the README file
Expand Down

0 comments on commit 5958fc1

Please sign in to comment.