UNPKG

6.66 kBJavaScriptView Raw
1/**
2 * @fileoverview
3 * Simple Icons SDK.
4 */
5
6import fs from 'node:fs/promises';
7import path from 'node:path';
8import {fileURLToPath} from 'node:url';
9
10/**
11 * @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension
12 * @typedef {import("./sdk").IconData} IconData
13 */
14
15const TITLE_TO_SLUG_REPLACEMENTS = {
16 '+': 'plus',
17 '.': 'dot',
18 '&': 'and',
19 đ: 'd',
20 ħ: 'h',
21 ı: 'i',
22 ĸ: 'k',
23 ŀ: 'l',
24 ł: 'l',
25 ß: 'ss',
26 ŧ: 't',
27};
28
29const TITLE_TO_SLUG_CHARS_REGEX = new RegExp(
30 `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`,
31 'g',
32);
33
34const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z\d]/g;
35
36/**
37 * Regex to validate HTTPs URLs.
38 */
39export const URL_REGEX = /^https:\/\/[^\s"']+$/;
40
41/**
42 * Regex to validate SVG paths.
43 */
44export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae\d,. ]+$/i;
45
46/**
47 * Get the directory name where this file is located from `import.meta.url`,
48 * equivalent to the `__dirname` global variable in CommonJS.
49 * @param {String} importMetaUrl import.meta.url
50 * @returns {String} Directory name in which this file is located
51 */
52export const getDirnameFromImportMeta = (importMetaUrl) =>
53 path.dirname(fileURLToPath(importMetaUrl));
54
55/**
56 * Get the slug/filename for an icon.
57 * @param {IconData} icon The icon data as it appears in *_data/simple-icons.json*
58 * @returns {String} The slug/filename for the icon
59 */
60export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
61
62/**
63 * Extract the path from an icon SVG content.
64 * @param {String} svg The icon SVG content
65 * @returns {String} The path from the icon SVG content
66 **/
67export const svgToPath = (svg) => svg.split('"', 8)[7];
68
69/**
70 * Converts a brand title into a slug/filename.
71 * @param {String} title The title to convert
72 * @returns {String} The slug/filename for the title
73 */
74export const titleToSlug = (title) =>
75 title
76 .toLowerCase()
77 .replaceAll(
78 TITLE_TO_SLUG_CHARS_REGEX,
79 (char) => TITLE_TO_SLUG_REPLACEMENTS[char],
80 )
81 .normalize('NFD')
82 .replaceAll(TITLE_TO_SLUG_RANGE_REGEX, '');
83
84/**
85 * Converts a slug into a variable name that can be exported.
86 * @param {String} slug The slug to convert
87 * @returns {String} The variable name for the slug
88 */
89export const slugToVariableName = (slug) => {
90 const slugFirstLetter = slug[0].toUpperCase();
91 return `si${slugFirstLetter}${slug.slice(1)}`;
92};
93
94/**
95 * Converts a brand title as defined in *_data/simple-icons.json* into a brand
96 * title in HTML/SVG friendly format.
97 * @param {String} brandTitle The title to convert
98 * @returns {String} The brand title in HTML/SVG friendly format
99 */
100export const titleToHtmlFriendly = (brandTitle) =>
101 brandTitle
102 .replaceAll('&', '&')
103 .replaceAll('"', '"')
104 .replaceAll('<', '&lt;')
105 .replaceAll('>', '&gt;')
106 .replaceAll(/./g, (char) => {
107 const charCode = char.codePointAt(0);
108 return charCode > 127 ? `&#${charCode};` : char;
109 });
110
111/**
112 * Converts a brand title in HTML/SVG friendly format into a brand title (as
113 * it is seen in *_data/simple-icons.json*)
114 * @param {String} htmlFriendlyTitle The title to convert
115 * @returns {String} The brand title in HTML/SVG friendly format
116 */
117export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
118 htmlFriendlyTitle
119 .replaceAll(/&#(\d+);/g, (_, number_) =>
120 String.fromCodePoint(Number.parseInt(number_, 10)),
121 )
122 .replaceAll(
123 /&(quot|amp|lt|gt);/g,
124 (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference],
125 );
126
127/**
128 * Get path of *_data/simple-icons.json*.
129 * @param {String} rootDirectory Path to the root directory of the project
130 * @returns {String} Path of *_data/simple-icons.json*
131 */
132export const getIconDataPath = (
133 rootDirectory = getDirnameFromImportMeta(import.meta.url),
134) => {
135 return path.resolve(rootDirectory, '_data', 'simple-icons.json');
136};
137
138/**
139 * Get contents of *_data/simple-icons.json*.
140 * @param {String} rootDirectory Path to the root directory of the project
141 * @returns {String} Content of *_data/simple-icons.json*
142 */
143export const getIconsDataString = (
144 rootDirectory = getDirnameFromImportMeta(import.meta.url),
145) => {
146 return fs.readFile(getIconDataPath(rootDirectory), 'utf8');
147};
148
149/**
150 * Get icons data as object from *_data/simple-icons.json*.
151 * @param {String} rootDirectory Path to the root directory of the project
152 * @returns {IconData[]} Icons data as array from *_data/simple-icons.json*
153 */
154export const getIconsData = async (
155 rootDirectory = getDirnameFromImportMeta(import.meta.url),
156) => {
157 const fileContents = await getIconsDataString(rootDirectory);
158 return JSON.parse(fileContents).icons;
159};
160
161/**
162 * Replace Windows newline characters by Unix ones.
163 * @param {String} text The text to replace
164 * @returns {String} The text with Windows newline characters replaced by Unix ones
165 */
166export const normalizeNewlines = (text) => {
167 return text.replaceAll('\r\n', '\n');
168};
169
170/**
171 * Convert non-6-digit hex color to 6-digit with the character `#` stripped.
172 * @param {String} text The color text
173 * @returns {String} The color text in 6-digit hex format
174 */
175export const normalizeColor = (text) => {
176 let color = text.replace('#', '').toUpperCase();
177 if (color.length < 6) {
178 // eslint-disable-next-line unicorn/no-useless-spread
179 color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join('');
180 } else if (color.length > 6) {
181 color = color.slice(0, 6);
182 }
183
184 return color;
185};
186
187/**
188 * Get information about third party extensions from the README table.
189 * @param {String} readmePath Path to the README file
190 * @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions
191 */
192export const getThirdPartyExtensions = async (
193 readmePath = path.join(
194 getDirnameFromImportMeta(import.meta.url),
195 'README.md',
196 ),
197) =>
198 normalizeNewlines(await fs.readFile(readmePath, 'utf8'))
199 .split('## Third-Party Extensions\n\n')[1]
200 .split('\n\n', 1)[0]
201 .split('\n')
202 .slice(2)
203 .map((line) => {
204 let [module, author] = line.split(' | ');
205 module = module.split('<img src="')[0];
206 return {
207 module: {
208 name: /\[(.+)]/.exec(module)[1],
209 url: /\((.+)\)/.exec(module)[1],
210 },
211 author: {
212 name: /\[(.+)]/.exec(author)[1],
213 url: /\((.+)\)/.exec(author)[1],
214 },
215 };
216 });
217
218/**
219 * `Intl.Collator` object ready to be used for icon titles sorting.
220 * @type {Intl.Collator}
221 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
222 **/
223export const collator = new Intl.Collator('en', {
224 usage: 'search',
225 caseFirst: 'upper',
226});