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

Add add-icon-data script #7849

Merged
merged 23 commits into from Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 (confirm) {
mondeja marked this conversation as resolved.
Show resolved Hide resolved
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
45 changes: 40 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,20 @@ 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.
*
mondeja marked this conversation as resolved.
Show resolved Hide resolved
*/
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 +153,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