2023-04-19 16:23:13 +03:00
|
|
|
|
/**
|
2024-06-06 15:40:35 +03:00
|
|
|
|
* @file
|
2023-04-19 16:23:13 +03:00
|
|
|
|
* Simple Icons SDK.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import fs from 'node:fs/promises';
|
2024-03-24 20:38:18 +03:00
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import {fileURLToPath} from 'node:url';
|
2023-04-19 16:23:13 +03:00
|
|
|
|
|
|
|
|
|
/**
|
2024-06-06 15:40:35 +03:00
|
|
|
|
* @typedef {import("./sdk.d.ts").ThirdPartyExtension} ThirdPartyExtension
|
|
|
|
|
* @typedef {import("./sdk.d.ts").IconData} IconData
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
|
2024-06-06 15:40:35 +03:00
|
|
|
|
/** @type {{ [key: string]: string }} */
|
2023-04-19 16:23:13 +03:00
|
|
|
|
const TITLE_TO_SLUG_REPLACEMENTS = {
|
|
|
|
|
'+': 'plus',
|
|
|
|
|
'.': 'dot',
|
|
|
|
|
'&': 'and',
|
|
|
|
|
đ: 'd',
|
|
|
|
|
ħ: 'h',
|
|
|
|
|
ı: 'i',
|
|
|
|
|
ĸ: 'k',
|
|
|
|
|
ŀ: 'l',
|
|
|
|
|
ł: 'l',
|
|
|
|
|
ß: 'ss',
|
|
|
|
|
ŧ: 't',
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-24 20:38:18 +03:00
|
|
|
|
const TITLE_TO_SLUG_CHARS_REGEX = new RegExp(
|
2023-04-19 16:23:13 +03:00
|
|
|
|
`[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`,
|
|
|
|
|
'g',
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-24 20:38:18 +03:00
|
|
|
|
const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z\d]/g;
|
2023-04-19 16:23:13 +03:00
|
|
|
|
|
2023-08-08 07:38:52 +03:00
|
|
|
|
/**
|
|
|
|
|
* Regex to validate SVG paths.
|
|
|
|
|
*/
|
2024-03-24 20:38:18 +03:00
|
|
|
|
export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae\d,. ]+$/i;
|
2023-04-19 16:23:13 +03:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the directory name where this file is located from `import.meta.url`,
|
|
|
|
|
* equivalent to the `__dirname` global variable in CommonJS.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} importMetaUrl Relative `import.meta.url` value of the caller.
|
|
|
|
|
* @returns {string} Directory name in which this file is located.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getDirnameFromImportMeta = (importMetaUrl) =>
|
|
|
|
|
path.dirname(fileURLToPath(importMetaUrl));
|
|
|
|
|
|
2024-05-22 10:13:35 +03:00
|
|
|
|
/**
|
|
|
|
|
* Build a regex to validate HTTPs URLs.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} jsonschemaPath Path to the *.jsonschema.json* file.
|
|
|
|
|
* @returns {Promise<RegExp>} Regex to validate HTTPs URLs.
|
2024-05-22 10:13:35 +03:00
|
|
|
|
*/
|
|
|
|
|
export const urlRegex = async (
|
|
|
|
|
jsonschemaPath = path.join(
|
|
|
|
|
getDirnameFromImportMeta(import.meta.url),
|
|
|
|
|
'.jsonschema.json',
|
|
|
|
|
),
|
|
|
|
|
) => {
|
|
|
|
|
return new RegExp(
|
|
|
|
|
JSON.parse(
|
|
|
|
|
await fs.readFile(jsonschemaPath, 'utf8'),
|
|
|
|
|
).definitions.url.pattern,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-19 16:23:13 +03:00
|
|
|
|
/**
|
|
|
|
|
* Get the slug/filename for an icon.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {IconData} icon The icon data as it appears in *_data/simple-icons.json*.
|
|
|
|
|
* @returns {string} The slug/filename for the icon.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract the path from an icon SVG content.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} svg The icon SVG content.
|
|
|
|
|
* @returns {string} The path from the icon SVG content.
|
2024-06-06 15:40:35 +03:00
|
|
|
|
*/
|
2023-08-08 07:38:52 +03:00
|
|
|
|
export const svgToPath = (svg) => svg.split('"', 8)[7];
|
2023-04-19 16:23:13 +03:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a brand title into a slug/filename.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} title The title to convert.
|
|
|
|
|
* @returns {string} The slug/filename for the title.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const titleToSlug = (title) =>
|
|
|
|
|
title
|
|
|
|
|
.toLowerCase()
|
2024-03-24 20:38:18 +03:00
|
|
|
|
.replaceAll(
|
2023-04-19 16:23:13 +03:00
|
|
|
|
TITLE_TO_SLUG_CHARS_REGEX,
|
|
|
|
|
(char) => TITLE_TO_SLUG_REPLACEMENTS[char],
|
|
|
|
|
)
|
|
|
|
|
.normalize('NFD')
|
2024-03-24 20:38:18 +03:00
|
|
|
|
.replaceAll(TITLE_TO_SLUG_RANGE_REGEX, '');
|
2023-04-19 16:23:13 +03:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a slug into a variable name that can be exported.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} slug The slug to convert.
|
|
|
|
|
* @returns {string} The variable name for the slug.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const slugToVariableName = (slug) => {
|
|
|
|
|
const slugFirstLetter = slug[0].toUpperCase();
|
2023-08-08 07:38:52 +03:00
|
|
|
|
return `si${slugFirstLetter}${slug.slice(1)}`;
|
2023-04-19 16:23:13 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a brand title as defined in *_data/simple-icons.json* into a brand
|
|
|
|
|
* title in HTML/SVG friendly format.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} brandTitle The title to convert.
|
|
|
|
|
* @returns {string} The brand title in HTML/SVG friendly format.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const titleToHtmlFriendly = (brandTitle) =>
|
|
|
|
|
brandTitle
|
2024-03-24 20:38:18 +03:00
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
.replaceAll(/./g, (char) => {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
/** @type {number} */
|
|
|
|
|
// @ts-ignore
|
2024-03-24 20:38:18 +03:00
|
|
|
|
const charCode = char.codePointAt(0);
|
2023-04-19 16:23:13 +03:00
|
|
|
|
return charCode > 127 ? `&#${charCode};` : char;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a brand title in HTML/SVG friendly format into a brand title (as
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* it is seen in *_data/simple-icons.json*).
|
|
|
|
|
* @param {string} htmlFriendlyTitle The title to convert.
|
|
|
|
|
* @returns {string} The brand title in HTML/SVG friendly format.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
|
|
|
|
|
htmlFriendlyTitle
|
2024-03-24 20:38:18 +03:00
|
|
|
|
.replaceAll(/&#(\d+);/g, (_, number_) =>
|
|
|
|
|
String.fromCodePoint(Number.parseInt(number_, 10)),
|
|
|
|
|
)
|
|
|
|
|
.replaceAll(
|
2023-04-19 16:23:13 +03:00
|
|
|
|
/&(quot|amp|lt|gt);/g,
|
2024-06-06 15:40:35 +03:00
|
|
|
|
/**
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* Replace HTML entity references with their respective decoded characters.
|
|
|
|
|
* @param {string} _ Full match.
|
|
|
|
|
* @param {'quot' | 'amp' | 'lt' | 'gt'} reference Reference to replace.
|
|
|
|
|
* @returns {string} Replacement for the reference.
|
2024-06-06 15:40:35 +03:00
|
|
|
|
*/
|
2024-03-24 20:38:18 +03:00
|
|
|
|
(_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference],
|
2023-04-19 16:23:13 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
2024-03-29 00:44:08 +03:00
|
|
|
|
* Get path of *_data/simple-icons.json*.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} rootDirectory Path to the root directory of the project.
|
|
|
|
|
* @returns {string} Path of *_data/simple-icons.json*.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getIconDataPath = (
|
2024-03-24 20:38:18 +03:00
|
|
|
|
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
2023-04-19 16:23:13 +03:00
|
|
|
|
) => {
|
2024-03-24 20:38:18 +03:00
|
|
|
|
return path.resolve(rootDirectory, '_data', 'simple-icons.json');
|
2023-04-19 16:23:13 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get contents of *_data/simple-icons.json*.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} rootDirectory Path to the root directory of the project.
|
|
|
|
|
* @returns {Promise<string>} Content of *_data/simple-icons.json*.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getIconsDataString = (
|
2024-03-24 20:38:18 +03:00
|
|
|
|
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
2023-04-19 16:23:13 +03:00
|
|
|
|
) => {
|
2024-03-24 20:38:18 +03:00
|
|
|
|
return fs.readFile(getIconDataPath(rootDirectory), 'utf8');
|
2023-04-19 16:23:13 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get icons data as object from *_data/simple-icons.json*.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} rootDirectory Path to the root directory of the project.
|
|
|
|
|
* @returns {Promise<IconData[]>} Icons data as array from *_data/simple-icons.json*.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getIconsData = async (
|
2024-03-24 20:38:18 +03:00
|
|
|
|
rootDirectory = getDirnameFromImportMeta(import.meta.url),
|
2023-04-19 16:23:13 +03:00
|
|
|
|
) => {
|
2024-03-24 20:38:18 +03:00
|
|
|
|
const fileContents = await getIconsDataString(rootDirectory);
|
2023-04-19 16:23:13 +03:00
|
|
|
|
return JSON.parse(fileContents).icons;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace Windows newline characters by Unix ones.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} text The text to replace.
|
|
|
|
|
* @returns {string} The text with Windows newline characters replaced by Unix ones.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const normalizeNewlines = (text) => {
|
2024-03-24 20:38:18 +03:00
|
|
|
|
return text.replaceAll('\r\n', '\n');
|
2023-04-19 16:23:13 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert non-6-digit hex color to 6-digit with the character `#` stripped.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} text The color text.
|
|
|
|
|
* @returns {string} The color text in 6-digit hex format.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const normalizeColor = (text) => {
|
|
|
|
|
let color = text.replace('#', '').toUpperCase();
|
|
|
|
|
if (color.length < 6) {
|
2024-03-27 07:36:27 +03:00
|
|
|
|
// eslint-disable-next-line unicorn/no-useless-spread
|
|
|
|
|
color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join('');
|
2023-04-19 16:23:13 +03:00
|
|
|
|
} else if (color.length > 6) {
|
|
|
|
|
color = color.slice(0, 6);
|
|
|
|
|
}
|
2024-03-24 20:38:18 +03:00
|
|
|
|
|
2023-04-19 16:23:13 +03:00
|
|
|
|
return color;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get information about third party extensions from the README table.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} readmePath Path to the README file.
|
|
|
|
|
* @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions.
|
2023-04-19 16:23:13 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getThirdPartyExtensions = async (
|
|
|
|
|
readmePath = path.join(
|
|
|
|
|
getDirnameFromImportMeta(import.meta.url),
|
|
|
|
|
'README.md',
|
|
|
|
|
),
|
|
|
|
|
) =>
|
|
|
|
|
normalizeNewlines(await fs.readFile(readmePath, 'utf8'))
|
2024-05-20 14:25:30 +03:00
|
|
|
|
.split('## Third-Party Extensions')[1]
|
2024-05-20 15:06:02 +03:00
|
|
|
|
.split('|\n\n')[0]
|
|
|
|
|
.split('|\n|')
|
2024-05-20 14:25:30 +03:00
|
|
|
|
.slice(2)
|
|
|
|
|
.map((line) => {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
const [module_, author] = line.split(' | ');
|
|
|
|
|
const module = module_.split('<img src="')[0];
|
|
|
|
|
const moduleName = /\[(.+)]/.exec(module)?.[1];
|
|
|
|
|
if (moduleName === undefined) {
|
|
|
|
|
throw new Error(`Module name improperly parsed from line: ${line}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const moduleUrl = /\((.+)\)/.exec(module)?.[1];
|
|
|
|
|
if (moduleUrl === undefined) {
|
|
|
|
|
throw new Error(`Module URL improperly parsed from line: ${line}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const authorName = /\[(.+)]/.exec(author)?.[1];
|
|
|
|
|
if (authorName === undefined) {
|
|
|
|
|
throw new Error(`Author improperly parsed from line: ${line}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const authorUrl = /\((.+)\)/.exec(author)?.[1];
|
|
|
|
|
if (authorUrl === undefined) {
|
|
|
|
|
throw new Error(`Author URL improperly parsed from line: ${line}`);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-20 14:25:30 +03:00
|
|
|
|
return {
|
|
|
|
|
module: {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
name: moduleName,
|
|
|
|
|
url: moduleUrl,
|
2024-05-20 14:25:30 +03:00
|
|
|
|
},
|
|
|
|
|
author: {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
name: authorName,
|
|
|
|
|
url: authorUrl,
|
2024-05-20 14:25:30 +03:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get information about third party libraries from the README table.
|
2024-09-29 17:28:34 +03:00
|
|
|
|
* @param {string} readmePath Path to the README file.
|
|
|
|
|
* @returns {Promise<ThirdPartyExtension[]>} Information about third party libraries.
|
2024-05-20 14:25:30 +03:00
|
|
|
|
*/
|
|
|
|
|
export const getThirdPartyLibraries = async (
|
|
|
|
|
readmePath = path.join(
|
|
|
|
|
getDirnameFromImportMeta(import.meta.url),
|
|
|
|
|
'README.md',
|
|
|
|
|
),
|
|
|
|
|
) =>
|
|
|
|
|
normalizeNewlines(await fs.readFile(readmePath, 'utf8'))
|
|
|
|
|
.split('## Third-Party Libraries')[1]
|
2024-05-20 15:06:02 +03:00
|
|
|
|
.split('|\n\n')[0]
|
|
|
|
|
.split('|\n|')
|
2023-04-19 16:23:13 +03:00
|
|
|
|
.slice(2)
|
|
|
|
|
.map((line) => {
|
|
|
|
|
let [module, author] = line.split(' | ');
|
2023-07-28 16:50:15 +03:00
|
|
|
|
module = module.split('<img src="')[0];
|
2024-06-06 15:40:35 +03:00
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-19 16:23:13 +03:00
|
|
|
|
return {
|
|
|
|
|
module: {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
name: moduleName,
|
|
|
|
|
url: moduleUrl,
|
2023-04-19 16:23:13 +03:00
|
|
|
|
},
|
|
|
|
|
author: {
|
2024-06-06 15:40:35 +03:00
|
|
|
|
name: authorName,
|
|
|
|
|
url: authorUrl,
|
2023-04-19 16:23:13 +03:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* `Intl.Collator` object ready to be used for icon titles sorting.
|
2024-06-06 15:40:35 +03:00
|
|
|
|
* @see {@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
|
|
|
|
|
*/
|
2023-04-19 16:23:13 +03:00
|
|
|
|
export const collator = new Intl.Collator('en', {
|
|
|
|
|
usage: 'search',
|
|
|
|
|
caseFirst: 'upper',
|
|
|
|
|
});
|