Add XO linter (#10643)

This commit is contained in:
LitoMore 2024-03-25 01:38:18 +08:00 committed by GitHub
parent d66bdb1380
commit bf69b6dee0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 633 additions and 493 deletions

View File

@ -54,13 +54,13 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm i --ignore-scripts --no-audit --no-fund run: npm i --ignore-scripts --no-audit --no-fund
- name: Update major version in CDN URLs - name: Update major version in CDN URLs
run: node ./scripts/release/update-cdn-urls.js run: ./scripts/release/update-cdn-urls.js
- name: Update SVGs count milestone - name: Update SVGs count milestone
run: node ./scripts/release/update-svgs-count.js run: ./scripts/release/update-svgs-count.js
- name: Update slugs table - name: Update slugs table
run: node ./scripts/release/update-slugs-table.js run: ./scripts/release/update-slugs-table.js
- name: Update SDK Typescript definitions - name: Update SDK Typescript definitions
run: node ./scripts/release/update-sdk-ts-defs.js run: ./scripts/release/update-sdk-ts-defs.js
- name: Commit version bump - name: Commit version bump
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v5
with: with:

View File

@ -43,9 +43,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm i --ignore-scripts --no-audit --no-fund run: npm i --ignore-scripts --no-audit --no-fund
- name: Reformat to regular markdown - name: Reformat to regular markdown
run: node ./scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}" run: ./scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}"
- name: Update SDK Typescript definitions - name: Update SDK Typescript definitions
run: node ./scripts/release/update-sdk-ts-defs.js run: ./scripts/release/update-sdk-ts-defs.js
- name: Build NodeJS package - name: Build NodeJS package
run: npm run build run: npm run build
- name: Deploy to NPM - name: Deploy to NPM
@ -65,7 +65,7 @@ jobs:
- id: get-version - id: get-version
uses: ./.github/actions/get-version uses: ./.github/actions/get-version
- name: Reformat to regular markdown - name: Reformat to regular markdown
run: node ./scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}" run: ./scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}"
- name: Configure GIT credentials - name: Configure GIT credentials
run: | run: |
git config user.name "${GITHUB_ACTOR}" git config user.name "${GITHUB_ACTOR}"

View File

@ -6,12 +6,3 @@
# We use our own formatting for the data files. # We use our own formatting for the data files.
_data/simple-icons.json _data/simple-icons.json
# JavaScript templates are invalid JavaScript so cannot be formatted.
scripts/build/templates/*.js
# Generated JavaScript files don't need to be formatted
index.js
index.mjs
index.d.ts
sdk.js

View File

@ -1,3 +1,4 @@
{ {
"singleQuote": true "singleQuote": true,
"bracketSpacing": false
} }

49
.xo-config.json Normal file
View File

@ -0,0 +1,49 @@
{
"prettier": true,
"space": 2,
"plugins": ["import"],
"rules": {
"n/no-unsupported-features": "off",
"n/no-unsupported-features/node-builtins": "off",
"n/file-extension-in-import": "off",
"sort-imports": [
"error",
{
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
"allowSeparatedGroups": false
}
],
"import/no-named-as-default": "off",
"import/extensions": "off",
"import/order": [
"error",
{
"groups": ["builtin", "external", "parent", "sibling", "index"],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"warnOnUnassignedImports": true,
"newlines-between": "never"
}
]
},
"overrides": [
{
"files": ["sdk.mjs", "sdk.d.ts"],
"nodeVersion": ">=14"
},
{
"files": [
"scripts/**/*",
"tests/**/*",
"svglint.config.mjs",
"svgo.config.mjs"
],
"nodeVersion": ">=18"
}
]
}

View File

@ -88,6 +88,7 @@
"chalk": "5.3.0", "chalk": "5.3.0",
"editorconfig-checker": "5.1.5", "editorconfig-checker": "5.1.5",
"esbuild": "0.19.4", "esbuild": "0.19.4",
"eslint-plugin-import": "2.29.1",
"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",
@ -97,23 +98,24 @@
"markdown-link-check": "3.11.2", "markdown-link-check": "3.11.2",
"mocha": "10.2.0", "mocha": "10.2.0",
"named-html-entities-json": "1.0.0", "named-html-entities-json": "1.0.0",
"prettier": "3.0.3",
"svg-path-bbox": "1.2.5", "svg-path-bbox": "1.2.5",
"svg-path-segments": "1.0.0", "svg-path-segments": "1.0.0",
"svglint": "2.4.0", "svglint": "2.4.0",
"svgo": "3.0.2", "svgo": "3.0.2",
"svgpath": "2.6.0", "svgpath": "2.6.0",
"typescript": "5.2.2" "typescript": "5.2.2",
"xo": "0.58.0"
}, },
"scripts": { "scripts": {
"build": "node scripts/build/package.js", "build": "./scripts/build/package.js",
"clean": "node scripts/build/clean.js", "clean": "./scripts/build/clean.js",
"format": "prettier --cache --write .", "format": "prettier --cache --write --ignore-unknown '**/*.!(js|jsx|mjs|cjs|ts|tsx|mts|cts|svg)' && xo --fix",
"lint": "npm run ourlint && npm run jslint && npm run jsonlint && npm run svglint && npm run wslint", "lint": "npm run ourlint && npm run prettierlint && npm run jslint && npm run jsonlint && npm run svglint && npm run wslint",
"ourlint": "node scripts/lint/ourlint.js", "ourlint": "./scripts/lint/ourlint.js",
"jslint": "prettier --cache --check .", "prettierlint": "prettier --cache --check --ignore-unknown '**/*.!(js|jsx|mjs|cjs|ts|tsx|mts|cts|svg)'",
"jsonlint": "node scripts/lint/jsonlint.js", "jslint": "xo",
"svglint": "svglint --ci $npm_config_icons", "jsonlint": "./scripts/lint/jsonlint.js",
"svglint": "svglint --ci $npm_config_icons --config svglint.config.mjs",
"wslint": "editorconfig-checker", "wslint": "editorconfig-checker",
"prepare": "husky", "prepare": "husky",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
@ -121,8 +123,8 @@
"test": "mocha tests --reporter tests/min-reporter.cjs --inline-diffs", "test": "mocha tests --reporter tests/min-reporter.cjs --inline-diffs",
"pretest": "npm run prepublishOnly", "pretest": "npm run prepublishOnly",
"posttest": "npm run postpublish", "posttest": "npm run postpublish",
"get-filename": "node scripts/get-filename.js", "get-filename": "./scripts/get-filename.js",
"add-icon-data": "node scripts/add-icon-data.js" "add-icon-data": "./scripts/add-icon-data.js"
}, },
"engines": { "engines": {
"node": ">=0.12.18" "node": ">=0.12.18"

23
scripts/add-icon-data.js Normal file → Executable file
View File

@ -1,22 +1,23 @@
#!/usr/bin/env node
import process from 'node:process'; import process from 'node:process';
import {ExitPromptError, checkbox, confirm, input} from '@inquirer/prompts';
import chalk from 'chalk'; import chalk from 'chalk';
import { input, confirm, checkbox, ExitPromptError } from '@inquirer/prompts';
import autocomplete from 'inquirer-autocomplete-standalone';
import getRelativeLuminance from 'get-relative-luminance';
import {search} from 'fast-fuzzy'; import {search} from 'fast-fuzzy';
import getRelativeLuminance from 'get-relative-luminance';
import autocomplete from 'inquirer-autocomplete-standalone';
import { import {
URL_REGEX, URL_REGEX,
collator, collator,
getIconsDataString, getIconsDataString,
titleToSlug,
normalizeColor, normalizeColor,
titleToSlug,
} from '../sdk.mjs'; } from '../sdk.mjs';
import {getJsonSchemaData, writeIconsData} from './utils.js'; import {getJsonSchemaData, writeIconsData} from './utils.js';
const iconsData = JSON.parse(await getIconsDataString()); const iconsData = JSON.parse(await getIconsDataString());
const jsonSchema = await getJsonSchemaData(); const jsonSchema = await getJsonSchemaData();
const HEX_REGEX = /^#?[a-f0-9]{3,8}$/i; const HEX_REGEX = /^#?[a-f\d]{3,8}$/i;
const aliasTypes = ['aka', 'old'].map((key) => ({ const aliasTypes = ['aka', 'old'].map((key) => ({
name: `${key} (${jsonSchema.definitions.brand.properties.aliases.properties[key].description})`, name: `${key} (${jsonSchema.definitions.brand.properties.aliases.properties[key].description})`,
@ -35,7 +36,7 @@ const isValidHexColor = (input) =>
HEX_REGEX.test(input) || 'Must be a valid hex code.'; HEX_REGEX.test(input) || 'Must be a valid hex code.';
const isNewIcon = (input) => const isNewIcon = (input) =>
!iconsData.icons.find( !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.'; ) || 'This icon title or slug already exists.';
@ -83,7 +84,7 @@ try {
? { ? {
type: await autocomplete({ type: await autocomplete({
message: "What is the icon's license?", message: "What is the icon's license?",
source: async (input) => { async source(input) {
input = (input || '').trim(); input = (input || '').trim();
return input return input
? search(input, licenseTypes, {keySelector: (x) => x.value}) ? search(input, licenseTypes, {keySelector: (x) => x.value})
@ -107,12 +108,14 @@ try {
}).then(async (aliases) => { }).then(async (aliases) => {
const result = {}; const result = {};
for (const alias of aliases) { for (const alias of aliases) {
// 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)`,
}).then((aliases) => }).then((aliases) =>
aliases.split(',').map((alias) => alias.trim()), aliases.split(',').map((alias) => alias.trim()),
); );
} }
return result; return result;
}) })
: undefined, : undefined,
@ -136,11 +139,11 @@ try {
console.log(chalk.red('\nAborted.')); console.log(chalk.red('\nAborted.'));
process.exit(1); process.exit(1);
} }
} catch (err) { } catch (error) {
if (err instanceof ExitPromptError) { if (error instanceof ExitPromptError) {
console.log(chalk.red('\nAborted.')); console.log(chalk.red('\nAborted.'));
process.exit(1); process.exit(1);
} }
throw err; throw error;
} }

20
scripts/build/clean.js Normal file → Executable file
View File

@ -1,10 +1,12 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Clean files built by the build process. * Clean files built by the build process.
*/ */
import fs from 'node:fs'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process';
import {getDirnameFromImportMeta} from '../../sdk.mjs'; import {getDirnameFromImportMeta} from '../../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
@ -12,8 +14,12 @@ const rootDirectory = path.resolve(__dirname, '..', '..');
const files = ['index.js', 'index.mjs', 'index.d.ts', 'sdk.js']; const files = ['index.js', 'index.mjs', 'index.d.ts', 'sdk.js'];
const fileExists = (fpath) => const fileExists = (fpath) =>
new Promise((r) => fs.access(fpath, fs.constants.F_OK, (e) => r(!e))); fs
.access(fpath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
try {
Promise.all( Promise.all(
files.map(async (file) => { files.map(async (file) => {
const filepath = path.join(rootDirectory, file); const filepath = path.join(rootDirectory, file);
@ -21,9 +27,11 @@ Promise.all(
console.error(`File ${file} does not exist, skipping...`); console.error(`File ${file} does not exist, skipping...`);
return; return;
} }
return fs.promises.unlink(filepath);
return fs.unlink(filepath);
}), }),
).catch((error) => { );
console.error(`Error cleaning files: ${error.message}`); } catch (error) {
console.error('Error cleaning files:', error);
process.exit(1); process.exit(1);
}); }

57
scripts/build/package.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Simple Icons package build script. * Simple Icons package build script.
@ -8,29 +9,32 @@ import path from 'node:path';
import util from 'node:util'; import util from 'node:util';
import {transform as esbuildTransform} from 'esbuild'; import {transform as esbuildTransform} from 'esbuild';
import { import {
collator,
getDirnameFromImportMeta,
getIconSlug, getIconSlug,
getIconsData,
slugToVariableName,
svgToPath, svgToPath,
titleToHtmlFriendly, titleToHtmlFriendly,
slugToVariableName,
getIconsData,
getDirnameFromImportMeta,
collator,
} from '../../sdk.mjs'; } from '../../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const UTF8 = 'utf8'; const UTF8 = 'utf8';
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const iconsDir = path.resolve(rootDir, 'icons'); const iconsDirectory = path.resolve(rootDirectory, 'icons');
const indexJsFile = path.resolve(rootDir, 'index.js'); const indexJsFile = path.resolve(rootDirectory, 'index.js');
const indexMjsFile = path.resolve(rootDir, 'index.mjs'); const indexMjsFile = path.resolve(rootDirectory, 'index.mjs');
const sdkJsFile = path.resolve(rootDir, 'sdk.js'); const sdkJsFile = path.resolve(rootDirectory, 'sdk.js');
const sdkMjsFile = path.resolve(rootDir, 'sdk.mjs'); const sdkMjsFile = path.resolve(rootDirectory, 'sdk.mjs');
const indexDtsFile = path.resolve(rootDir, 'index.d.ts'); const indexDtsFile = path.resolve(rootDirectory, 'index.d.ts');
const templatesDir = path.resolve(__dirname, 'templates'); const templatesDirectory = path.resolve(__dirname, 'templates');
const iconObjectTemplateFile = path.resolve(templatesDir, 'icon-object.js'); const iconObjectTemplateFile = path.resolve(
templatesDirectory,
'icon-object.js.template',
);
const build = async () => { const build = async () => {
const icons = await getIconsData(); const icons = await getIconsData();
@ -38,8 +42,9 @@ const build = async () => {
// Local helper functions // Local helper functions
const escape = (value) => { const escape = (value) => {
return value.replace(/(?<!\\)'/g, "\\'"); return value.replaceAll(/(?<!\\)'/g, "\\'");
}; };
const licenseToObject = (license) => { const licenseToObject = (license) => {
if (license === undefined) { if (license === undefined) {
return; return;
@ -48,8 +53,10 @@ const build = async () => {
if (license.url === undefined) { if (license.url === undefined) {
license.url = `https://spdx.org/licenses/${license.type}`; license.url = `https://spdx.org/licenses/${license.type}`;
} }
return license; return license;
}; };
const iconToObject = (icon) => { const iconToObject = (icon) => {
return util.format( return util.format(
iconObjectTemplate, iconObjectTemplate,
@ -65,11 +72,13 @@ const build = async () => {
: '', : '',
); );
}; };
const writeJs = async (filepath, rawJavaScript, opts = null) => {
opts = opts === null ? { minify: true } : opts; const writeJs = async (filepath, rawJavaScript, options = null) => {
const { code } = await esbuildTransform(rawJavaScript, opts); options = options === null ? {minify: true} : options;
const {code} = await esbuildTransform(rawJavaScript, options);
await fs.writeFile(filepath, code); await fs.writeFile(filepath, code);
}; };
const writeTs = async (filepath, rawTypeScript) => { const writeTs = async (filepath, rawTypeScript) => {
await fs.writeFile(filepath, rawTypeScript); await fs.writeFile(filepath, rawTypeScript);
}; };
@ -78,7 +87,7 @@ const build = async () => {
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(iconsDir, `${filename}.svg`); const svgFilepath = path.resolve(iconsDirectory, `${filename}.svg`);
icon.svg = await fs.readFile(svgFilepath, UTF8); icon.svg = await fs.readFile(svgFilepath, UTF8);
icon.path = svgToPath(icon.svg); icon.path = svgToPath(icon.svg);
icon.slug = filename; icon.slug = filename;
@ -99,27 +108,27 @@ const build = async () => {
iconsBarrelMjs.push(`export const ${iconExportName}=${iconObject}`); iconsBarrelMjs.push(`export const ${iconExportName}=${iconObject}`);
} }
// constants used in templates to reduce package size // Constants used in templates to reduce package size
const constantsString = `const a='<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>',b='</title><path d="',c='"/></svg>';`; const constantsString = `const a='<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>',b='</title><path d="',c='"/></svg>';`;
// write our file containing the exports of all icons in CommonJS ... // Write our file containing the exports of all icons in CommonJS ...
const rawIndexJs = `${constantsString}module.exports={${iconsBarrelJs.join( const rawIndexJs = `${constantsString}module.exports={${iconsBarrelJs.join(
'', '',
)}};`; )}};`;
await writeJs(indexJsFile, rawIndexJs); await writeJs(indexJsFile, rawIndexJs);
// and ESM // ... and ESM
const rawIndexMjs = constantsString + iconsBarrelMjs.join(''); const rawIndexMjs = constantsString + iconsBarrelMjs.join('');
await writeJs(indexMjsFile, rawIndexMjs); await writeJs(indexMjsFile, rawIndexMjs);
// and create a type declaration file // ... and create a type declaration file
const rawIndexDts = `import {SimpleIcon} from "./types";export {SimpleIcon};type I=SimpleIcon;${iconsBarrelDts.join( const rawIndexDts = `import {SimpleIcon} from "./types";export {SimpleIcon};type I=SimpleIcon;${iconsBarrelDts.join(
'', '',
)}`; )}`;
await writeTs(indexDtsFile, rawIndexDts); await writeTs(indexDtsFile, rawIndexDts);
// create a CommonJS SDK file // Create a CommonJS SDK file
await writeJs(sdkJsFile, await fs.readFile(sdkMjsFile, UTF8), { await writeJs(sdkJsFile, await fs.readFile(sdkMjsFile, UTF8), {
format: 'cjs', format: 'cjs',
}); });
}; };
build(); await build();

6
scripts/get-filename.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Script that takes a brand name as argument and outputs the corresponding * Script that takes a brand name as argument and outputs the corresponding
@ -11,10 +12,7 @@ if (process.argv.length < 3) {
console.error('Provide a brand name as argument'); console.error('Provide a brand name as argument');
process.exit(1); process.exit(1);
} else { } else {
const brandName = process.argv const brandName = process.argv[2];
.slice(3)
.reduce((acc, arg) => `${acc} ${arg}`, process.argv[2]);
const filename = titleToSlug(brandName); const filename = titleToSlug(brandName);
console.log(`For '${brandName}' use the file 'icons/${filename}.svg'`); console.log(`For '${brandName}' use the file 'icons/${filename}.svg'`);
} }

3
scripts/lint/jsonlint.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* CLI tool to run jsonschema on the simple-icons.json data file. * CLI tool to run jsonschema on the simple-icons.json data file.
@ -14,7 +15,7 @@ const schema = await getJsonSchemaData();
const validator = new Validator(); const validator = new Validator();
const result = validator.validate({icons}, schema); const result = validator.validate({icons}, schema);
if (result.errors.length > 0) { if (result.errors.length > 0) {
result.errors.forEach((error) => console.error(error)); for (const error of result.errors) console.error(error);
console.error(`Found ${result.errors.length} error(s) in simple-icons.json`); console.error(`Found ${result.errors.length} error(s) in simple-icons.json`);
process.exit(1); process.exit(1);
} }

38
scripts/lint/ourlint.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* 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
@ -5,9 +6,8 @@
*/ */
import process from 'node:process'; import process from 'node:process';
import { URL } from 'node:url';
import fakeDiff from 'fake-diff'; import fakeDiff from 'fake-diff';
import { getIconsDataString, normalizeNewlines, collator } 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.
@ -15,39 +15,43 @@ import { getIconsDataString, normalizeNewlines, collator } from '../../sdk.mjs';
*/ */
const TESTS = { const TESTS = {
/* Tests whether our icons are in alphabetical order */ /* Tests whether our icons are in alphabetical order */
alphabetical: (data) => { alphabetical(data) {
const collector = (invalidEntries, icon, index, array) => { const collector = (invalidEntries, icon, index, array) => {
if (index > 0) { if (index > 0) {
const prev = array[index - 1]; const previous = array[index - 1];
const comparison = collator.compare(icon.title, prev.title); const comparison = collator.compare(icon.title, previous.title);
if (comparison < 0) { if (comparison < 0) {
invalidEntries.push(icon); invalidEntries.push(icon);
} else if (comparison === 0) { } else if (
if (prev.slug) { comparison === 0 &&
if (!icon.slug || collator.compare(icon.slug, prev.slug) < 0) { previous.slug &&
(!icon.slug || collator.compare(icon.slug, previous.slug) < 0)
) {
invalidEntries.push(icon); invalidEntries.push(icon);
} }
} }
}
}
return invalidEntries; return invalidEntries;
}; };
const format = (icon) => { const format = (icon) => {
if (icon.slug) { if (icon.slug) {
return `${icon.title} (${icon.slug})`; return `${icon.title} (${icon.slug})`;
} }
return icon.title; return icon.title;
}; };
// eslint-disable-next-line unicorn/no-array-reduce, unicorn/no-array-callback-reference
const invalids = data.icons.reduce(collector, []); const invalids = data.icons.reduce(collector, []);
if (invalids.length) { if (invalids.length > 0) {
return `Some icons aren't in alphabetical order: return `Some icons aren't in alphabetical order:
${invalids.map((icon) => format(icon)).join(', ')}`; ${invalids.map((icon) => format(icon)).join(', ')}`;
} }
}, },
/* Check the formatting of the data file */ /* Check the formatting of the data file */
prettified: (data, dataString) => { prettified(data, dataString) {
const normalizedDataString = normalizeNewlines(dataString); const normalizedDataString = normalizeNewlines(dataString);
const dataPretty = `${JSON.stringify(data, null, 4)}\n`; const dataPretty = `${JSON.stringify(data, null, 4)}\n`;
@ -58,9 +62,9 @@ const TESTS = {
}, },
/* Check redundant trailing slash in URL */ /* Check redundant trailing slash in URL */
checkUrl: (data) => { checkUrl(data) {
const hasRedundantTrailingSlash = (url) => { const hasRedundantTrailingSlash = (url) => {
const origin = new URL(url).origin; const {origin} = new global.URL(url);
return /^\/+$/.test(url.replace(origin, '')); return /^\/+$/.test(url.replace(origin, ''));
}; };
@ -89,9 +93,11 @@ const data = JSON.parse(dataString);
const errors = ( const errors = (
await Promise.all(Object.values(TESTS).map((test) => test(data, dataString))) await Promise.all(Object.values(TESTS).map((test) => test(data, dataString)))
).filter(Boolean); )
// eslint-disable-next-line unicorn/no-await-expression-member
.filter(Boolean);
if (errors.length > 0) { if (errors.length > 0) {
errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`)); for (const error of errors) console.error(`\u001B[31m${error}\u001B[0m`);
process.exit(1); process.exit(1);
} }

22
scripts/release/reformat-markdown.js Normal file → Executable file
View File

@ -1,19 +1,21 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Rewrite some Markdown files. * Rewrite some Markdown files.
*/ */
import {readFile, writeFile} from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { writeFile, readFile } from 'node:fs/promises'; import process from 'node:process';
import {getDirnameFromImportMeta} from '../../sdk.mjs'; import {getDirnameFromImportMeta} from '../../sdk.mjs';
const LINKS_BRANCH = process.argv[2] || 'develop'; const LINKS_BRANCH = process.argv[2] || 'develop';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const readmeFile = path.resolve(rootDir, 'README.md'); const readmeFile = path.resolve(rootDirectory, 'README.md');
const disclaimerFile = path.resolve(rootDir, 'DISCLAIMER.md'); const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md');
const reformat = async (filePath) => { const reformat = async (filePath) => {
const fileContent = await readFile(filePath, 'utf8'); const fileContent = await readFile(filePath, 'utf8');
@ -21,17 +23,17 @@ const reformat = async (filePath) => {
filePath, filePath,
fileContent fileContent
// Replace all CDN links with raw links // Replace all CDN links with raw links
.replace( .replaceAll(
/https:\/\/cdn.simpleicons.org\/(.+)\/000\/fff/g, /https:\/\/cdn.simpleicons.org\/(.+)\/000\/fff/g,
`https://raw.githubusercontent.com/simple-icons/simple-icons/${LINKS_BRANCH}/icons/$1.svg`, `https://raw.githubusercontent.com/simple-icons/simple-icons/${LINKS_BRANCH}/icons/$1.svg`,
) )
// Replace all GitHub blockquotes with regular markdown // Replace all GitHub blockquotes with regular markdown
// Reference: https://github.com/orgs/community/discussions/16925 // Reference: https://github.com/orgs/community/discussions/16925
.replace( .replaceAll(
/\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?!\()/g, /\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)](?!\()/g,
function (str, $0) { function (string_, $0) {
const capital = $0.substr(0, 1); const capital = $0.slice(0, 1);
const body = $0.substr(1).toLowerCase(); const body = $0.slice(1).toLowerCase();
return `**${capital + body}**`; return `**${capital + body}**`;
}, },
), ),

21
scripts/release/update-cdn-urls.js Normal file → Executable file
View File

@ -1,35 +1,36 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* 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.
*/ */
import process from 'node:process';
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 {getDirnameFromImportMeta} from '../../sdk.mjs'; import {getDirnameFromImportMeta} from '../../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const packageJsonFile = path.resolve(rootDir, 'package.json'); const packageJsonFile = path.resolve(rootDirectory, 'package.json');
const readmeFile = path.resolve(rootDir, 'README.md'); const readmeFile = path.resolve(rootDirectory, 'README.md');
const getMajorVersion = (semVerVersion) => { const getMajorVersion = (semVersion) => {
const majorVersionAsString = semVerVersion.split('.')[0]; const majorVersionAsString = semVersion.split('.')[0];
return parseInt(majorVersionAsString); return Number.parseInt(majorVersionAsString, 10);
}; };
const getManifest = async () => { const getManifest = async () => {
const manifestRaw = await fs.readFile(packageJsonFile, 'utf-8'); const manifestRaw = await fs.readFile(packageJsonFile, 'utf8');
return JSON.parse(manifestRaw); return JSON.parse(manifestRaw);
}; };
const updateVersionInReadmeIfNecessary = async (majorVersion) => { const updateVersionInReadmeIfNecessary = async (majorVersion) => {
let content = await fs.readFile(readmeFile, 'utf8'); let content = await fs.readFile(readmeFile, 'utf8');
content = content.replace( content = content.replaceAll(
/simple-icons@v[0-9]+/g, /simple-icons@v\d+/g,
`simple-icons@v${majorVersion}`, `simple-icons@v${majorVersion}`,
); );

32
scripts/release/update-sdk-ts-defs.js Normal file → Executable file
View File

@ -1,33 +1,34 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* 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.
*/ */
import process from 'node:process'; import {execSync} from 'node:child_process';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { execSync } from 'node:child_process'; import process from 'node:process';
import {getDirnameFromImportMeta} from '../../sdk.mjs'; import {getDirnameFromImportMeta} from '../../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const sdkTs = path.resolve(rootDir, 'sdk.d.ts'); const sdkTs = path.resolve(rootDirectory, 'sdk.d.ts');
const sdkMts = path.resolve(rootDir, 'sdk.d.mts'); const sdkMts = path.resolve(rootDirectory, 'sdk.d.mts');
const sdkMjs = path.resolve(rootDir, 'sdk.mjs'); const sdkMjs = path.resolve(rootDirectory, 'sdk.mjs');
const generateSdkMts = async () => { const generateSdkMts = async () => {
// remove temporally type definitions imported with comments // Remove temporally type definitions imported with comments
// in sdk.mjs to avoid circular imports // in sdk.mjs to avoid circular imports
const originalSdkMjsContent = await fs.readFile(sdkMjs, 'utf-8'); const originalSdkMjsContent = await fs.readFile(sdkMjs, 'utf8');
const tempSdkMjsContent = originalSdkMjsContent const temporarySdkMjsContent = originalSdkMjsContent
.split('\n') .split('\n')
.filter((line) => { .filter((line) => {
return !line.startsWith(' * @typedef {import("./sdk")'); return !line.startsWith(' * @typedef {import("./sdk")');
}) })
.join('\n'); .join('\n');
await fs.writeFile(sdkMjs, tempSdkMjsContent); await fs.writeFile(sdkMjs, temporarySdkMjsContent);
try { try {
execSync( execSync(
'npx tsc sdk.mjs' + 'npx tsc sdk.mjs' +
@ -41,6 +42,7 @@ const generateSdkMts = async () => {
); );
process.exit(1); process.exit(1);
} }
await fs.writeFile(sdkMjs, originalSdkMjsContent); await fs.writeFile(sdkMjs, originalSdkMjsContent);
}; };
@ -49,13 +51,15 @@ const generateSdkTs = async () => {
.access(sdkMts) .access(sdkMts)
.then(() => true) .then(() => true)
.catch(() => false); .catch(() => false);
fileExists && (await fs.unlink(sdkMts)); if (fileExists) await fs.unlink(sdkMts);
await generateSdkMts(); await generateSdkMts();
const autogeneratedMsg = '/* The next code is autogenerated from sdk.mjs */'; const autogeneratedMessage =
'/* The next code is autogenerated from sdk.mjs */';
const newSdkTsContent = const newSdkTsContent =
(await fs.readFile(sdkTs, 'utf-8')).split(autogeneratedMsg)[0] + // eslint-disable-next-line unicorn/no-await-expression-member
`${autogeneratedMsg}\n\n${await fs.readFile(sdkMts, 'utf-8')}`; (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, newSdkTsContent);
await fs.unlink(sdkMts); await fs.unlink(sdkMts);

12
scripts/release/update-slugs-table.js Normal file → Executable file
View File

@ -1,22 +1,23 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Generates a MarkDown file that lists every brand name and their slug. * Generates a MarkDown file that lists every brand name and their slug.
*/ */
import { promises as fs } from 'node:fs'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
import { getIconsData, getIconSlug } from '../../sdk.mjs'; import {getIconSlug, getIconsData} from '../../sdk.mjs';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const slugsFile = path.resolve(rootDir, 'slugs.md'); const slugsFile = path.resolve(rootDirectory, 'slugs.md');
let content = `<!-- let content = `<!--
This file is automatically generated. If you want to change something, please This file is automatically generated. If you want to change something, please
update the script at '${path.relative(rootDir, __filename)}'. update the script at '${path.relative(rootDirectory, __filename)}'.
--> -->
# Simple Icons slugs # Simple Icons slugs
@ -31,4 +32,5 @@ for (const icon of icons) {
const brandSlug = getIconSlug(icon); const brandSlug = getIconSlug(icon);
content += `| \`${brandName}\` | \`${brandSlug}\` |\n`; content += `| \`${brandName}\` | \`${brandSlug}\` |\n`;
} }
await fs.writeFile(slugsFile, content); await fs.writeFile(slugsFile, content);

18
scripts/release/update-svgs-count.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env node
/** /**
* @fileoverview * @fileoverview
* Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located * Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located
@ -5,32 +6,33 @@
* more than the previous milestone. * more than the previous milestone.
*/ */
import process from 'node:process';
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 {getDirnameFromImportMeta, getIconsData} from '../../sdk.mjs'; import {getDirnameFromImportMeta, getIconsData} from '../../sdk.mjs';
const regexMatcher = /Over\s(\d+)\s/; const regexMatcher = /Over\s(\d+)\s/;
const updateRange = 100; const updateRange = 100;
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..'); const rootDirectory = path.resolve(__dirname, '..', '..');
const readmeFile = path.resolve(rootDir, 'README.md'); const readmeFile = path.resolve(rootDirectory, 'README.md');
const readmeContent = await fs.readFile(readmeFile, 'utf-8'); const readmeContent = await fs.readFile(readmeFile, 'utf8');
let overNIconsInReadme; let overNIconsInReadme;
try { try {
overNIconsInReadme = parseInt(regexMatcher.exec(readmeContent)[1]); overNIconsInReadme = Number.parseInt(regexMatcher.exec(readmeContent)[1], 10);
} catch (err) { } catch (error) {
console.error( console.error(
'Failed to obtain number of SVG icons of current milestone in README:', 'Failed to obtain number of SVG icons of current milestone in README:',
err, error,
); );
process.exit(1); process.exit(1);
} }
const nIcons = (await getIconsData()).length; const iconsData = await getIconsData();
const nIcons = iconsData.length;
const newNIcons = overNIconsInReadme + updateRange; const newNIcons = overNIconsInReadme + updateRange;
if (nIcons > newNIcons) { if (nIcons > newNIcons) {

View File

@ -1,17 +1,17 @@
import path from 'node:path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
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);
/** /**
* Get JSON schema data. * Get JSON schema data.
* @param {String} rootDir Path to the root directory of the project. * @param {String} rootDirectory Path to the root directory of the project.
*/ */
export const getJsonSchemaData = async ( export const getJsonSchemaData = async (
rootDir = path.resolve(__dirname, '..'), rootDirectory = path.resolve(__dirname, '..'),
) => { ) => {
const jsonSchemaPath = path.resolve(rootDir, '.jsonschema.json'); const jsonSchemaPath = path.resolve(rootDirectory, '.jsonschema.json');
const jsonSchemaString = await fs.readFile(jsonSchemaPath, 'utf8'); const jsonSchemaString = await fs.readFile(jsonSchemaPath, 'utf8');
return JSON.parse(jsonSchemaString); return JSON.parse(jsonSchemaString);
}; };
@ -19,14 +19,14 @@ 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 {Object} iconsData Icons data object.
* @param {String} rootDir 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,
rootDir = path.resolve(__dirname, '..'), rootDirectory = path.resolve(__dirname, '..'),
) => { ) => {
await fs.writeFile( await fs.writeFile(
getIconDataPath(rootDir), getIconDataPath(rootDirectory),
`${JSON.stringify(iconsData, null, 4)}\n`, `${JSON.stringify(iconsData, null, 4)}\n`,
'utf8', 'utf8',
); );

14
sdk.d.ts vendored
View File

@ -33,14 +33,14 @@ type ThirdPartyExtensionSubject = {
export type Aliases = { export type Aliases = {
aka?: string[]; aka?: string[];
dup?: DuplicateAlias[]; dup?: DuplicateAlias[];
loc?: { [key: string]: string }; loc?: Record<string, string>;
}; };
type DuplicateAlias = { type DuplicateAlias = {
title: string; title: string;
hex?: string; hex?: string;
guidelines?: string; guidelines?: string;
loc?: { [key: string]: string }; loc?: Record<string, string>;
}; };
/** /**
@ -62,8 +62,8 @@ export type IconData = {
/* The next code is autogenerated from sdk.mjs */ /* The next code is autogenerated from sdk.mjs */
export const URL_REGEX: RegExp; export const URL_REGEX: RegExp; // eslint-disable-line @typescript-eslint/naming-convention
export const SVG_PATH_REGEX: RegExp; export const SVG_PATH_REGEX: RegExp; // eslint-disable-line @typescript-eslint/naming-convention
export function getDirnameFromImportMeta(importMetaUrl: string): string; export function getDirnameFromImportMeta(importMetaUrl: string): string;
export function getIconSlug(icon: IconData): string; export function getIconSlug(icon: IconData): string;
export function svgToPath(svg: string): string; export function svgToPath(svg: string): string;
@ -71,9 +71,9 @@ export function titleToSlug(title: string): string;
export function slugToVariableName(slug: string): string; 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(rootDir?: string): string; export function getIconDataPath(rootDirectory?: string): string;
export function getIconsDataString(rootDir?: string): string; export function getIconsDataString(rootDirectory?: string): string;
export function getIconsData(rootDir?: string): IconData[]; export function getIconsData(rootDirectory?: string): 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(

62
sdk.mjs
View File

@ -3,8 +3,8 @@
* Simple Icons SDK. * Simple Icons SDK.
*/ */
import path from 'node:path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
/** /**
@ -26,12 +26,12 @@ const TITLE_TO_SLUG_REPLACEMENTS = {
ŧ: 't', ŧ: 't',
}; };
const TITLE_TO_SLUG_CHARS_REGEX = RegExp( const TITLE_TO_SLUG_CHARS_REGEX = new RegExp(
`[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`, `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`,
'g', 'g',
); );
const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z0-9]/g; const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z\d]/g;
/** /**
* Regex to validate HTTPs URLs. * Regex to validate HTTPs URLs.
@ -41,7 +41,7 @@ export const URL_REGEX = /^https:\/\/[^\s"']+$/;
/** /**
* Regex to validate SVG paths. * Regex to validate SVG paths.
*/ */
export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae0-9,. ]+$/i; 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`,
@ -74,12 +74,12 @@ export const svgToPath = (svg) => svg.split('"', 8)[7];
export const titleToSlug = (title) => export const titleToSlug = (title) =>
title title
.toLowerCase() .toLowerCase()
.replace( .replaceAll(
TITLE_TO_SLUG_CHARS_REGEX, TITLE_TO_SLUG_CHARS_REGEX,
(char) => TITLE_TO_SLUG_REPLACEMENTS[char], (char) => TITLE_TO_SLUG_REPLACEMENTS[char],
) )
.normalize('NFD') .normalize('NFD')
.replace(TITLE_TO_SLUG_RANGE_REGEX, ''); .replaceAll(TITLE_TO_SLUG_RANGE_REGEX, '');
/** /**
* Converts a slug into a variable name that can be exported. * Converts a slug into a variable name that can be exported.
@ -99,12 +99,12 @@ export const slugToVariableName = (slug) => {
*/ */
export const titleToHtmlFriendly = (brandTitle) => export const titleToHtmlFriendly = (brandTitle) =>
brandTitle brandTitle
.replace(/&/g, '&amp;') .replaceAll('&', '&amp;')
.replace(/"/g, '&quot;') .replaceAll('"', '&quot;')
.replace(/</g, '&lt;') .replaceAll('<', '&lt;')
.replace(/>/g, '&gt;') .replaceAll('>', '&gt;')
.replace(/./g, (char) => { .replaceAll(/./g, (char) => {
const charCode = char.charCodeAt(0); const charCode = char.codePointAt(0);
return charCode > 127 ? `&#${charCode};` : char; return charCode > 127 ? `&#${charCode};` : char;
}); });
@ -116,43 +116,45 @@ export const titleToHtmlFriendly = (brandTitle) =>
*/ */
export const htmlFriendlyToTitle = (htmlFriendlyTitle) => export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
htmlFriendlyTitle htmlFriendlyTitle
.replace(/&#([0-9]+);/g, (_, num) => String.fromCodePoint(parseInt(num))) .replaceAll(/&#(\d+);/g, (_, number_) =>
.replace( String.fromCodePoint(Number.parseInt(number_, 10)),
)
.replaceAll(
/&(quot|amp|lt|gt);/g, /&(quot|amp|lt|gt);/g,
(_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' })[ref], (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference],
); );
/** /**
* Get path of *_data/simpe-icons.json*. * Get path of *_data/simpe-icons.json*.
* @param {String} rootDir 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 = (
rootDir = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
) => { ) => {
return path.resolve(rootDir, '_data', 'simple-icons.json'); return path.resolve(rootDirectory, '_data', 'simple-icons.json');
}; };
/** /**
* Get contents of *_data/simple-icons.json*. * Get contents of *_data/simple-icons.json*.
* @param {String} rootDir 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 {String} Content of *_data/simple-icons.json*
*/ */
export const getIconsDataString = ( export const getIconsDataString = (
rootDir = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
) => { ) => {
return fs.readFile(getIconDataPath(rootDir), 'utf8'); return fs.readFile(getIconDataPath(rootDirectory), 'utf8');
}; };
/** /**
* Get icons data as object from *_data/simple-icons.json*. * Get icons data as object from *_data/simple-icons.json*.
* @param {String} rootDir 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 {IconData[]} Icons data as array from *_data/simple-icons.json*
*/ */
export const getIconsData = async ( export const getIconsData = async (
rootDir = getDirnameFromImportMeta(import.meta.url), rootDirectory = getDirnameFromImportMeta(import.meta.url),
) => { ) => {
const fileContents = await getIconsDataString(rootDir); const fileContents = await getIconsDataString(rootDirectory);
return JSON.parse(fileContents).icons; return JSON.parse(fileContents).icons;
}; };
@ -162,7 +164,7 @@ export const getIconsData = async (
* @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.replace(/\r\n/g, '\n'); return text.replaceAll('\r\n', '\n');
}; };
/** /**
@ -173,10 +175,14 @@ export const normalizeNewlines = (text) => {
export const normalizeColor = (text) => { export const normalizeColor = (text) => {
let color = text.replace('#', '').toUpperCase(); let color = text.replace('#', '').toUpperCase();
if (color.length < 6) { if (color.length < 6) {
color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join(''); color = color
.slice(0, 3)
.map((x) => x.repeat(2))
.join('');
} else if (color.length > 6) { } else if (color.length > 6) {
color = color.slice(0, 6); color = color.slice(0, 6);
} }
return color; return color;
}; };
@ -201,11 +207,11 @@ export const getThirdPartyExtensions = async (
module = module.split('<img src="')[0]; module = module.split('<img src="')[0];
return { return {
module: { module: {
name: /\[(.+)\]/.exec(module)[1], name: /\[(.+)]/.exec(module)[1],
url: /\((.+)\)/.exec(module)[1], url: /\((.+)\)/.exec(module)[1],
}, },
author: { author: {
name: /\[(.+)\]/.exec(author)[1], name: /\[(.+)]/.exec(author)[1],
url: /\((.+)\)/.exec(author)[1], url: /\((.+)\)/.exec(author)[1],
}, },
}; };

View File

@ -1,15 +1,16 @@
/* eslint complexity: off, max-depth: off */
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 {
SVG_PATH_REGEX,
getDirnameFromImportMeta,
htmlFriendlyToTitle,
collator,
} from './sdk.mjs';
import svgpath from 'svgpath';
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 {
SVG_PATH_REGEX,
collator,
getDirnameFromImportMeta,
htmlFriendlyToTitle,
} from './sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url); const __dirname = getDirnameFromImportMeta(import.meta.url);
const dataFile = path.join(__dirname, '_data', 'simple-icons.json'); const dataFile = path.join(__dirname, '_data', 'simple-icons.json');
@ -30,8 +31,8 @@ const svglintIgnores = JSON.parse(
); );
const svgRegexp = const svgRegexp =
/^<svg( [^\s]*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>$/; /^<svg( \S*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>$/;
const negativeZerosRegexp = /-0(?=[^\.]|[\s\d\w]|$)/g; const negativeZerosRegexp = /-0(?=[^.]|[\s\d\w]|$)/g;
const iconSize = 24; const iconSize = 24;
const iconTargetCenter = iconSize / 2; const iconTargetCenter = iconSize / 2;
@ -39,25 +40,29 @@ const iconFloatPrecision = 3;
const iconMaxFloatPrecision = 5; const iconMaxFloatPrecision = 5;
const iconTolerance = 0.001; const iconTolerance = 0.001;
// set env SI_UPDATE_IGNORE to recreate the ignore file // Set env SI_UPDATE_IGNORE to recreate the ignore file
const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true'; 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;
const sortObjectByKey = (obj) => { const sortObjectByKey = (object) => {
return Object.keys(obj) return Object.fromEntries(
Object.keys(object)
.sort() .sort()
.reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); .map((k) => [k, object[k]]),
);
}; };
const sortObjectByValue = (obj) => { const sortObjectByValue = (object) => {
return Object.keys(obj) return Object.fromEntries(
.sort((a, b) => collator.compare(obj[a], obj[b])) Object.keys(object)
.reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); .sort((a, b) => collator.compare(object[a], object[b]))
.map((k) => [k, object[k]]),
);
}; };
const removeLeadingZeros = (number) => { const removeLeadingZeros = (number) => {
// convert 0.03 to '.03' // Convert 0.03 to '.03'
return number.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3'); return number.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3');
}; };
@ -65,6 +70,7 @@ const removeLeadingZeros = (number) => {
* 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.
**/ **/
// 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;
}; };
@ -73,15 +79,16 @@ 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 num The number of interest.
*/ */
const countDecimals = (num) => { const countDecimals = (number_) => {
if (num && num % 1) { if (number_ && number_ % 1) {
let [base, op, trail] = num.toExponential().split(/e([+-])/); const [base, op, trail] = number_.toExponential().split(/e([+-])/);
let elen = parseInt(trail, 10); const elen = Number.parseInt(trail, 10);
let idx = base.indexOf('.'); const index = base.indexOf('.');
return idx == -1 return index === -1
? elen ? elen
: base.length - idx - 1 + (op === '+' ? -elen : elen); : base.length - index - 1 + (op === '+' ? -elen : elen);
} }
return 0; return 0;
}; };
@ -108,47 +115,49 @@ const getTitleTextIndex = (svgFileContent) => {
* @param hex The hexadecimal number representation to convert. * @param hex The hexadecimal number representation to convert.
**/ **/
const hexadecimalToDecimal = (hex) => { const hexadecimalToDecimal = (hex) => {
let result = 0, let result = 0;
digitValue; let digitValue;
for (const digit of hex.toLowerCase()) { for (const digit of hex.toLowerCase()) {
digitValue = '0123456789abcdefgh'.indexOf(digit); digitValue = '0123456789abcdefgh'.indexOf(digit);
result = result * 16 + digitValue; result = result * 16 + digitValue;
} }
return result; return result;
}; };
const maybeShortenedWithEllipsis = (str) => { const maybeShortenedWithEllipsis = (string_) => {
return str.length > 20 ? `${str.substring(0, 20)}...` : str; return string_.length > 20 ? `${string_.slice(0, 20)}...` : 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.
*/ */
const memoize = (func) => { const memoize = (function_) => {
const results = {}; const results = {};
return (arg, defaultKey = null) => { return (argument, defaultKey = null) => {
const key = defaultKey || arg; const key = defaultKey || argument;
results[key] ||= function_(argument);
if (!results[key]) {
results[key] = func(arg);
}
return results[key]; return results[key];
}; };
}; };
const getIconPath = memoize(($icon, filepath) => $icon.find('path').attr('d')); const getIconPath = memoize(($icon, _filepath) => $icon.find('path').attr('d'));
const getIconPathSegments = memoize((iconPath) => parsePath(iconPath)); const getIconPathSegments = memoize((iconPath) => parsePath(iconPath));
const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath));
if (updateIgnoreFile) { if (updateIgnoreFile) {
process.on('exit', async () => { process.on('exit', async () => {
// ensure object output order is consistent due to async svglint processing // Ensure object output order is consistent due to async svglint processing
const sorted = sortObjectByKey(iconIgnored); const sorted = sortObjectByKey(iconIgnored);
for (const linterName in sorted) { for (const linterName in sorted) {
if (linterName) {
sorted[linterName] = sortObjectByValue(sorted[linterName]); sorted[linterName] = sortObjectByValue(sorted[linterName]);
} }
}
await fs.writeFile(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', { await fs.writeFile(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', {
flag: 'w', flag: 'w',
@ -158,14 +167,12 @@ if (updateIgnoreFile) {
const isIgnored = (linterName, path) => { const isIgnored = (linterName, path) => {
return ( return (
iconIgnored[linterName] && iconIgnored[linterName].hasOwnProperty(path) iconIgnored[linterName] && Object.hasOwn(iconIgnored[linterName], path)
); );
}; };
const ignoreIcon = (linterName, path, $) => { const ignoreIcon = (linterName, path, $) => {
if (!iconIgnored[linterName]) { iconIgnored[linterName] ||= {};
iconIgnored[linterName] = {};
}
const title = $.find('title').text(); const title = $.find('title').text();
const iconName = htmlFriendlyToTitle(title); const iconName = htmlFriendlyToTitle(title);
@ -173,7 +180,7 @@ const ignoreIcon = (linterName, path, $) => {
iconIgnored[linterName][path] = iconName; iconIgnored[linterName][path] = iconName;
}; };
export default { export const config = {
rules: { rules: {
elm: { elm: {
svg: 1, svg: 1,
@ -183,7 +190,7 @@ export default {
}, },
attr: [ attr: [
{ {
// ensure that the SVG element has the appropriate attributes // Ensure that the SVG element has the appropriate attributes
// alphabetically ordered // alphabetically ordered
role: 'img', role: 'img',
viewBox: `0 0 ${iconSize} ${iconSize}`, viewBox: `0 0 ${iconSize} ${iconSize}`,
@ -193,12 +200,12 @@ export default {
'rule::order': true, 'rule::order': true,
}, },
{ {
// ensure that the title element has the appropriate attribute // Ensure that the title element has the appropriate attribute
'rule::selector': 'svg > title', 'rule::selector': 'svg > title',
'rule::whitelist': true, 'rule::whitelist': true,
}, },
{ {
// ensure that the path element only has the 'd' attribute // Ensure that the path element only has the 'd' attribute
// (no style, opacity, etc.) // (no style, opacity, etc.)
d: SVG_PATH_REGEX, d: SVG_PATH_REGEX,
'rule::selector': 'svg > path', 'rule::selector': 'svg > path',
@ -209,15 +216,15 @@ export default {
(reporter, $, ast) => { (reporter, $, ast) => {
reporter.name = 'icon-title'; reporter.name = 'icon-title';
const iconTitleText = $.find('title').text(), const iconTitleText = $.find('title').text();
xmlNamedEntitiesCodepoints = [38, 60, 62], const xmlNamedEntitiesCodepoints = [38, 60, 62];
xmlNamedEntities = ['amp', 'lt', 'gt']; const xmlNamedEntities = ['amp', 'lt', 'gt'];
let _validCodepointsRepr = true; let _validCodepointsRepr = true;
// avoid character codepoints as hexadecimal representation // Avoid character codepoints as hexadecimal representation
const hexadecimalCodepoints = Array.from( const hexadecimalCodepoints = [
iconTitleText.matchAll(/&#x([A-Fa-f0-9]+);/g), ...iconTitleText.matchAll(/&#x([A-Fa-f\d]+);/g),
); ];
if (hexadecimalCodepoints.length > 0) { if (hexadecimalCodepoints.length > 0) {
_validCodepointsRepr = false; _validCodepointsRepr = false;
@ -245,10 +252,10 @@ export default {
} }
} }
// avoid character codepoints as named entities // Avoid character codepoints as named entities
const namedEntitiesCodepoints = Array.from( const namedEntitiesCodepoints = [
iconTitleText.matchAll(/&([A-Za-z0-9]+);/g), ...iconTitleText.matchAll(/&([A-Za-z\d]+);/g),
); ];
if (namedEntitiesCodepoints.length > 0) { if (namedEntitiesCodepoints.length > 0) {
for (const match of namedEntitiesCodepoints) { for (const match of namedEntitiesCodepoints) {
const namedEntiyReprIndex = const namedEntiyReprIndex =
@ -261,16 +268,15 @@ export default {
if ( if (
namedEntityJsRepr === undefined || namedEntityJsRepr === undefined ||
namedEntityJsRepr.length != 1 namedEntityJsRepr.length !== 1
) { ) {
replacement = 'its decimal or literal representation'; replacement = 'its decimal or literal representation';
} else { } else {
const namedEntityDec = namedEntityJsRepr.codePointAt(0); const namedEntityDec = namedEntityJsRepr.codePointAt(0);
if (namedEntityDec < 128) { replacement =
replacement = `"${namedEntityJsRepr}"`; namedEntityDec < 128
} else { ? `"${namedEntityJsRepr}"`
replacement = `"&#${namedEntityDec};"`; : `"&#${namedEntityDec};"`;
}
} }
reporter.error( reporter.error(
@ -283,11 +289,11 @@ export default {
} }
if (_validCodepointsRepr) { if (_validCodepointsRepr) {
// compare encoded title with original title and report error if not equal // Compare encoded title with original title and report error if not equal
const encodingMatches = Array.from( const encodingMatches = [
iconTitleText.matchAll(/&(#([0-9]+)|(amp|quot|lt|gt));/g), ...iconTitleText.matchAll(/&(#(\d+)|(amp|quot|lt|gt));/g),
), ];
encodedBuf = []; const encodedBuf = [];
const indexesToIgnore = []; const indexesToIgnore = [];
for (const match of encodingMatches) { for (const match of encodingMatches) {
@ -300,8 +306,8 @@ export default {
if (indexesToIgnore.includes(i)) { if (indexesToIgnore.includes(i)) {
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)
let charDecimalCode = iconTitleText.charCodeAt(i); const charDecimalCode = iconTitleText.codePointAt(i);
if (charDecimalCode > 127) { if (charDecimalCode > 127) {
encodedBuf.unshift(`&#${charDecimalCode};`); encodedBuf.unshift(`&#${charDecimalCode};`);
@ -318,6 +324,7 @@ export default {
} }
} }
} }
const encodedIconTitleText = encodedBuf.join(''); const encodedIconTitleText = encodedBuf.join('');
if (encodedIconTitleText !== iconTitleText) { if (encodedIconTitleText !== iconTitleText) {
_validCodepointsRepr = false; _validCodepointsRepr = false;
@ -328,17 +335,20 @@ export default {
); );
} }
// 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) => !isNaN(m[2]))) { for (const match of encodingMatches.filter((m) => !isNaN(m[2]))) {
const decimalNumber = parseInt(match[2]); const decimalNumber = Number.parseInt(match[2], 10);
if (decimalNumber > 127) { if (decimalNumber > 127) {
continue; continue;
} }
_validCodepointsRepr = false; _validCodepointsRepr = false;
const decimalCodepointCharIndex = const decimalCodepointCharIndex =
getTitleTextIndex(ast.source) + match.index + 1; getTitleTextIndex(ast.source) + match.index + 1;
let replacement;
if (xmlNamedEntitiesCodepoints.includes(decimalNumber)) { if (xmlNamedEntitiesCodepoints.includes(decimalNumber)) {
replacement = `"&${ replacement = `"&${
xmlNamedEntities[ xmlNamedEntities[
@ -347,7 +357,7 @@ export default {
};"`; };"`;
} else { } else {
replacement = String.fromCodePoint(decimalNumber); replacement = String.fromCodePoint(decimalNumber);
replacement = replacement == '"' ? `'"'` : `"${replacement}"`; replacement = replacement === '"' ? `'"'` : `"${replacement}"`;
} }
reporter.error( reporter.error(
@ -379,8 +389,8 @@ export default {
} }
const [minX, minY, maxX, maxY] = getIconPathBbox(iconPath); const [minX, minY, maxX, maxY] = getIconPathBbox(iconPath);
const width = +(maxX - minX).toFixed(iconFloatPrecision); const width = Number((maxX - minX).toFixed(iconFloatPrecision));
const height = +(maxY - minY).toFixed(iconFloatPrecision); const height = Number((maxY - minY).toFixed(iconFloatPrecision));
if (width === 0 && height === 0) { if (width === 0 && height === 0) {
reporter.error( reporter.error(
@ -407,24 +417,26 @@ export default {
for (const segment of segments) { for (const segment of segments) {
const precisionMax = Math.max( const precisionMax = Math.max(
// eslint-disable-next-line unicorn/no-array-callback-reference
...segment.params.slice(1).map(countDecimals), ...segment.params.slice(1).map(countDecimals),
); );
if (precisionMax > iconMaxFloatPrecision) { if (precisionMax > iconMaxFloatPrecision) {
let errorMsg = let errorMessage =
`found ${precisionMax} decimals in segment` + `found ${precisionMax} decimals in segment` +
` "${iconPath.substring(segment.start, segment.end)}"`; ` "${iconPath.slice(segment.start, segment.end)}"`;
if (segment.chained) { if (segment.chained) {
const readableChain = maybeShortenedWithEllipsis( const readableChain = maybeShortenedWithEllipsis(
iconPath.substring(segment.chainStart, segment.chainEnd), iconPath.slice(segment.chainStart, segment.chainEnd),
); );
errorMsg += ` of chain "${readableChain}"`; errorMessage += ` of chain "${readableChain}"`;
} }
errorMsg += ` at index ${
errorMessage += ` at index ${
segment.start + getPathDIndex(ast.source) segment.start + getPathDIndex(ast.source)
}`; }`;
reporter.error( reporter.error(
'Maximum precision should not be greater than' + 'Maximum precision should not be greater than' +
` ${iconMaxFloatPrecision}; ${errorMsg}`, ` ${iconMaxFloatPrecision}; ${errorMessage}`,
); );
} }
} }
@ -446,10 +458,10 @@ export default {
]; ];
const upperMovementCommands = ['M', 'L']; const upperMovementCommands = ['M', 'L'];
const upperHorDirectionCommand = 'H'; const upperHorDirectionCommand = 'H';
const upperVerDirectionCommand = 'V'; const upperVersionDirectionCommand = 'V';
const upperDirectionCommands = [ const upperDirectionCommands = [
upperHorDirectionCommand, upperHorDirectionCommand,
upperVerDirectionCommand, upperVersionDirectionCommand,
]; ];
const upperCurveCommand = 'C'; const upperCurveCommand = 'C';
const upperShorthandCurveCommand = 'S'; const upperShorthandCurveCommand = 'S';
@ -458,23 +470,25 @@ export default {
upperShorthandCurveCommand, upperShorthandCurveCommand,
]; ];
const curveCommands = [...lowerCurveCommands, ...upperCurveCommands]; const curveCommands = [...lowerCurveCommands, ...upperCurveCommands];
const commands = [ const commands = new Set([
...lowerMovementCommands, ...lowerMovementCommands,
...lowerDirectionCommands, ...lowerDirectionCommands,
...upperMovementCommands, ...upperMovementCommands,
...upperDirectionCommands, ...upperDirectionCommands,
...curveCommands, ...curveCommands,
]; ]);
const isInvalidSegment = ( const isInvalidSegment = (
[command, x1Coord, y1Coord, ...rest], [command, x1Coord, y1Coord, ...rest],
index, index,
previousSegmentIsZ, previousSegmentIsZ,
) => { ) => {
if (commands.includes(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) {
return true; return true;
} }
// Relative movement (m or l) having a distance of 0 // Relative movement (m or l) having a distance of 0
if ( if (
index > 0 && index > 0 &&
@ -486,6 +500,7 @@ export default {
// a relative placement (m) as if it were absolute (M) // a relative placement (m) as if it were absolute (M)
return command.toLowerCase() === 'm' ? !previousSegmentIsZ : true; return command.toLowerCase() === 'm' ? !previousSegmentIsZ : true;
} }
if ( if (
lowerCurveCommands.includes(command) && lowerCurveCommands.includes(command) &&
x1Coord === 0 && x1Coord === 0 &&
@ -503,45 +518,49 @@ export default {
return true; return true;
} }
} }
if (index > 0) { if (index > 0) {
let [yPrevCoord, xPrevCoord] = [ let [yPreviousCoord, xPreviousCoord] = [
...absSegments[index - 1], ...absSegments[index - 1],
].reverse(); ].reverse();
// If the previous command was a direction one, // If the previous command was a direction one,
// we need to iterate back until we find the missing coordinates // we need to iterate back until we find the missing coordinates
if (upperDirectionCommands.includes(xPrevCoord)) { if (upperDirectionCommands.includes(xPreviousCoord)) {
xPrevCoord = undefined; xPreviousCoord = undefined;
yPrevCoord = undefined; yPreviousCoord = undefined;
let idx = index; let index_ = index;
while ( while (
--idx > 0 && --index_ > 0 &&
(xPrevCoord === undefined || yPrevCoord === undefined) (xPreviousCoord === undefined || yPreviousCoord === undefined)
) { ) {
let [yPrevCoordDeep, xPrevCoordDeep] = [ let [yPreviousCoordDeep, xPreviousCoordDeep] = [
...absSegments[idx], ...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 === xPrevCoordDeep) { if (upperHorDirectionCommand === xPreviousCoordDeep) {
xPrevCoordDeep = yPrevCoordDeep; xPreviousCoordDeep = yPreviousCoordDeep;
yPrevCoordDeep = undefined; yPreviousCoordDeep = undefined;
} }
// If the previous command was a vertical movement, // If the previous command was a vertical movement,
// we need to consider the single coordinate as y // we need to consider the single coordinate as y
if (upperVerDirectionCommand === xPrevCoordDeep) { if (upperVersionDirectionCommand === xPreviousCoordDeep) {
xPrevCoordDeep = undefined; xPreviousCoordDeep = undefined;
} }
if ( if (
xPrevCoord === undefined && xPreviousCoord === undefined &&
xPrevCoordDeep !== undefined xPreviousCoordDeep !== undefined
) { ) {
xPrevCoord = xPrevCoordDeep; xPreviousCoord = xPreviousCoordDeep;
} }
if ( if (
yPrevCoord === undefined && yPreviousCoord === undefined &&
yPrevCoordDeep !== undefined yPreviousCoordDeep !== undefined
) { ) {
yPrevCoord = yPrevCoordDeep; yPreviousCoord = yPreviousCoordDeep;
} }
} }
} }
@ -553,20 +572,21 @@ export default {
// and a control point equal to the ending point // and a control point equal to the ending point
if ( if (
upperShorthandCurveCommand === command && upperShorthandCurveCommand === command &&
x1Coord === xPrevCoord && x1Coord === xPreviousCoord &&
y1Coord === yPrevCoord && y1Coord === yPreviousCoord &&
x1Coord === x2Coord && x1Coord === x2Coord &&
y1Coord === y2Coord y1Coord === y2Coord
) { ) {
return true; return true;
} }
// Absolute bézier curve (C) having // Absolute bézier curve (C) having
// the same coordinate as the previous segment // the same coordinate as the previous segment
// and last control point equal to the ending point // and last control point equal to the ending point
if ( if (
upperCurveCommand === command && upperCurveCommand === command &&
x1Coord === xPrevCoord && x1Coord === xPreviousCoord &&
y1Coord === yPrevCoord && y1Coord === yPreviousCoord &&
x2Coord === xCoord && x2Coord === xCoord &&
y2Coord === yCoord y2Coord === yCoord
) { ) {
@ -578,16 +598,16 @@ export default {
// Absolute horizontal direction (H) having // Absolute horizontal direction (H) having
// the same x coordinate as the previous segment // the same x coordinate as the previous segment
(upperHorDirectionCommand === command && (upperHorDirectionCommand === command &&
x1Coord === xPrevCoord) || x1Coord === xPreviousCoord) ||
// Absolute vertical direction (V) having // Absolute vertical direction (V) having
// the same y coordinate as the previous segment // the same y coordinate as the previous segment
(upperVerDirectionCommand === command && (upperVersionDirectionCommand === command &&
x1Coord === yPrevCoord) || x1Coord === yPreviousCoord) ||
// Absolute movement (M or L) having the same // Absolute movement (M or L) having the same
// coordinate as the previous segment // coordinate as the previous segment
(upperMovementCommands.includes(command) && (upperMovementCommands.includes(command) &&
x1Coord === xPrevCoord && x1Coord === xPreviousCoord &&
y1Coord === yPrevCoord) y1Coord === yPreviousCoord)
); );
} }
} }
@ -599,13 +619,13 @@ export default {
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.params, index, previousSegmentIsZ)) {
const [command, x1, y1, ...rest] = segment.params; const [command, _x1, _y1, ...rest] = segment.params;
let errorMsg = `Ineffective segment "${iconPath.substring( let errorMessage = `Ineffective segment "${iconPath.slice(
segment.start, segment.start,
segment.end, segment.end,
)}" found`, )}" found`;
resolutionTip = 'should be removed'; let resolutionTip = 'should be removed';
if (curveCommands.includes(command)) { if (curveCommands.includes(command)) {
const [x2, y2, x, y] = rest; const [x2, y2, x, y] = rest;
@ -618,16 +638,19 @@ export default {
x2, x2,
)} ${removeLeadingZeros(y2)}" or removed`; )} ${removeLeadingZeros(y2)}" or removed`;
} }
if (command === upperShorthandCurveCommand) { if (command === upperShorthandCurveCommand) {
resolutionTip = `should be "L${removeLeadingZeros( resolutionTip = `should be "L${removeLeadingZeros(
x2, x2,
)} ${removeLeadingZeros(y2)}" or removed`; )} ${removeLeadingZeros(y2)}" or removed`;
} }
if (command === lowerCurveCommand && (x !== 0 || y !== 0)) { if (command === lowerCurveCommand && (x !== 0 || y !== 0)) {
resolutionTip = `should be "l${removeLeadingZeros( resolutionTip = `should be "l${removeLeadingZeros(
x, x,
)} ${removeLeadingZeros(y)}" or removed`; )} ${removeLeadingZeros(y)}" or removed`;
} }
if (command === upperCurveCommand) { if (command === upperCurveCommand) {
resolutionTip = `should be "L${removeLeadingZeros( resolutionTip = `should be "L${removeLeadingZeros(
x, x,
@ -637,15 +660,16 @@ export default {
if (segment.chained) { if (segment.chained) {
const readableChain = maybeShortenedWithEllipsis( const readableChain = maybeShortenedWithEllipsis(
iconPath.substring(segment.chainStart, segment.chainEnd), iconPath.slice(segment.chainStart, segment.chainEnd),
); );
errorMsg += ` in chain "${readableChain}"`; errorMessage += ` in chain "${readableChain}"`;
} }
errorMsg += ` at index ${
errorMessage += ` at index ${
segment.start + getPathDIndex(ast.source) segment.start + getPathDIndex(ast.source)
}`; }`;
reporter.error(`${errorMsg} (${resolutionTip})`); reporter.error(`${errorMessage} (${resolutionTip})`);
} }
} }
}, },
@ -657,192 +681,222 @@ export default {
* (does not extracts collinear coordinates from curves). * (does not extracts collinear coordinates from curves).
**/ **/
const getCollinearSegments = (iconPath) => { const getCollinearSegments = (iconPath) => {
const segments = getIconPathSegments(iconPath), const segments = getIconPathSegments(iconPath);
collinearSegments = [], const collinearSegments = [];
straightLineCommands = 'HhVvLlMm'; const straightLineCommands = 'HhVvLlMm';
let currLine = [], let currentLine = [];
currAbsCoord = [undefined, undefined], let currentAbsCoord = [undefined, undefined];
startPoint, let startPoint;
_inStraightLine = false, let _inStraightLine = false;
_nextInStraightLine = false, let _nextInStraightLine = false;
_resetStartPoint = false; let _resetStartPoint = false;
for (let s = 0; s < segments.length; s++) { for (let s = 0; s < segments.length; s++) {
const seg = segments[s], const seg = segments[s];
parms = seg.params, const parms = seg.params;
cmd = parms[0], const cmd = parms[0];
nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; const nextCmd = s + 1 < segments.length ? segments[s + 1][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': {
currAbsCoord[0] = parms[1]; currentAbsCoord[0] = parms[1];
currAbsCoord[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,
// the subsequent pairs are treated as implicit lineto commands. // the subsequent pairs are treated as implicit lineto commands.
if (!seg.chained || seg.chainStart === seg.start) { if (!seg.chained || seg.chainStart === seg.start) {
startPoint = undefined; startPoint = undefined;
} }
break; break;
case 'm': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; case 'm': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
if (!seg.chained || seg.chainStart === seg.start) { if (!seg.chained || seg.chainStart === seg.start) {
startPoint = undefined; startPoint = undefined;
} }
break; break;
case 'H': }
currAbsCoord[0] = parms[1];
case 'H': {
currentAbsCoord[0] = parms[1];
break; break;
case 'h': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; case 'h': {
currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
break; break;
case 'V': }
currAbsCoord[1] = parms[1];
case 'V': {
currentAbsCoord[1] = parms[1];
break; break;
case 'v': }
currAbsCoord[1] =
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[1]; case 'v': {
currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1];
break; break;
case 'L': }
currAbsCoord[0] = parms[1];
currAbsCoord[1] = parms[2]; case 'L': {
currentAbsCoord[0] = parms[1];
currentAbsCoord[1] = parms[2];
break; break;
case 'l': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; case 'l': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
}
case 'Z': case 'Z':
case 'z': case 'z': {
// TODO: Overlapping in Z should be handled in another rule // TODO: Overlapping in Z should be handled in another rule
currAbsCoord = [startPoint[0], startPoint[1]]; currentAbsCoord = [startPoint[0], startPoint[1]];
_resetStartPoint = true; _resetStartPoint = true;
break; break;
case 'C': }
currAbsCoord[0] = parms[5];
currAbsCoord[1] = parms[6]; case 'C': {
currentAbsCoord[0] = parms[5];
currentAbsCoord[1] = parms[6];
break; break;
case 'c': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[5]; case 'c': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[6]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6];
break; break;
case 'A': }
currAbsCoord[0] = parms[6];
currAbsCoord[1] = parms[7]; case 'A': {
currentAbsCoord[0] = parms[6];
currentAbsCoord[1] = parms[7];
break; break;
case 'a': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[6]; case 'a': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[7]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7];
break; break;
case 's': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; case 's': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
case 'S': }
currAbsCoord[0] = parms[1];
currAbsCoord[1] = parms[2]; case 'S': {
currentAbsCoord[0] = parms[1];
currentAbsCoord[1] = parms[2];
break; break;
case 't': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; case 't': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2];
break; break;
case 'T': }
currAbsCoord[0] = parms[1];
currAbsCoord[1] = parms[2]; case 'T': {
currentAbsCoord[0] = parms[1];
currentAbsCoord[1] = parms[2];
break; break;
case 'Q': }
currAbsCoord[0] = parms[3];
currAbsCoord[1] = parms[4]; case 'Q': {
currentAbsCoord[0] = parms[3];
currentAbsCoord[1] = parms[4];
break; break;
case 'q': }
currAbsCoord[0] =
(!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[3]; case 'q': {
currAbsCoord[1] = currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3];
(!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[4]; currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4];
break; break;
default: }
default: {
throw new Error(`"${cmd}" command not handled`); throw new Error(`"${cmd}" command not handled`);
} }
}
if (startPoint === undefined) { if (startPoint === undefined) {
startPoint = [currAbsCoord[0], currAbsCoord[1]]; startPoint = [currentAbsCoord[0], currentAbsCoord[1]];
} else if (_resetStartPoint) { } else if (_resetStartPoint) {
startPoint = undefined; startPoint = undefined;
_resetStartPoint = false; _resetStartPoint = false;
} }
_nextInStraightLine = straightLineCommands.includes(nextCmd); _nextInStraightLine = straightLineCommands.includes(nextCmd);
let _exitingStraightLine = _inStraightLine && !_nextInStraightLine; const _exitingStraightLine =
_inStraightLine && !_nextInStraightLine;
_inStraightLine = straightLineCommands.includes(cmd); _inStraightLine = straightLineCommands.includes(cmd);
if (_inStraightLine) { if (_inStraightLine) {
currLine.push([currAbsCoord[0], currAbsCoord[1]]); currentLine.push([currentAbsCoord[0], currentAbsCoord[1]]);
} else { } else {
if (_exitingStraightLine) { if (_exitingStraightLine) {
if (straightLineCommands.includes(cmd)) { if (straightLineCommands.includes(cmd)) {
currLine.push([currAbsCoord[0], currAbsCoord[1]]); currentLine.push([currentAbsCoord[0], currentAbsCoord[1]]);
} }
// Get collinear coordinates // Get collinear coordinates
for (let p = 1; p < currLine.length - 1; p++) { for (let p = 1; p < currentLine.length - 1; p++) {
let _collinearCoord = collinear( const _collinearCoord = collinear(
currLine[p - 1][0], currentLine[p - 1][0],
currLine[p - 1][1], currentLine[p - 1][1],
currLine[p][0], currentLine[p][0],
currLine[p][1], currentLine[p][1],
currLine[p + 1][0], currentLine[p + 1][0],
currLine[p + 1][1], currentLine[p + 1][1],
); );
if (_collinearCoord) { if (_collinearCoord) {
collinearSegments.push( collinearSegments.push(
segments[s - currLine.length + p + 1], segments[s - currentLine.length + p + 1],
); );
} }
} }
} }
currLine = [];
currentLine = [];
} }
} }
return collinearSegments; return collinearSegments;
}; };
const iconPath = getIconPath($, filepath), const iconPath = getIconPath($, filepath);
collinearSegments = getCollinearSegments(iconPath); const collinearSegments = getCollinearSegments(iconPath);
if (collinearSegments.length === 0) { if (collinearSegments.length === 0) {
return; return;
} }
const pathDIndex = getPathDIndex(ast.source); const pathDIndex = getPathDIndex(ast.source);
for (const segment of collinearSegments) { for (const segment of collinearSegments) {
let errorMsg = `Collinear segment "${iconPath.substring( let errorMessage = `Collinear segment "${iconPath.slice(
segment.start, segment.start,
segment.end, segment.end,
)}" found`; )}" found`;
if (segment.chained) { if (segment.chained) {
let readableChain = maybeShortenedWithEllipsis( const readableChain = maybeShortenedWithEllipsis(
iconPath.substring(segment.chainStart, segment.chainEnd), iconPath.slice(segment.chainStart, segment.chainEnd),
); );
errorMsg += ` in chain "${readableChain}"`; errorMessage += ` in chain "${readableChain}"`;
} }
errorMsg += ` at index ${
errorMessage += ` at index ${
segment.start + pathDIndex segment.start + pathDIndex
} (should be removed)`; } (should be removed)`;
reporter.error(errorMsg); reporter.error(errorMessage);
} }
}, },
(reporter, $, ast) => { (reporter, $, ast) => {
@ -867,10 +921,8 @@ export default {
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($, filepath);
// Find negative zeros inside path // Find negative zeros inside path
const negativeZeroMatches = Array.from( const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)];
iconPath.matchAll(negativeZerosRegexp), if (negativeZeroMatches.length > 0) {
);
if (negativeZeroMatches.length) {
// Calculate the index for each match in the file // Calculate the index for each match in the file
const pathDIndex = getPathDIndex(ast.source); const pathDIndex = getPathDIndex(ast.source);
@ -896,9 +948,9 @@ export default {
} }
const [minX, minY, maxX, maxY] = getIconPathBbox(iconPath); const [minX, minY, maxX, maxY] = getIconPathBbox(iconPath);
const centerX = +((minX + maxX) / 2).toFixed(iconFloatPrecision); const centerX = Number(((minX + maxX) / 2).toFixed(iconFloatPrecision));
const devianceX = centerX - iconTargetCenter; const devianceX = centerX - iconTargetCenter;
const centerY = +((minY + maxY) / 2).toFixed(iconFloatPrecision); const centerY = Number(((minY + maxY) / 2).toFixed(iconFloatPrecision));
const devianceY = centerY - iconTargetCenter; const devianceY = centerY - iconTargetCenter;
if ( if (
@ -920,38 +972,38 @@ export default {
const iconPath = getIconPath($, filepath); const iconPath = getIconPath($, filepath);
if (!SVG_PATH_REGEX.test(iconPath)) { if (!SVG_PATH_REGEX.test(iconPath)) {
let errorMsg = 'Invalid path format', const errorMessage = 'Invalid path format';
reason; let reason;
if (!iconPath.startsWith('M') && !iconPath.startsWith('m')) { if (!iconPath.startsWith('M') && !iconPath.startsWith('m')) {
// doesn't start with moveto // Doesn't start with moveto
reason = reason =
'should start with "moveto" command ("M" or "m"),' + 'should start with "moveto" command ("M" or "m"),' +
` but starts with \"${iconPath[0]}\"`; ` but starts with "${iconPath[0]}"`;
reporter.error(`${errorMsg}: ${reason}`); reporter.error(`${errorMessage}: ${reason}`);
} }
const validPathCharacters = SVG_PATH_REGEX.source.replace( const validPathCharacters = SVG_PATH_REGEX.source.replaceAll(
/[\[\]+^$]/g, /[[\]+^$]/g,
'', '',
), );
invalidCharactersMsgs = [], const invalidCharactersMsgs = [];
pathDIndex = getPathDIndex(ast.source); const pathDIndex = getPathDIndex(ast.source);
for (let [i, char] of Object.entries(iconPath)) { for (const [i, char] of Object.entries(iconPath)) {
if (validPathCharacters.indexOf(char) === -1) { if (!validPathCharacters.includes(char)) {
invalidCharactersMsgs.push( invalidCharactersMsgs.push(
`"${char}" at index ${pathDIndex + parseInt(i)}`, `"${char}" at index ${pathDIndex + Number.parseInt(i, 10)}`,
); );
} }
} }
// contains invalid characters // Contains invalid characters
if (invalidCharactersMsgs.length > 0) { if (invalidCharactersMsgs.length > 0) {
reason = `unexpected character${ reason = `unexpected character${
invalidCharactersMsgs.length > 1 ? 's' : '' invalidCharactersMsgs.length > 1 ? 's' : ''
} found (${invalidCharactersMsgs.join(', ')})`; } found (${invalidCharactersMsgs.join(', ')})`;
reporter.error(`${errorMsg}: ${reason}`); reporter.error(`${errorMessage}: ${reason}`);
} }
} }
}, },

View File

@ -1,4 +1,4 @@
export default { const config = {
multipass: true, multipass: true,
eol: 'lf', eol: 'lf',
plugins: [ plugins: [
@ -62,7 +62,7 @@ export default {
// Convert basic shapes (such as <circle>) to <path> // Convert basic shapes (such as <circle>) to <path>
name: 'convertShapeToPath', name: 'convertShapeToPath',
params: { params: {
// including <arc> // Including <arc>
convertArcs: true, convertArcs: true,
}, },
}, },
@ -102,3 +102,5 @@ export default {
'reusePaths', 'reusePaths',
], ],
}; };
export default config;

View File

@ -1,5 +1,5 @@
import { test } from 'mocha';
import {strict as assert} from 'node:assert'; import {strict as assert} from 'node:assert';
import {test} from 'mocha';
import {getThirdPartyExtensions} from '../sdk.mjs'; import {getThirdPartyExtensions} from '../sdk.mjs';
test('README third party extensions must be alphabetically sorted', async () => { test('README third party extensions must be alphabetically sorted', async () => {
@ -7,10 +7,10 @@ test('README third party extensions must be alphabetically sorted', async () =>
assert.ok(thirdPartyExtensions.length > 0); assert.ok(thirdPartyExtensions.length > 0);
const thirdPartyExtensionsNames = thirdPartyExtensions.map( const thirdPartyExtensionsNames = thirdPartyExtensions.map(
(ext) => ext.module.name, (extension) => extension.module.name,
); );
const expectedOrder = thirdPartyExtensionsNames.slice().sort(); const expectedOrder = [...thirdPartyExtensionsNames].sort();
assert.deepEqual( assert.deepEqual(
thirdPartyExtensionsNames, thirdPartyExtensionsNames,
expectedOrder, expectedOrder,

View File

@ -1,5 +1,5 @@
import { getIconsData, getIconSlug, slugToVariableName } from '../sdk.mjs';
import * as simpleIcons from '../index.mjs'; import * as simpleIcons from '../index.mjs';
import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs';
import {testIcon} from './test-icon.js'; import {testIcon} from './test-icon.js';
for (const icon of await getIconsData()) { for (const icon of await getIconsData()) {

View File

@ -1,6 +1,6 @@
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';
import { strict as assert } from 'node:assert';
import {describe, it} from 'mocha'; import {describe, it} from 'mocha';
import { import {
SVG_PATH_REGEX, SVG_PATH_REGEX,
@ -9,7 +9,7 @@ import {
titleToSlug, titleToSlug,
} from '../sdk.mjs'; } from '../sdk.mjs';
const iconsDir = path.resolve( const iconsDirectory = path.resolve(
getDirnameFromImportMeta(import.meta.url), getDirnameFromImportMeta(import.meta.url),
'..', '..',
'icons', 'icons',
@ -26,7 +26,7 @@ const iconsDir = path.resolve(
* @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(iconsDir, `${slug}.svg`); const svgPath = path.resolve(iconsDirectory, `${slug}.svg`);
describe(icon.title, () => { describe(icon.title, () => {
it('has the correct "title"', () => { it('has the correct "title"', () => {
@ -81,7 +81,7 @@ export const testIcon = (icon, subject, slug) => {
}); });
if (icon.slug) { if (icon.slug) {
// if an icon data has a slug, it must be different to the // If an icon data has a slug, it must be different to the
// slug inferred from the title, which prevents adding // slug inferred from the title, which prevents adding
// unnecessary slugs to icons data // unnecessary slugs to icons data
it(`'${icon.title}' slug must be necessary`, () => { it(`'${icon.title}' slug must be necessary`, () => {

5
types.d.ts vendored
View File

@ -5,6 +5,7 @@
*/ */
export type License = SPDXLicense | CustomLicense; export type License = SPDXLicense | CustomLicense;
// eslint-disable-next-line @typescript-eslint/naming-convention
export type SPDXLicense = { export type SPDXLicense = {
type: string; type: string;
url: string; url: string;
@ -18,7 +19,7 @@ export type CustomLicense = {
/** /**
* The data for a Simple Icon as is exported by the npm package. * The data for a Simple Icon as is exported by the npm package.
*/ */
export interface SimpleIcon { export type SimpleIcon = {
title: string; title: string;
slug: string; slug: string;
svg: string; svg: string;
@ -27,4 +28,4 @@ export interface SimpleIcon {
hex: string; hex: string;
guidelines?: string; guidelines?: string;
license?: License; license?: License;
} };