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:
parent
1224e341d7
commit
236f5fc715
@ -16,3 +16,4 @@
|
|||||||
!sdk.js
|
!sdk.js
|
||||||
!sdk.d.ts
|
!sdk.d.ts
|
||||||
!.jsonschema.json
|
!.jsonschema.json
|
||||||
|
!jsconfig.json
|
||||||
|
@ -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
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": {
|
"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",
|
||||||
|
@ -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)`,
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -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};
|
||||||
}),
|
}),
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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
9
sdk.d.ts
vendored
@ -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
133
sdk.mjs
@ -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('<', '<')
|
.replaceAll('<', '<')
|
||||||
.replaceAll('>', '>')
|
.replaceAll('>', '>')
|
||||||
.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',
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
@ -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
5
types.d.ts
vendored
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user