mirror of
https://github.com/simple-icons/simple-icons.git
synced 2024-11-26 01:00:27 +02:00
Add types to source code (#10637)
This commit is contained in:
parent
1224e341d7
commit
236f5fc715
@ -16,3 +16,4 @@
|
||||
!sdk.js
|
||||
!sdk.d.ts
|
||||
!.jsonschema.json
|
||||
!jsconfig.json
|
||||
|
@ -2,6 +2,7 @@
|
||||
"prettier": true,
|
||||
"space": 2,
|
||||
"plugins": ["import"],
|
||||
"extends": ["plugin:jsdoc/recommended"],
|
||||
"rules": {
|
||||
"sort-imports": [
|
||||
"error",
|
||||
@ -27,7 +28,8 @@
|
||||
"newlines-between": "never"
|
||||
}
|
||||
],
|
||||
"no-console": ["error", {"allow": ["warn", "error"]}]
|
||||
"no-console": ["error", {"allow": ["warn", "error"]}],
|
||||
"jsdoc/require-file-overview": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
@ -46,6 +48,12 @@
|
||||
"svgo.config.mjs"
|
||||
],
|
||||
"nodeVersion": ">=18"
|
||||
},
|
||||
{
|
||||
"files": ["svglint.config.mjs"],
|
||||
"rules": {
|
||||
"max-depth": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
13
jsconfig.json
Normal file
13
jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"checkJs": true,
|
||||
"skipLibCheck": false,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
@ -86,10 +86,12 @@
|
||||
"devDependencies": {
|
||||
"@inquirer/core": "8.1.0",
|
||||
"@inquirer/prompts": "5.0.2",
|
||||
"@types/node": "20.14.2",
|
||||
"chalk": "5.3.0",
|
||||
"editorconfig-checker": "5.1.5",
|
||||
"esbuild": "0.20.2",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsdoc": "48.2.8",
|
||||
"fake-diff": "1.0.0",
|
||||
"fast-fuzzy": "1.12.0",
|
||||
"get-relative-luminance": "1.0.0",
|
||||
@ -99,8 +101,8 @@
|
||||
"markdown-link-check": "3.12.1",
|
||||
"mocha": "10.4.0",
|
||||
"named-html-entities-json": "1.0.0",
|
||||
"svg-path-bbox": "1.2.6",
|
||||
"svg-path-segments": "2.0.0",
|
||||
"svg-path-bbox": "2.0.0",
|
||||
"svg-path-segments": "2.0.1",
|
||||
"svglint": "2.7.1",
|
||||
"svgo": "3.2.0",
|
||||
"svgpath": "2.6.0",
|
||||
|
@ -1,4 +1,12 @@
|
||||
#!/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 {ExitPromptError} from '@inquirer/core';
|
||||
import {checkbox, confirm, input} from '@inquirer/prompts';
|
||||
@ -15,6 +23,7 @@ import {
|
||||
} from '../sdk.mjs';
|
||||
import {getJsonSchemaData, writeIconsData} from './utils.js';
|
||||
|
||||
/** @type {{icons: import('../sdk.js').IconData[]}} */
|
||||
const iconsData = JSON.parse(await getIconsDataString());
|
||||
const jsonSchema = await getJsonSchemaData();
|
||||
|
||||
@ -25,25 +34,42 @@ const aliasTypes = ['aka', 'old'].map((key) => ({
|
||||
value: key,
|
||||
}));
|
||||
|
||||
/** @type {{name: string, value: string}[]} */
|
||||
const licenseTypes =
|
||||
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 regex = await urlRegex();
|
||||
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) =>
|
||||
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) =>
|
||||
!iconsData.icons.some(
|
||||
(icon) =>
|
||||
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 color = normalizeColor(input);
|
||||
const luminance = HEX_REGEX.test(input)
|
||||
@ -60,7 +86,9 @@ try {
|
||||
title: await input({
|
||||
message: 'What is the title of this icon?',
|
||||
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(
|
||||
await input({
|
||||
@ -111,6 +139,7 @@ try {
|
||||
}).then(async (aliases) => {
|
||||
const result = {};
|
||||
for (const alias of aliases) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
result[alias] = await input({
|
||||
message: `What ${alias} aliases would you like to add? (separate with commas)`,
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Clean files built by the build process.
|
||||
*/
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Simple Icons package build script.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../types.js').License} License
|
||||
* @typedef {import('esbuild').TransformOptions} EsBuildTransformOptions
|
||||
*/
|
||||
|
||||
import {promises as fs} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import util from 'node:util';
|
||||
@ -36,62 +41,78 @@ const iconObjectTemplateFile = path.resolve(
|
||||
'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 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(
|
||||
icons.map(async (icon) => {
|
||||
const filename = getIconSlug(icon);
|
||||
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);
|
||||
// @ts-ignore
|
||||
icon.path = svgToPath(icon.svg);
|
||||
icon.slug = filename;
|
||||
const iconObject = iconToObject(icon);
|
||||
const iconObject = iconToJsObject(icon);
|
||||
const iconExportName = slugToVariableName(icon.slug);
|
||||
return {icon, iconObject, iconExportName};
|
||||
}),
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Script that takes a brand name as argument and outputs the corresponding
|
||||
* icon SVG filename to standard output.
|
||||
*/
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* CLI tool to run jsonschema on the simple-icons.json data file.
|
||||
*/
|
||||
|
||||
|
@ -1,21 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Linters for the package that can't easily be implemented in the existing
|
||||
* 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 fakeDiff from 'fake-diff';
|
||||
import {collator, getIconsDataString, normalizeNewlines} from '../../sdk.mjs';
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
/* 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) {
|
||||
/**
|
||||
* 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) => {
|
||||
if (index > 0) {
|
||||
const previous = array[index - 1];
|
||||
@ -34,6 +52,11 @@ const TESTS = {
|
||||
return invalidEntries;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format an icon for display in the error message
|
||||
* @param {IconData} icon Icon to format
|
||||
* @returns {string} Formatted icon
|
||||
*/
|
||||
const format = (icon) => {
|
||||
if (icon.slug) {
|
||||
return `${icon.title} (${icon.slug})`;
|
||||
@ -63,6 +86,11 @@ const TESTS = {
|
||||
|
||||
/* Check redundant trailing slash in URL */
|
||||
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 {origin} = new global.URL(url);
|
||||
return /^\/+$/.test(url.replace(origin, ''));
|
||||
@ -71,7 +99,17 @@ const TESTS = {
|
||||
const allUrlFields = [
|
||||
...new Set(
|
||||
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),
|
||||
),
|
||||
];
|
||||
@ -88,11 +126,13 @@ const TESTS = {
|
||||
},
|
||||
};
|
||||
|
||||
const dataString = await getIconsDataString();
|
||||
const data = JSON.parse(dataString);
|
||||
const iconsDataString = await getIconsDataString();
|
||||
const iconsData = JSON.parse(iconsDataString);
|
||||
|
||||
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
|
||||
.filter(Boolean);
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Rewrite some Markdown files.
|
||||
*/
|
||||
|
||||
@ -17,6 +17,10 @@ const rootDirectory = path.resolve(__dirname, '..', '..');
|
||||
const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md');
|
||||
|
||||
/**
|
||||
* Reformat a file.
|
||||
* @param {string} filePath Path to the file
|
||||
*/
|
||||
const reformat = async (filePath) => {
|
||||
const fileContent = await readFile(filePath, 'utf8');
|
||||
await writeFile(
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* 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.
|
||||
*/
|
||||
@ -16,16 +16,27 @@ const rootDirectory = path.resolve(__dirname, '..', '..');
|
||||
const packageJsonFile = path.resolve(rootDirectory, 'package.json');
|
||||
const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
|
||||
/**
|
||||
* @param {string} semVersion A semantic version string.
|
||||
* @returns {number} The major version number.
|
||||
*/
|
||||
const getMajorVersion = (semVersion) => {
|
||||
const majorVersionAsString = semVersion.split('.')[0];
|
||||
return Number.parseInt(majorVersionAsString, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the package.json manifest.
|
||||
* @returns {Promise<{version: string}>} The package.json manifest.
|
||||
*/
|
||||
const getManifest = async () => {
|
||||
const manifestRaw = await fs.readFile(packageJsonFile, 'utf8');
|
||||
return JSON.parse(manifestRaw);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} majorVersion The major version number.
|
||||
*/
|
||||
const updateVersionInReadmeIfNecessary = async (majorVersion) => {
|
||||
let content = await fs.readFile(readmeFile, 'utf8');
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Updates the SDK Typescript definitions located in the file sdk.d.ts
|
||||
* to match the current definitions of functions of sdk.mjs.
|
||||
*/
|
||||
@ -34,19 +34,53 @@ const generateSdkMts = async () => {
|
||||
'npx tsc sdk.mjs' +
|
||||
' --declaration --emitDeclarationOnly --allowJs --removeComments',
|
||||
);
|
||||
} catch (error) {
|
||||
} catch (/** @type {unknown} */ error) {
|
||||
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(
|
||||
`Error ${error.status} generating Typescript` +
|
||||
` definitions: '${error.message}'` +
|
||||
'\n',
|
||||
`Error generating Typescript definitions: '${errorMessage}'\n`,
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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 fileExists = await fs
|
||||
.access(sdkMts)
|
||||
@ -62,15 +96,21 @@ const generateSdkTs = async () => {
|
||||
(await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] +
|
||||
`${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`;
|
||||
|
||||
await fs.writeFile(sdkTs, newSdkTsContent);
|
||||
await fs.writeFile(sdkTs, removeDuplicatedExportTypes(newSdkTsContent));
|
||||
await fs.unlink(sdkMts);
|
||||
|
||||
try {
|
||||
execSync('npx prettier -w sdk.d.ts');
|
||||
} catch (error) {
|
||||
let errorMessage = error;
|
||||
if (error instanceof Error) {
|
||||
// The `execSync` function throws a generic Node.js Error
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`Error ${error.status} executing Prettier` +
|
||||
` to prettify SDK TS definitions: '${error.message}'` +
|
||||
'Error executing Prettier to prettify' +
|
||||
` SDK TS definitions: '${errorMessage}'` +
|
||||
'\n',
|
||||
);
|
||||
process.exit(1);
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Generates a MarkDown file that lists every brand name and their slug.
|
||||
*/
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located
|
||||
* at README every time the number of current icons is more than `updateRange`
|
||||
* more than the previous milestone.
|
||||
@ -20,22 +20,32 @@ const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
|
||||
const readmeContent = await fs.readFile(readmeFile, 'utf8');
|
||||
|
||||
let overNIconsInReadme;
|
||||
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) {
|
||||
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,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
@ -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 path from 'node:path';
|
||||
import {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs';
|
||||
|
||||
const __dirname = getDirnameFromImportMeta(import.meta.url);
|
||||
|
||||
/**
|
||||
* @typedef {import("../sdk.js").IconData} IconData
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
rootDirectory = path.resolve(__dirname, '..'),
|
||||
@ -18,8 +30,8 @@ export const getJsonSchemaData = async (
|
||||
|
||||
/**
|
||||
* Write icons data to _data/simple-icons.json.
|
||||
* @param {Object} iconsData Icons data object.
|
||||
* @param {String} rootDirectory Path to the root directory of the project.
|
||||
* @param {{icons: IconData[]}} iconsData Icons data object.
|
||||
* @param {string} rootDirectory Path to the root directory of the project.
|
||||
*/
|
||||
export const writeIconsData = async (
|
||||
iconsData,
|
||||
|
9
sdk.d.ts
vendored
9
sdk.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Types for Simple Icons SDK.
|
||||
*/
|
||||
|
||||
@ -10,7 +10,6 @@ import type {CustomLicense, SPDXLicense} from './types';
|
||||
*
|
||||
* Includes the module and author of the extension,
|
||||
* both including a name and URL.
|
||||
*
|
||||
* @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions}
|
||||
*/
|
||||
export type ThirdPartyExtension = {
|
||||
@ -27,7 +26,6 @@ type ThirdPartyExtensionSubject = {
|
||||
* The aliases for a Simple Icon.
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
export type Aliases = {
|
||||
@ -47,7 +45,6 @@ type DuplicateAlias = {
|
||||
* The data for a Simple Icon.
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
export type IconData = {
|
||||
@ -73,8 +70,8 @@ export function slugToVariableName(slug: string): string;
|
||||
export function titleToHtmlFriendly(brandTitle: string): string;
|
||||
export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string;
|
||||
export function getIconDataPath(rootDirectory?: string): string;
|
||||
export function getIconsDataString(rootDirectory?: string): string;
|
||||
export function getIconsData(rootDirectory?: string): IconData[];
|
||||
export function getIconsDataString(rootDirectory?: string): Promise<string>;
|
||||
export function getIconsData(rootDirectory?: string): Promise<IconData[]>;
|
||||
export function normalizeNewlines(text: string): string;
|
||||
export function normalizeColor(text: string): string;
|
||||
export function getThirdPartyExtensions(
|
||||
|
133
sdk.mjs
133
sdk.mjs
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* @file
|
||||
* Simple Icons SDK.
|
||||
*/
|
||||
|
||||
@ -8,10 +8,11 @@ import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
/**
|
||||
* @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension
|
||||
* @typedef {import("./sdk").IconData} IconData
|
||||
* @typedef {import("./sdk.d.ts").ThirdPartyExtension} ThirdPartyExtension
|
||||
* @typedef {import("./sdk.d.ts").IconData} IconData
|
||||
*/
|
||||
|
||||
/** @type {{ [key: string]: string }} */
|
||||
const TITLE_TO_SLUG_REPLACEMENTS = {
|
||||
'+': 'plus',
|
||||
'.': '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`,
|
||||
* equivalent to the `__dirname` global variable in CommonJS.
|
||||
* @param {String} importMetaUrl import.meta.url
|
||||
* @returns {String} Directory name in which this file is located
|
||||
* @param {string} importMetaUrl import.meta.url
|
||||
* @returns {string} Directory name in which this file is located
|
||||
*/
|
||||
export const getDirnameFromImportMeta = (importMetaUrl) =>
|
||||
path.dirname(fileURLToPath(importMetaUrl));
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const urlRegex = async (
|
||||
@ -68,21 +69,21 @@ export const urlRegex = async (
|
||||
/**
|
||||
* Get the slug/filename for an icon.
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Extract the path from an icon SVG content.
|
||||
* @param {String} svg The icon SVG content
|
||||
* @returns {String} The path from the icon SVG content
|
||||
**/
|
||||
* @param {string} svg The icon SVG content
|
||||
* @returns {string} The path from the icon SVG content
|
||||
*/
|
||||
export const svgToPath = (svg) => svg.split('"', 8)[7];
|
||||
|
||||
/**
|
||||
* Converts a brand title into a slug/filename.
|
||||
* @param {String} title The title to convert
|
||||
* @returns {String} The slug/filename for the title
|
||||
* @param {string} title The title to convert
|
||||
* @returns {string} The slug/filename for the title
|
||||
*/
|
||||
export const titleToSlug = (title) =>
|
||||
title
|
||||
@ -96,8 +97,8 @@ export const titleToSlug = (title) =>
|
||||
|
||||
/**
|
||||
* Converts a slug into a variable name that can be exported.
|
||||
* @param {String} slug The slug to convert
|
||||
* @returns {String} The variable name for the slug
|
||||
* @param {string} slug The slug to convert
|
||||
* @returns {string} The variable name for the slug
|
||||
*/
|
||||
export const slugToVariableName = (slug) => {
|
||||
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
|
||||
* title in HTML/SVG friendly format.
|
||||
* @param {String} brandTitle The title to convert
|
||||
* @returns {String} The brand title in HTML/SVG friendly format
|
||||
* @param {string} brandTitle The title to convert
|
||||
* @returns {string} The brand title in HTML/SVG friendly format
|
||||
*/
|
||||
export const titleToHtmlFriendly = (brandTitle) =>
|
||||
brandTitle
|
||||
@ -117,6 +118,8 @@ export const titleToHtmlFriendly = (brandTitle) =>
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll(/./g, (char) => {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
const charCode = char.codePointAt(0);
|
||||
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
|
||||
* it is seen in *_data/simple-icons.json*)
|
||||
* @param {String} htmlFriendlyTitle The title to convert
|
||||
* @returns {String} The brand title in HTML/SVG friendly format
|
||||
* @param {string} htmlFriendlyTitle The title to convert
|
||||
* @returns {string} The brand title in HTML/SVG friendly format
|
||||
*/
|
||||
export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
|
||||
htmlFriendlyTitle
|
||||
@ -134,13 +137,18 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
|
||||
)
|
||||
.replaceAll(
|
||||
/&(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],
|
||||
);
|
||||
|
||||
/**
|
||||
* Get path of *_data/simple-icons.json*.
|
||||
* @param {String} rootDirectory Path to the root directory of the project
|
||||
* @returns {String} Path of *_data/simple-icons.json*
|
||||
* @param {string} rootDirectory Path to the root directory of the project
|
||||
* @returns {string} Path of *_data/simple-icons.json*
|
||||
*/
|
||||
export const getIconDataPath = (
|
||||
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
||||
@ -150,8 +158,8 @@ export const getIconDataPath = (
|
||||
|
||||
/**
|
||||
* Get contents of *_data/simple-icons.json*.
|
||||
* @param {String} rootDirectory Path to the root directory of the project
|
||||
* @returns {String} Content of *_data/simple-icons.json*
|
||||
* @param {string} rootDirectory Path to the root directory of the project
|
||||
* @returns {Promise<string>} Content of *_data/simple-icons.json*
|
||||
*/
|
||||
export const getIconsDataString = (
|
||||
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
||||
@ -161,8 +169,8 @@ export const getIconsDataString = (
|
||||
|
||||
/**
|
||||
* Get icons data as object from *_data/simple-icons.json*.
|
||||
* @param {String} rootDirectory Path to the root directory of the project
|
||||
* @returns {IconData[]} Icons data as array from *_data/simple-icons.json*
|
||||
* @param {string} rootDirectory Path to the root directory of the project
|
||||
* @returns {Promise<IconData[]>} Icons data as array from *_data/simple-icons.json*
|
||||
*/
|
||||
export const getIconsData = async (
|
||||
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
||||
@ -173,8 +181,8 @@ export const getIconsData = async (
|
||||
|
||||
/**
|
||||
* Replace Windows newline characters by Unix ones.
|
||||
* @param {String} text The text to replace
|
||||
* @returns {String} The text with Windows newline characters replaced by Unix ones
|
||||
* @param {string} text The text to replace
|
||||
* @returns {string} The text with Windows newline characters replaced by Unix ones
|
||||
*/
|
||||
export const normalizeNewlines = (text) => {
|
||||
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.
|
||||
* @param {String} text The color text
|
||||
* @returns {String} The color text in 6-digit hex format
|
||||
* @param {string} text The color text
|
||||
* @returns {string} The color text in 6-digit hex format
|
||||
*/
|
||||
export const normalizeColor = (text) => {
|
||||
let color = text.replace('#', '').toUpperCase();
|
||||
@ -199,7 +207,7 @@ export const normalizeColor = (text) => {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const getThirdPartyExtensions = async (
|
||||
@ -214,23 +222,43 @@ export const getThirdPartyExtensions = async (
|
||||
.split('|\n|')
|
||||
.slice(2)
|
||||
.map((line) => {
|
||||
let [module, author] = line.split(' | ');
|
||||
module = module.split('<img src="')[0];
|
||||
const [module_, author] = line.split(' | ');
|
||||
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 {
|
||||
module: {
|
||||
name: /\[(.+)]/.exec(module)[1],
|
||||
url: /\((.+)\)/.exec(module)[1],
|
||||
name: moduleName,
|
||||
url: moduleUrl,
|
||||
},
|
||||
author: {
|
||||
name: /\[(.+)]/.exec(author)[1],
|
||||
url: /\((.+)\)/.exec(author)[1],
|
||||
name: authorName,
|
||||
url: authorUrl,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const getThirdPartyLibraries = async (
|
||||
@ -247,23 +275,42 @@ export const getThirdPartyLibraries = async (
|
||||
.map((line) => {
|
||||
let [module, author] = line.split(' | ');
|
||||
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 {
|
||||
module: {
|
||||
name: /\[(.+)]/.exec(module)[1],
|
||||
url: /\((.+)\)/.exec(module)[1],
|
||||
name: moduleName,
|
||||
url: moduleUrl,
|
||||
},
|
||||
author: {
|
||||
name: /\[(.+)]/.exec(author)[1],
|
||||
url: /\((.+)\)/.exec(author)[1],
|
||||
name: authorName,
|
||||
url: authorUrl,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* `Intl.Collator` object ready to be used for icon titles sorting.
|
||||
* @type {Intl.Collator}
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
|
||||
**/
|
||||
* @see {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
|
||||
*/
|
||||
export const collator = new Intl.Collator('en', {
|
||||
usage: 'search',
|
||||
caseFirst: 'upper',
|
||||
|
@ -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 path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import svgPathBbox from 'svg-path-bbox';
|
||||
import {svgPathBbox} from 'svg-path-bbox';
|
||||
import parsePath from 'svg-path-segments';
|
||||
import svgpath from 'svgpath';
|
||||
import {
|
||||
@ -26,6 +30,7 @@ const icons = await getIconsData();
|
||||
const htmlNamedEntities = JSON.parse(
|
||||
await fs.readFile(htmlNamedEntitiesFile, 'utf8'),
|
||||
);
|
||||
/** @type {{ [key: string]: { [key: string]: string } }} */
|
||||
const svglintIgnores = JSON.parse(
|
||||
await fs.readFile(svglintIgnoredFile, 'utf8'),
|
||||
);
|
||||
@ -45,6 +50,10 @@ const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true';
|
||||
const ignoreFile = './.svglint-ignored.json';
|
||||
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) => {
|
||||
return Object.fromEntries(
|
||||
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) => {
|
||||
return Object.fromEntries(
|
||||
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'
|
||||
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
|
||||
* 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
|
||||
const collinear = (x1, y1, x2, y2, x3, y3) => {
|
||||
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.
|
||||
* @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_) => {
|
||||
if (number_ && number_ % 1) {
|
||||
@ -94,7 +120,8 @@ const countDecimals = (number_) => {
|
||||
|
||||
/**
|
||||
* 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 pathDStart = '<path d="';
|
||||
@ -103,8 +130,9 @@ const getPathDIndex = (svgFileContent) => {
|
||||
|
||||
/**
|
||||
* 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 titleStart = '<title>';
|
||||
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.
|
||||
* @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) => {
|
||||
let result = 0;
|
||||
let digitValue;
|
||||
@ -125,6 +154,11 @@ const hexadecimalToDecimal = (hex) => {
|
||||
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_) => {
|
||||
return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_;
|
||||
};
|
||||
@ -132,21 +166,32 @@ const maybeShortenedWithEllipsis = (string_) => {
|
||||
/**
|
||||
* Memoize a function which accepts a single argument.
|
||||
* 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_) => {
|
||||
/** @type {{ [key: string]: any }} */
|
||||
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[key];
|
||||
return results[argument];
|
||||
};
|
||||
};
|
||||
|
||||
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));
|
||||
/** @type {(iconPath: string) => import('svg-path-bbox').BBox} */
|
||||
const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath));
|
||||
|
||||
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 (
|
||||
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 iconName = htmlFriendlyToTitle(title);
|
||||
|
||||
iconIgnored[linterName][path] = iconName;
|
||||
iconIgnored[linterRule][path] = iconName;
|
||||
};
|
||||
|
||||
/** @type {import('svglint').Config} */
|
||||
const config = {
|
||||
rules: {
|
||||
elm: {
|
||||
@ -213,6 +271,7 @@ const config = {
|
||||
},
|
||||
],
|
||||
custom: [
|
||||
// eslint-disable-next-line complexity
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'icon-title';
|
||||
|
||||
@ -307,6 +366,8 @@ const config = {
|
||||
encodedBuf.unshift(iconTitleText[i]);
|
||||
} else {
|
||||
// Encode all non ascii characters plus "'&<> (XML named entities)
|
||||
/** @type {number} */
|
||||
// @ts-ignore Coerce to number
|
||||
const charDecimalCode = iconTitleText.codePointAt(i);
|
||||
|
||||
if (charDecimalCode > 127) {
|
||||
@ -337,8 +398,12 @@ const config = {
|
||||
|
||||
// Check if there are some other encoded characters in decimal notation
|
||||
// which shouldn't be encoded
|
||||
// eslint-disable-next-line unicorn/prefer-number-properties
|
||||
for (const match of encodingMatches.filter((m) => !isNaN(m[2]))) {
|
||||
for (const match of encodingMatches.filter((m) => {
|
||||
// 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);
|
||||
if (decimalNumber > 127) {
|
||||
continue;
|
||||
@ -378,10 +443,10 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $) => {
|
||||
reporter.name = 'icon-size';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
|
||||
return;
|
||||
}
|
||||
@ -407,16 +472,19 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'icon-precision';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
const segments = getIconPathSegments(iconPath);
|
||||
|
||||
for (const segment of segments) {
|
||||
/** @type {number[]} */
|
||||
// @ts-ignore
|
||||
const numberParameters = segment.params.slice(1);
|
||||
const precisionMax = Math.max(
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
...segment.params.slice(1).map(countDecimals),
|
||||
...numberParameters.map(countDecimals),
|
||||
);
|
||||
if (precisionMax > iconMaxFloatPrecision) {
|
||||
let errorMessage =
|
||||
@ -439,11 +507,16 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'ineffective-segments';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
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 lowerMovementCommands = ['m', 'l'];
|
||||
@ -476,11 +549,16 @@ const config = {
|
||||
...curveCommands,
|
||||
]);
|
||||
|
||||
const isInvalidSegment = (
|
||||
[command, x1Coord, y1Coord, ...rest],
|
||||
index,
|
||||
previousSegmentIsZ,
|
||||
) => {
|
||||
/**
|
||||
* Check if a segment is ineffective.
|
||||
* @param {import('svg-path-segments').Segment} segment The segment to check.
|
||||
* @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)) {
|
||||
// Relative directions (h or v) having a length of 0
|
||||
if (lowerDirectionCommands.includes(command) && x1Coord === 0) {
|
||||
@ -534,6 +612,7 @@ const config = {
|
||||
let [yPreviousCoordDeep, xPreviousCoordDeep] = [
|
||||
...absSegments[index_],
|
||||
].reverse();
|
||||
|
||||
// If the previous command was a horizontal movement,
|
||||
// we need to consider the single coordinate as x
|
||||
if (upperHorDirectionCommand === xPreviousCoordDeep) {
|
||||
@ -609,6 +688,8 @@ const config = {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let index = 0; index < segments.length; index++) {
|
||||
@ -616,7 +697,7 @@ const config = {
|
||||
const previousSegmentIsZ =
|
||||
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;
|
||||
|
||||
let errorMessage = `Ineffective segment "${iconPath.slice(
|
||||
@ -671,13 +752,15 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'collinear-segments';
|
||||
|
||||
/**
|
||||
* 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 segments = getIconPathSegments(iconPath);
|
||||
const collinearSegments = [];
|
||||
@ -694,13 +777,18 @@ const config = {
|
||||
const seg = segments[s];
|
||||
const parms = seg.params;
|
||||
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) {
|
||||
// Next switch cases have been ordered by frequency
|
||||
// of occurrence in the SVG paths of the icons
|
||||
case 'M': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[2];
|
||||
// SVG 1.1:
|
||||
// If a moveto is followed by multiple pairs of coordinates,
|
||||
@ -713,7 +801,11 @@ const config = {
|
||||
}
|
||||
|
||||
case 'm': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
|
||||
if (seg.chain === undefined || seg.chain.start === seg.start) {
|
||||
startPoint = undefined;
|
||||
@ -723,33 +815,49 @@ const config = {
|
||||
}
|
||||
|
||||
case 'H': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[1];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'h': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'V': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[1];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'v': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'L': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'l': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
|
||||
break;
|
||||
}
|
||||
@ -763,61 +871,101 @@ const config = {
|
||||
}
|
||||
|
||||
case 'C': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[5];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[6];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'c': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'A': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[6];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[7];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'a': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7];
|
||||
break;
|
||||
}
|
||||
|
||||
case 's': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'S': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case 't': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'T': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[1];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Q': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = parms[3];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = parms[4];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'q': {
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3];
|
||||
/** @type {number} */
|
||||
// @ts-ignore
|
||||
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4];
|
||||
break;
|
||||
}
|
||||
@ -872,7 +1020,7 @@ const config = {
|
||||
return collinearSegments;
|
||||
};
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
const collinearSegments = getCollinearSegments(iconPath);
|
||||
if (collinearSegments.length === 0) {
|
||||
return;
|
||||
@ -913,10 +1061,10 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'negative-zeros';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
|
||||
// Find negative zeros inside path
|
||||
const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)];
|
||||
@ -937,10 +1085,10 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $) => {
|
||||
reporter.name = 'icon-centered';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) {
|
||||
return;
|
||||
}
|
||||
@ -964,15 +1112,17 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'final-closepath';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
const segments = getIconPathSegments(iconPath);
|
||||
|
||||
// Unnecessary characters after the final closepath
|
||||
/** @type {import('svg-path-segments').Segment} */
|
||||
// @ts-ignore
|
||||
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) {
|
||||
const ending = iconPath.slice(lastSegment.start + 1);
|
||||
const closepath = iconPath.at(lastSegment.start);
|
||||
@ -985,10 +1135,10 @@ const config = {
|
||||
reporter.error(errorMessage);
|
||||
}
|
||||
},
|
||||
(reporter, $, ast, {filepath}) => {
|
||||
(reporter, $, ast) => {
|
||||
reporter.name = 'path-format';
|
||||
|
||||
const iconPath = getIconPath($, filepath);
|
||||
const iconPath = getIconPath($);
|
||||
|
||||
if (!SVG_PATH_REGEX.test(iconPath)) {
|
||||
const errorMessage = 'Invalid path format';
|
||||
|
@ -1,6 +1,10 @@
|
||||
/**
|
||||
* @file SVGO configuration for Simple Icons.
|
||||
*/
|
||||
|
||||
/** @type {import("svgo").Config} */
|
||||
const config = {
|
||||
multipass: true,
|
||||
eol: 'lf',
|
||||
plugins: [
|
||||
'cleanupAttrs',
|
||||
'inlineStyles',
|
||||
@ -72,7 +76,6 @@ const config = {
|
||||
name: 'sortAttrs',
|
||||
params: {
|
||||
order: ['role', 'viewBox', 'xmlns'],
|
||||
xmlnsOrder: 'end',
|
||||
},
|
||||
},
|
||||
'sortDefsChildren',
|
||||
@ -87,7 +90,6 @@ const config = {
|
||||
],
|
||||
},
|
||||
},
|
||||
'removeElementsByAttr',
|
||||
{
|
||||
// Keep the role="img" attribute and automatically add it
|
||||
// to the <svg> tag if it's not there already
|
||||
|
@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file Tests for the documentation.
|
||||
*/
|
||||
|
||||
import {strict as assert} from 'node:assert';
|
||||
import {test} from 'mocha';
|
||||
import {getThirdPartyExtensions, getThirdPartyLibraries} from '../sdk.mjs';
|
||||
|
@ -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 {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs';
|
||||
import {testIcon} from './test-icon.js';
|
||||
@ -5,6 +11,8 @@ import {testIcon} from './test-icon.js';
|
||||
for (const icon of await getIconsData()) {
|
||||
const slug = getIconSlug(icon);
|
||||
const variableName = slugToVariableName(slug);
|
||||
/** @type {import('../types.d.ts').SimpleIcon} */
|
||||
// @ts-ignore
|
||||
const subject = simpleIcons[variableName];
|
||||
|
||||
testIcon(icon, subject, slug);
|
||||
|
@ -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 {EVENT_RUN_END} = Runner.constants;
|
||||
|
||||
class EvenMoreMin extends reporters.Base {
|
||||
/**
|
||||
* @param {import('mocha').Runner} runner Mocha test runner
|
||||
*/
|
||||
constructor(runner) {
|
||||
super(runner);
|
||||
runner.once(EVENT_RUN_END, () => this.epilogue());
|
||||
|
@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file Icon tester.
|
||||
*/
|
||||
|
||||
import {strict as assert} from 'node:assert';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
@ -14,15 +18,11 @@ const iconsDirectory = path.resolve(
|
||||
'icons',
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef {import('..').SimpleIcon} SimpleIcon
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if icon data matches a subject icon.
|
||||
* @param {SimpleIcon} icon Icon data
|
||||
* @param {SimpleIcon} subject Icon to check against icon data
|
||||
* @param {String} slug Icon data slug
|
||||
* @param {import('../sdk.d.ts').IconData} icon Icon data
|
||||
* @param {import('../types.d.ts').SimpleIcon} subject Icon object to check against icon data
|
||||
* @param {string} slug Icon data slug
|
||||
*/
|
||||
export const testIcon = (icon, subject, slug) => {
|
||||
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"`, () => {
|
||||
if (icon.license) {
|
||||
assert.equal(subject.license.type, icon.license.type);
|
||||
assert.equal(subject.license?.type, icon.license.type);
|
||||
if (icon.license.type === 'custom') {
|
||||
// TODO: `Omit` not working smoothly here
|
||||
// @ts-ignore
|
||||
assert.equal(subject.license.url, icon.license.url);
|
||||
}
|
||||
} else {
|
||||
|
5
types.d.ts
vendored
5
types.d.ts
vendored
@ -1,6 +1,9 @@
|
||||
/**
|
||||
* @file Types for Simple Icons package.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The license for a Simple Icon.
|
||||
*
|
||||
* @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data}
|
||||
*/
|
||||
export type License = SPDXLicense | CustomLicense;
|
||||
|
Loading…
Reference in New Issue
Block a user