From 69c4799a180911b2c8aceeea2895914b5495d8bd Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 14 Mar 2024 17:12:56 +0800 Subject: [PATCH] Add XO linter --- .github/renovate.json5 | 2 +- .github/workflows/create-release.yml | 8 +- .github/workflows/publish.yml | 6 +- .jsonschema.json | 8 +- .prettierignore | 9 - .prettierrc.json | 3 +- .xo-config.json | 49 ++ package.json | 26 +- scripts/add-icon-data.js | 29 +- scripts/build/clean.js | 38 +- scripts/build/package.js | 65 +- ...icon-object.js => icon-object.js.template} | 0 scripts/get-filename.js | 8 +- scripts/lint/jsonlint.js | 11 +- scripts/lint/ourlint.js | 40 +- scripts/release/reformat-markdown.js | 24 +- scripts/release/update-cdn-urls.js | 23 +- scripts/release/update-sdk-ts-defs.js | 34 +- scripts/release/update-slugs-table.js | 14 +- scripts/release/update-svgs-count.js | 20 +- scripts/utils.js | 16 +- sdk.d.ts | 16 +- sdk.mjs | 64 +- .svglintrc.mjs => svglint.config.mjs | 566 ++++++++++-------- svgo.config.mjs | 8 +- tests/docs.test.js | 10 +- tests/index.test.js | 4 +- tests/min-reporter.cjs | 4 +- tests/test-icon.js | 10 +- types.d.ts | 5 +- 30 files changed, 630 insertions(+), 490 deletions(-) create mode 100644 .xo-config.json mode change 100644 => 100755 scripts/add-icon-data.js mode change 100644 => 100755 scripts/build/clean.js mode change 100644 => 100755 scripts/build/package.js rename scripts/build/templates/{icon-object.js => icon-object.js.template} (100%) mode change 100644 => 100755 scripts/get-filename.js mode change 100644 => 100755 scripts/lint/jsonlint.js mode change 100644 => 100755 scripts/lint/ourlint.js mode change 100644 => 100755 scripts/release/reformat-markdown.js mode change 100644 => 100755 scripts/release/update-cdn-urls.js mode change 100644 => 100755 scripts/release/update-sdk-ts-defs.js mode change 100644 => 100755 scripts/release/update-slugs-table.js mode change 100644 => 100755 scripts/release/update-svgs-count.js rename .svglintrc.mjs => svglint.config.mjs (65%) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 051d2a373fd9..a6accf89b68d 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -16,5 +16,5 @@ separateMajorMinor: false, // We manually update digest dependencies (eg. hashes in Github actions) - digest: { enabled: false }, + digest: {enabled: false}, } diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 77a4b5243840..7eacca8d50b4 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -54,13 +54,13 @@ jobs: - name: Install dependencies run: npm i --ignore-scripts --no-audit --no-fund - 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 - run: node ./scripts/release/update-svgs-count.js + run: ./scripts/release/update-svgs-count.js - name: Update slugs table - run: node ./scripts/release/update-slugs-table.js + run: ./scripts/release/update-slugs-table.js - 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 uses: stefanzweifel/git-auto-commit-action@v5 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7158e9541323..24af181537e4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,9 +43,9 @@ jobs: - name: Install dependencies run: npm i --ignore-scripts --no-audit --no-fund - 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 - run: node ./scripts/release/update-sdk-ts-defs.js + run: ./scripts/release/update-sdk-ts-defs.js - name: Build NodeJS package run: npm run build - name: Deploy to NPM @@ -65,7 +65,7 @@ jobs: - id: get-version uses: ./.github/actions/get-version - 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 run: | git config user.name "${GITHUB_ACTOR}" diff --git a/.jsonschema.json b/.jsonschema.json index 881e74011c20..2bc5ac875b18 100644 --- a/.jsonschema.json +++ b/.jsonschema.json @@ -32,12 +32,12 @@ "aka": { "description": "The brand is also known as (e.g. full length name or abbreviation)", "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} }, "dup": { "description": "Different brands that use the exact same icon", "type": "array", - "items": { "$ref": "#/definitions/duplicate" } + "items": {"$ref": "#/definitions/duplicate"} }, "loc": { "description": "Localized names of the brand", @@ -46,7 +46,7 @@ "old": { "description": "Old names, for backwards compatibility", "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} } }, "minProperties": 1, @@ -702,7 +702,7 @@ "icons": { "description": "A list of brands", "type": "array", - "items": { "$ref": "#/definitions/brand" } + "items": {"$ref": "#/definitions/brand"} } }, "additionalProperties": false, diff --git a/.prettierignore b/.prettierignore index a2378b87c728..c055463eec7b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,12 +6,3 @@ # We use our own formatting for the data files. _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 diff --git a/.prettierrc.json b/.prettierrc.json index 544138be4565..a0f24c991eba 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,4 @@ { - "singleQuote": true + "singleQuote": true, + "bracketSpacing": false } diff --git a/.xo-config.json b/.xo-config.json new file mode 100644 index 000000000000..6cd08b29b73c --- /dev/null +++ b/.xo-config.json @@ -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" + } + ] +} diff --git a/package.json b/package.json index f73db04c221e..e8cade0f1185 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "chalk": "5.3.0", "editorconfig-checker": "5.1.5", "esbuild": "0.19.4", + "eslint-plugin-import": "2.29.1", "fake-diff": "1.0.0", "fast-fuzzy": "1.12.0", "get-relative-luminance": "1.0.0", @@ -97,23 +98,24 @@ "markdown-link-check": "3.11.2", "mocha": "10.2.0", "named-html-entities-json": "1.0.0", - "prettier": "3.0.3", "svg-path-bbox": "1.2.5", "svg-path-segments": "1.0.0", "svglint": "2.4.0", "svgo": "3.0.2", "svgpath": "2.6.0", - "typescript": "5.2.2" + "typescript": "5.2.2", + "xo": "0.58.0" }, "scripts": { - "build": "node scripts/build/package.js", - "clean": "node scripts/build/clean.js", - "format": "prettier --cache --write .", - "lint": "npm run ourlint && npm run jslint && npm run jsonlint && npm run svglint && npm run wslint", - "ourlint": "node scripts/lint/ourlint.js", - "jslint": "prettier --cache --check .", - "jsonlint": "node scripts/lint/jsonlint.js", - "svglint": "svglint --ci $npm_config_icons", + "build": "./scripts/build/package.js", + "clean": "./scripts/build/clean.js", + "format": "prettier --cache --write --ignore-unknown '**/*.!(js|jsx|mjs|cjs|ts|tsx|mts|cts|svg)' && xo --fix", + "lint": "npm run ourlint && npm run prettierlint && npm run jslint && npm run jsonlint && npm run svglint && npm run wslint", + "ourlint": "./scripts/lint/ourlint.js", + "prettierlint": "prettier --cache --check --ignore-unknown '**/*.!(js|jsx|mjs|cjs|ts|tsx|mts|cts|svg)'", + "jslint": "xo", + "jsonlint": "./scripts/lint/jsonlint.js", + "svglint": "svglint --ci $npm_config_icons --config svglint.config.mjs", "wslint": "editorconfig-checker", "prepare": "husky", "prepublishOnly": "npm run build", @@ -121,8 +123,8 @@ "test": "mocha tests --reporter tests/min-reporter.cjs --inline-diffs", "pretest": "npm run prepublishOnly", "posttest": "npm run postpublish", - "get-filename": "node scripts/get-filename.js", - "add-icon-data": "node scripts/add-icon-data.js" + "get-filename": "./scripts/get-filename.js", + "add-icon-data": "./scripts/add-icon-data.js" }, "engines": { "node": ">=0.12.18" diff --git a/scripts/add-icon-data.js b/scripts/add-icon-data.js old mode 100644 new mode 100755 index 785130d5c213..7ee85e258bb2 --- a/scripts/add-icon-data.js +++ b/scripts/add-icon-data.js @@ -1,22 +1,23 @@ +#!/usr/bin/env node import process from 'node:process'; +import {ExitPromptError, checkbox, confirm, input} from '@inquirer/prompts'; import chalk from 'chalk'; -import { input, confirm, checkbox, ExitPromptError } from '@inquirer/prompts'; -import autocomplete from 'inquirer-autocomplete-standalone'; +import {search} from 'fast-fuzzy'; import getRelativeLuminance from 'get-relative-luminance'; -import { search } from 'fast-fuzzy'; +import autocomplete from 'inquirer-autocomplete-standalone'; import { URL_REGEX, collator, getIconsDataString, - titleToSlug, normalizeColor, + titleToSlug, } from '../sdk.mjs'; -import { getJsonSchemaData, writeIconsData } from './utils.js'; +import {getJsonSchemaData, writeIconsData} from './utils.js'; const iconsData = JSON.parse(await getIconsDataString()); 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) => ({ name: `${key} (${jsonSchema.definitions.brand.properties.aliases.properties[key].description})`, @@ -25,7 +26,7 @@ const aliasTypes = ['aka', 'old'].map((key) => ({ const licenseTypes = jsonSchema.definitions.brand.properties.license.oneOf[0].properties.type.enum.map( - (license) => ({ name: license, value: license }), + (license) => ({name: license, value: license}), ); const isValidURL = (input) => @@ -35,7 +36,7 @@ const isValidHexColor = (input) => HEX_REGEX.test(input) || 'Must be a valid hex code.'; const isNewIcon = (input) => - !iconsData.icons.find( + !iconsData.icons.some( (icon) => icon.title === input || titleToSlug(icon.title) === titleToSlug(input), ) || 'This icon title or slug already exists.'; @@ -83,10 +84,10 @@ try { ? { type: await autocomplete({ message: "What is the icon's license?", - source: async (input) => { + async source(input) { input = (input || '').trim(); return input - ? search(input, licenseTypes, { keySelector: (x) => x.value }) + ? search(input, licenseTypes, {keySelector: (x) => x.value}) : licenseTypes; }, }), @@ -107,12 +108,14 @@ try { }).then(async (aliases) => { const result = {}; for (const alias of aliases) { + // eslint-disable-next-line no-await-in-loop result[alias] = await input({ message: `What ${alias} aliases would you like to add? (separate with commas)`, }).then((aliases) => aliases.split(',').map((alias) => alias.trim()), ); } + return result; }) : undefined, @@ -136,11 +139,11 @@ try { console.log(chalk.red('\nAborted.')); process.exit(1); } -} catch (err) { - if (err instanceof ExitPromptError) { +} catch (error) { + if (error instanceof ExitPromptError) { console.log(chalk.red('\nAborted.')); process.exit(1); } - throw err; + throw error; } diff --git a/scripts/build/clean.js b/scripts/build/clean.js old mode 100644 new mode 100755 index ff95c4b9e1ca..83423f25d7a5 --- a/scripts/build/clean.js +++ b/scripts/build/clean.js @@ -1,29 +1,37 @@ +#!/usr/bin/env node /** * @fileoverview * Clean files built by the build process. */ -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; -import { getDirnameFromImportMeta } from '../../sdk.mjs'; +import process from 'node:process'; +import {getDirnameFromImportMeta} from '../../sdk.mjs'; const __dirname = getDirnameFromImportMeta(import.meta.url); const rootDirectory = path.resolve(__dirname, '..', '..'); const files = ['index.js', 'index.mjs', 'index.d.ts', 'sdk.js']; 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); -Promise.all( - files.map(async (file) => { - const filepath = path.join(rootDirectory, file); - if (!(await fileExists(filepath))) { - console.error(`File ${file} does not exist, skipping...`); - return; - } - return fs.promises.unlink(filepath); - }), -).catch((error) => { - console.error(`Error cleaning files: ${error.message}`); +try { + Promise.all( + files.map(async (file) => { + const filepath = path.join(rootDirectory, file); + if (!(await fileExists(filepath))) { + console.error(`File ${file} does not exist, skipping...`); + return; + } + + return fs.unlink(filepath); + }), + ); +} catch (error) { + console.error('Error cleaning files:', error); process.exit(1); -}); +} diff --git a/scripts/build/package.js b/scripts/build/package.js old mode 100644 new mode 100755 index 05ca17bba437..edf73768223b --- a/scripts/build/package.js +++ b/scripts/build/package.js @@ -1,36 +1,40 @@ +#!/usr/bin/env node /** * @fileoverview * Simple Icons package build script. */ -import { promises as fs } from 'node:fs'; +import {promises as fs} from 'node:fs'; import path from 'node:path'; import util from 'node:util'; -import { transform as esbuildTransform } from 'esbuild'; +import {transform as esbuildTransform} from 'esbuild'; import { + collator, + getDirnameFromImportMeta, getIconSlug, + getIconsData, + slugToVariableName, svgToPath, titleToHtmlFriendly, - slugToVariableName, - getIconsData, - getDirnameFromImportMeta, - collator, } from '../../sdk.mjs'; const __dirname = getDirnameFromImportMeta(import.meta.url); const UTF8 = 'utf8'; -const rootDir = path.resolve(__dirname, '..', '..'); -const iconsDir = path.resolve(rootDir, 'icons'); -const indexJsFile = path.resolve(rootDir, 'index.js'); -const indexMjsFile = path.resolve(rootDir, 'index.mjs'); -const sdkJsFile = path.resolve(rootDir, 'sdk.js'); -const sdkMjsFile = path.resolve(rootDir, 'sdk.mjs'); -const indexDtsFile = path.resolve(rootDir, 'index.d.ts'); +const rootDirectory = path.resolve(__dirname, '..', '..'); +const iconsDirectory = path.resolve(rootDirectory, 'icons'); +const indexJsFile = path.resolve(rootDirectory, 'index.js'); +const indexMjsFile = path.resolve(rootDirectory, 'index.mjs'); +const sdkJsFile = path.resolve(rootDirectory, 'sdk.js'); +const sdkMjsFile = path.resolve(rootDirectory, 'sdk.mjs'); +const indexDtsFile = path.resolve(rootDirectory, 'index.d.ts'); -const templatesDir = path.resolve(__dirname, 'templates'); -const iconObjectTemplateFile = path.resolve(templatesDir, 'icon-object.js'); +const templatesDirectory = path.resolve(__dirname, 'templates'); +const iconObjectTemplateFile = path.resolve( + templatesDirectory, + 'icon-object.js.template', +); const build = async () => { const icons = await getIconsData(); @@ -38,8 +42,9 @@ const build = async () => { // Local helper functions const escape = (value) => { - return value.replace(/(? { if (license === undefined) { return; @@ -48,8 +53,10 @@ const build = async () => { if (license.url === undefined) { license.url = `https://spdx.org/licenses/${license.type}`; } + return license; }; + const iconToObject = (icon) => { return util.format( iconObjectTemplate, @@ -65,11 +72,13 @@ const build = async () => { : '', ); }; - const writeJs = async (filepath, rawJavaScript, opts = null) => { - opts = opts === null ? { minify: true } : opts; - const { code } = await esbuildTransform(rawJavaScript, opts); + + 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); }; @@ -78,13 +87,13 @@ const build = async () => { const buildIcons = await Promise.all( icons.map(async (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.path = svgToPath(icon.svg); icon.slug = filename; const iconObject = iconToObject(icon); const iconExportName = slugToVariableName(icon.slug); - return { icon, iconObject, iconExportName }; + return {icon, iconObject, iconExportName}; }), ); @@ -93,33 +102,33 @@ const build = async () => { const iconsBarrelMjs = []; buildIcons.sort((a, b) => collator.compare(a.icon.title, b.icon.title)); - for (const { iconObject, iconExportName } of buildIcons) { + for (const {iconObject, iconExportName} of buildIcons) { iconsBarrelDts.push(`export const ${iconExportName}:I;`); iconsBarrelJs.push(`${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='',b='';`; - // 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( '', )}};`; await writeJs(indexJsFile, rawIndexJs); - // and ESM + // ... and ESM const rawIndexMjs = constantsString + iconsBarrelMjs.join(''); 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( '', )}`; await writeTs(indexDtsFile, rawIndexDts); - // create a CommonJS SDK file + // Create a CommonJS SDK file await writeJs(sdkJsFile, await fs.readFile(sdkMjsFile, UTF8), { format: 'cjs', }); }; -build(); +await build(); diff --git a/scripts/build/templates/icon-object.js b/scripts/build/templates/icon-object.js.template similarity index 100% rename from scripts/build/templates/icon-object.js rename to scripts/build/templates/icon-object.js.template diff --git a/scripts/get-filename.js b/scripts/get-filename.js old mode 100644 new mode 100755 index 826b43c3ebc7..29118cbc7e92 --- a/scripts/get-filename.js +++ b/scripts/get-filename.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /** * @fileoverview * Script that takes a brand name as argument and outputs the corresponding @@ -5,16 +6,13 @@ */ import process from 'node:process'; -import { titleToSlug } from '../sdk.mjs'; +import {titleToSlug} from '../sdk.mjs'; if (process.argv.length < 3) { console.error('Provide a brand name as argument'); process.exit(1); } else { - const brandName = process.argv - .slice(3) - .reduce((acc, arg) => `${acc} ${arg}`, process.argv[2]); - + const brandName = process.argv[2]; const filename = titleToSlug(brandName); console.log(`For '${brandName}' use the file 'icons/${filename}.svg'`); } diff --git a/scripts/lint/jsonlint.js b/scripts/lint/jsonlint.js old mode 100644 new mode 100755 index 0479dc988f41..a439bd732b29 --- a/scripts/lint/jsonlint.js +++ b/scripts/lint/jsonlint.js @@ -1,20 +1,21 @@ +#!/usr/bin/env node /** * @fileoverview * CLI tool to run jsonschema on the simple-icons.json data file. */ import process from 'node:process'; -import { Validator } from 'jsonschema'; -import { getIconsData } from '../../sdk.mjs'; -import { getJsonSchemaData } from '../utils.js'; +import {Validator} from 'jsonschema'; +import {getIconsData} from '../../sdk.mjs'; +import {getJsonSchemaData} from '../utils.js'; const icons = await getIconsData(); const schema = await getJsonSchemaData(); const validator = new Validator(); -const result = validator.validate({ icons }, schema); +const result = validator.validate({icons}, schema); 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`); process.exit(1); } diff --git a/scripts/lint/ourlint.js b/scripts/lint/ourlint.js old mode 100644 new mode 100755 index 49abcb206b62..fcb320498c8a --- a/scripts/lint/ourlint.js +++ b/scripts/lint/ourlint.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /** * @fileoverview * Linters for the package that can't easily be implemented in the existing @@ -5,9 +6,8 @@ */ import process from 'node:process'; -import { URL } from 'node:url'; 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. @@ -15,39 +15,43 @@ import { getIconsDataString, normalizeNewlines, collator } from '../../sdk.mjs'; */ const TESTS = { /* Tests whether our icons are in alphabetical order */ - alphabetical: (data) => { + alphabetical(data) { const collector = (invalidEntries, icon, index, array) => { if (index > 0) { - const prev = array[index - 1]; - const comparison = collator.compare(icon.title, prev.title); + const previous = array[index - 1]; + const comparison = collator.compare(icon.title, previous.title); if (comparison < 0) { invalidEntries.push(icon); - } else if (comparison === 0) { - if (prev.slug) { - if (!icon.slug || collator.compare(icon.slug, prev.slug) < 0) { - invalidEntries.push(icon); - } - } + } else if ( + comparison === 0 && + previous.slug && + (!icon.slug || collator.compare(icon.slug, previous.slug) < 0) + ) { + invalidEntries.push(icon); } } + return invalidEntries; }; + const format = (icon) => { if (icon.slug) { return `${icon.title} (${icon.slug})`; } + return icon.title; }; + // eslint-disable-next-line unicorn/no-array-reduce, unicorn/no-array-callback-reference const invalids = data.icons.reduce(collector, []); - if (invalids.length) { + if (invalids.length > 0) { return `Some icons aren't in alphabetical order: ${invalids.map((icon) => format(icon)).join(', ')}`; } }, /* Check the formatting of the data file */ - prettified: (data, dataString) => { + prettified(data, dataString) { const normalizedDataString = normalizeNewlines(dataString); const dataPretty = `${JSON.stringify(data, null, 4)}\n`; @@ -58,9 +62,9 @@ const TESTS = { }, /* Check redundant trailing slash in URL */ - checkUrl: (data) => { + checkUrl(data) { const hasRedundantTrailingSlash = (url) => { - const origin = new URL(url).origin; + const {origin} = new global.URL(url); return /^\/+$/.test(url.replace(origin, '')); }; @@ -89,9 +93,11 @@ const data = JSON.parse(dataString); const errors = ( 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) { - 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); } diff --git a/scripts/release/reformat-markdown.js b/scripts/release/reformat-markdown.js old mode 100644 new mode 100755 index 2606a58da8e6..8cbc9196dd02 --- a/scripts/release/reformat-markdown.js +++ b/scripts/release/reformat-markdown.js @@ -1,19 +1,21 @@ +#!/usr/bin/env node /** * @fileoverview * Rewrite some Markdown files. */ +import {readFile, writeFile} from 'node:fs/promises'; import path from 'node:path'; -import { writeFile, readFile } from 'node:fs/promises'; -import { getDirnameFromImportMeta } from '../../sdk.mjs'; +import process from 'node:process'; +import {getDirnameFromImportMeta} from '../../sdk.mjs'; const LINKS_BRANCH = process.argv[2] || 'develop'; const __dirname = getDirnameFromImportMeta(import.meta.url); -const rootDir = path.resolve(__dirname, '..', '..'); -const readmeFile = path.resolve(rootDir, 'README.md'); -const disclaimerFile = path.resolve(rootDir, 'DISCLAIMER.md'); +const rootDirectory = path.resolve(__dirname, '..', '..'); +const readmeFile = path.resolve(rootDirectory, 'README.md'); +const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md'); const reformat = async (filePath) => { const fileContent = await readFile(filePath, 'utf8'); @@ -21,17 +23,17 @@ const reformat = async (filePath) => { filePath, fileContent // Replace all CDN links with raw links - .replace( + .replaceAll( /https:\/\/cdn.simpleicons.org\/(.+)\/000\/fff/g, `https://raw.githubusercontent.com/simple-icons/simple-icons/${LINKS_BRANCH}/icons/$1.svg`, ) // Replace all GitHub blockquotes with regular markdown // Reference: https://github.com/orgs/community/discussions/16925 - .replace( - /\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?!\()/g, - function (str, $0) { - const capital = $0.substr(0, 1); - const body = $0.substr(1).toLowerCase(); + .replaceAll( + /\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)](?!\()/g, + function (string_, $0) { + const capital = $0.slice(0, 1); + const body = $0.slice(1).toLowerCase(); return `**${capital + body}**`; }, ), diff --git a/scripts/release/update-cdn-urls.js b/scripts/release/update-cdn-urls.js old mode 100644 new mode 100755 index f6be9e9ba611..38a1f76ea7c5 --- a/scripts/release/update-cdn-urls.js +++ b/scripts/release/update-cdn-urls.js @@ -1,35 +1,36 @@ +#!/usr/bin/env node /** * @fileoverview * 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. */ -import process from 'node:process'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { getDirnameFromImportMeta } from '../../sdk.mjs'; +import process from 'node:process'; +import {getDirnameFromImportMeta} from '../../sdk.mjs'; const __dirname = getDirnameFromImportMeta(import.meta.url); -const rootDir = path.resolve(__dirname, '..', '..'); -const packageJsonFile = path.resolve(rootDir, 'package.json'); -const readmeFile = path.resolve(rootDir, 'README.md'); +const rootDirectory = path.resolve(__dirname, '..', '..'); +const packageJsonFile = path.resolve(rootDirectory, 'package.json'); +const readmeFile = path.resolve(rootDirectory, 'README.md'); -const getMajorVersion = (semVerVersion) => { - const majorVersionAsString = semVerVersion.split('.')[0]; - return parseInt(majorVersionAsString); +const getMajorVersion = (semVersion) => { + const majorVersionAsString = semVersion.split('.')[0]; + return Number.parseInt(majorVersionAsString, 10); }; const getManifest = async () => { - const manifestRaw = await fs.readFile(packageJsonFile, 'utf-8'); + const manifestRaw = await fs.readFile(packageJsonFile, 'utf8'); return JSON.parse(manifestRaw); }; const updateVersionInReadmeIfNecessary = async (majorVersion) => { let content = await fs.readFile(readmeFile, 'utf8'); - content = content.replace( - /simple-icons@v[0-9]+/g, + content = content.replaceAll( + /simple-icons@v\d+/g, `simple-icons@v${majorVersion}`, ); diff --git a/scripts/release/update-sdk-ts-defs.js b/scripts/release/update-sdk-ts-defs.js old mode 100644 new mode 100755 index c87e87625a82..7d0bc04152ec --- a/scripts/release/update-sdk-ts-defs.js +++ b/scripts/release/update-sdk-ts-defs.js @@ -1,33 +1,34 @@ +#!/usr/bin/env node /** * @fileoverview * Updates the SDK Typescript definitions located in the file sdk.d.ts * 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 path from 'node:path'; -import { execSync } from 'node:child_process'; -import { getDirnameFromImportMeta } from '../../sdk.mjs'; +import process from 'node:process'; +import {getDirnameFromImportMeta} from '../../sdk.mjs'; 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 sdkMts = path.resolve(rootDir, 'sdk.d.mts'); -const sdkMjs = path.resolve(rootDir, 'sdk.mjs'); +const sdkTs = path.resolve(rootDirectory, 'sdk.d.ts'); +const sdkMts = path.resolve(rootDirectory, 'sdk.d.mts'); +const sdkMjs = path.resolve(rootDirectory, 'sdk.mjs'); const generateSdkMts = async () => { - // remove temporally type definitions imported with comments + // Remove temporally type definitions imported with comments // in sdk.mjs to avoid circular imports - const originalSdkMjsContent = await fs.readFile(sdkMjs, 'utf-8'); - const tempSdkMjsContent = originalSdkMjsContent + const originalSdkMjsContent = await fs.readFile(sdkMjs, 'utf8'); + const temporarySdkMjsContent = originalSdkMjsContent .split('\n') .filter((line) => { return !line.startsWith(' * @typedef {import("./sdk")'); }) .join('\n'); - await fs.writeFile(sdkMjs, tempSdkMjsContent); + await fs.writeFile(sdkMjs, temporarySdkMjsContent); try { execSync( 'npx tsc sdk.mjs' + @@ -41,6 +42,7 @@ const generateSdkMts = async () => { ); process.exit(1); } + await fs.writeFile(sdkMjs, originalSdkMjsContent); }; @@ -49,13 +51,15 @@ const generateSdkTs = async () => { .access(sdkMts) .then(() => true) .catch(() => false); - fileExists && (await fs.unlink(sdkMts)); + if (fileExists) await fs.unlink(sdkMts); await generateSdkMts(); - const autogeneratedMsg = '/* The next code is autogenerated from sdk.mjs */'; + const autogeneratedMessage = + '/* The next code is autogenerated from sdk.mjs */'; const newSdkTsContent = - (await fs.readFile(sdkTs, 'utf-8')).split(autogeneratedMsg)[0] + - `${autogeneratedMsg}\n\n${await fs.readFile(sdkMts, 'utf-8')}`; + // eslint-disable-next-line unicorn/no-await-expression-member + (await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] + + `${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`; await fs.writeFile(sdkTs, newSdkTsContent); await fs.unlink(sdkMts); diff --git a/scripts/release/update-slugs-table.js b/scripts/release/update-slugs-table.js old mode 100644 new mode 100755 index fffe85f7e0c4..74f61dbaa993 --- a/scripts/release/update-slugs-table.js +++ b/scripts/release/update-slugs-table.js @@ -1,22 +1,23 @@ +#!/usr/bin/env node /** * @fileoverview * 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 { fileURLToPath } from 'node:url'; -import { getIconsData, getIconSlug } from '../../sdk.mjs'; +import {fileURLToPath} from 'node:url'; +import {getIconSlug, getIconsData} from '../../sdk.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const rootDir = path.resolve(__dirname, '..', '..'); -const slugsFile = path.resolve(rootDir, 'slugs.md'); +const rootDirectory = path.resolve(__dirname, '..', '..'); +const slugsFile = path.resolve(rootDirectory, 'slugs.md'); let content = ` # Simple Icons slugs @@ -31,4 +32,5 @@ for (const icon of icons) { const brandSlug = getIconSlug(icon); content += `| \`${brandName}\` | \`${brandSlug}\` |\n`; } + await fs.writeFile(slugsFile, content); diff --git a/scripts/release/update-svgs-count.js b/scripts/release/update-svgs-count.js old mode 100644 new mode 100755 index 05af0fc17a95..b90f7af1c02f --- a/scripts/release/update-svgs-count.js +++ b/scripts/release/update-svgs-count.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /** * @fileoverview * Replaces the SVG count milestone "Over Free SVG icons..." located @@ -5,32 +6,33 @@ * more than the previous milestone. */ -import process from 'node:process'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { getDirnameFromImportMeta, getIconsData } from '../../sdk.mjs'; +import process from 'node:process'; +import {getDirnameFromImportMeta, getIconsData} from '../../sdk.mjs'; const regexMatcher = /Over\s(\d+)\s/; const updateRange = 100; const __dirname = getDirnameFromImportMeta(import.meta.url); -const rootDir = path.resolve(__dirname, '..', '..'); -const readmeFile = path.resolve(rootDir, 'README.md'); +const rootDirectory = path.resolve(__dirname, '..', '..'); +const readmeFile = path.resolve(rootDirectory, 'README.md'); -const readmeContent = await fs.readFile(readmeFile, 'utf-8'); +const readmeContent = await fs.readFile(readmeFile, 'utf8'); let overNIconsInReadme; try { - overNIconsInReadme = parseInt(regexMatcher.exec(readmeContent)[1]); -} catch (err) { + overNIconsInReadme = Number.parseInt(regexMatcher.exec(readmeContent)[1], 10); +} catch (error) { console.error( 'Failed to obtain number of SVG icons of current milestone in README:', - err, + error, ); process.exit(1); } -const nIcons = (await getIconsData()).length; +const iconsData = await getIconsData(); +const nIcons = iconsData.length; const newNIcons = overNIconsInReadme + updateRange; if (nIcons > newNIcons) { diff --git a/scripts/utils.js b/scripts/utils.js index dfea7d75a4cf..474a4ba4ae20 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,17 +1,17 @@ -import path from 'node:path'; import fs from 'node:fs/promises'; -import { getDirnameFromImportMeta, getIconDataPath } from '../sdk.mjs'; +import path from 'node:path'; +import {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs'; const __dirname = getDirnameFromImportMeta(import.meta.url); /** * 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 ( - 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'); return JSON.parse(jsonSchemaString); }; @@ -19,14 +19,14 @@ export const getJsonSchemaData = async ( /** * Write icons data to _data/simple-icons.json. * @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 ( iconsData, - rootDir = path.resolve(__dirname, '..'), + rootDirectory = path.resolve(__dirname, '..'), ) => { await fs.writeFile( - getIconDataPath(rootDir), + getIconDataPath(rootDirectory), `${JSON.stringify(iconsData, null, 4)}\n`, 'utf8', ); diff --git a/sdk.d.ts b/sdk.d.ts index 517cbd013258..371b69f7d4e6 100644 --- a/sdk.d.ts +++ b/sdk.d.ts @@ -3,7 +3,7 @@ * Types for Simple Icons SDK. */ -import type { CustomLicense, SPDXLicense } from './types'; +import type {CustomLicense, SPDXLicense} from './types'; /** * The data for a third-party extension. @@ -33,14 +33,14 @@ type ThirdPartyExtensionSubject = { export type Aliases = { aka?: string[]; dup?: DuplicateAlias[]; - loc?: { [key: string]: string }; + loc?: Record; }; type DuplicateAlias = { title: string; hex?: string; guidelines?: string; - loc?: { [key: string]: string }; + loc?: Record; }; /** @@ -62,8 +62,8 @@ export type IconData = { /* The next code is autogenerated from sdk.mjs */ -export const URL_REGEX: RegExp; -export const SVG_PATH_REGEX: RegExp; +export const URL_REGEX: RegExp; // eslint-disable-line @typescript-eslint/naming-convention +export const SVG_PATH_REGEX: RegExp; // eslint-disable-line @typescript-eslint/naming-convention export function getDirnameFromImportMeta(importMetaUrl: string): string; export function getIconSlug(icon: IconData): 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 titleToHtmlFriendly(brandTitle: string): string; export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string; -export function getIconDataPath(rootDir?: string): string; -export function getIconsDataString(rootDir?: string): string; -export function getIconsData(rootDir?: string): IconData[]; +export function getIconDataPath(rootDirectory?: string): string; +export function getIconsDataString(rootDirectory?: string): string; +export function getIconsData(rootDirectory?: string): IconData[]; export function normalizeNewlines(text: string): string; export function normalizeColor(text: string): string; export function getThirdPartyExtensions( diff --git a/sdk.mjs b/sdk.mjs index 90dfc8449c07..945c58c8b31d 100644 --- a/sdk.mjs +++ b/sdk.mjs @@ -3,9 +3,9 @@ * Simple Icons SDK. */ -import path from 'node:path'; import fs from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; /** * @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension @@ -26,12 +26,12 @@ const TITLE_TO_SLUG_REPLACEMENTS = { ŧ: 't', }; -const TITLE_TO_SLUG_CHARS_REGEX = RegExp( +const TITLE_TO_SLUG_CHARS_REGEX = new RegExp( `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`, '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. @@ -41,7 +41,7 @@ export const URL_REGEX = /^https:\/\/[^\s"']+$/; /** * 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`, @@ -74,12 +74,12 @@ export const svgToPath = (svg) => svg.split('"', 8)[7]; export const titleToSlug = (title) => title .toLowerCase() - .replace( + .replaceAll( TITLE_TO_SLUG_CHARS_REGEX, (char) => TITLE_TO_SLUG_REPLACEMENTS[char], ) .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. @@ -99,12 +99,12 @@ export const slugToVariableName = (slug) => { */ export const titleToHtmlFriendly = (brandTitle) => brandTitle - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(//g, '>') - .replace(/./g, (char) => { - const charCode = char.charCodeAt(0); + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll(/./g, (char) => { + const charCode = char.codePointAt(0); return charCode > 127 ? `&#${charCode};` : char; }); @@ -116,43 +116,45 @@ export const titleToHtmlFriendly = (brandTitle) => */ export const htmlFriendlyToTitle = (htmlFriendlyTitle) => htmlFriendlyTitle - .replace(/&#([0-9]+);/g, (_, num) => String.fromCodePoint(parseInt(num))) - .replace( + .replaceAll(/&#(\d+);/g, (_, number_) => + String.fromCodePoint(Number.parseInt(number_, 10)), + ) + .replaceAll( /&(quot|amp|lt|gt);/g, - (_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' })[ref], + (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference], ); /** * 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* */ 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*. - * @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* */ 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*. - * @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* */ 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; }; @@ -162,7 +164,7 @@ export const getIconsData = async ( * @returns {String} The text with Windows newline characters replaced by Unix ones */ 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) => { let color = text.replace('#', '').toUpperCase(); 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) { color = color.slice(0, 6); } + return color; }; @@ -201,11 +207,11 @@ export const getThirdPartyExtensions = async ( module = module.split('.*<\/title><path d=".*"\/><\/svg>$/; -const negativeZerosRegexp = /-0(?=[^\.]|[\s\d\w]|$)/g; + /^<svg( \S*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>$/; +const negativeZerosRegexp = /-0(?=[^.]|[\s\d\w]|$)/g; const iconSize = 24; const iconTargetCenter = iconSize / 2; @@ -39,25 +40,29 @@ const iconFloatPrecision = 3; const iconMaxFloatPrecision = 5; 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 ignoreFile = './.svglint-ignored.json'; -const iconIgnored = !updateIgnoreFile ? svglintIgnores : {}; +const iconIgnored = updateIgnoreFile ? {} : svglintIgnores; -const sortObjectByKey = (obj) => { - return Object.keys(obj) - .sort() - .reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); +const sortObjectByKey = (object) => { + return Object.fromEntries( + Object.keys(object) + .sort() + .map((k) => [k, object[k]]), + ); }; -const sortObjectByValue = (obj) => { - return Object.keys(obj) - .sort((a, b) => collator.compare(obj[a], obj[b])) - .reduce((r, k) => Object.assign(r, { [k]: obj[k] }), {}); +const sortObjectByValue = (object) => { + return Object.fromEntries( + Object.keys(object) + .sort((a, b) => collator.compare(object[a], object[b])) + .map((k) => [k, object[k]]), + ); }; const removeLeadingZeros = (number) => { - // convert 0.03 to '.03' + // Convert 0.03 to '.03' 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 * to the line formed by the two limit points. **/ +// eslint-disable-next-line max-params const collinear = (x1, y1, x2, y2, x3, y3) => { return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) === 0; }; @@ -73,15 +79,16 @@ const collinear = (x1, y1, x2, y2, x3, y3) => { * Returns the number of digits after the decimal point. * @param num The number of interest. */ -const countDecimals = (num) => { - if (num && num % 1) { - let [base, op, trail] = num.toExponential().split(/e([+-])/); - let elen = parseInt(trail, 10); - let idx = base.indexOf('.'); - return idx == -1 +const countDecimals = (number_) => { + if (number_ && number_ % 1) { + const [base, op, trail] = number_.toExponential().split(/e([+-])/); + const elen = Number.parseInt(trail, 10); + const index = base.indexOf('.'); + return index === -1 ? elen - : base.length - idx - 1 + (op === '+' ? -elen : elen); + : base.length - index - 1 + (op === '+' ? -elen : elen); } + return 0; }; @@ -108,46 +115,48 @@ const getTitleTextIndex = (svgFileContent) => { * @param hex The hexadecimal number representation to convert. **/ const hexadecimalToDecimal = (hex) => { - let result = 0, - digitValue; + let result = 0; + let digitValue; for (const digit of hex.toLowerCase()) { digitValue = '0123456789abcdefgh'.indexOf(digit); result = result * 16 + digitValue; } + return result; }; -const maybeShortenedWithEllipsis = (str) => { - return str.length > 20 ? `${str.substring(0, 20)}...` : str; +const maybeShortenedWithEllipsis = (string_) => { + return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_; }; /** * Memoize a function which accepts a single argument. * A second argument can be passed to be used as key. */ -const memoize = (func) => { +const memoize = (function_) => { const results = {}; - return (arg, defaultKey = null) => { - const key = defaultKey || arg; + return (argument, defaultKey = null) => { + const key = defaultKey || argument; + + results[key] ||= function_(argument); - if (!results[key]) { - results[key] = func(arg); - } 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 getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); if (updateIgnoreFile) { 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); for (const linterName in sorted) { - sorted[linterName] = sortObjectByValue(sorted[linterName]); + if (linterName) { + sorted[linterName] = sortObjectByValue(sorted[linterName]); + } } await fs.writeFile(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', { @@ -158,14 +167,12 @@ if (updateIgnoreFile) { const isIgnored = (linterName, path) => { return ( - iconIgnored[linterName] && iconIgnored[linterName].hasOwnProperty(path) + iconIgnored[linterName] && Object.hasOwn(iconIgnored[linterName], path) ); }; const ignoreIcon = (linterName, path, $) => { - if (!iconIgnored[linterName]) { - iconIgnored[linterName] = {}; - } + iconIgnored[linterName] ||= {}; const title = $.find('title').text(); const iconName = htmlFriendlyToTitle(title); @@ -173,7 +180,7 @@ const ignoreIcon = (linterName, path, $) => { iconIgnored[linterName][path] = iconName; }; -export default { +export const config = { rules: { elm: { svg: 1, @@ -183,7 +190,7 @@ export default { }, attr: [ { - // ensure that the SVG element has the appropriate attributes + // Ensure that the SVG element has the appropriate attributes // alphabetically ordered role: 'img', viewBox: `0 0 ${iconSize} ${iconSize}`, @@ -193,12 +200,12 @@ export default { '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::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.) d: SVG_PATH_REGEX, 'rule::selector': 'svg > path', @@ -209,15 +216,15 @@ export default { (reporter, $, ast) => { reporter.name = 'icon-title'; - const iconTitleText = $.find('title').text(), - xmlNamedEntitiesCodepoints = [38, 60, 62], - xmlNamedEntities = ['amp', 'lt', 'gt']; + const iconTitleText = $.find('title').text(); + const xmlNamedEntitiesCodepoints = [38, 60, 62]; + const xmlNamedEntities = ['amp', 'lt', 'gt']; let _validCodepointsRepr = true; - // avoid character codepoints as hexadecimal representation - const hexadecimalCodepoints = Array.from( - iconTitleText.matchAll(/&#x([A-Fa-f0-9]+);/g), - ); + // Avoid character codepoints as hexadecimal representation + const hexadecimalCodepoints = [ + ...iconTitleText.matchAll(/&#x([A-Fa-f\d]+);/g), + ]; if (hexadecimalCodepoints.length > 0) { _validCodepointsRepr = false; @@ -245,10 +252,10 @@ export default { } } - // avoid character codepoints as named entities - const namedEntitiesCodepoints = Array.from( - iconTitleText.matchAll(/&([A-Za-z0-9]+);/g), - ); + // Avoid character codepoints as named entities + const namedEntitiesCodepoints = [ + ...iconTitleText.matchAll(/&([A-Za-z\d]+);/g), + ]; if (namedEntitiesCodepoints.length > 0) { for (const match of namedEntitiesCodepoints) { const namedEntiyReprIndex = @@ -261,16 +268,15 @@ export default { if ( namedEntityJsRepr === undefined || - namedEntityJsRepr.length != 1 + namedEntityJsRepr.length !== 1 ) { replacement = 'its decimal or literal representation'; } else { const namedEntityDec = namedEntityJsRepr.codePointAt(0); - if (namedEntityDec < 128) { - replacement = `"${namedEntityJsRepr}"`; - } else { - replacement = `"&#${namedEntityDec};"`; - } + replacement = + namedEntityDec < 128 + ? `"${namedEntityJsRepr}"` + : `"&#${namedEntityDec};"`; } reporter.error( @@ -283,11 +289,11 @@ export default { } if (_validCodepointsRepr) { - // compare encoded title with original title and report error if not equal - const encodingMatches = Array.from( - iconTitleText.matchAll(/&(#([0-9]+)|(amp|quot|lt|gt));/g), - ), - encodedBuf = []; + // Compare encoded title with original title and report error if not equal + const encodingMatches = [ + ...iconTitleText.matchAll(/&(#(\d+)|(amp|quot|lt|gt));/g), + ]; + const encodedBuf = []; const indexesToIgnore = []; for (const match of encodingMatches) { @@ -300,8 +306,8 @@ export default { if (indexesToIgnore.includes(i)) { encodedBuf.unshift(iconTitleText[i]); } else { - // encode all non ascii characters plus "'&<> (XML named entities) - let charDecimalCode = iconTitleText.charCodeAt(i); + // Encode all non ascii characters plus "'&<> (XML named entities) + const charDecimalCode = iconTitleText.codePointAt(i); if (charDecimalCode > 127) { encodedBuf.unshift(`&#${charDecimalCode};`); @@ -318,6 +324,7 @@ export default { } } } + const encodedIconTitleText = encodedBuf.join(''); if (encodedIconTitleText !== iconTitleText) { _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 + // eslint-disable-next-line unicorn/prefer-number-properties 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) { continue; } + _validCodepointsRepr = false; const decimalCodepointCharIndex = getTitleTextIndex(ast.source) + match.index + 1; + let replacement; if (xmlNamedEntitiesCodepoints.includes(decimalNumber)) { replacement = `"&${ xmlNamedEntities[ @@ -347,7 +357,7 @@ export default { };"`; } else { replacement = String.fromCodePoint(decimalNumber); - replacement = replacement == '"' ? `'"'` : `"${replacement}"`; + replacement = replacement === '"' ? `'"'` : `"${replacement}"`; } reporter.error( @@ -370,7 +380,7 @@ export default { } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'icon-size'; const iconPath = getIconPath($, filepath); @@ -379,8 +389,8 @@ export default { } const [minX, minY, maxX, maxY] = getIconPathBbox(iconPath); - const width = +(maxX - minX).toFixed(iconFloatPrecision); - const height = +(maxY - minY).toFixed(iconFloatPrecision); + const width = Number((maxX - minX).toFixed(iconFloatPrecision)); + const height = Number((maxY - minY).toFixed(iconFloatPrecision)); if (width === 0 && height === 0) { reporter.error( @@ -399,7 +409,7 @@ export default { } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'icon-precision'; const iconPath = getIconPath($, filepath); @@ -407,29 +417,31 @@ export default { for (const segment of segments) { const precisionMax = Math.max( + // eslint-disable-next-line unicorn/no-array-callback-reference ...segment.params.slice(1).map(countDecimals), ); if (precisionMax > iconMaxFloatPrecision) { - let errorMsg = + let errorMessage = `found ${precisionMax} decimals in segment` + - ` "${iconPath.substring(segment.start, segment.end)}"`; + ` "${iconPath.slice(segment.start, segment.end)}"`; if (segment.chained) { 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) }`; reporter.error( 'Maximum precision should not be greater than' + - ` ${iconMaxFloatPrecision}; ${errorMsg}`, + ` ${iconMaxFloatPrecision}; ${errorMessage}`, ); } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'ineffective-segments'; const iconPath = getIconPath($, filepath); @@ -446,10 +458,10 @@ export default { ]; const upperMovementCommands = ['M', 'L']; const upperHorDirectionCommand = 'H'; - const upperVerDirectionCommand = 'V'; + const upperVersionDirectionCommand = 'V'; const upperDirectionCommands = [ upperHorDirectionCommand, - upperVerDirectionCommand, + upperVersionDirectionCommand, ]; const upperCurveCommand = 'C'; const upperShorthandCurveCommand = 'S'; @@ -458,23 +470,25 @@ export default { upperShorthandCurveCommand, ]; const curveCommands = [...lowerCurveCommands, ...upperCurveCommands]; - const commands = [ + const commands = new Set([ ...lowerMovementCommands, ...lowerDirectionCommands, ...upperMovementCommands, ...upperDirectionCommands, ...curveCommands, - ]; + ]); + const isInvalidSegment = ( [command, x1Coord, y1Coord, ...rest], index, previousSegmentIsZ, ) => { - if (commands.includes(command)) { + if (commands.has(command)) { // Relative directions (h or v) having a length of 0 if (lowerDirectionCommands.includes(command) && x1Coord === 0) { return true; } + // Relative movement (m or l) having a distance of 0 if ( index > 0 && @@ -486,6 +500,7 @@ export default { // a relative placement (m) as if it were absolute (M) return command.toLowerCase() === 'm' ? !previousSegmentIsZ : true; } + if ( lowerCurveCommands.includes(command) && x1Coord === 0 && @@ -503,45 +518,49 @@ export default { return true; } } + if (index > 0) { - let [yPrevCoord, xPrevCoord] = [ + let [yPreviousCoord, xPreviousCoord] = [ ...absSegments[index - 1], ].reverse(); // If the previous command was a direction one, // we need to iterate back until we find the missing coordinates - if (upperDirectionCommands.includes(xPrevCoord)) { - xPrevCoord = undefined; - yPrevCoord = undefined; - let idx = index; + if (upperDirectionCommands.includes(xPreviousCoord)) { + xPreviousCoord = undefined; + yPreviousCoord = undefined; + let index_ = index; while ( - --idx > 0 && - (xPrevCoord === undefined || yPrevCoord === undefined) + --index_ > 0 && + (xPreviousCoord === undefined || yPreviousCoord === undefined) ) { - let [yPrevCoordDeep, xPrevCoordDeep] = [ - ...absSegments[idx], + let [yPreviousCoordDeep, xPreviousCoordDeep] = [ + ...absSegments[index_], ].reverse(); // If the previous command was a horizontal movement, // we need to consider the single coordinate as x - if (upperHorDirectionCommand === xPrevCoordDeep) { - xPrevCoordDeep = yPrevCoordDeep; - yPrevCoordDeep = undefined; + if (upperHorDirectionCommand === xPreviousCoordDeep) { + xPreviousCoordDeep = yPreviousCoordDeep; + yPreviousCoordDeep = undefined; } + // If the previous command was a vertical movement, // we need to consider the single coordinate as y - if (upperVerDirectionCommand === xPrevCoordDeep) { - xPrevCoordDeep = undefined; + if (upperVersionDirectionCommand === xPreviousCoordDeep) { + xPreviousCoordDeep = undefined; } + if ( - xPrevCoord === undefined && - xPrevCoordDeep !== undefined + xPreviousCoord === undefined && + xPreviousCoordDeep !== undefined ) { - xPrevCoord = xPrevCoordDeep; + xPreviousCoord = xPreviousCoordDeep; } + if ( - yPrevCoord === undefined && - yPrevCoordDeep !== undefined + yPreviousCoord === undefined && + yPreviousCoordDeep !== undefined ) { - yPrevCoord = yPrevCoordDeep; + yPreviousCoord = yPreviousCoordDeep; } } } @@ -553,20 +572,21 @@ export default { // and a control point equal to the ending point if ( upperShorthandCurveCommand === command && - x1Coord === xPrevCoord && - y1Coord === yPrevCoord && + x1Coord === xPreviousCoord && + y1Coord === yPreviousCoord && x1Coord === x2Coord && y1Coord === y2Coord ) { return true; } + // Absolute bézier curve (C) having // the same coordinate as the previous segment // and last control point equal to the ending point if ( upperCurveCommand === command && - x1Coord === xPrevCoord && - y1Coord === yPrevCoord && + x1Coord === xPreviousCoord && + y1Coord === yPreviousCoord && x2Coord === xCoord && y2Coord === yCoord ) { @@ -578,16 +598,16 @@ export default { // Absolute horizontal direction (H) having // the same x coordinate as the previous segment (upperHorDirectionCommand === command && - x1Coord === xPrevCoord) || + x1Coord === xPreviousCoord) || // Absolute vertical direction (V) having // the same y coordinate as the previous segment - (upperVerDirectionCommand === command && - x1Coord === yPrevCoord) || + (upperVersionDirectionCommand === command && + x1Coord === yPreviousCoord) || // Absolute movement (M or L) having the same // coordinate as the previous segment (upperMovementCommands.includes(command) && - x1Coord === xPrevCoord && - y1Coord === yPrevCoord) + x1Coord === xPreviousCoord && + y1Coord === yPreviousCoord) ); } } @@ -599,13 +619,13 @@ export default { index > 0 && segments[index - 1].params[0].toLowerCase() === 'z'; 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( - segment.start, - segment.end, - )}" found`, - resolutionTip = 'should be removed'; + let errorMessage = `Ineffective segment "${iconPath.slice( + segment.start, + segment.end, + )}" found`; + let resolutionTip = 'should be removed'; if (curveCommands.includes(command)) { const [x2, y2, x, y] = rest; @@ -618,16 +638,19 @@ export default { x2, )} ${removeLeadingZeros(y2)}" or removed`; } + if (command === upperShorthandCurveCommand) { resolutionTip = `should be "L${removeLeadingZeros( x2, )} ${removeLeadingZeros(y2)}" or removed`; } + if (command === lowerCurveCommand && (x !== 0 || y !== 0)) { resolutionTip = `should be "l${removeLeadingZeros( x, )} ${removeLeadingZeros(y)}" or removed`; } + if (command === upperCurveCommand) { resolutionTip = `should be "L${removeLeadingZeros( x, @@ -637,19 +660,20 @@ export default { if (segment.chained) { 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) }`; - reporter.error(`${errorMsg} (${resolutionTip})`); + reporter.error(`${errorMessage} (${resolutionTip})`); } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'collinear-segments'; /** @@ -657,192 +681,222 @@ export default { * (does not extracts collinear coordinates from curves). **/ const getCollinearSegments = (iconPath) => { - const segments = getIconPathSegments(iconPath), - collinearSegments = [], - straightLineCommands = 'HhVvLlMm'; + const segments = getIconPathSegments(iconPath); + const collinearSegments = []; + const straightLineCommands = 'HhVvLlMm'; - let currLine = [], - currAbsCoord = [undefined, undefined], - startPoint, - _inStraightLine = false, - _nextInStraightLine = false, - _resetStartPoint = false; + let currentLine = []; + let currentAbsCoord = [undefined, undefined]; + let startPoint; + let _inStraightLine = false; + let _nextInStraightLine = false; + let _resetStartPoint = false; for (let s = 0; s < segments.length; s++) { - const seg = segments[s], - parms = seg.params, - cmd = parms[0], - nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; + const seg = segments[s]; + const parms = seg.params; + const cmd = parms[0]; + const nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; switch (cmd) { // Next switch cases have been ordered by frequency // of occurrence in the SVG paths of the icons - case 'M': - currAbsCoord[0] = parms[1]; - currAbsCoord[1] = parms[2]; + case 'M': { + currentAbsCoord[0] = parms[1]; + currentAbsCoord[1] = parms[2]; // SVG 1.1: // If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands. if (!seg.chained || seg.chainStart === seg.start) { startPoint = undefined; } + break; - case 'm': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; + } + + case 'm': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; if (!seg.chained || seg.chainStart === seg.start) { startPoint = undefined; } + break; - case 'H': - currAbsCoord[0] = parms[1]; + } + + case 'H': { + currentAbsCoord[0] = parms[1]; break; - case 'h': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; + } + + case 'h': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; break; - case 'V': - currAbsCoord[1] = parms[1]; + } + + case 'V': { + currentAbsCoord[1] = parms[1]; break; - case 'v': - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[1]; + } + + case 'v': { + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1]; break; - case 'L': - currAbsCoord[0] = parms[1]; - currAbsCoord[1] = parms[2]; + } + + case 'L': { + currentAbsCoord[0] = parms[1]; + currentAbsCoord[1] = parms[2]; break; - case 'l': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; + } + + case 'l': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; + } + case 'Z': - case 'z': + case 'z': { // TODO: Overlapping in Z should be handled in another rule - currAbsCoord = [startPoint[0], startPoint[1]]; + currentAbsCoord = [startPoint[0], startPoint[1]]; _resetStartPoint = true; break; - case 'C': - currAbsCoord[0] = parms[5]; - currAbsCoord[1] = parms[6]; + } + + case 'C': { + currentAbsCoord[0] = parms[5]; + currentAbsCoord[1] = parms[6]; break; - case 'c': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[5]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[6]; + } + + case 'c': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6]; break; - case 'A': - currAbsCoord[0] = parms[6]; - currAbsCoord[1] = parms[7]; + } + + case 'A': { + currentAbsCoord[0] = parms[6]; + currentAbsCoord[1] = parms[7]; break; - case 'a': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[6]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[7]; + } + + case 'a': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7]; break; - case 's': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; + } + + case 's': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; - case 'S': - currAbsCoord[0] = parms[1]; - currAbsCoord[1] = parms[2]; + } + + case 'S': { + currentAbsCoord[0] = parms[1]; + currentAbsCoord[1] = parms[2]; break; - case 't': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[1]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[2]; + } + + case 't': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; - case 'T': - currAbsCoord[0] = parms[1]; - currAbsCoord[1] = parms[2]; + } + + case 'T': { + currentAbsCoord[0] = parms[1]; + currentAbsCoord[1] = parms[2]; break; - case 'Q': - currAbsCoord[0] = parms[3]; - currAbsCoord[1] = parms[4]; + } + + case 'Q': { + currentAbsCoord[0] = parms[3]; + currentAbsCoord[1] = parms[4]; break; - case 'q': - currAbsCoord[0] = - (!currAbsCoord[0] ? 0 : currAbsCoord[0]) + parms[3]; - currAbsCoord[1] = - (!currAbsCoord[1] ? 0 : currAbsCoord[1]) + parms[4]; + } + + case 'q': { + currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3]; + currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4]; break; - default: + } + + default: { throw new Error(`"${cmd}" command not handled`); + } } if (startPoint === undefined) { - startPoint = [currAbsCoord[0], currAbsCoord[1]]; + startPoint = [currentAbsCoord[0], currentAbsCoord[1]]; } else if (_resetStartPoint) { startPoint = undefined; _resetStartPoint = false; } _nextInStraightLine = straightLineCommands.includes(nextCmd); - let _exitingStraightLine = _inStraightLine && !_nextInStraightLine; + const _exitingStraightLine = + _inStraightLine && !_nextInStraightLine; _inStraightLine = straightLineCommands.includes(cmd); if (_inStraightLine) { - currLine.push([currAbsCoord[0], currAbsCoord[1]]); + currentLine.push([currentAbsCoord[0], currentAbsCoord[1]]); } else { if (_exitingStraightLine) { if (straightLineCommands.includes(cmd)) { - currLine.push([currAbsCoord[0], currAbsCoord[1]]); + currentLine.push([currentAbsCoord[0], currentAbsCoord[1]]); } + // Get collinear coordinates - for (let p = 1; p < currLine.length - 1; p++) { - let _collinearCoord = collinear( - currLine[p - 1][0], - currLine[p - 1][1], - currLine[p][0], - currLine[p][1], - currLine[p + 1][0], - currLine[p + 1][1], + for (let p = 1; p < currentLine.length - 1; p++) { + const _collinearCoord = collinear( + currentLine[p - 1][0], + currentLine[p - 1][1], + currentLine[p][0], + currentLine[p][1], + currentLine[p + 1][0], + currentLine[p + 1][1], ); if (_collinearCoord) { collinearSegments.push( - segments[s - currLine.length + p + 1], + segments[s - currentLine.length + p + 1], ); } } } - currLine = []; + + currentLine = []; } } return collinearSegments; }; - const iconPath = getIconPath($, filepath), - collinearSegments = getCollinearSegments(iconPath); + const iconPath = getIconPath($, filepath); + const collinearSegments = getCollinearSegments(iconPath); if (collinearSegments.length === 0) { return; } + const pathDIndex = getPathDIndex(ast.source); for (const segment of collinearSegments) { - let errorMsg = `Collinear segment "${iconPath.substring( + let errorMessage = `Collinear segment "${iconPath.slice( segment.start, segment.end, )}" found`; if (segment.chained) { - let readableChain = maybeShortenedWithEllipsis( - iconPath.substring(segment.chainStart, segment.chainEnd), + const readableChain = maybeShortenedWithEllipsis( + iconPath.slice(segment.chainStart, segment.chainEnd), ); - errorMsg += ` in chain "${readableChain}"`; + errorMessage += ` in chain "${readableChain}"`; } - errorMsg += ` at index ${ + + errorMessage += ` at index ${ segment.start + pathDIndex } (should be removed)`; - reporter.error(errorMsg); + reporter.error(errorMessage); } }, (reporter, $, ast) => { @@ -861,16 +915,14 @@ export default { } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'negative-zeros'; const iconPath = getIconPath($, filepath); // Find negative zeros inside path - const negativeZeroMatches = Array.from( - iconPath.matchAll(negativeZerosRegexp), - ); - if (negativeZeroMatches.length) { + const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)]; + if (negativeZeroMatches.length > 0) { // Calculate the index for each match in the file const pathDIndex = getPathDIndex(ast.source); @@ -887,7 +939,7 @@ export default { } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'icon-centered'; const iconPath = getIconPath($, filepath); @@ -896,9 +948,9 @@ export default { } 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 centerY = +((minY + maxY) / 2).toFixed(iconFloatPrecision); + const centerY = Number(((minY + maxY) / 2).toFixed(iconFloatPrecision)); const devianceY = centerY - iconTargetCenter; if ( @@ -914,44 +966,44 @@ export default { } } }, - (reporter, $, ast, { filepath }) => { + (reporter, $, ast, {filepath}) => { reporter.name = 'path-format'; const iconPath = getIconPath($, filepath); if (!SVG_PATH_REGEX.test(iconPath)) { - let errorMsg = 'Invalid path format', - reason; + const errorMessage = 'Invalid path format'; + let reason; if (!iconPath.startsWith('M') && !iconPath.startsWith('m')) { - // doesn't start with moveto + // Doesn't start with moveto reason = 'should start with "moveto" command ("M" or "m"),' + - ` but starts with \"${iconPath[0]}\"`; - reporter.error(`${errorMsg}: ${reason}`); + ` but starts with "${iconPath[0]}"`; + reporter.error(`${errorMessage}: ${reason}`); } - const validPathCharacters = SVG_PATH_REGEX.source.replace( - /[\[\]+^$]/g, - '', - ), - invalidCharactersMsgs = [], - pathDIndex = getPathDIndex(ast.source); + const validPathCharacters = SVG_PATH_REGEX.source.replaceAll( + /[[\]+^$]/g, + '', + ); + const invalidCharactersMsgs = []; + const pathDIndex = getPathDIndex(ast.source); - for (let [i, char] of Object.entries(iconPath)) { - if (validPathCharacters.indexOf(char) === -1) { + for (const [i, char] of Object.entries(iconPath)) { + if (!validPathCharacters.includes(char)) { 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) { reason = `unexpected character${ invalidCharactersMsgs.length > 1 ? 's' : '' } found (${invalidCharactersMsgs.join(', ')})`; - reporter.error(`${errorMsg}: ${reason}`); + reporter.error(`${errorMessage}: ${reason}`); } } }, diff --git a/svgo.config.mjs b/svgo.config.mjs index 0d89716843fa..f3e99960ec16 100644 --- a/svgo.config.mjs +++ b/svgo.config.mjs @@ -1,4 +1,4 @@ -export default { +const config = { multipass: true, eol: 'lf', plugins: [ @@ -62,7 +62,7 @@ export default { // Convert basic shapes (such as <circle>) to <path> name: 'convertShapeToPath', params: { - // including <arc> + // Including <arc> convertArcs: true, }, }, @@ -93,7 +93,7 @@ export default { // to the <svg> tag if it's not there already name: 'addAttributesToSVGElement', params: { - attributes: [{ role: 'img', xmlns: 'http://www.w3.org/2000/svg' }], + attributes: [{role: 'img', xmlns: 'http://www.w3.org/2000/svg'}], }, }, 'removeOffCanvasPaths', @@ -102,3 +102,5 @@ export default { 'reusePaths', ], }; + +export default config; diff --git a/tests/docs.test.js b/tests/docs.test.js index b5c56e70b3ef..9d79ed91f58e 100644 --- a/tests/docs.test.js +++ b/tests/docs.test.js @@ -1,16 +1,16 @@ -import { test } from 'mocha'; -import { strict as assert } from 'node:assert'; -import { getThirdPartyExtensions } from '../sdk.mjs'; +import {strict as assert} from 'node:assert'; +import {test} from 'mocha'; +import {getThirdPartyExtensions} from '../sdk.mjs'; test('README third party extensions must be alphabetically sorted', async () => { const thirdPartyExtensions = await getThirdPartyExtensions(); assert.ok(thirdPartyExtensions.length > 0); const thirdPartyExtensionsNames = thirdPartyExtensions.map( - (ext) => ext.module.name, + (extension) => extension.module.name, ); - const expectedOrder = thirdPartyExtensionsNames.slice().sort(); + const expectedOrder = [...thirdPartyExtensionsNames].sort(); assert.deepEqual( thirdPartyExtensionsNames, expectedOrder, diff --git a/tests/index.test.js b/tests/index.test.js index 19e735ff6b4f..2b10b96f856c 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,6 +1,6 @@ -import { getIconsData, getIconSlug, slugToVariableName } from '../sdk.mjs'; import * as simpleIcons from '../index.mjs'; -import { testIcon } from './test-icon.js'; +import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs'; +import {testIcon} from './test-icon.js'; for (const icon of await getIconsData()) { const slug = getIconSlug(icon); diff --git a/tests/min-reporter.cjs b/tests/min-reporter.cjs index 72b840f8c577..11f8410979f9 100644 --- a/tests/min-reporter.cjs +++ b/tests/min-reporter.cjs @@ -1,6 +1,6 @@ -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 { constructor(runner) { diff --git a/tests/test-icon.js b/tests/test-icon.js index c3ade33d430d..1097e6b3612f 100644 --- a/tests/test-icon.js +++ b/tests/test-icon.js @@ -1,7 +1,7 @@ +import {strict as assert} from 'node:assert'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { strict as assert } from 'node:assert'; -import { describe, it } from 'mocha'; +import {describe, it} from 'mocha'; import { SVG_PATH_REGEX, URL_REGEX, @@ -9,7 +9,7 @@ import { titleToSlug, } from '../sdk.mjs'; -const iconsDir = path.resolve( +const iconsDirectory = path.resolve( getDirnameFromImportMeta(import.meta.url), '..', 'icons', @@ -26,7 +26,7 @@ const iconsDir = path.resolve( * @param {String} slug Icon data slug */ export const testIcon = (icon, subject, slug) => { - const svgPath = path.resolve(iconsDir, `${slug}.svg`); + const svgPath = path.resolve(iconsDirectory, `${slug}.svg`); describe(icon.title, () => { it('has the correct "title"', () => { @@ -81,7 +81,7 @@ export const testIcon = (icon, subject, 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 // unnecessary slugs to icons data it(`'${icon.title}' slug must be necessary`, () => { diff --git a/types.d.ts b/types.d.ts index ef61ec3e3650..ab2ea35acb0e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -5,6 +5,7 @@ */ export type License = SPDXLicense | CustomLicense; +// eslint-disable-next-line @typescript-eslint/naming-convention export type SPDXLicense = { type: string; url: string; @@ -18,7 +19,7 @@ export type CustomLicense = { /** * The data for a Simple Icon as is exported by the npm package. */ -export interface SimpleIcon { +export type SimpleIcon = { title: string; slug: string; svg: string; @@ -27,4 +28,4 @@ export interface SimpleIcon { hex: string; guidelines?: string; license?: License; -} +};