1
0
mirror of https://github.com/simple-icons/simple-icons.git synced 2024-12-06 01:03:59 +02:00

Add types to source code (#10637)

This commit is contained in:
Álvaro Mondéjar Rubio 2024-06-06 14:40:35 +02:00 committed by GitHub
parent 1224e341d7
commit 236f5fc715
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 619 additions and 205 deletions

View File

@ -16,3 +16,4 @@
!sdk.js !sdk.js
!sdk.d.ts !sdk.d.ts
!.jsonschema.json !.jsonschema.json
!jsconfig.json

View File

@ -2,6 +2,7 @@
"prettier": true, "prettier": true,
"space": 2, "space": 2,
"plugins": ["import"], "plugins": ["import"],
"extends": ["plugin:jsdoc/recommended"],
"rules": { "rules": {
"sort-imports": [ "sort-imports": [
"error", "error",
@ -27,7 +28,8 @@
"newlines-between": "never" "newlines-between": "never"
} }
], ],
"no-console": ["error", {"allow": ["warn", "error"]}] "no-console": ["error", {"allow": ["warn", "error"]}],
"jsdoc/require-file-overview": "error"
}, },
"overrides": [ "overrides": [
{ {
@ -46,6 +48,12 @@
"svgo.config.mjs" "svgo.config.mjs"
], ],
"nodeVersion": ">=18" "nodeVersion": ">=18"
},
{
"files": ["svglint.config.mjs"],
"rules": {
"max-depth": "off"
}
} }
] ]
} }

13
jsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"checkJs": true,
"skipLibCheck": false,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@ -86,10 +86,12 @@
"devDependencies": { "devDependencies": {
"@inquirer/core": "8.1.0", "@inquirer/core": "8.1.0",
"@inquirer/prompts": "5.0.2", "@inquirer/prompts": "5.0.2",
"@types/node": "20.14.2",
"chalk": "5.3.0", "chalk": "5.3.0",
"editorconfig-checker": "5.1.5", "editorconfig-checker": "5.1.5",
"esbuild": "0.20.2", "esbuild": "0.20.2",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-jsdoc": "48.2.8",
"fake-diff": "1.0.0", "fake-diff": "1.0.0",
"fast-fuzzy": "1.12.0", "fast-fuzzy": "1.12.0",
"get-relative-luminance": "1.0.0", "get-relative-luminance": "1.0.0",
@ -99,8 +101,8 @@
"markdown-link-check": "3.12.1", "markdown-link-check": "3.12.1",
"mocha": "10.4.0", "mocha": "10.4.0",
"named-html-entities-json": "1.0.0", "named-html-entities-json": "1.0.0",
"svg-path-bbox": "1.2.6", "svg-path-bbox": "2.0.0",
"svg-path-segments": "2.0.0", "svg-path-segments": "2.0.1",
"svglint": "2.7.1", "svglint": "2.7.1",
"svgo": "3.2.0", "svgo": "3.2.0",
"svgpath": "2.6.0", "svgpath": "2.6.0",

View File

@ -1,4 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
/**
* @file
* Script to add data for a new icon to the simple-icons dataset.
*/
/**
* @typedef {import("../sdk.js").IconData} IconData
*/
import process from 'node:process'; import process from 'node:process';
import {ExitPromptError} from '@inquirer/core'; import {ExitPromptError} from '@inquirer/core';
import {checkbox, confirm, input} from '@inquirer/prompts'; import {checkbox, confirm, input} from '@inquirer/prompts';
@ -15,6 +23,7 @@ import {
} from '../sdk.mjs'; } from '../sdk.mjs';
import {getJsonSchemaData, writeIconsData} from './utils.js'; import {getJsonSchemaData, writeIconsData} from './utils.js';
/** @type {{icons: import('../sdk.js').IconData[]}} */
const iconsData = JSON.parse(await getIconsDataString()); const iconsData = JSON.parse(await getIconsDataString());
const jsonSchema = await getJsonSchemaData(); const jsonSchema = await getJsonSchemaData();
@ -25,25 +34,42 @@ const aliasTypes = ['aka', 'old'].map((key) => ({
value: key, value: key,
})); }));
/** @type {{name: string, value: string}[]} */
const licenseTypes = const licenseTypes =
jsonSchema.definitions.brand.properties.license.oneOf[0].properties.type.enum.map( jsonSchema.definitions.brand.properties.license.oneOf[0].properties.type.enum.map(
(license) => ({name: license, value: license}), (/** @type {string} */ license) => ({name: license, value: license}),
); );
/**
* @param {string} input URL input
* @returns {Promise<boolean|string>} Whether the input is a valid URL
*/
const isValidURL = async (input) => { const isValidURL = async (input) => {
const regex = await urlRegex(); const regex = await urlRegex();
return regex.test(input) || 'Must be a valid and secure (https://) URL.'; return regex.test(input) || 'Must be a valid and secure (https://) URL.';
}; };
/**
* @param {string} input Hex color
* @returns {boolean|string} Whether the input is a valid hex color
*/
const isValidHexColor = (input) => const isValidHexColor = (input) =>
HEX_REGEX.test(input) || 'Must be a valid hex code.'; HEX_REGEX.test(input) || 'Must be a valid hex code.';
/**
* @param {string} input New icon input
* @returns {boolean} Whether the icon is new
*/
const isNewIcon = (input) => const isNewIcon = (input) =>
!iconsData.icons.some( !iconsData.icons.some(
(icon) => (icon) =>
icon.title === input || titleToSlug(icon.title) === titleToSlug(input), icon.title === input || titleToSlug(icon.title) === titleToSlug(input),
) || 'This icon title or slug already exists.'; );
/**
* @param {string} input Color input
* @returns {string} Preview of the color
*/
const previewHexColor = (input) => { const previewHexColor = (input) => {
const color = normalizeColor(input); const color = normalizeColor(input);
const luminance = HEX_REGEX.test(input) const luminance = HEX_REGEX.test(input)
@ -60,7 +86,9 @@ try {
title: await input({ title: await input({
message: 'What is the title of this icon?', message: 'What is the title of this icon?',
validate: (input) => validate: (input) =>
input.trim().length > 0 ? isNewIcon(input) : 'This field is required.', input.trim().length > 0
? isNewIcon(input) || 'This icon title or slug already exists.'
: 'This field is required.',
}), }),
hex: normalizeColor( hex: normalizeColor(
await input({ await input({
@ -111,6 +139,7 @@ try {
}).then(async (aliases) => { }).then(async (aliases) => {
const result = {}; const result = {};
for (const alias of aliases) { for (const alias of aliases) {
// @ts-ignore
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
result[alias] = await input({ result[alias] = await input({
message: `What ${alias} aliases would you like to add? (separate with commas)`, message: `What ${alias} aliases would you like to add? (separate with commas)`,

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Clean files built by the build process. * Clean files built by the build process.
*/ */

View File

@ -1,9 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Simple Icons package build script. * Simple Icons package build script.
*/ */
/**
* @typedef {import('../../types.js').License} License
* @typedef {import('esbuild').TransformOptions} EsBuildTransformOptions
*/
import {promises as fs} from 'node:fs'; import {promises as fs} from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import util from 'node:util'; import util from 'node:util';
@ -36,62 +41,78 @@ const iconObjectTemplateFile = path.resolve(
'icon-object.js.template', 'icon-object.js.template',
); );
const icons = await getIconsData();
const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8);
/**
* @param {string} value The value to escape
* @returns {string} The escaped value
*/
const escape = (value) => {
return value.replaceAll(/(?<!\\)'/g, "\\'");
};
/**
* @param {License} license The license object or URL
* @returns {License} The license object with a URL
*/
const licenseToObject = (license) => {
if (license.url === undefined) {
license.url = `https://spdx.org/licenses/${license.type}`;
}
return license;
};
// TODO: Find a way to type this object without decreasing performance
// @ts-ignore
const iconToJsObject = (icon) => {
return util.format(
iconObjectTemplate,
escape(icon.title),
escape(icon.slug),
escape(titleToHtmlFriendly(icon.title)),
escape(icon.path),
escape(icon.source),
escape(icon.hex),
icon.guidelines ? `\n guidelines: '${escape(icon.guidelines)}',` : '',
icon.license === undefined
? ''
: `\n license: ${JSON.stringify(licenseToObject(icon.license))},`,
);
};
/**
* @param {string} filepath The path to the file to write
* @param {string} rawJavaScript The raw JavaScript content to write to the file
* @param {EsBuildTransformOptions | null} options The options to pass to esbuild
*/
const writeJs = async (filepath, rawJavaScript, options = null) => {
options = options === null ? {minify: true} : options;
const {code} = await esbuildTransform(rawJavaScript, options);
await fs.writeFile(filepath, code);
};
/**
* @param {string} filepath The path to the file to write
* @param {string} rawTypeScript The raw TypeScript content to write to the file
*/
const writeTs = async (filepath, rawTypeScript) => {
await fs.writeFile(filepath, rawTypeScript);
};
const build = async () => { const build = async () => {
const icons = await getIconsData();
const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8);
// Local helper functions
const escape = (value) => {
return value.replaceAll(/(?<!\\)'/g, "\\'");
};
const licenseToObject = (license) => {
if (license === undefined) {
return;
}
if (license.url === undefined) {
license.url = `https://spdx.org/licenses/${license.type}`;
}
return license;
};
const iconToObject = (icon) => {
return util.format(
iconObjectTemplate,
escape(icon.title),
escape(icon.slug),
escape(titleToHtmlFriendly(icon.title)),
escape(icon.path),
escape(icon.source),
escape(icon.hex),
icon.guidelines ? `\n guidelines: '${escape(icon.guidelines)}',` : '',
licenseToObject(icon.license)
? `\n license: ${JSON.stringify(licenseToObject(icon.license))},`
: '',
);
};
const writeJs = async (filepath, rawJavaScript, options = null) => {
options = options === null ? {minify: true} : options;
const {code} = await esbuildTransform(rawJavaScript, options);
await fs.writeFile(filepath, code);
};
const writeTs = async (filepath, rawTypeScript) => {
await fs.writeFile(filepath, rawTypeScript);
};
// 'main'
const buildIcons = await Promise.all( const buildIcons = await Promise.all(
icons.map(async (icon) => { icons.map(async (icon) => {
const filename = getIconSlug(icon); const filename = getIconSlug(icon);
const svgFilepath = path.resolve(iconsDirectory, `${filename}.svg`); const svgFilepath = path.resolve(iconsDirectory, `${filename}.svg`);
// TODO: Find a way to type these objects without decreasing performance
// @ts-ignore
icon.svg = await fs.readFile(svgFilepath, UTF8); icon.svg = await fs.readFile(svgFilepath, UTF8);
// @ts-ignore
icon.path = svgToPath(icon.svg); icon.path = svgToPath(icon.svg);
icon.slug = filename; icon.slug = filename;
const iconObject = iconToObject(icon); const iconObject = iconToJsObject(icon);
const iconExportName = slugToVariableName(icon.slug); const iconExportName = slugToVariableName(icon.slug);
return {icon, iconObject, iconExportName}; return {icon, iconObject, iconExportName};
}), }),

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Script that takes a brand name as argument and outputs the corresponding * Script that takes a brand name as argument and outputs the corresponding
* icon SVG filename to standard output. * icon SVG filename to standard output.
*/ */

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* CLI tool to run jsonschema on the simple-icons.json data file. * CLI tool to run jsonschema on the simple-icons.json data file.
*/ */

View File

@ -1,21 +1,39 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Linters for the package that can't easily be implemented in the existing * Linters for the package that can't easily be implemented in the existing
* linters (e.g. jsonlint/svglint). * linters (e.g. jsonlint/svglint).
*/ */
/**
* @typedef {import("../../sdk.mjs").IconData} IconData
* @typedef {import("../../types.js").CustomLicense} CustomLicense
* @typedef {IconData[]} IconsData
*/
import process from 'node:process'; import process from 'node:process';
import fakeDiff from 'fake-diff'; import fakeDiff from 'fake-diff';
import {collator, getIconsDataString, normalizeNewlines} from '../../sdk.mjs'; import {collator, getIconsDataString, normalizeNewlines} from '../../sdk.mjs';
/** /**
* Contains our tests so they can be isolated from each other. * Contains our tests so they can be isolated from each other.
* @type {{[k:string]: () => (string|undefined)}} * @type {{[k: string]: (arg0: {icons: IconsData}, arg1: string) => string | undefined}}
*/ */
const TESTS = { const TESTS = {
/* Tests whether our icons are in alphabetical order */ /**
* Tests whether our icons are in alphabetical order
* @param {{icons: IconsData}} data Icons data
* @returns {string|undefined} Error message or undefined
*/
alphabetical(data) { alphabetical(data) {
/**
* Collects invalid alphabet ordered icons
* @param {IconData[]} invalidEntries Invalid icons reference
* @param {IconData} icon Icon to check
* @param {number} index Index of the icon
* @param {IconData[]} array Array of icons
* @returns {IconData[]} Invalid icons
*/
const collector = (invalidEntries, icon, index, array) => { const collector = (invalidEntries, icon, index, array) => {
if (index > 0) { if (index > 0) {
const previous = array[index - 1]; const previous = array[index - 1];
@ -34,6 +52,11 @@ const TESTS = {
return invalidEntries; return invalidEntries;
}; };
/**
* Format an icon for display in the error message
* @param {IconData} icon Icon to format
* @returns {string} Formatted icon
*/
const format = (icon) => { const format = (icon) => {
if (icon.slug) { if (icon.slug) {
return `${icon.title} (${icon.slug})`; return `${icon.title} (${icon.slug})`;
@ -63,6 +86,11 @@ const TESTS = {
/* Check redundant trailing slash in URL */ /* Check redundant trailing slash in URL */
checkUrl(data) { checkUrl(data) {
/**
* Check if an URL has a redundant trailing slash.
* @param {string} url URL to check
* @returns {boolean} Whether the URL has a redundant trailing slash
*/
const hasRedundantTrailingSlash = (url) => { const hasRedundantTrailingSlash = (url) => {
const {origin} = new global.URL(url); const {origin} = new global.URL(url);
return /^\/+$/.test(url.replace(origin, '')); return /^\/+$/.test(url.replace(origin, ''));
@ -71,7 +99,17 @@ const TESTS = {
const allUrlFields = [ const allUrlFields = [
...new Set( ...new Set(
data.icons data.icons
.flatMap((icon) => [icon.source, icon.guidelines, icon.license?.url]) .flatMap((icon) => {
// TODO: `Omit` is not working smoothly here
const license =
// @ts-ignore
icon.license && icon.license.url
? // @ts-ignore
[icon.license.url]
: [];
return [icon.source, icon.guidelines, ...license];
})
.filter(Boolean), .filter(Boolean),
), ),
]; ];
@ -88,11 +126,13 @@ const TESTS = {
}, },
}; };
const dataString = await getIconsDataString(); const iconsDataString = await getIconsDataString();
const data = JSON.parse(dataString); const iconsData = JSON.parse(iconsDataString);
const errors = ( const errors = (
await Promise.all(Object.values(TESTS).map((test) => test(data, dataString))) await Promise.all(
Object.values(TESTS).map((test) => test(iconsData, iconsDataString)),
)
) )
// eslint-disable-next-line unicorn/no-await-expression-member // eslint-disable-next-line unicorn/no-await-expression-member
.filter(Boolean); .filter(Boolean);

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Rewrite some Markdown files. * Rewrite some Markdown files.
*/ */
@ -17,6 +17,10 @@ const rootDirectory = path.resolve(__dirname, '..', '..');
const readmeFile = path.resolve(rootDirectory, 'README.md'); const readmeFile = path.resolve(rootDirectory, 'README.md');
const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md'); const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md');
/**
* Reformat a file.
* @param {string} filePath Path to the file
*/
const reformat = async (filePath) => { const reformat = async (filePath) => {
const fileContent = await readFile(filePath, 'utf8'); const fileContent = await readFile(filePath, 'utf8');
await writeFile( await writeFile(

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Updates the CDN URLs in the README.md to match the major version in the * Updates the CDN URLs in the README.md to match the major version in the
* NPM package manifest. Does nothing if the README.md is already up-to-date. * NPM package manifest. Does nothing if the README.md is already up-to-date.
*/ */
@ -16,16 +16,27 @@ const rootDirectory = path.resolve(__dirname, '..', '..');
const packageJsonFile = path.resolve(rootDirectory, 'package.json'); const packageJsonFile = path.resolve(rootDirectory, 'package.json');
const readmeFile = path.resolve(rootDirectory, 'README.md'); const readmeFile = path.resolve(rootDirectory, 'README.md');
/**
* @param {string} semVersion A semantic version string.
* @returns {number} The major version number.
*/
const getMajorVersion = (semVersion) => { const getMajorVersion = (semVersion) => {
const majorVersionAsString = semVersion.split('.')[0]; const majorVersionAsString = semVersion.split('.')[0];
return Number.parseInt(majorVersionAsString, 10); return Number.parseInt(majorVersionAsString, 10);
}; };
/**
* Get the package.json manifest.
* @returns {Promise<{version: string}>} The package.json manifest.
*/
const getManifest = async () => { const getManifest = async () => {
const manifestRaw = await fs.readFile(packageJsonFile, 'utf8'); const manifestRaw = await fs.readFile(packageJsonFile, 'utf8');
return JSON.parse(manifestRaw); return JSON.parse(manifestRaw);
}; };
/**
* @param {number} majorVersion The major version number.
*/
const updateVersionInReadmeIfNecessary = async (majorVersion) => { const updateVersionInReadmeIfNecessary = async (majorVersion) => {
let content = await fs.readFile(readmeFile, 'utf8'); let content = await fs.readFile(readmeFile, 'utf8');

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Updates the SDK Typescript definitions located in the file sdk.d.ts * Updates the SDK Typescript definitions located in the file sdk.d.ts
* to match the current definitions of functions of sdk.mjs. * to match the current definitions of functions of sdk.mjs.
*/ */
@ -34,19 +34,53 @@ const generateSdkMts = async () => {
'npx tsc sdk.mjs' + 'npx tsc sdk.mjs' +
' --declaration --emitDeclarationOnly --allowJs --removeComments', ' --declaration --emitDeclarationOnly --allowJs --removeComments',
); );
} catch (error) { } catch (/** @type {unknown} */ error) {
await fs.writeFile(sdkMjs, originalSdkMjsContent); await fs.writeFile(sdkMjs, originalSdkMjsContent);
let errorMessage = error;
if (error instanceof Error) {
// The `execSync` function throws a generic Node.js Error
errorMessage = error.message;
}
process.stdout.write( process.stdout.write(
`Error ${error.status} generating Typescript` + `Error generating Typescript definitions: '${errorMessage}'\n`,
` definitions: '${error.message}'` +
'\n',
); );
process.exit(1); process.exit(1);
} }
await fs.writeFile(sdkMjs, originalSdkMjsContent); await fs.writeFile(sdkMjs, originalSdkMjsContent);
}; };
/**
* We must remove the duplicated export types that tsc generates from
* JSDoc `typedef` comments.
* See {@link https://github.com/microsoft/TypeScript/issues/46011}
* @param {string} content Content of the file
* @returns {string} The content without duplicated export types
*/
const removeDuplicatedExportTypes = (content) => {
const newContent = [];
const lines = content.split('\n');
/** @type {string[]} */
const exportTypesFound = [];
for (const line of lines) {
if (line.startsWith('export type ')) {
const type = line.split(' ')[2];
if (!exportTypesFound.includes(type)) {
newContent.push(line);
exportTypesFound.push(type);
}
} else {
newContent.push(line);
}
}
return newContent.join('\n');
};
const generateSdkTs = async () => { const generateSdkTs = async () => {
const fileExists = await fs const fileExists = await fs
.access(sdkMts) .access(sdkMts)
@ -62,15 +96,21 @@ const generateSdkTs = async () => {
(await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] + (await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] +
`${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`; `${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`;
await fs.writeFile(sdkTs, newSdkTsContent); await fs.writeFile(sdkTs, removeDuplicatedExportTypes(newSdkTsContent));
await fs.unlink(sdkMts); await fs.unlink(sdkMts);
try { try {
execSync('npx prettier -w sdk.d.ts'); execSync('npx prettier -w sdk.d.ts');
} catch (error) { } catch (error) {
let errorMessage = error;
if (error instanceof Error) {
// The `execSync` function throws a generic Node.js Error
errorMessage = error.message;
}
process.stdout.write( process.stdout.write(
`Error ${error.status} executing Prettier` + 'Error executing Prettier to prettify' +
` to prettify SDK TS definitions: '${error.message}'` + ` SDK TS definitions: '${errorMessage}'` +
'\n', '\n',
); );
process.exit(1); process.exit(1);

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Generates a MarkDown file that lists every brand name and their slug. * Generates a MarkDown file that lists every brand name and their slug.
*/ */

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* @fileoverview * @file
* Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located * Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located
* at README every time the number of current icons is more than `updateRange` * at README every time the number of current icons is more than `updateRange`
* more than the previous milestone. * more than the previous milestone.
@ -20,22 +20,32 @@ const readmeFile = path.resolve(rootDirectory, 'README.md');
const readmeContent = await fs.readFile(readmeFile, 'utf8'); const readmeContent = await fs.readFile(readmeFile, 'utf8');
let overNIconsInReadme;
try { try {
overNIconsInReadme = Number.parseInt(regexMatcher.exec(readmeContent)[1], 10); const match = regexMatcher.exec(readmeContent);
if (match === null) {
console.error(
'Failed to obtain number of SVG icons of current milestone in README:',
'No match found',
);
process.exit(1);
} else {
const overNIconsInReadme = Number.parseInt(match[1], 10);
const iconsData = await getIconsData();
const nIcons = iconsData.length;
const newNIcons = overNIconsInReadme + updateRange;
if (nIcons > newNIcons) {
const newContent = readmeContent.replace(
regexMatcher,
`Over ${newNIcons} `,
);
await fs.writeFile(readmeFile, newContent);
}
}
} catch (error) { } catch (error) {
console.error( console.error(
'Failed to obtain number of SVG icons of current milestone in README:', 'Failed to update number of SVG icons of current milestone in README:',
error, error,
); );
process.exit(1); process.exit(1);
} }
const iconsData = await getIconsData();
const nIcons = iconsData.length;
const newNIcons = overNIconsInReadme + updateRange;
if (nIcons > newNIcons) {
const newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `);
await fs.writeFile(readmeFile, newContent);
}

View File

@ -1,12 +1,24 @@
/**
* @file Internal utilities.
*
* Here resides all the functionality that does not qualifies to reside
* in the SDK because is not publicly exposed.
*/
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs'; import {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
/**
* @typedef {import("../sdk.js").IconData} IconData
*/
/** /**
* Get JSON schema data. * Get JSON schema data.
* @param {String} rootDirectory Path to the root directory of the project. * @param {string} rootDirectory Path to the root directory of the project.
* @returns {Promise<any>} JSON schema data.
*/ */
export const getJsonSchemaData = async ( export const getJsonSchemaData = async (
rootDirectory = path.resolve(__dirname, '..'), rootDirectory = path.resolve(__dirname, '..'),
@ -18,8 +30,8 @@ export const getJsonSchemaData = async (
/** /**
* Write icons data to _data/simple-icons.json. * Write icons data to _data/simple-icons.json.
* @param {Object} iconsData Icons data object. * @param {{icons: IconData[]}} iconsData Icons data object.
* @param {String} rootDirectory Path to the root directory of the project. * @param {string} rootDirectory Path to the root directory of the project.
*/ */
export const writeIconsData = async ( export const writeIconsData = async (
iconsData, iconsData,

9
sdk.d.ts vendored
View File

@ -1,5 +1,5 @@
/** /**
* @fileoverview * @file
* Types for Simple Icons SDK. * Types for Simple Icons SDK.
*/ */
@ -10,7 +10,6 @@ import type {CustomLicense, SPDXLicense} from './types';
* *
* Includes the module and author of the extension, * Includes the module and author of the extension,
* both including a name and URL. * both including a name and URL.
*
* @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions} * @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions}
*/ */
export type ThirdPartyExtension = { export type ThirdPartyExtension = {
@ -27,7 +26,6 @@ type ThirdPartyExtensionSubject = {
* The aliases for a Simple Icon. * The aliases for a Simple Icon.
* *
* Corresponds to the `aliases` property in the *_data/simple-icons.json* file. * Corresponds to the `aliases` property in the *_data/simple-icons.json* file.
*
* @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases} * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases}
*/ */
export type Aliases = { export type Aliases = {
@ -47,7 +45,6 @@ type DuplicateAlias = {
* The data for a Simple Icon. * The data for a Simple Icon.
* *
* Corresponds to the data stored for each icon in the *_data/simple-icons.json* file. * Corresponds to the data stored for each icon in the *_data/simple-icons.json* file.
*
* @see {@link https://github.com/mondeja/simple-icons/blob/utils-entrypoint/CONTRIBUTING.md#7-update-the-json-data-for-simpleiconsorg Update the JSON Data for SimpleIcons.org} * @see {@link https://github.com/mondeja/simple-icons/blob/utils-entrypoint/CONTRIBUTING.md#7-update-the-json-data-for-simpleiconsorg Update the JSON Data for SimpleIcons.org}
*/ */
export type IconData = { export type IconData = {
@ -73,8 +70,8 @@ export function slugToVariableName(slug: string): string;
export function titleToHtmlFriendly(brandTitle: string): string; export function titleToHtmlFriendly(brandTitle: string): string;
export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string; export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string;
export function getIconDataPath(rootDirectory?: string): string; export function getIconDataPath(rootDirectory?: string): string;
export function getIconsDataString(rootDirectory?: string): string; export function getIconsDataString(rootDirectory?: string): Promise<string>;
export function getIconsData(rootDirectory?: string): IconData[]; export function getIconsData(rootDirectory?: string): Promise<IconData[]>;
export function normalizeNewlines(text: string): string; export function normalizeNewlines(text: string): string;
export function normalizeColor(text: string): string; export function normalizeColor(text: string): string;
export function getThirdPartyExtensions( export function getThirdPartyExtensions(

133
sdk.mjs
View File

@ -1,5 +1,5 @@
/** /**
* @fileoverview * @file
* Simple Icons SDK. * Simple Icons SDK.
*/ */
@ -8,10 +8,11 @@ import path from 'node:path';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
/** /**
* @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension * @typedef {import("./sdk.d.ts").ThirdPartyExtension} ThirdPartyExtension
* @typedef {import("./sdk").IconData} IconData * @typedef {import("./sdk.d.ts").IconData} IconData
*/ */
/** @type {{ [key: string]: string }} */
const TITLE_TO_SLUG_REPLACEMENTS = { const TITLE_TO_SLUG_REPLACEMENTS = {
'+': 'plus', '+': 'plus',
'.': 'dot', '.': 'dot',
@ -41,15 +42,15 @@ export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae\d,. ]+$/i;
/** /**
* Get the directory name where this file is located from `import.meta.url`, * Get the directory name where this file is located from `import.meta.url`,
* equivalent to the `__dirname` global variable in CommonJS. * equivalent to the `__dirname` global variable in CommonJS.
* @param {String} importMetaUrl import.meta.url * @param {string} importMetaUrl import.meta.url
* @returns {String} Directory name in which this file is located * @returns {string} Directory name in which this file is located
*/ */
export const getDirnameFromImportMeta = (importMetaUrl) => export const getDirnameFromImportMeta = (importMetaUrl) =>
path.dirname(fileURLToPath(importMetaUrl)); path.dirname(fileURLToPath(importMetaUrl));
/** /**
* Build a regex to validate HTTPs URLs. * Build a regex to validate HTTPs URLs.
* @param {String} jsonschemaPath Path to the *.jsonschema.json* file * @param {string} jsonschemaPath Path to the *.jsonschema.json* file
* @returns {Promise<RegExp>} Regex to validate HTTPs URLs * @returns {Promise<RegExp>} Regex to validate HTTPs URLs
*/ */
export const urlRegex = async ( export const urlRegex = async (
@ -68,21 +69,21 @@ export const urlRegex = async (
/** /**
* Get the slug/filename for an icon. * Get the slug/filename for an icon.
* @param {IconData} icon The icon data as it appears in *_data/simple-icons.json* * @param {IconData} icon The icon data as it appears in *_data/simple-icons.json*
* @returns {String} The slug/filename for the icon * @returns {string} The slug/filename for the icon
*/ */
export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title); export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
/** /**
* Extract the path from an icon SVG content. * Extract the path from an icon SVG content.
* @param {String} svg The icon SVG content * @param {string} svg The icon SVG content
* @returns {String} The path from the icon SVG content * @returns {string} The path from the icon SVG content
**/ */
export const svgToPath = (svg) => svg.split('"', 8)[7]; export const svgToPath = (svg) => svg.split('"', 8)[7];
/** /**
* Converts a brand title into a slug/filename. * Converts a brand title into a slug/filename.
* @param {String} title The title to convert * @param {string} title The title to convert
* @returns {String} The slug/filename for the title * @returns {string} The slug/filename for the title
*/ */
export const titleToSlug = (title) => export const titleToSlug = (title) =>
title title
@ -96,8 +97,8 @@ export const titleToSlug = (title) =>
/** /**
* Converts a slug into a variable name that can be exported. * Converts a slug into a variable name that can be exported.
* @param {String} slug The slug to convert * @param {string} slug The slug to convert
* @returns {String} The variable name for the slug * @returns {string} The variable name for the slug
*/ */
export const slugToVariableName = (slug) => { export const slugToVariableName = (slug) => {
const slugFirstLetter = slug[0].toUpperCase(); const slugFirstLetter = slug[0].toUpperCase();
@ -107,8 +108,8 @@ export const slugToVariableName = (slug) => {
/** /**
* Converts a brand title as defined in *_data/simple-icons.json* into a brand * Converts a brand title as defined in *_data/simple-icons.json* into a brand
* title in HTML/SVG friendly format. * title in HTML/SVG friendly format.
* @param {String} brandTitle The title to convert * @param {string} brandTitle The title to convert
* @returns {String} The brand title in HTML/SVG friendly format * @returns {string} The brand title in HTML/SVG friendly format
*/ */
export const titleToHtmlFriendly = (brandTitle) => export const titleToHtmlFriendly = (brandTitle) =>
brandTitle brandTitle
@ -117,6 +118,8 @@ export const titleToHtmlFriendly = (brandTitle) =>
.replaceAll('<', '&lt;') .replaceAll('<', '&lt;')
.replaceAll('>', '&gt;') .replaceAll('>', '&gt;')
.replaceAll(/./g, (char) => { .replaceAll(/./g, (char) => {
/** @type {number} */
// @ts-ignore
const charCode = char.codePointAt(0); const charCode = char.codePointAt(0);
return charCode > 127 ? `&#${charCode};` : char; return charCode > 127 ? `&#${charCode};` : char;
}); });
@ -124,8 +127,8 @@ export const titleToHtmlFriendly = (brandTitle) =>
/** /**
* Converts a brand title in HTML/SVG friendly format into a brand title (as * Converts a brand title in HTML/SVG friendly format into a brand title (as
* it is seen in *_data/simple-icons.json*) * it is seen in *_data/simple-icons.json*)
* @param {String} htmlFriendlyTitle The title to convert * @param {string} htmlFriendlyTitle The title to convert
* @returns {String} The brand title in HTML/SVG friendly format * @returns {string} The brand title in HTML/SVG friendly format
*/ */
export const htmlFriendlyToTitle = (htmlFriendlyTitle) => export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
htmlFriendlyTitle htmlFriendlyTitle
@ -134,13 +137,18 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
) )
.replaceAll( .replaceAll(
/&(quot|amp|lt|gt);/g, /&(quot|amp|lt|gt);/g,
/**
* @param {string} _ Full match
* @param {'quot' | 'amp' | 'lt' | 'gt'} reference Reference to replace
* @returns {string} Replacement for the reference
*/
(_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference], (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference],
); );
/** /**
* Get path of *_data/simple-icons.json*. * Get path of *_data/simple-icons.json*.
* @param {String} rootDirectory Path to the root directory of the project * @param {string} rootDirectory Path to the root directory of the project
* @returns {String} Path of *_data/simple-icons.json* * @returns {string} Path of *_data/simple-icons.json*
*/ */
export const getIconDataPath = ( export const getIconDataPath = (
rootDirectory = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
@ -150,8 +158,8 @@ export const getIconDataPath = (
/** /**
* Get contents of *_data/simple-icons.json*. * Get contents of *_data/simple-icons.json*.
* @param {String} rootDirectory Path to the root directory of the project * @param {string} rootDirectory Path to the root directory of the project
* @returns {String} Content of *_data/simple-icons.json* * @returns {Promise<string>} Content of *_data/simple-icons.json*
*/ */
export const getIconsDataString = ( export const getIconsDataString = (
rootDirectory = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
@ -161,8 +169,8 @@ export const getIconsDataString = (
/** /**
* Get icons data as object from *_data/simple-icons.json*. * Get icons data as object from *_data/simple-icons.json*.
* @param {String} rootDirectory Path to the root directory of the project * @param {string} rootDirectory Path to the root directory of the project
* @returns {IconData[]} Icons data as array from *_data/simple-icons.json* * @returns {Promise<IconData[]>} Icons data as array from *_data/simple-icons.json*
*/ */
export const getIconsData = async ( export const getIconsData = async (
rootDirectory = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
@ -173,8 +181,8 @@ export const getIconsData = async (
/** /**
* Replace Windows newline characters by Unix ones. * Replace Windows newline characters by Unix ones.
* @param {String} text The text to replace * @param {string} text The text to replace
* @returns {String} The text with Windows newline characters replaced by Unix ones * @returns {string} The text with Windows newline characters replaced by Unix ones
*/ */
export const normalizeNewlines = (text) => { export const normalizeNewlines = (text) => {
return text.replaceAll('\r\n', '\n'); return text.replaceAll('\r\n', '\n');
@ -182,8 +190,8 @@ export const normalizeNewlines = (text) => {
/** /**
* Convert non-6-digit hex color to 6-digit with the character `#` stripped. * Convert non-6-digit hex color to 6-digit with the character `#` stripped.
* @param {String} text The color text * @param {string} text The color text
* @returns {String} The color text in 6-digit hex format * @returns {string} The color text in 6-digit hex format
*/ */
export const normalizeColor = (text) => { export const normalizeColor = (text) => {
let color = text.replace('#', '').toUpperCase(); let color = text.replace('#', '').toUpperCase();
@ -199,7 +207,7 @@ export const normalizeColor = (text) => {
/** /**
* Get information about third party extensions from the README table. * Get information about third party extensions from the README table.
* @param {String} readmePath Path to the README file * @param {string} readmePath Path to the README file
* @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions * @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions
*/ */
export const getThirdPartyExtensions = async ( export const getThirdPartyExtensions = async (
@ -214,23 +222,43 @@ export const getThirdPartyExtensions = async (
.split('|\n|') .split('|\n|')
.slice(2) .slice(2)
.map((line) => { .map((line) => {
let [module, author] = line.split(' | '); const [module_, author] = line.split(' | ');
module = module.split('<img src="')[0]; const module = module_.split('<img src="')[0];
const moduleName = /\[(.+)]/.exec(module)?.[1];
if (moduleName === undefined) {
throw new Error(`Module name improperly parsed from line: ${line}`);
}
const moduleUrl = /\((.+)\)/.exec(module)?.[1];
if (moduleUrl === undefined) {
throw new Error(`Module URL improperly parsed from line: ${line}`);
}
const authorName = /\[(.+)]/.exec(author)?.[1];
if (authorName === undefined) {
throw new Error(`Author improperly parsed from line: ${line}`);
}
const authorUrl = /\((.+)\)/.exec(author)?.[1];
if (authorUrl === undefined) {
throw new Error(`Author URL improperly parsed from line: ${line}`);
}
return { return {
module: { module: {
name: /\[(.+)]/.exec(module)[1], name: moduleName,
url: /\((.+)\)/.exec(module)[1], url: moduleUrl,
}, },
author: { author: {
name: /\[(.+)]/.exec(author)[1], name: authorName,
url: /\((.+)\)/.exec(author)[1], url: authorUrl,
}, },
}; };
}); });
/** /**
* Get information about third party libraries from the README table. * Get information about third party libraries from the README table.
* @param {String} readmePath Path to the README file * @param {string} readmePath Path to the README file
* @returns {Promise<ThirdPartyExtension[]>} Information about third party libraries * @returns {Promise<ThirdPartyExtension[]>} Information about third party libraries
*/ */
export const getThirdPartyLibraries = async ( export const getThirdPartyLibraries = async (
@ -247,23 +275,42 @@ export const getThirdPartyLibraries = async (
.map((line) => { .map((line) => {
let [module, author] = line.split(' | '); let [module, author] = line.split(' | ');
module = module.split('<img src="')[0]; module = module.split('<img src="')[0];
const moduleName = /\[(.+)]/.exec(module)?.[1];
if (moduleName === undefined) {
throw new Error(`Module name improperly parsed from line: ${line}`);
}
const moduleUrl = /\((.+)\)/.exec(module)?.[1];
if (moduleUrl === undefined) {
throw new Error(`Module URL improperly parsed from line: ${line}`);
}
const authorName = /\[(.+)]/.exec(author)?.[1];
if (authorName === undefined) {
throw new Error(`Author improperly parsed from line: ${line}`);
}
const authorUrl = /\((.+)\)/.exec(author)?.[1];
if (authorUrl === undefined) {
throw new Error(`Author URL improperly parsed from line: ${line}`);
}
return { return {
module: { module: {
name: /\[(.+)]/.exec(module)[1], name: moduleName,
url: /\((.+)\)/.exec(module)[1], url: moduleUrl,
}, },
author: { author: {
name: /\[(.+)]/.exec(author)[1], name: authorName,
url: /\((.+)\)/.exec(author)[1], url: authorUrl,
}, },
}; };
}); });
/** /**
* `Intl.Collator` object ready to be used for icon titles sorting. * `Intl.Collator` object ready to be used for icon titles sorting.
* @type {Intl.Collator} * @see {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator} */
**/
export const collator = new Intl.Collator('en', { export const collator = new Intl.Collator('en', {
usage: 'search', usage: 'search',
caseFirst: 'upper', caseFirst: 'upper',

View File

@ -1,8 +1,12 @@
/* eslint complexity: off, max-depth: off */ /**
* @file
* Linting rules for SVGLint to check SVG icons.
*/
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import svgPathBbox from 'svg-path-bbox'; import {svgPathBbox} from 'svg-path-bbox';
import parsePath from 'svg-path-segments'; import parsePath from 'svg-path-segments';
import svgpath from 'svgpath'; import svgpath from 'svgpath';
import { import {
@ -26,6 +30,7 @@ const icons = await getIconsData();
const htmlNamedEntities = JSON.parse( const htmlNamedEntities = JSON.parse(
await fs.readFile(htmlNamedEntitiesFile, 'utf8'), await fs.readFile(htmlNamedEntitiesFile, 'utf8'),
); );
/** @type {{ [key: string]: { [key: string]: string } }} */
const svglintIgnores = JSON.parse( const svglintIgnores = JSON.parse(
await fs.readFile(svglintIgnoredFile, 'utf8'), await fs.readFile(svglintIgnoredFile, 'utf8'),
); );
@ -45,6 +50,10 @@ const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true';
const ignoreFile = './.svglint-ignored.json'; const ignoreFile = './.svglint-ignored.json';
const iconIgnored = updateIgnoreFile ? {} : svglintIgnores; const iconIgnored = updateIgnoreFile ? {} : svglintIgnores;
/**
* @param {{ [key: string]: any }} object Object to sort by key
* @returns {{ [key: string]: any }} Object sorted by key
*/
const sortObjectByKey = (object) => { const sortObjectByKey = (object) => {
return Object.fromEntries( return Object.fromEntries(
Object.keys(object) Object.keys(object)
@ -53,6 +62,10 @@ const sortObjectByKey = (object) => {
); );
}; };
/**
* @param {{ [key: string]: any }} object Object to sort by value
* @returns {{ [key: string]: any }} Object sorted by value
*/
const sortObjectByValue = (object) => { const sortObjectByValue = (object) => {
return Object.fromEntries( return Object.fromEntries(
Object.keys(object) Object.keys(object)
@ -61,15 +74,27 @@ const sortObjectByValue = (object) => {
); );
}; };
const removeLeadingZeros = (number) => { /**
* Remove leading zeros from a number as a string.
* @param {number | string} numberOrString The number or string to remove leading zeros from.
* @returns {string} The number as a string without leading zeros.
*/
const removeLeadingZeros = (numberOrString) => {
// Convert 0.03 to '.03' // Convert 0.03 to '.03'
return number.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3'); return numberOrString.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3');
}; };
/** /**
* Given three points, returns if the middle one (x2, y2) is collinear * Given three points, returns if the middle one (x2, y2) is collinear
* to the line formed by the two limit points. * to the line formed by the two limit points.
**/ * @param {number} x1 The x coordinate of the first point.
* @param {number} y1 The y coordinate of the first point.
* @param {number} x2 The x coordinate of the second point.
* @param {number} y2 The y coordinate of the second point.
* @param {number} x3 The x coordinate of the third point.
* @param {number} y3 The y coordinate of the third point.
* @returns {boolean} Whether the middle point is collinear to the line.
*/
// eslint-disable-next-line max-params // eslint-disable-next-line max-params
const collinear = (x1, y1, x2, y2, x3, y3) => { const collinear = (x1, y1, x2, y2, x3, y3) => {
return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) === 0; return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) === 0;
@ -77,7 +102,8 @@ const collinear = (x1, y1, x2, y2, x3, y3) => {
/** /**
* Returns the number of digits after the decimal point. * Returns the number of digits after the decimal point.
* @param num The number of interest. * @param {number} number_ The number to count the decimals of.
* @returns {number} The number of digits after the decimal point.
*/ */
const countDecimals = (number_) => { const countDecimals = (number_) => {
if (number_ && number_ % 1) { if (number_ && number_ % 1) {
@ -94,7 +120,8 @@ const countDecimals = (number_) => {
/** /**
* Get the index at which the first path value of an SVG starts. * Get the index at which the first path value of an SVG starts.
* @param svgFileContent The raw SVG as text. * @param {string} svgFileContent The raw SVG as text.
* @returns {number} The index at which the path value starts.
*/ */
const getPathDIndex = (svgFileContent) => { const getPathDIndex = (svgFileContent) => {
const pathDStart = '<path d="'; const pathDStart = '<path d="';
@ -103,8 +130,9 @@ const getPathDIndex = (svgFileContent) => {
/** /**
* Get the index at which the text of the first `<title></title>` tag starts. * Get the index at which the text of the first `<title></title>` tag starts.
* @param svgFileContent The raw SVG as text. * @param {string} svgFileContent The raw SVG as text.
**/ * @returns {number} The index at which the title text starts.
*/
const getTitleTextIndex = (svgFileContent) => { const getTitleTextIndex = (svgFileContent) => {
const titleStart = '<title>'; const titleStart = '<title>';
return svgFileContent.indexOf(titleStart) + titleStart.length; return svgFileContent.indexOf(titleStart) + titleStart.length;
@ -112,8 +140,9 @@ const getTitleTextIndex = (svgFileContent) => {
/** /**
* Convert a hexadecimal number passed as string to decimal number as integer. * Convert a hexadecimal number passed as string to decimal number as integer.
* @param hex The hexadecimal number representation to convert. * @param {string} hex The hexadecimal number representation to convert.
**/ * @returns {number} The decimal number representation.
*/
const hexadecimalToDecimal = (hex) => { const hexadecimalToDecimal = (hex) => {
let result = 0; let result = 0;
let digitValue; let digitValue;
@ -125,6 +154,11 @@ const hexadecimalToDecimal = (hex) => {
return result; return result;
}; };
/**
* Shorten a string with ellipsis if it exceeds 20 characters.
* @param {string} string_ The string to shorten.
* @returns {string} The shortened string.
*/
const maybeShortenedWithEllipsis = (string_) => { const maybeShortenedWithEllipsis = (string_) => {
return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_; return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_;
}; };
@ -132,21 +166,32 @@ const maybeShortenedWithEllipsis = (string_) => {
/** /**
* Memoize a function which accepts a single argument. * Memoize a function which accepts a single argument.
* A second argument can be passed to be used as key. * A second argument can be passed to be used as key.
* @param {(arg0: any) => any} function_ The function to memoize.
* @returns {(arg0: any) => any} The memoized function.
*/ */
const memoize = (function_) => { const memoize = (function_) => {
/** @type {{ [key: string]: any }} */
const results = {}; const results = {};
return (argument, defaultKey = null) => { /**
const key = defaultKey || argument; * Memoized function.
* @param {any} argument The argument to memoize.
* @returns {any} The result of the memoized function.
*/
return (argument) => {
results[argument] ||= function_(argument);
results[key] ||= function_(argument); return results[argument];
return results[key];
}; };
}; };
const getIconPath = memoize(($icon, _filepath) => $icon.find('path').attr('d')); /** @typedef {import('cheerio').Cheerio<import('domhandler').Document>} Cheerio */
/** @type {($icon: Cheerio) => string} */
const getIconPath = memoize(($icon) => $icon.find('path').attr('d'));
/** @type {(iconPath: string) => import('svg-path-segments').Segment[]} */
const getIconPathSegments = memoize((iconPath) => parsePath(iconPath)); const getIconPathSegments = memoize((iconPath) => parsePath(iconPath));
/** @type {(iconPath: string) => import('svg-path-bbox').BBox} */
const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath));
if (updateIgnoreFile) { if (updateIgnoreFile) {
@ -165,21 +210,34 @@ if (updateIgnoreFile) {
}); });
} }
const isIgnored = (linterName, path) => { /**
* Check if an icon is ignored by a linter rule.
* @param {string} linterRule The name of the linter rule.
* @param {string} path SVG path of the icon.
* @returns {boolean} Whether the icon is ignored by the linter rule
*/
const isIgnored = (linterRule, path) => {
return ( return (
iconIgnored[linterName] && Object.hasOwn(iconIgnored[linterName], path) iconIgnored[linterRule] && Object.hasOwn(iconIgnored[linterRule], path)
); );
}; };
const ignoreIcon = (linterName, path, $) => { /**
iconIgnored[linterName] ||= {}; * Ignore an icon for a linter rule.
* @param {string} linterRule The name of the linter rule.
* @param {string} path SVG path of the icon.
* @param {Cheerio} $ The SVG object
*/
const ignoreIcon = (linterRule, path, $) => {
iconIgnored[linterRule] ||= {};
const title = $.find('title').text(); const title = $.find('title').text();
const iconName = htmlFriendlyToTitle(title); const iconName = htmlFriendlyToTitle(title);
iconIgnored[linterName][path] = iconName; iconIgnored[linterRule][path] = iconName;
}; };
/** @type {import('svglint').Config} */
const config = { const config = {
rules: { rules: {
elm: { elm: {
@ -213,6 +271,7 @@ const config = {
}, },
], ],
custom: [ custom: [
// eslint-disable-next-line complexity
(reporter, $, ast) => { (reporter, $, ast) => {
reporter.name = 'icon-title'; reporter.name = 'icon-title';
@ -307,6 +366,8 @@ const config = {
encodedBuf.unshift(iconTitleText[i]); encodedBuf.unshift(iconTitleText[i]);
} else { } else {
// Encode all non ascii characters plus "'&<> (XML named entities) // Encode all non ascii characters plus "'&<> (XML named entities)
/** @type {number} */
// @ts-ignore Coerce to number
const charDecimalCode = iconTitleText.codePointAt(i); const charDecimalCode = iconTitleText.codePointAt(i);
if (charDecimalCode > 127) { if (charDecimalCode > 127) {
@ -337,8 +398,12 @@ const config = {
// Check if there are some other encoded characters in decimal notation // Check if there are some other encoded characters in decimal notation
// which shouldn't be encoded // which shouldn't be encoded
// eslint-disable-next-line unicorn/prefer-number-properties for (const match of encodingMatches.filter((m) => {
for (const match of encodingMatches.filter((m) => !isNaN(m[2]))) { // TODO: this fails using `Number.isNaN`, investigate
// @ts-ignore
// eslint-disable-next-line unicorn/prefer-number-properties
return !isNaN(m[2]);
})) {
const decimalNumber = Number.parseInt(match[2], 10); const decimalNumber = Number.parseInt(match[2], 10);
if (decimalNumber > 127) { if (decimalNumber > 127) {
continue; continue;
@ -378,10 +443,10 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $) => {
reporter.name = 'icon-size'; reporter.name = 'icon-size';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
return; return;
} }
@ -407,16 +472,19 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'icon-precision'; reporter.name = 'icon-precision';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
const segments = getIconPathSegments(iconPath); const segments = getIconPathSegments(iconPath);
for (const segment of segments) { for (const segment of segments) {
/** @type {number[]} */
// @ts-ignore
const numberParameters = segment.params.slice(1);
const precisionMax = Math.max( const precisionMax = Math.max(
// eslint-disable-next-line unicorn/no-array-callback-reference // eslint-disable-next-line unicorn/no-array-callback-reference
...segment.params.slice(1).map(countDecimals), ...numberParameters.map(countDecimals),
); );
if (precisionMax > iconMaxFloatPrecision) { if (precisionMax > iconMaxFloatPrecision) {
let errorMessage = let errorMessage =
@ -439,11 +507,16 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'ineffective-segments'; reporter.name = 'ineffective-segments';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
const segments = getIconPathSegments(iconPath); const segments = getIconPathSegments(iconPath);
/** @type {import('svg-path-segments').Segment[]} */
// TODO: svgpath does not includes the segment property on the interface,
// see https://github.com/fontello/svgpath/pull/67/files
// @ts-ignore
const absSegments = svgpath(iconPath).abs().unshort().segments; const absSegments = svgpath(iconPath).abs().unshort().segments;
const lowerMovementCommands = ['m', 'l']; const lowerMovementCommands = ['m', 'l'];
@ -476,11 +549,16 @@ const config = {
...curveCommands, ...curveCommands,
]); ]);
const isInvalidSegment = ( /**
[command, x1Coord, y1Coord, ...rest], * Check if a segment is ineffective.
index, * @param {import('svg-path-segments').Segment} segment The segment to check.
previousSegmentIsZ, * @param {number} index The index of the segment in the path.
) => { * @param {boolean} previousSegmentIsZ Whether the previous segment is a Z command.
* @returns {boolean} Whether the segment is ineffective.
*/
// eslint-disable-next-line complexity
const isInvalidSegment = (segment, index, previousSegmentIsZ) => {
const [command, x1Coord, y1Coord, ...rest] = segment.params;
if (commands.has(command)) { if (commands.has(command)) {
// Relative directions (h or v) having a length of 0 // Relative directions (h or v) having a length of 0
if (lowerDirectionCommands.includes(command) && x1Coord === 0) { if (lowerDirectionCommands.includes(command) && x1Coord === 0) {
@ -534,6 +612,7 @@ const config = {
let [yPreviousCoordDeep, xPreviousCoordDeep] = [ let [yPreviousCoordDeep, xPreviousCoordDeep] = [
...absSegments[index_], ...absSegments[index_],
].reverse(); ].reverse();
// If the previous command was a horizontal movement, // If the previous command was a horizontal movement,
// we need to consider the single coordinate as x // we need to consider the single coordinate as x
if (upperHorDirectionCommand === xPreviousCoordDeep) { if (upperHorDirectionCommand === xPreviousCoordDeep) {
@ -609,6 +688,8 @@ const config = {
); );
} }
} }
return false;
}; };
for (let index = 0; index < segments.length; index++) { for (let index = 0; index < segments.length; index++) {
@ -616,7 +697,7 @@ const config = {
const previousSegmentIsZ = const previousSegmentIsZ =
index > 0 && segments[index - 1].params[0].toLowerCase() === 'z'; index > 0 && segments[index - 1].params[0].toLowerCase() === 'z';
if (isInvalidSegment(segment.params, index, previousSegmentIsZ)) { if (isInvalidSegment(segment, index, previousSegmentIsZ)) {
const [command, _x1, _y1, ...rest] = segment.params; const [command, _x1, _y1, ...rest] = segment.params;
let errorMessage = `Ineffective segment "${iconPath.slice( let errorMessage = `Ineffective segment "${iconPath.slice(
@ -671,13 +752,15 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'collinear-segments'; reporter.name = 'collinear-segments';
/** /**
* Extracts collinear coordinates from SVG path straight lines * Extracts collinear coordinates from SVG path straight lines
* (does not extracts collinear coordinates from curves). * (does not extracts collinear coordinates from curves).
**/ * @param {string} iconPath The SVG path of the icon.
* @returns {import('svg-path-segments').Segment[]} The collinear segments.
*/
// eslint-disable-next-line complexity
const getCollinearSegments = (iconPath) => { const getCollinearSegments = (iconPath) => {
const segments = getIconPathSegments(iconPath); const segments = getIconPathSegments(iconPath);
const collinearSegments = []; const collinearSegments = [];
@ -694,13 +777,18 @@ const config = {
const seg = segments[s]; const seg = segments[s];
const parms = seg.params; const parms = seg.params;
const cmd = parms[0]; const cmd = parms[0];
const nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; const nextCmd =
s + 1 < segments.length ? segments[s + 1].params[0] : null;
switch (cmd) { switch (cmd) {
// Next switch cases have been ordered by frequency // Next switch cases have been ordered by frequency
// of occurrence in the SVG paths of the icons // of occurrence in the SVG paths of the icons
case 'M': { case 'M': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[2]; currentAbsCoord[1] = parms[2];
// SVG 1.1: // SVG 1.1:
// If a moveto is followed by multiple pairs of coordinates, // If a moveto is followed by multiple pairs of coordinates,
@ -713,7 +801,11 @@ const config = {
} }
case 'm': { case 'm': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
if (seg.chain === undefined || seg.chain.start === seg.start) { if (seg.chain === undefined || seg.chain.start === seg.start) {
startPoint = undefined; startPoint = undefined;
@ -723,33 +815,49 @@ const config = {
} }
case 'H': { case 'H': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
break; break;
} }
case 'h': { case 'h': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
break; break;
} }
case 'V': { case 'V': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[1]; currentAbsCoord[1] = parms[1];
break; break;
} }
case 'v': { case 'v': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1];
break; break;
} }
case 'L': { case 'L': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[2]; currentAbsCoord[1] = parms[2];
break; break;
} }
case 'l': { case 'l': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
} }
@ -763,61 +871,101 @@ const config = {
} }
case 'C': { case 'C': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[5]; currentAbsCoord[0] = parms[5];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[6]; currentAbsCoord[1] = parms[6];
break; break;
} }
case 'c': { case 'c': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6];
break; break;
} }
case 'A': { case 'A': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[6]; currentAbsCoord[0] = parms[6];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[7]; currentAbsCoord[1] = parms[7];
break; break;
} }
case 'a': { case 'a': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7];
break; break;
} }
case 's': { case 's': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
} }
case 'S': { case 'S': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[2]; currentAbsCoord[1] = parms[2];
break; break;
} }
case 't': { case 't': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
} }
case 'T': { case 'T': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[2]; currentAbsCoord[1] = parms[2];
break; break;
} }
case 'Q': { case 'Q': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = parms[3]; currentAbsCoord[0] = parms[3];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = parms[4]; currentAbsCoord[1] = parms[4];
break; break;
} }
case 'q': { case 'q': {
/** @type {number} */
// @ts-ignore
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3]; currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3];
/** @type {number} */
// @ts-ignore
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4];
break; break;
} }
@ -872,7 +1020,7 @@ const config = {
return collinearSegments; return collinearSegments;
}; };
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
const collinearSegments = getCollinearSegments(iconPath); const collinearSegments = getCollinearSegments(iconPath);
if (collinearSegments.length === 0) { if (collinearSegments.length === 0) {
return; return;
@ -913,10 +1061,10 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'negative-zeros'; reporter.name = 'negative-zeros';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
// Find negative zeros inside path // Find negative zeros inside path
const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)]; const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)];
@ -937,10 +1085,10 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $) => {
reporter.name = 'icon-centered'; reporter.name = 'icon-centered';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
return; return;
} }
@ -964,15 +1112,17 @@ const config = {
} }
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'final-closepath'; reporter.name = 'final-closepath';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
const segments = getIconPathSegments(iconPath); const segments = getIconPathSegments(iconPath);
// Unnecessary characters after the final closepath // Unnecessary characters after the final closepath
/** @type {import('svg-path-segments').Segment} */
// @ts-ignore
const lastSegment = segments.at(-1); const lastSegment = segments.at(-1);
const endsWithZ = ['z', 'Z'].includes(lastSegment.params.at(0)); const endsWithZ = ['z', 'Z'].includes(lastSegment.params[0]);
if (endsWithZ && lastSegment.end - lastSegment.start > 1) { if (endsWithZ && lastSegment.end - lastSegment.start > 1) {
const ending = iconPath.slice(lastSegment.start + 1); const ending = iconPath.slice(lastSegment.start + 1);
const closepath = iconPath.at(lastSegment.start); const closepath = iconPath.at(lastSegment.start);
@ -985,10 +1135,10 @@ const config = {
reporter.error(errorMessage); reporter.error(errorMessage);
} }
}, },
(reporter, $, ast, {filepath}) => { (reporter, $, ast) => {
reporter.name = 'path-format'; reporter.name = 'path-format';
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($);
if (!SVG_PATH_REGEX.test(iconPath)) { if (!SVG_PATH_REGEX.test(iconPath)) {
const errorMessage = 'Invalid path format'; const errorMessage = 'Invalid path format';

View File

@ -1,6 +1,10 @@
/**
* @file SVGO configuration for Simple Icons.
*/
/** @type {import("svgo").Config} */
const config = { const config = {
multipass: true, multipass: true,
eol: 'lf',
plugins: [ plugins: [
'cleanupAttrs', 'cleanupAttrs',
'inlineStyles', 'inlineStyles',
@ -72,7 +76,6 @@ const config = {
name: 'sortAttrs', name: 'sortAttrs',
params: { params: {
order: ['role', 'viewBox', 'xmlns'], order: ['role', 'viewBox', 'xmlns'],
xmlnsOrder: 'end',
}, },
}, },
'sortDefsChildren', 'sortDefsChildren',
@ -87,7 +90,6 @@ const config = {
], ],
}, },
}, },
'removeElementsByAttr',
{ {
// Keep the role="img" attribute and automatically add it // Keep the role="img" attribute and automatically add it
// to the <svg> tag if it's not there already // to the <svg> tag if it's not there already

View File

@ -1,3 +1,7 @@
/**
* @file Tests for the documentation.
*/
import {strict as assert} from 'node:assert'; import {strict as assert} from 'node:assert';
import {test} from 'mocha'; import {test} from 'mocha';
import {getThirdPartyExtensions, getThirdPartyLibraries} from '../sdk.mjs'; import {getThirdPartyExtensions, getThirdPartyLibraries} from '../sdk.mjs';

View File

@ -1,3 +1,9 @@
/**
* @file Tests for the index file of npm package.
*/
// The index.mjs file is generated on build before running tests
// @ts-ignore
import * as simpleIcons from '../index.mjs'; import * as simpleIcons from '../index.mjs';
import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs'; import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs';
import {testIcon} from './test-icon.js'; import {testIcon} from './test-icon.js';
@ -5,6 +11,8 @@ import {testIcon} from './test-icon.js';
for (const icon of await getIconsData()) { for (const icon of await getIconsData()) {
const slug = getIconSlug(icon); const slug = getIconSlug(icon);
const variableName = slugToVariableName(slug); const variableName = slugToVariableName(slug);
/** @type {import('../types.d.ts').SimpleIcon} */
// @ts-ignore
const subject = simpleIcons[variableName]; const subject = simpleIcons[variableName];
testIcon(icon, subject, slug); testIcon(icon, subject, slug);

View File

@ -1,8 +1,18 @@
/**
* @file Custom mocha reporter.
*
* Serves to clear the console after the test run is finished.
* See {@link https://github.com/mochajs/mocha/issues/2312}
*/
const {reporters, Runner} = require('mocha'); const {reporters, Runner} = require('mocha');
const {EVENT_RUN_END} = Runner.constants; const {EVENT_RUN_END} = Runner.constants;
class EvenMoreMin extends reporters.Base { class EvenMoreMin extends reporters.Base {
/**
* @param {import('mocha').Runner} runner Mocha test runner
*/
constructor(runner) { constructor(runner) {
super(runner); super(runner);
runner.once(EVENT_RUN_END, () => this.epilogue()); runner.once(EVENT_RUN_END, () => this.epilogue());

View File

@ -1,3 +1,7 @@
/**
* @file Icon tester.
*/
import {strict as assert} from 'node:assert'; import {strict as assert} from 'node:assert';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
@ -14,15 +18,11 @@ const iconsDirectory = path.resolve(
'icons', 'icons',
); );
/**
* @typedef {import('..').SimpleIcon} SimpleIcon
*/
/** /**
* Checks if icon data matches a subject icon. * Checks if icon data matches a subject icon.
* @param {SimpleIcon} icon Icon data * @param {import('../sdk.d.ts').IconData} icon Icon data
* @param {SimpleIcon} subject Icon to check against icon data * @param {import('../types.d.ts').SimpleIcon} subject Icon object to check against icon data
* @param {String} slug Icon data slug * @param {string} slug Icon data slug
*/ */
export const testIcon = (icon, subject, slug) => { export const testIcon = (icon, subject, slug) => {
const svgPath = path.resolve(iconsDirectory, `${slug}.svg`); const svgPath = path.resolve(iconsDirectory, `${slug}.svg`);
@ -62,8 +62,10 @@ export const testIcon = (icon, subject, slug) => {
it(`has ${icon.license ? 'the correct' : 'no'} "license"`, () => { it(`has ${icon.license ? 'the correct' : 'no'} "license"`, () => {
if (icon.license) { if (icon.license) {
assert.equal(subject.license.type, icon.license.type); assert.equal(subject.license?.type, icon.license.type);
if (icon.license.type === 'custom') { if (icon.license.type === 'custom') {
// TODO: `Omit` not working smoothly here
// @ts-ignore
assert.equal(subject.license.url, icon.license.url); assert.equal(subject.license.url, icon.license.url);
} }
} else { } else {

5
types.d.ts vendored
View File

@ -1,6 +1,9 @@
/**
* @file Types for Simple Icons package.
*/
/** /**
* The license for a Simple Icon. * The license for a Simple Icon.
*
* @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data} * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data}
*/ */
export type License = SPDXLicense | CustomLicense; export type License = SPDXLicense | CustomLicense;