mirror of
				https://github.com/Mibew/simple-icons.git
				synced 2025-10-31 02:25:59 +03:00 
			
		
		
		
	Add types to source code (#10637)
This commit is contained in:
		
							parent
							
								
									1224e341d7
								
							
						
					
					
						commit
						236f5fc715
					
				| @ -16,3 +16,4 @@ | ||||
| !sdk.js | ||||
| !sdk.d.ts | ||||
| !.jsonschema.json | ||||
| !jsconfig.json | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|   "prettier": true, | ||||
|   "space": 2, | ||||
|   "plugins": ["import"], | ||||
|   "extends": ["plugin:jsdoc/recommended"], | ||||
|   "rules": { | ||||
|     "sort-imports": [ | ||||
|       "error", | ||||
| @ -27,7 +28,8 @@ | ||||
|         "newlines-between": "never" | ||||
|       } | ||||
|     ], | ||||
|     "no-console": ["error", {"allow": ["warn", "error"]}] | ||||
|     "no-console": ["error", {"allow": ["warn", "error"]}], | ||||
|     "jsdoc/require-file-overview": "error" | ||||
|   }, | ||||
|   "overrides": [ | ||||
|     { | ||||
| @ -46,6 +48,12 @@ | ||||
|         "svgo.config.mjs" | ||||
|       ], | ||||
|       "nodeVersion": ">=18" | ||||
|     }, | ||||
|     { | ||||
|       "files": ["svglint.config.mjs"], | ||||
|       "rules": { | ||||
|         "max-depth": "off" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | ||||
							
								
								
									
										13
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es2022", | ||||
|     "module": "node16", | ||||
|     "moduleResolution": "node16", | ||||
|     "checkJs": true, | ||||
|     "skipLibCheck": false, | ||||
|     "strict": true, | ||||
|     "noImplicitAny": true, | ||||
|     "noImplicitThis": true, | ||||
|     "forceConsistentCasingInFileNames": true | ||||
|   } | ||||
| } | ||||
| @ -86,10 +86,12 @@ | ||||
|   "devDependencies": { | ||||
|     "@inquirer/core": "8.1.0", | ||||
|     "@inquirer/prompts": "5.0.2", | ||||
|     "@types/node": "20.14.2", | ||||
|     "chalk": "5.3.0", | ||||
|     "editorconfig-checker": "5.1.5", | ||||
|     "esbuild": "0.20.2", | ||||
|     "eslint-plugin-import": "2.29.1", | ||||
|     "eslint-plugin-jsdoc": "48.2.8", | ||||
|     "fake-diff": "1.0.0", | ||||
|     "fast-fuzzy": "1.12.0", | ||||
|     "get-relative-luminance": "1.0.0", | ||||
| @ -99,8 +101,8 @@ | ||||
|     "markdown-link-check": "3.12.1", | ||||
|     "mocha": "10.4.0", | ||||
|     "named-html-entities-json": "1.0.0", | ||||
|     "svg-path-bbox": "1.2.6", | ||||
|     "svg-path-segments": "2.0.0", | ||||
|     "svg-path-bbox": "2.0.0", | ||||
|     "svg-path-segments": "2.0.1", | ||||
|     "svglint": "2.7.1", | ||||
|     "svgo": "3.2.0", | ||||
|     "svgpath": "2.6.0", | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @file | ||||
|  * Script to add data for a new icon to the simple-icons dataset. | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import("../sdk.js").IconData} IconData | ||||
|  */ | ||||
| import process from 'node:process'; | ||||
| import {ExitPromptError} from '@inquirer/core'; | ||||
| import {checkbox, confirm, input} from '@inquirer/prompts'; | ||||
| @ -15,6 +23,7 @@ import { | ||||
| } from '../sdk.mjs'; | ||||
| import {getJsonSchemaData, writeIconsData} from './utils.js'; | ||||
| 
 | ||||
| /** @type {{icons: import('../sdk.js').IconData[]}} */ | ||||
| const iconsData = JSON.parse(await getIconsDataString()); | ||||
| const jsonSchema = await getJsonSchemaData(); | ||||
| 
 | ||||
| @ -25,25 +34,42 @@ const aliasTypes = ['aka', 'old'].map((key) => ({ | ||||
|   value: key, | ||||
| })); | ||||
| 
 | ||||
| /** @type {{name: string, value: string}[]} */ | ||||
| const licenseTypes = | ||||
|   jsonSchema.definitions.brand.properties.license.oneOf[0].properties.type.enum.map( | ||||
|     (license) => ({name: license, value: license}), | ||||
|     (/** @type {string} */ license) => ({name: license, value: license}), | ||||
|   ); | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} input URL input | ||||
|  * @returns {Promise<boolean|string>} Whether the input is a valid URL | ||||
|  */ | ||||
| const isValidURL = async (input) => { | ||||
|   const regex = await urlRegex(); | ||||
|   return regex.test(input) || 'Must be a valid and secure (https://) URL.'; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} input Hex color | ||||
|  * @returns {boolean|string} Whether the input is a valid hex color | ||||
|  */ | ||||
| const isValidHexColor = (input) => | ||||
|   HEX_REGEX.test(input) || 'Must be a valid hex code.'; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} input New icon input | ||||
|  * @returns {boolean} Whether the icon is new | ||||
|  */ | ||||
| const isNewIcon = (input) => | ||||
|   !iconsData.icons.some( | ||||
|     (icon) => | ||||
|       icon.title === input || titleToSlug(icon.title) === titleToSlug(input), | ||||
|   ) || 'This icon title or slug already exists.'; | ||||
|   ); | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} input Color input | ||||
|  * @returns {string} Preview of the color | ||||
|  */ | ||||
| const previewHexColor = (input) => { | ||||
|   const color = normalizeColor(input); | ||||
|   const luminance = HEX_REGEX.test(input) | ||||
| @ -60,7 +86,9 @@ try { | ||||
|     title: await input({ | ||||
|       message: 'What is the title of this icon?', | ||||
|       validate: (input) => | ||||
|         input.trim().length > 0 ? isNewIcon(input) : 'This field is required.', | ||||
|         input.trim().length > 0 | ||||
|           ? isNewIcon(input) || 'This icon title or slug already exists.' | ||||
|           : 'This field is required.', | ||||
|     }), | ||||
|     hex: normalizeColor( | ||||
|       await input({ | ||||
| @ -111,6 +139,7 @@ try { | ||||
|         }).then(async (aliases) => { | ||||
|           const result = {}; | ||||
|           for (const alias of aliases) { | ||||
|             // @ts-ignore
 | ||||
|             // eslint-disable-next-line no-await-in-loop
 | ||||
|             result[alias] = await input({ | ||||
|               message: `What ${alias} aliases would you like to add? (separate with commas)`, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Clean files built by the build process. | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,14 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Simple Icons package build script. | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import('../../types.js').License} License | ||||
|  * @typedef {import('esbuild').TransformOptions} EsBuildTransformOptions | ||||
|  */ | ||||
| 
 | ||||
| import {promises as fs} from 'node:fs'; | ||||
| import path from 'node:path'; | ||||
| import util from 'node:util'; | ||||
| @ -36,20 +41,22 @@ const iconObjectTemplateFile = path.resolve( | ||||
|   'icon-object.js.template', | ||||
| ); | ||||
| 
 | ||||
| const build = async () => { | ||||
| const icons = await getIconsData(); | ||||
| const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8); | ||||
| 
 | ||||
|   // Local helper functions
 | ||||
| /** | ||||
|  * @param {string} value The value to escape | ||||
|  * @returns {string} The escaped value | ||||
|  */ | ||||
| const escape = (value) => { | ||||
|   return value.replaceAll(/(?<!\\)'/g, "\\'"); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {License} license The license object or URL | ||||
|  * @returns {License} The license object with a URL | ||||
|  */ | ||||
| const licenseToObject = (license) => { | ||||
|     if (license === undefined) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|   if (license.url === undefined) { | ||||
|     license.url = `https://spdx.org/licenses/${license.type}`; | ||||
|   } | ||||
| @ -57,7 +64,9 @@ const build = async () => { | ||||
|   return license; | ||||
| }; | ||||
| 
 | ||||
|   const iconToObject = (icon) => { | ||||
| // TODO: Find a way to type this object without decreasing performance
 | ||||
| // @ts-ignore
 | ||||
| const iconToJsObject = (icon) => { | ||||
|   return util.format( | ||||
|     iconObjectTemplate, | ||||
|     escape(icon.title), | ||||
| @ -67,31 +76,43 @@ const build = async () => { | ||||
|     escape(icon.source), | ||||
|     escape(icon.hex), | ||||
|     icon.guidelines ? `\n  guidelines: '${escape(icon.guidelines)}',` : '', | ||||
|       licenseToObject(icon.license) | ||||
|         ? `\n  license: ${JSON.stringify(licenseToObject(icon.license))},` | ||||
|         : '', | ||||
|     icon.license === undefined | ||||
|       ? '' | ||||
|       : `\n  license: ${JSON.stringify(licenseToObject(icon.license))},`, | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} filepath The path to the file to write | ||||
|  * @param {string} rawJavaScript The raw JavaScript content to write to the file | ||||
|  * @param {EsBuildTransformOptions | null} options The options to pass to esbuild | ||||
|  */ | ||||
| const writeJs = async (filepath, rawJavaScript, options = null) => { | ||||
|   options = options === null ? {minify: true} : options; | ||||
|   const {code} = await esbuildTransform(rawJavaScript, options); | ||||
|   await fs.writeFile(filepath, code); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} filepath The path to the file to write | ||||
|  * @param {string} rawTypeScript The raw TypeScript content to write to the file | ||||
|  */ | ||||
| const writeTs = async (filepath, rawTypeScript) => { | ||||
|   await fs.writeFile(filepath, rawTypeScript); | ||||
| }; | ||||
| 
 | ||||
|   // 'main'
 | ||||
| const build = async () => { | ||||
|   const buildIcons = await Promise.all( | ||||
|     icons.map(async (icon) => { | ||||
|       const filename = getIconSlug(icon); | ||||
|       const svgFilepath = path.resolve(iconsDirectory, `${filename}.svg`); | ||||
|       // TODO: Find a way to type these objects without decreasing performance
 | ||||
|       // @ts-ignore
 | ||||
|       icon.svg = await fs.readFile(svgFilepath, UTF8); | ||||
|       // @ts-ignore
 | ||||
|       icon.path = svgToPath(icon.svg); | ||||
|       icon.slug = filename; | ||||
|       const iconObject = iconToObject(icon); | ||||
|       const iconObject = iconToJsObject(icon); | ||||
|       const iconExportName = slugToVariableName(icon.slug); | ||||
|       return {icon, iconObject, iconExportName}; | ||||
|     }), | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Script that takes a brand name as argument and outputs the corresponding | ||||
|  * icon SVG filename to standard output. | ||||
|  */ | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * CLI tool to run jsonschema on the simple-icons.json data file. | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,21 +1,39 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Linters for the package that can't easily be implemented in the existing | ||||
|  * linters (e.g. jsonlint/svglint). | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import("../../sdk.mjs").IconData} IconData | ||||
|  * @typedef {import("../../types.js").CustomLicense} CustomLicense | ||||
|  * @typedef {IconData[]} IconsData | ||||
|  */ | ||||
| 
 | ||||
| import process from 'node:process'; | ||||
| import fakeDiff from 'fake-diff'; | ||||
| import {collator, getIconsDataString, normalizeNewlines} from '../../sdk.mjs'; | ||||
| 
 | ||||
| /** | ||||
|  * Contains our tests so they can be isolated from each other. | ||||
|  * @type {{[k:string]: () => (string|undefined)}} | ||||
|  * @type {{[k: string]: (arg0: {icons: IconsData}, arg1: string) => string | undefined}} | ||||
|  */ | ||||
| const TESTS = { | ||||
|   /* Tests whether our icons are in alphabetical order */ | ||||
|   /** | ||||
|    * Tests whether our icons are in alphabetical order | ||||
|    * @param {{icons: IconsData}} data Icons data | ||||
|    * @returns {string|undefined} Error message or undefined | ||||
|    */ | ||||
|   alphabetical(data) { | ||||
|     /** | ||||
|      * Collects invalid alphabet ordered icons | ||||
|      * @param {IconData[]} invalidEntries Invalid icons reference | ||||
|      * @param {IconData} icon Icon to check | ||||
|      * @param {number} index Index of the icon | ||||
|      * @param {IconData[]} array Array of icons | ||||
|      * @returns {IconData[]} Invalid icons | ||||
|      */ | ||||
|     const collector = (invalidEntries, icon, index, array) => { | ||||
|       if (index > 0) { | ||||
|         const previous = array[index - 1]; | ||||
| @ -34,6 +52,11 @@ const TESTS = { | ||||
|       return invalidEntries; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Format an icon for display in the error message | ||||
|      * @param {IconData} icon Icon to format | ||||
|      * @returns {string} Formatted icon | ||||
|      */ | ||||
|     const format = (icon) => { | ||||
|       if (icon.slug) { | ||||
|         return `${icon.title} (${icon.slug})`; | ||||
| @ -63,6 +86,11 @@ const TESTS = { | ||||
| 
 | ||||
|   /* Check redundant trailing slash in URL */ | ||||
|   checkUrl(data) { | ||||
|     /** | ||||
|      * Check if an URL has a redundant trailing slash. | ||||
|      * @param {string} url URL to check | ||||
|      * @returns {boolean} Whether the URL has a redundant trailing slash | ||||
|      */ | ||||
|     const hasRedundantTrailingSlash = (url) => { | ||||
|       const {origin} = new global.URL(url); | ||||
|       return /^\/+$/.test(url.replace(origin, '')); | ||||
| @ -71,7 +99,17 @@ const TESTS = { | ||||
|     const allUrlFields = [ | ||||
|       ...new Set( | ||||
|         data.icons | ||||
|           .flatMap((icon) => [icon.source, icon.guidelines, icon.license?.url]) | ||||
|           .flatMap((icon) => { | ||||
|             // TODO: `Omit` is not working smoothly here
 | ||||
|             const license = | ||||
|               // @ts-ignore
 | ||||
|               icon.license && icon.license.url | ||||
|                 ? // @ts-ignore
 | ||||
|                   [icon.license.url] | ||||
|                 : []; | ||||
|             return [icon.source, icon.guidelines, ...license]; | ||||
|           }) | ||||
| 
 | ||||
|           .filter(Boolean), | ||||
|       ), | ||||
|     ]; | ||||
| @ -88,11 +126,13 @@ const TESTS = { | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| const dataString = await getIconsDataString(); | ||||
| const data = JSON.parse(dataString); | ||||
| const iconsDataString = await getIconsDataString(); | ||||
| const iconsData = JSON.parse(iconsDataString); | ||||
| 
 | ||||
| const errors = ( | ||||
|   await Promise.all(Object.values(TESTS).map((test) => test(data, dataString))) | ||||
|   await Promise.all( | ||||
|     Object.values(TESTS).map((test) => test(iconsData, iconsDataString)), | ||||
|   ) | ||||
| ) | ||||
|   // eslint-disable-next-line unicorn/no-await-expression-member
 | ||||
|   .filter(Boolean); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Rewrite some Markdown files. | ||||
|  */ | ||||
| 
 | ||||
| @ -17,6 +17,10 @@ const rootDirectory = path.resolve(__dirname, '..', '..'); | ||||
| const readmeFile = path.resolve(rootDirectory, 'README.md'); | ||||
| const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md'); | ||||
| 
 | ||||
| /** | ||||
|  * Reformat a file. | ||||
|  * @param {string} filePath Path to the file | ||||
|  */ | ||||
| const reformat = async (filePath) => { | ||||
|   const fileContent = await readFile(filePath, 'utf8'); | ||||
|   await writeFile( | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Updates the CDN URLs in the README.md to match the major version in the | ||||
|  * NPM package manifest. Does nothing if the README.md is already up-to-date. | ||||
|  */ | ||||
| @ -16,16 +16,27 @@ const rootDirectory = path.resolve(__dirname, '..', '..'); | ||||
| const packageJsonFile = path.resolve(rootDirectory, 'package.json'); | ||||
| const readmeFile = path.resolve(rootDirectory, 'README.md'); | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} semVersion A semantic version string. | ||||
|  * @returns {number} The major version number. | ||||
|  */ | ||||
| const getMajorVersion = (semVersion) => { | ||||
|   const majorVersionAsString = semVersion.split('.')[0]; | ||||
|   return Number.parseInt(majorVersionAsString, 10); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get the package.json manifest. | ||||
|  * @returns {Promise<{version: string}>} The package.json manifest. | ||||
|  */ | ||||
| const getManifest = async () => { | ||||
|   const manifestRaw = await fs.readFile(packageJsonFile, 'utf8'); | ||||
|   return JSON.parse(manifestRaw); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {number} majorVersion The major version number. | ||||
|  */ | ||||
| const updateVersionInReadmeIfNecessary = async (majorVersion) => { | ||||
|   let content = await fs.readFile(readmeFile, 'utf8'); | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Updates the SDK Typescript definitions located in the file sdk.d.ts | ||||
|  * to match the current definitions of functions of sdk.mjs. | ||||
|  */ | ||||
| @ -34,19 +34,53 @@ const generateSdkMts = async () => { | ||||
|       'npx tsc sdk.mjs' + | ||||
|         ' --declaration --emitDeclarationOnly --allowJs --removeComments', | ||||
|     ); | ||||
|   } catch (error) { | ||||
|   } catch (/** @type {unknown} */ error) { | ||||
|     await fs.writeFile(sdkMjs, originalSdkMjsContent); | ||||
| 
 | ||||
|     let errorMessage = error; | ||||
|     if (error instanceof Error) { | ||||
|       // The `execSync` function throws a generic Node.js Error
 | ||||
|       errorMessage = error.message; | ||||
|     } | ||||
| 
 | ||||
|     process.stdout.write( | ||||
|       `Error ${error.status} generating Typescript` + | ||||
|         ` definitions: '${error.message}'` + | ||||
|         '\n', | ||||
|       `Error generating Typescript definitions: '${errorMessage}'\n`, | ||||
|     ); | ||||
| 
 | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   await fs.writeFile(sdkMjs, originalSdkMjsContent); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * We must remove the duplicated export types that tsc generates from | ||||
|  * JSDoc `typedef` comments. | ||||
|  * See {@link https://github.com/microsoft/TypeScript/issues/46011}
 | ||||
|  * @param {string} content Content of the file | ||||
|  * @returns {string} The content without duplicated export types | ||||
|  */ | ||||
| const removeDuplicatedExportTypes = (content) => { | ||||
|   const newContent = []; | ||||
|   const lines = content.split('\n'); | ||||
|   /** @type {string[]} */ | ||||
|   const exportTypesFound = []; | ||||
| 
 | ||||
|   for (const line of lines) { | ||||
|     if (line.startsWith('export type ')) { | ||||
|       const type = line.split(' ')[2]; | ||||
|       if (!exportTypesFound.includes(type)) { | ||||
|         newContent.push(line); | ||||
|         exportTypesFound.push(type); | ||||
|       } | ||||
|     } else { | ||||
|       newContent.push(line); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return newContent.join('\n'); | ||||
| }; | ||||
| 
 | ||||
| const generateSdkTs = async () => { | ||||
|   const fileExists = await fs | ||||
|     .access(sdkMts) | ||||
| @ -62,15 +96,21 @@ const generateSdkTs = async () => { | ||||
|     (await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] + | ||||
|     `${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`; | ||||
| 
 | ||||
|   await fs.writeFile(sdkTs, newSdkTsContent); | ||||
|   await fs.writeFile(sdkTs, removeDuplicatedExportTypes(newSdkTsContent)); | ||||
|   await fs.unlink(sdkMts); | ||||
| 
 | ||||
|   try { | ||||
|     execSync('npx prettier -w sdk.d.ts'); | ||||
|   } catch (error) { | ||||
|     let errorMessage = error; | ||||
|     if (error instanceof Error) { | ||||
|       // The `execSync` function throws a generic Node.js Error
 | ||||
|       errorMessage = error.message; | ||||
|     } | ||||
| 
 | ||||
|     process.stdout.write( | ||||
|       `Error ${error.status} executing Prettier` + | ||||
|         ` to prettify SDK TS definitions: '${error.message}'` + | ||||
|       'Error executing Prettier to prettify' + | ||||
|         ` SDK TS definitions: '${errorMessage}'` + | ||||
|         '\n', | ||||
|     ); | ||||
|     process.exit(1); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Generates a MarkDown file that lists every brand name and their slug. | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Replaces the SVG count milestone "Over <NUMBER> Free SVG icons..." located | ||||
|  * at README every time the number of current icons is more than `updateRange` | ||||
|  * more than the previous milestone. | ||||
| @ -20,22 +20,32 @@ const readmeFile = path.resolve(rootDirectory, 'README.md'); | ||||
| 
 | ||||
| const readmeContent = await fs.readFile(readmeFile, 'utf8'); | ||||
| 
 | ||||
| let overNIconsInReadme; | ||||
| try { | ||||
|   overNIconsInReadme = Number.parseInt(regexMatcher.exec(readmeContent)[1], 10); | ||||
| } catch (error) { | ||||
|   const match = regexMatcher.exec(readmeContent); | ||||
|   if (match === null) { | ||||
|     console.error( | ||||
|       'Failed to obtain number of SVG icons of current milestone in README:', | ||||
|     error, | ||||
|       'No match found', | ||||
|     ); | ||||
|     process.exit(1); | ||||
| } | ||||
| 
 | ||||
|   } else { | ||||
|     const overNIconsInReadme = Number.parseInt(match[1], 10); | ||||
|     const iconsData = await getIconsData(); | ||||
|     const nIcons = iconsData.length; | ||||
|     const newNIcons = overNIconsInReadme + updateRange; | ||||
| 
 | ||||
|     if (nIcons > newNIcons) { | ||||
|   const newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `); | ||||
|       const newContent = readmeContent.replace( | ||||
|         regexMatcher, | ||||
|         `Over ${newNIcons} `, | ||||
|       ); | ||||
|       await fs.writeFile(readmeFile, newContent); | ||||
|     } | ||||
|   } | ||||
| } catch (error) { | ||||
|   console.error( | ||||
|     'Failed to update number of SVG icons of current milestone in README:', | ||||
|     error, | ||||
|   ); | ||||
|   process.exit(1); | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,24 @@ | ||||
| /** | ||||
|  * @file Internal utilities. | ||||
|  * | ||||
|  * Here resides all the functionality that does not qualifies to reside | ||||
|  * in the SDK because is not publicly exposed. | ||||
|  */ | ||||
| 
 | ||||
| import fs from 'node:fs/promises'; | ||||
| import path from 'node:path'; | ||||
| import {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs'; | ||||
| 
 | ||||
| const __dirname = getDirnameFromImportMeta(import.meta.url); | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import("../sdk.js").IconData} IconData | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * Get JSON schema data. | ||||
|  * @param {String} rootDirectory Path to the root directory of the project. | ||||
|  * @param {string} rootDirectory Path to the root directory of the project. | ||||
|  * @returns {Promise<any>} JSON schema data. | ||||
|  */ | ||||
| export const getJsonSchemaData = async ( | ||||
|   rootDirectory = path.resolve(__dirname, '..'), | ||||
| @ -18,8 +30,8 @@ export const getJsonSchemaData = async ( | ||||
| 
 | ||||
| /** | ||||
|  * Write icons data to _data/simple-icons.json. | ||||
|  * @param {Object} iconsData Icons data object. | ||||
|  * @param {String} rootDirectory Path to the root directory of the project. | ||||
|  * @param {{icons: IconData[]}} iconsData Icons data object. | ||||
|  * @param {string} rootDirectory Path to the root directory of the project. | ||||
|  */ | ||||
| export const writeIconsData = async ( | ||||
|   iconsData, | ||||
|  | ||||
							
								
								
									
										9
									
								
								sdk.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								sdk.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Types for Simple Icons SDK. | ||||
|  */ | ||||
| 
 | ||||
| @ -10,7 +10,6 @@ import type {CustomLicense, SPDXLicense} from './types'; | ||||
|  * | ||||
|  * Includes the module and author of the extension, | ||||
|  * both including a name and URL. | ||||
|  * | ||||
|  * @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions}
 | ||||
|  */ | ||||
| export type ThirdPartyExtension = { | ||||
| @ -27,7 +26,6 @@ type ThirdPartyExtensionSubject = { | ||||
|  * The aliases for a Simple Icon. | ||||
|  * | ||||
|  * Corresponds to the `aliases` property in the *_data/simple-icons.json* file. | ||||
|  * | ||||
|  * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases}
 | ||||
|  */ | ||||
| export type Aliases = { | ||||
| @ -47,7 +45,6 @@ type DuplicateAlias = { | ||||
|  * The data for a Simple Icon. | ||||
|  * | ||||
|  * Corresponds to the data stored for each icon in the *_data/simple-icons.json* file. | ||||
|  * | ||||
|  * @see {@link https://github.com/mondeja/simple-icons/blob/utils-entrypoint/CONTRIBUTING.md#7-update-the-json-data-for-simpleiconsorg Update the JSON Data for SimpleIcons.org}
 | ||||
|  */ | ||||
| export type IconData = { | ||||
| @ -73,8 +70,8 @@ export function slugToVariableName(slug: string): string; | ||||
| export function titleToHtmlFriendly(brandTitle: string): string; | ||||
| export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string; | ||||
| export function getIconDataPath(rootDirectory?: string): string; | ||||
| export function getIconsDataString(rootDirectory?: string): string; | ||||
| export function getIconsData(rootDirectory?: string): IconData[]; | ||||
| export function getIconsDataString(rootDirectory?: string): Promise<string>; | ||||
| export function getIconsData(rootDirectory?: string): Promise<IconData[]>; | ||||
| export function normalizeNewlines(text: string): string; | ||||
| export function normalizeColor(text: string): string; | ||||
| export function getThirdPartyExtensions( | ||||
|  | ||||
							
								
								
									
										133
									
								
								sdk.mjs
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								sdk.mjs
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| /** | ||||
|  * @fileoverview | ||||
|  * @file | ||||
|  * Simple Icons SDK. | ||||
|  */ | ||||
| 
 | ||||
| @ -8,10 +8,11 @@ import path from 'node:path'; | ||||
| import {fileURLToPath} from 'node:url'; | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension | ||||
|  * @typedef {import("./sdk").IconData} IconData | ||||
|  * @typedef {import("./sdk.d.ts").ThirdPartyExtension} ThirdPartyExtension | ||||
|  * @typedef {import("./sdk.d.ts").IconData} IconData | ||||
|  */ | ||||
| 
 | ||||
| /** @type {{ [key: string]: string }} */ | ||||
| const TITLE_TO_SLUG_REPLACEMENTS = { | ||||
|   '+': 'plus', | ||||
|   '.': 'dot', | ||||
| @ -41,15 +42,15 @@ export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae\d,. ]+$/i; | ||||
| /** | ||||
|  * Get the directory name where this file is located from `import.meta.url`, | ||||
|  * equivalent to the `__dirname` global variable in CommonJS. | ||||
|  * @param {String} importMetaUrl import.meta.url | ||||
|  * @returns {String} Directory name in which this file is located | ||||
|  * @param {string} importMetaUrl import.meta.url | ||||
|  * @returns {string} Directory name in which this file is located | ||||
|  */ | ||||
| export const getDirnameFromImportMeta = (importMetaUrl) => | ||||
|   path.dirname(fileURLToPath(importMetaUrl)); | ||||
| 
 | ||||
| /** | ||||
|  * Build a regex to validate HTTPs URLs. | ||||
|  * @param {String} jsonschemaPath Path to the *.jsonschema.json* file | ||||
|  * @param {string} jsonschemaPath Path to the *.jsonschema.json* file | ||||
|  * @returns {Promise<RegExp>} Regex to validate HTTPs URLs | ||||
|  */ | ||||
| export const urlRegex = async ( | ||||
| @ -68,21 +69,21 @@ export const urlRegex = async ( | ||||
| /** | ||||
|  * Get the slug/filename for an icon. | ||||
|  * @param {IconData} icon The icon data as it appears in *_data/simple-icons.json* | ||||
|  * @returns {String} The slug/filename for the icon | ||||
|  * @returns {string} The slug/filename for the icon | ||||
|  */ | ||||
| export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title); | ||||
| 
 | ||||
| /** | ||||
|  * Extract the path from an icon SVG content. | ||||
|  * @param {String} svg The icon SVG content | ||||
|  * @returns {String} The path from the icon SVG content | ||||
|  **/ | ||||
|  * @param {string} svg The icon SVG content | ||||
|  * @returns {string} The path from the icon SVG content | ||||
|  */ | ||||
| export const svgToPath = (svg) => svg.split('"', 8)[7]; | ||||
| 
 | ||||
| /** | ||||
|  * Converts a brand title into a slug/filename. | ||||
|  * @param {String} title The title to convert | ||||
|  * @returns {String} The slug/filename for the title | ||||
|  * @param {string} title The title to convert | ||||
|  * @returns {string} The slug/filename for the title | ||||
|  */ | ||||
| export const titleToSlug = (title) => | ||||
|   title | ||||
| @ -96,8 +97,8 @@ export const titleToSlug = (title) => | ||||
| 
 | ||||
| /** | ||||
|  * Converts a slug into a variable name that can be exported. | ||||
|  * @param {String} slug The slug to convert | ||||
|  * @returns {String} The variable name for the slug | ||||
|  * @param {string} slug The slug to convert | ||||
|  * @returns {string} The variable name for the slug | ||||
|  */ | ||||
| export const slugToVariableName = (slug) => { | ||||
|   const slugFirstLetter = slug[0].toUpperCase(); | ||||
| @ -107,8 +108,8 @@ export const slugToVariableName = (slug) => { | ||||
| /** | ||||
|  * Converts a brand title as defined in *_data/simple-icons.json* into a brand | ||||
|  * title in HTML/SVG friendly format. | ||||
|  * @param {String} brandTitle The title to convert | ||||
|  * @returns {String} The brand title in HTML/SVG friendly format | ||||
|  * @param {string} brandTitle The title to convert | ||||
|  * @returns {string} The brand title in HTML/SVG friendly format | ||||
|  */ | ||||
| export const titleToHtmlFriendly = (brandTitle) => | ||||
|   brandTitle | ||||
| @ -117,6 +118,8 @@ export const titleToHtmlFriendly = (brandTitle) => | ||||
|     .replaceAll('<', '<') | ||||
|     .replaceAll('>', '>') | ||||
|     .replaceAll(/./g, (char) => { | ||||
|       /** @type {number} */ | ||||
|       // @ts-ignore
 | ||||
|       const charCode = char.codePointAt(0); | ||||
|       return charCode > 127 ? `&#${charCode};` : char; | ||||
|     }); | ||||
| @ -124,8 +127,8 @@ export const titleToHtmlFriendly = (brandTitle) => | ||||
| /** | ||||
|  * Converts a brand title in HTML/SVG friendly format into a brand title (as | ||||
|  * it is seen in *_data/simple-icons.json*) | ||||
|  * @param {String} htmlFriendlyTitle The title to convert | ||||
|  * @returns {String} The brand title in HTML/SVG friendly format | ||||
|  * @param {string} htmlFriendlyTitle The title to convert | ||||
|  * @returns {string} The brand title in HTML/SVG friendly format | ||||
|  */ | ||||
| export const htmlFriendlyToTitle = (htmlFriendlyTitle) => | ||||
|   htmlFriendlyTitle | ||||
| @ -134,13 +137,18 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) => | ||||
|     ) | ||||
|     .replaceAll( | ||||
|       /&(quot|amp|lt|gt);/g, | ||||
|       /** | ||||
|        * @param {string} _ Full match | ||||
|        * @param {'quot' | 'amp' | 'lt' | 'gt'} reference Reference to replace | ||||
|        * @returns {string} Replacement for the reference | ||||
|        */ | ||||
|       (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference], | ||||
|     ); | ||||
| 
 | ||||
| /** | ||||
|  * Get path of *_data/simple-icons.json*. | ||||
|  * @param {String} rootDirectory Path to the root directory of the project | ||||
|  * @returns {String} Path of *_data/simple-icons.json* | ||||
|  * @param {string} rootDirectory Path to the root directory of the project | ||||
|  * @returns {string} Path of *_data/simple-icons.json* | ||||
|  */ | ||||
| export const getIconDataPath = ( | ||||
|   rootDirectory = getDirnameFromImportMeta(import.meta.url), | ||||
| @ -150,8 +158,8 @@ export const getIconDataPath = ( | ||||
| 
 | ||||
| /** | ||||
|  * Get contents of *_data/simple-icons.json*. | ||||
|  * @param {String} rootDirectory Path to the root directory of the project | ||||
|  * @returns {String} Content of *_data/simple-icons.json* | ||||
|  * @param {string} rootDirectory Path to the root directory of the project | ||||
|  * @returns {Promise<string>} Content of *_data/simple-icons.json* | ||||
|  */ | ||||
| export const getIconsDataString = ( | ||||
|   rootDirectory = getDirnameFromImportMeta(import.meta.url), | ||||
| @ -161,8 +169,8 @@ export const getIconsDataString = ( | ||||
| 
 | ||||
| /** | ||||
|  * Get icons data as object from *_data/simple-icons.json*. | ||||
|  * @param {String} rootDirectory Path to the root directory of the project | ||||
|  * @returns {IconData[]} Icons data as array from *_data/simple-icons.json* | ||||
|  * @param {string} rootDirectory Path to the root directory of the project | ||||
|  * @returns {Promise<IconData[]>} Icons data as array from *_data/simple-icons.json* | ||||
|  */ | ||||
| export const getIconsData = async ( | ||||
|   rootDirectory = getDirnameFromImportMeta(import.meta.url), | ||||
| @ -173,8 +181,8 @@ export const getIconsData = async ( | ||||
| 
 | ||||
| /** | ||||
|  * Replace Windows newline characters by Unix ones. | ||||
|  * @param {String} text The text to replace | ||||
|  * @returns {String} The text with Windows newline characters replaced by Unix ones | ||||
|  * @param {string} text The text to replace | ||||
|  * @returns {string} The text with Windows newline characters replaced by Unix ones | ||||
|  */ | ||||
| export const normalizeNewlines = (text) => { | ||||
|   return text.replaceAll('\r\n', '\n'); | ||||
| @ -182,8 +190,8 @@ export const normalizeNewlines = (text) => { | ||||
| 
 | ||||
| /** | ||||
|  * Convert non-6-digit hex color to 6-digit with the character `#` stripped. | ||||
|  * @param {String} text The color text | ||||
|  * @returns {String} The color text in 6-digit hex format | ||||
|  * @param {string} text The color text | ||||
|  * @returns {string} The color text in 6-digit hex format | ||||
|  */ | ||||
| export const normalizeColor = (text) => { | ||||
|   let color = text.replace('#', '').toUpperCase(); | ||||
| @ -199,7 +207,7 @@ export const normalizeColor = (text) => { | ||||
| 
 | ||||
| /** | ||||
|  * Get information about third party extensions from the README table. | ||||
|  * @param {String} readmePath Path to the README file | ||||
|  * @param {string} readmePath Path to the README file | ||||
|  * @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions | ||||
|  */ | ||||
| export const getThirdPartyExtensions = async ( | ||||
| @ -214,23 +222,43 @@ export const getThirdPartyExtensions = async ( | ||||
|     .split('|\n|') | ||||
|     .slice(2) | ||||
|     .map((line) => { | ||||
|       let [module, author] = line.split(' | '); | ||||
|       module = module.split('<img src="')[0]; | ||||
|       const [module_, author] = line.split(' | '); | ||||
|       const module = module_.split('<img src="')[0]; | ||||
|       const moduleName = /\[(.+)]/.exec(module)?.[1]; | ||||
|       if (moduleName === undefined) { | ||||
|         throw new Error(`Module name improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const moduleUrl = /\((.+)\)/.exec(module)?.[1]; | ||||
|       if (moduleUrl === undefined) { | ||||
|         throw new Error(`Module URL improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const authorName = /\[(.+)]/.exec(author)?.[1]; | ||||
|       if (authorName === undefined) { | ||||
|         throw new Error(`Author improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const authorUrl = /\((.+)\)/.exec(author)?.[1]; | ||||
|       if (authorUrl === undefined) { | ||||
|         throw new Error(`Author URL improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         module: { | ||||
|           name: /\[(.+)]/.exec(module)[1], | ||||
|           url: /\((.+)\)/.exec(module)[1], | ||||
|           name: moduleName, | ||||
|           url: moduleUrl, | ||||
|         }, | ||||
|         author: { | ||||
|           name: /\[(.+)]/.exec(author)[1], | ||||
|           url: /\((.+)\)/.exec(author)[1], | ||||
|           name: authorName, | ||||
|           url: authorUrl, | ||||
|         }, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
| /** | ||||
|  * Get information about third party libraries from the README table. | ||||
|  * @param {String} readmePath Path to the README file | ||||
|  * @param {string} readmePath Path to the README file | ||||
|  * @returns {Promise<ThirdPartyExtension[]>} Information about third party libraries | ||||
|  */ | ||||
| export const getThirdPartyLibraries = async ( | ||||
| @ -247,23 +275,42 @@ export const getThirdPartyLibraries = async ( | ||||
|     .map((line) => { | ||||
|       let [module, author] = line.split(' | '); | ||||
|       module = module.split('<img src="')[0]; | ||||
|       const moduleName = /\[(.+)]/.exec(module)?.[1]; | ||||
|       if (moduleName === undefined) { | ||||
|         throw new Error(`Module name improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const moduleUrl = /\((.+)\)/.exec(module)?.[1]; | ||||
|       if (moduleUrl === undefined) { | ||||
|         throw new Error(`Module URL improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const authorName = /\[(.+)]/.exec(author)?.[1]; | ||||
|       if (authorName === undefined) { | ||||
|         throw new Error(`Author improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       const authorUrl = /\((.+)\)/.exec(author)?.[1]; | ||||
|       if (authorUrl === undefined) { | ||||
|         throw new Error(`Author URL improperly parsed from line: ${line}`); | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         module: { | ||||
|           name: /\[(.+)]/.exec(module)[1], | ||||
|           url: /\((.+)\)/.exec(module)[1], | ||||
|           name: moduleName, | ||||
|           url: moduleUrl, | ||||
|         }, | ||||
|         author: { | ||||
|           name: /\[(.+)]/.exec(author)[1], | ||||
|           url: /\((.+)\)/.exec(author)[1], | ||||
|           name: authorName, | ||||
|           url: authorUrl, | ||||
|         }, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
| /** | ||||
|  * `Intl.Collator` object ready to be used for icon titles sorting. | ||||
|  * @type {Intl.Collator} | ||||
|  * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
 | ||||
|  **/ | ||||
|  * @see {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
 | ||||
|  */ | ||||
| export const collator = new Intl.Collator('en', { | ||||
|   usage: 'search', | ||||
|   caseFirst: 'upper', | ||||
|  | ||||
| @ -1,8 +1,12 @@ | ||||
| /* eslint complexity: off, max-depth: off */ | ||||
| /** | ||||
|  * @file | ||||
|  * Linting rules for SVGLint to check SVG icons. | ||||
|  */ | ||||
| 
 | ||||
| import fs from 'node:fs/promises'; | ||||
| import path from 'node:path'; | ||||
| import process from 'node:process'; | ||||
| import svgPathBbox from 'svg-path-bbox'; | ||||
| import {svgPathBbox} from 'svg-path-bbox'; | ||||
| import parsePath from 'svg-path-segments'; | ||||
| import svgpath from 'svgpath'; | ||||
| import { | ||||
| @ -26,6 +30,7 @@ const icons = await getIconsData(); | ||||
| const htmlNamedEntities = JSON.parse( | ||||
|   await fs.readFile(htmlNamedEntitiesFile, 'utf8'), | ||||
| ); | ||||
| /** @type {{ [key: string]: { [key: string]: string } }} */ | ||||
| const svglintIgnores = JSON.parse( | ||||
|   await fs.readFile(svglintIgnoredFile, 'utf8'), | ||||
| ); | ||||
| @ -45,6 +50,10 @@ const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true'; | ||||
| const ignoreFile = './.svglint-ignored.json'; | ||||
| const iconIgnored = updateIgnoreFile ? {} : svglintIgnores; | ||||
| 
 | ||||
| /** | ||||
|  * @param {{ [key: string]: any }} object Object to sort by key | ||||
|  * @returns {{ [key: string]: any }} Object sorted by key | ||||
|  */ | ||||
| const sortObjectByKey = (object) => { | ||||
|   return Object.fromEntries( | ||||
|     Object.keys(object) | ||||
| @ -53,6 +62,10 @@ const sortObjectByKey = (object) => { | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {{ [key: string]: any }} object Object to sort by value | ||||
|  * @returns {{ [key: string]: any }} Object sorted by value | ||||
|  */ | ||||
| const sortObjectByValue = (object) => { | ||||
|   return Object.fromEntries( | ||||
|     Object.keys(object) | ||||
| @ -61,15 +74,27 @@ const sortObjectByValue = (object) => { | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const removeLeadingZeros = (number) => { | ||||
| /** | ||||
|  * Remove leading zeros from a number as a string. | ||||
|  * @param {number | string} numberOrString The number or string to remove leading zeros from. | ||||
|  * @returns {string} The number as a string without leading zeros. | ||||
|  */ | ||||
| const removeLeadingZeros = (numberOrString) => { | ||||
|   // Convert 0.03 to '.03'
 | ||||
|   return number.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3'); | ||||
|   return numberOrString.toString().replace(/^(-?)(0)(\.?.+)/, '$1$3'); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Given three points, returns if the middle one (x2, y2) is collinear | ||||
|  *   to the line formed by the two limit points. | ||||
|  **/ | ||||
|  * @param {number} x1 The x coordinate of the first point. | ||||
|  * @param {number} y1 The y coordinate of the first point. | ||||
|  * @param {number} x2 The x coordinate of the second point. | ||||
|  * @param {number} y2 The y coordinate of the second point. | ||||
|  * @param {number} x3 The x coordinate of the third point. | ||||
|  * @param {number} y3 The y coordinate of the third point. | ||||
|  * @returns {boolean} Whether the middle point is collinear to the line. | ||||
|  */ | ||||
| // eslint-disable-next-line max-params
 | ||||
| const collinear = (x1, y1, x2, y2, x3, y3) => { | ||||
|   return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) === 0; | ||||
| @ -77,7 +102,8 @@ const collinear = (x1, y1, x2, y2, x3, y3) => { | ||||
| 
 | ||||
| /** | ||||
|  * Returns the number of digits after the decimal point. | ||||
|  * @param num The number of interest. | ||||
|  * @param {number} number_ The number to count the decimals of. | ||||
|  * @returns {number} The number of digits after the decimal point. | ||||
|  */ | ||||
| const countDecimals = (number_) => { | ||||
|   if (number_ && number_ % 1) { | ||||
| @ -94,7 +120,8 @@ const countDecimals = (number_) => { | ||||
| 
 | ||||
| /** | ||||
|  * Get the index at which the first path value of an SVG starts. | ||||
|  * @param svgFileContent The raw SVG as text. | ||||
|  * @param {string} svgFileContent The raw SVG as text. | ||||
|  * @returns {number} The index at which the path value starts. | ||||
|  */ | ||||
| const getPathDIndex = (svgFileContent) => { | ||||
|   const pathDStart = '<path d="'; | ||||
| @ -103,8 +130,9 @@ const getPathDIndex = (svgFileContent) => { | ||||
| 
 | ||||
| /** | ||||
|  * Get the index at which the text of the first `<title></title>` tag starts. | ||||
|  * @param svgFileContent The raw SVG as text. | ||||
|  **/ | ||||
|  * @param {string} svgFileContent The raw SVG as text. | ||||
|  * @returns {number} The index at which the title text starts. | ||||
|  */ | ||||
| const getTitleTextIndex = (svgFileContent) => { | ||||
|   const titleStart = '<title>'; | ||||
|   return svgFileContent.indexOf(titleStart) + titleStart.length; | ||||
| @ -112,8 +140,9 @@ const getTitleTextIndex = (svgFileContent) => { | ||||
| 
 | ||||
| /** | ||||
|  * Convert a hexadecimal number passed as string to decimal number as integer. | ||||
|  * @param hex The hexadecimal number representation to convert. | ||||
|  **/ | ||||
|  * @param {string} hex The hexadecimal number representation to convert. | ||||
|  * @returns {number} The decimal number representation. | ||||
|  */ | ||||
| const hexadecimalToDecimal = (hex) => { | ||||
|   let result = 0; | ||||
|   let digitValue; | ||||
| @ -125,6 +154,11 @@ const hexadecimalToDecimal = (hex) => { | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Shorten a string with ellipsis if it exceeds 20 characters. | ||||
|  * @param {string} string_ The string to shorten. | ||||
|  * @returns {string} The shortened string. | ||||
|  */ | ||||
| const maybeShortenedWithEllipsis = (string_) => { | ||||
|   return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_; | ||||
| }; | ||||
| @ -132,21 +166,32 @@ const maybeShortenedWithEllipsis = (string_) => { | ||||
| /** | ||||
|  * Memoize a function which accepts a single argument. | ||||
|  * A second argument can be passed to be used as key. | ||||
|  * @param {(arg0: any) => any} function_ The function to memoize. | ||||
|  * @returns {(arg0: any) => any} The memoized function. | ||||
|  */ | ||||
| const memoize = (function_) => { | ||||
|   /** @type {{ [key: string]: any }} */ | ||||
|   const results = {}; | ||||
| 
 | ||||
|   return (argument, defaultKey = null) => { | ||||
|     const key = defaultKey || argument; | ||||
|   /** | ||||
|    * Memoized function. | ||||
|    * @param {any} argument The argument to memoize. | ||||
|    * @returns {any} The result of the memoized function. | ||||
|    */ | ||||
|   return (argument) => { | ||||
|     results[argument] ||= function_(argument); | ||||
| 
 | ||||
|     results[key] ||= function_(argument); | ||||
| 
 | ||||
|     return results[key]; | ||||
|     return results[argument]; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const getIconPath = memoize(($icon, _filepath) => $icon.find('path').attr('d')); | ||||
| /** @typedef {import('cheerio').Cheerio<import('domhandler').Document>} Cheerio */ | ||||
| 
 | ||||
| /** @type {($icon: Cheerio) => string} */ | ||||
| const getIconPath = memoize(($icon) => $icon.find('path').attr('d')); | ||||
| /** @type {(iconPath: string) => import('svg-path-segments').Segment[]} */ | ||||
| const getIconPathSegments = memoize((iconPath) => parsePath(iconPath)); | ||||
| /** @type {(iconPath: string) => import('svg-path-bbox').BBox} */ | ||||
| const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); | ||||
| 
 | ||||
| if (updateIgnoreFile) { | ||||
| @ -165,21 +210,34 @@ if (updateIgnoreFile) { | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| const isIgnored = (linterName, path) => { | ||||
| /** | ||||
|  * Check if an icon is ignored by a linter rule. | ||||
|  * @param {string} linterRule The name of the linter rule. | ||||
|  * @param {string} path SVG path of the icon. | ||||
|  * @returns {boolean} Whether the icon is ignored by the linter rule | ||||
|  */ | ||||
| const isIgnored = (linterRule, path) => { | ||||
|   return ( | ||||
|     iconIgnored[linterName] && Object.hasOwn(iconIgnored[linterName], path) | ||||
|     iconIgnored[linterRule] && Object.hasOwn(iconIgnored[linterRule], path) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const ignoreIcon = (linterName, path, $) => { | ||||
|   iconIgnored[linterName] ||= {}; | ||||
| /** | ||||
|  * Ignore an icon for a linter rule. | ||||
|  * @param {string} linterRule The name of the linter rule. | ||||
|  * @param {string} path SVG path of the icon. | ||||
|  * @param {Cheerio} $ The SVG object | ||||
|  */ | ||||
| const ignoreIcon = (linterRule, path, $) => { | ||||
|   iconIgnored[linterRule] ||= {}; | ||||
| 
 | ||||
|   const title = $.find('title').text(); | ||||
|   const iconName = htmlFriendlyToTitle(title); | ||||
| 
 | ||||
|   iconIgnored[linterName][path] = iconName; | ||||
|   iconIgnored[linterRule][path] = iconName; | ||||
| }; | ||||
| 
 | ||||
| /** @type {import('svglint').Config} */ | ||||
| const config = { | ||||
|   rules: { | ||||
|     elm: { | ||||
| @ -213,6 +271,7 @@ const config = { | ||||
|       }, | ||||
|     ], | ||||
|     custom: [ | ||||
|       // eslint-disable-next-line complexity
 | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'icon-title'; | ||||
| 
 | ||||
| @ -307,6 +366,8 @@ const config = { | ||||
|               encodedBuf.unshift(iconTitleText[i]); | ||||
|             } else { | ||||
|               // Encode all non ascii characters plus "'&<> (XML named entities)
 | ||||
|               /** @type {number} */ | ||||
|               // @ts-ignore Coerce to number
 | ||||
|               const charDecimalCode = iconTitleText.codePointAt(i); | ||||
| 
 | ||||
|               if (charDecimalCode > 127) { | ||||
| @ -337,8 +398,12 @@ const config = { | ||||
| 
 | ||||
|           // Check if there are some other encoded characters in decimal notation
 | ||||
|           // which shouldn't be encoded
 | ||||
|           for (const match of encodingMatches.filter((m) => { | ||||
|             // TODO: this fails using `Number.isNaN`, investigate
 | ||||
|             // @ts-ignore
 | ||||
|             // eslint-disable-next-line unicorn/prefer-number-properties
 | ||||
|           for (const match of encodingMatches.filter((m) => !isNaN(m[2]))) { | ||||
|             return !isNaN(m[2]); | ||||
|           })) { | ||||
|             const decimalNumber = Number.parseInt(match[2], 10); | ||||
|             if (decimalNumber > 127) { | ||||
|               continue; | ||||
| @ -378,10 +443,10 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $) => { | ||||
|         reporter.name = 'icon-size'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { | ||||
|           return; | ||||
|         } | ||||
| @ -407,16 +472,19 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'icon-precision'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         const segments = getIconPathSegments(iconPath); | ||||
| 
 | ||||
|         for (const segment of segments) { | ||||
|           /** @type {number[]} */ | ||||
|           // @ts-ignore
 | ||||
|           const numberParameters = segment.params.slice(1); | ||||
|           const precisionMax = Math.max( | ||||
|             // eslint-disable-next-line unicorn/no-array-callback-reference
 | ||||
|             ...segment.params.slice(1).map(countDecimals), | ||||
|             ...numberParameters.map(countDecimals), | ||||
|           ); | ||||
|           if (precisionMax > iconMaxFloatPrecision) { | ||||
|             let errorMessage = | ||||
| @ -439,11 +507,16 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'ineffective-segments'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         const segments = getIconPathSegments(iconPath); | ||||
| 
 | ||||
|         /** @type {import('svg-path-segments').Segment[]} */ | ||||
|         // TODO: svgpath does not includes the segment property on the interface,
 | ||||
|         //       see https://github.com/fontello/svgpath/pull/67/files
 | ||||
|         // @ts-ignore
 | ||||
|         const absSegments = svgpath(iconPath).abs().unshort().segments; | ||||
| 
 | ||||
|         const lowerMovementCommands = ['m', 'l']; | ||||
| @ -476,11 +549,16 @@ const config = { | ||||
|           ...curveCommands, | ||||
|         ]); | ||||
| 
 | ||||
|         const isInvalidSegment = ( | ||||
|           [command, x1Coord, y1Coord, ...rest], | ||||
|           index, | ||||
|           previousSegmentIsZ, | ||||
|         ) => { | ||||
|         /** | ||||
|          * Check if a segment is ineffective. | ||||
|          * @param {import('svg-path-segments').Segment} segment The segment to check. | ||||
|          * @param {number} index The index of the segment in the path. | ||||
|          * @param {boolean} previousSegmentIsZ Whether the previous segment is a Z command. | ||||
|          * @returns {boolean} Whether the segment is ineffective. | ||||
|          */ | ||||
|         // eslint-disable-next-line complexity
 | ||||
|         const isInvalidSegment = (segment, index, previousSegmentIsZ) => { | ||||
|           const [command, x1Coord, y1Coord, ...rest] = segment.params; | ||||
|           if (commands.has(command)) { | ||||
|             // Relative directions (h or v) having a length of 0
 | ||||
|             if (lowerDirectionCommands.includes(command) && x1Coord === 0) { | ||||
| @ -534,6 +612,7 @@ const config = { | ||||
|                   let [yPreviousCoordDeep, xPreviousCoordDeep] = [ | ||||
|                     ...absSegments[index_], | ||||
|                   ].reverse(); | ||||
| 
 | ||||
|                   // If the previous command was a horizontal movement,
 | ||||
|                   // we need to consider the single coordinate as x
 | ||||
|                   if (upperHorDirectionCommand === xPreviousCoordDeep) { | ||||
| @ -609,6 +688,8 @@ const config = { | ||||
|               ); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           return false; | ||||
|         }; | ||||
| 
 | ||||
|         for (let index = 0; index < segments.length; index++) { | ||||
| @ -616,7 +697,7 @@ const config = { | ||||
|           const previousSegmentIsZ = | ||||
|             index > 0 && segments[index - 1].params[0].toLowerCase() === 'z'; | ||||
| 
 | ||||
|           if (isInvalidSegment(segment.params, index, previousSegmentIsZ)) { | ||||
|           if (isInvalidSegment(segment, index, previousSegmentIsZ)) { | ||||
|             const [command, _x1, _y1, ...rest] = segment.params; | ||||
| 
 | ||||
|             let errorMessage = `Ineffective segment "${iconPath.slice( | ||||
| @ -671,13 +752,15 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'collinear-segments'; | ||||
| 
 | ||||
|         /** | ||||
|          * Extracts collinear coordinates from SVG path straight lines | ||||
|          * (does not extracts collinear coordinates from curves). | ||||
|          **/ | ||||
|          * @param {string} iconPath The SVG path of the icon. | ||||
|          * @returns {import('svg-path-segments').Segment[]} The collinear segments. | ||||
|          */ | ||||
|         // eslint-disable-next-line complexity
 | ||||
|         const getCollinearSegments = (iconPath) => { | ||||
|           const segments = getIconPathSegments(iconPath); | ||||
|           const collinearSegments = []; | ||||
| @ -694,13 +777,18 @@ const config = { | ||||
|             const seg = segments[s]; | ||||
|             const parms = seg.params; | ||||
|             const cmd = parms[0]; | ||||
|             const nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; | ||||
|             const nextCmd = | ||||
|               s + 1 < segments.length ? segments[s + 1].params[0] : null; | ||||
| 
 | ||||
|             switch (cmd) { | ||||
|               // Next switch cases have been ordered by frequency
 | ||||
|               // of occurrence in the SVG paths of the icons
 | ||||
|               case 'M': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[2]; | ||||
|                 // SVG 1.1:
 | ||||
|                 // If a moveto is followed by multiple pairs of coordinates,
 | ||||
| @ -713,7 +801,11 @@ const config = { | ||||
|               } | ||||
| 
 | ||||
|               case 'm': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; | ||||
|                 if (seg.chain === undefined || seg.chain.start === seg.start) { | ||||
|                   startPoint = undefined; | ||||
| @ -723,33 +815,49 @@ const config = { | ||||
|               } | ||||
| 
 | ||||
|               case 'H': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[1]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'h': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'V': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[1]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'v': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'L': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'l': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| @ -763,61 +871,101 @@ const config = { | ||||
|               } | ||||
| 
 | ||||
|               case 'C': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[5]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[6]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'c': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'A': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[6]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[7]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'a': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 's': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'S': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 't': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'T': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[1]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[2]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'Q': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = parms[3]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = parms[4]; | ||||
|                 break; | ||||
|               } | ||||
| 
 | ||||
|               case 'q': { | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3]; | ||||
|                 /** @type {number} */ | ||||
|                 // @ts-ignore
 | ||||
|                 currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4]; | ||||
|                 break; | ||||
|               } | ||||
| @ -872,7 +1020,7 @@ const config = { | ||||
|           return collinearSegments; | ||||
|         }; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         const collinearSegments = getCollinearSegments(iconPath); | ||||
|         if (collinearSegments.length === 0) { | ||||
|           return; | ||||
| @ -913,10 +1061,10 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'negative-zeros'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
| 
 | ||||
|         // Find negative zeros inside path
 | ||||
|         const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)]; | ||||
| @ -937,10 +1085,10 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $) => { | ||||
|         reporter.name = 'icon-centered'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { | ||||
|           return; | ||||
|         } | ||||
| @ -964,15 +1112,17 @@ const config = { | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'final-closepath'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
|         const segments = getIconPathSegments(iconPath); | ||||
| 
 | ||||
|         // Unnecessary characters after the final closepath
 | ||||
|         /** @type {import('svg-path-segments').Segment} */ | ||||
|         // @ts-ignore
 | ||||
|         const lastSegment = segments.at(-1); | ||||
|         const endsWithZ = ['z', 'Z'].includes(lastSegment.params.at(0)); | ||||
|         const endsWithZ = ['z', 'Z'].includes(lastSegment.params[0]); | ||||
|         if (endsWithZ && lastSegment.end - lastSegment.start > 1) { | ||||
|           const ending = iconPath.slice(lastSegment.start + 1); | ||||
|           const closepath = iconPath.at(lastSegment.start); | ||||
| @ -985,10 +1135,10 @@ const config = { | ||||
|           reporter.error(errorMessage); | ||||
|         } | ||||
|       }, | ||||
|       (reporter, $, ast, {filepath}) => { | ||||
|       (reporter, $, ast) => { | ||||
|         reporter.name = 'path-format'; | ||||
| 
 | ||||
|         const iconPath = getIconPath($, filepath); | ||||
|         const iconPath = getIconPath($); | ||||
| 
 | ||||
|         if (!SVG_PATH_REGEX.test(iconPath)) { | ||||
|           const errorMessage = 'Invalid path format'; | ||||
|  | ||||
| @ -1,6 +1,10 @@ | ||||
| /** | ||||
|  * @file SVGO configuration for Simple Icons. | ||||
|  */ | ||||
| 
 | ||||
| /** @type {import("svgo").Config} */ | ||||
| const config = { | ||||
|   multipass: true, | ||||
|   eol: 'lf', | ||||
|   plugins: [ | ||||
|     'cleanupAttrs', | ||||
|     'inlineStyles', | ||||
| @ -72,7 +76,6 @@ const config = { | ||||
|       name: 'sortAttrs', | ||||
|       params: { | ||||
|         order: ['role', 'viewBox', 'xmlns'], | ||||
|         xmlnsOrder: 'end', | ||||
|       }, | ||||
|     }, | ||||
|     'sortDefsChildren', | ||||
| @ -87,7 +90,6 @@ const config = { | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|     'removeElementsByAttr', | ||||
|     { | ||||
|       // Keep the role="img" attribute and automatically add it
 | ||||
|       // to the <svg> tag if it's not there already
 | ||||
|  | ||||
| @ -1,3 +1,7 @@ | ||||
| /** | ||||
|  * @file Tests for the documentation. | ||||
|  */ | ||||
| 
 | ||||
| import {strict as assert} from 'node:assert'; | ||||
| import {test} from 'mocha'; | ||||
| import {getThirdPartyExtensions, getThirdPartyLibraries} from '../sdk.mjs'; | ||||
|  | ||||
| @ -1,3 +1,9 @@ | ||||
| /** | ||||
|  * @file Tests for the index file of npm package. | ||||
|  */ | ||||
| 
 | ||||
| // The index.mjs file is generated on build before running tests
 | ||||
| // @ts-ignore
 | ||||
| import * as simpleIcons from '../index.mjs'; | ||||
| import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs'; | ||||
| import {testIcon} from './test-icon.js'; | ||||
| @ -5,6 +11,8 @@ import {testIcon} from './test-icon.js'; | ||||
| for (const icon of await getIconsData()) { | ||||
|   const slug = getIconSlug(icon); | ||||
|   const variableName = slugToVariableName(slug); | ||||
|   /** @type {import('../types.d.ts').SimpleIcon} */ | ||||
|   // @ts-ignore
 | ||||
|   const subject = simpleIcons[variableName]; | ||||
| 
 | ||||
|   testIcon(icon, subject, slug); | ||||
|  | ||||
| @ -1,8 +1,18 @@ | ||||
| /** | ||||
|  * @file Custom mocha reporter. | ||||
|  * | ||||
|  * Serves to clear the console after the test run is finished. | ||||
|  * See {@link https://github.com/mochajs/mocha/issues/2312}
 | ||||
|  */ | ||||
| 
 | ||||
| const {reporters, Runner} = require('mocha'); | ||||
| 
 | ||||
| const {EVENT_RUN_END} = Runner.constants; | ||||
| 
 | ||||
| class EvenMoreMin extends reporters.Base { | ||||
|   /** | ||||
|    * @param {import('mocha').Runner} runner Mocha test runner | ||||
|    */ | ||||
|   constructor(runner) { | ||||
|     super(runner); | ||||
|     runner.once(EVENT_RUN_END, () => this.epilogue()); | ||||
|  | ||||
| @ -1,3 +1,7 @@ | ||||
| /** | ||||
|  * @file Icon tester. | ||||
|  */ | ||||
| 
 | ||||
| import {strict as assert} from 'node:assert'; | ||||
| import fs from 'node:fs/promises'; | ||||
| import path from 'node:path'; | ||||
| @ -14,15 +18,11 @@ const iconsDirectory = path.resolve( | ||||
|   'icons', | ||||
| ); | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {import('..').SimpleIcon} SimpleIcon | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * Checks if icon data matches a subject icon. | ||||
|  * @param {SimpleIcon} icon Icon data | ||||
|  * @param {SimpleIcon} subject Icon to check against icon data | ||||
|  * @param {String} slug Icon data slug | ||||
|  * @param {import('../sdk.d.ts').IconData} icon Icon data | ||||
|  * @param {import('../types.d.ts').SimpleIcon} subject Icon object to check against icon data | ||||
|  * @param {string} slug Icon data slug | ||||
|  */ | ||||
| export const testIcon = (icon, subject, slug) => { | ||||
|   const svgPath = path.resolve(iconsDirectory, `${slug}.svg`); | ||||
| @ -62,8 +62,10 @@ export const testIcon = (icon, subject, slug) => { | ||||
| 
 | ||||
|     it(`has ${icon.license ? 'the correct' : 'no'} "license"`, () => { | ||||
|       if (icon.license) { | ||||
|         assert.equal(subject.license.type, icon.license.type); | ||||
|         assert.equal(subject.license?.type, icon.license.type); | ||||
|         if (icon.license.type === 'custom') { | ||||
|           // TODO: `Omit` not working smoothly here
 | ||||
|           // @ts-ignore
 | ||||
|           assert.equal(subject.license.url, icon.license.url); | ||||
|         } | ||||
|       } else { | ||||
|  | ||||
							
								
								
									
										5
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,9 @@ | ||||
| /** | ||||
|  * @file Types for Simple Icons package. | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * The license for a Simple Icon. | ||||
|  * | ||||
|  * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data}
 | ||||
|  */ | ||||
| export type License = SPDXLicense | CustomLicense; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user