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