Repository: simple-icons/simple-icons (js)

Issue:
Expose internal utilities
### Description

With the next major v8 being released at the end of this week, we can start planning the exposure of internal utilities (*scripts/utils.js*) file. Note that this is not a breaking change. I suggest to expose the utilities in a new entrypoint `./utils`, so we could import the utilities using:

```javascript
import { getThirdPartyExtensions } from 'simple-icons/utils';
```

This would allow us to deduplicate a lot of code of other repositories like [`simple-icons-font`](https://github.com/simple-icons/simple-icons-font) and [`simple-icons-website`](https://github.com/simple-icons/simple-icons-website).

Pull request description:
Expose SDK in NPM package
Closes #8071 

All functions of *scripts/utils.js* except `getJsonSchemaData` and `writeIconsData` have been moved to a new file ~~*utils.mjs*~~ *sdk.mjs*, which is exposed by the ~~`./utils`~~ `./sdk` entrypoint.

Patch:
diff --git a/.dockerignore b/.dockerignore
index 3f4f042f..018644c5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -6,3 +6,4 @@ npm-debug.log
 /index.js
 /index.mjs
 /index.d.ts
+/sdk.js
diff --git a/.gitignore b/.gitignore
index f434dc32..f794d3ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 /index.js
 /index.mjs
 /index.d.ts
+/sdk.js
 
 # Ignore all files in the icons folder
 icons/*
@@ -21,6 +22,8 @@ yarn.lock
 # Dependency directories
 node_modules/
 
+# Generated files
+*.tgz
 
 ### macOS ###
 # General
diff --git a/.npmignore b/.npmignore
index 3689bed7..c97285f7 100644
--- a/.npmignore
+++ b/.npmignore
@@ -12,3 +12,6 @@
 !index.mjs
 !index.d.ts
 !types.d.ts
+!sdk.mjs
+!sdk.js
+!sdk.d.ts
diff --git a/.prettierignore b/.prettierignore
index 05fc64a7..1609b2c5 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -13,3 +13,4 @@ scripts/build/templates/*.js
 index.js
 index.mjs
 index.d.ts
+sdk.js
diff --git a/.svglintrc.mjs b/.svglintrc.mjs
index 9954a148..5b210675 100644
--- a/.svglintrc.mjs
+++ b/.svglintrc.mjs
@@ -4,7 +4,7 @@ import {
   getDirnameFromImportMeta,
   htmlFriendlyToTitle,
   collator,
-} from './scripts/utils.js';
+} from './sdk.mjs';
 import svgpath from 'svgpath';
 import svgPathBbox from 'svg-path-bbox';
 import parsePath from 'svg-path-segments';
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4a3f7c9f..fe744d23 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -396,3 +396,22 @@ Then, start a Docker container for simple-icons and attach to it:
 ```shell
 docker run -it --rm --entrypoint "/bin/ash" simple-icons
 ```
+
+## Developing Third-Party Extensions
+
+A SDK is included in the `simple-icons/sdk` entrypoint of the npm package to make it easier the development of third party extensions with Javascript and Typescript.
+
+```typescript
+import { getIconsData, type IconData } from 'simple-icons/sdk';
+
+const iconsData: IconData[] = getIconsData();
+```
+
+```javascript
+import { getIconsData } from 'simple-icons/sdk';
+
+/* @typedef {import("./simple-icons/sdk").IconData} IconData */
+
+/* @type {IconData[]} */
+const iconsData = getIconsData();
+```
diff --git a/package.json b/package.json
index eabb05a3..563cfe92 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,25 @@
     },
     "./icons/*": [
       "./icons/*"
-    ]
+    ],
+    "./sdk": {
+      "import": {
+        "types": "./sdk.d.ts",
+        "default": "./sdk.mjs"
+      },
+      "module": {
+        "types": "./sdk.d.ts",
+        "default": "./sdk.mjs"
+      },
+      "require": {
+        "types": "./sdk.d.ts",
+        "default": "./sdk.js"
+      },
+      "default": {
+        "types": "./sdk.d.ts",
+        "default": "./sdk.js"
+      }
+    }
   },
   "sideEffects": false,
   "repository": {
diff --git a/scripts/add-icon-data.js b/scripts/add-icon-data.js
index 8093bc34..eb1b8d18 100644
--- a/scripts/add-icon-data.js
+++ b/scripts/add-icon-data.js
@@ -1,4 +1,3 @@
-import fs from 'node:fs/promises';
 import inquirer from 'inquirer';
 import chalk from 'chalk';
 import getRelativeLuminance from 'get-relative-luminance';
@@ -7,11 +6,10 @@ import {
   collator,
   getJsonSchemaData,
   getIconsDataString,
-  getIconDataPath,
-  writeIconsData,
   titleToSlug,
   normalizeColor,
-} from './utils.js';
+} from '../sdk.mjs';
+import { writeIconsData } from './utils.js';
 
 const hexPattern = /^#?[a-f0-9]{3,8}$/i;
 
diff --git a/scripts/build/package.js b/scripts/build/package.js
index 62e9e1b7..d312b6d0 100644
--- a/scripts/build/package.js
+++ b/scripts/build/package.js
@@ -1,10 +1,7 @@
 #!/usr/bin/env node
 /**
  * @fileoverview
- * Compiles our icons into static .js files that can be imported in the browser
- * and are tree-shakeable. The static .js files go in icons/{filename}.js. Also
- * generates an index.js that exports all icons by title, but is not
- * tree-shakeable
+ * Simple Icons package build script.
  */
 
 import { promises as fs } from 'node:fs';
@@ -19,7 +16,7 @@ import {
   getIconsData,
   getDirnameFromImportMeta,
   collator,
-} from '../utils.js';
+} from '../../sdk.mjs';
 
 const __dirname = getDirnameFromImportMeta(import.meta.url);
 
@@ -29,6 +26,8 @@ const rootDir = path.resolve(__dirname, '..', '..');
 const iconsDir = path.resolve(rootDir, 'icons');
 const indexJsFile = path.resolve(rootDir, 'index.js');
 const indexMjsFile = path.resolve(rootDir, 'index.mjs');
+const sdkJsFile = path.resolve(rootDir, 'sdk.js');
+const sdkMjsFile = path.resolve(rootDir, 'sdk.mjs');
 const indexDtsFile = path.resolve(rootDir, 'index.d.ts');
 
 const templatesDir = path.resolve(__dirname, 'templates');
@@ -68,10 +67,9 @@ const build = async () => {
       licenseToObject(icon.license),
     );
   };
-  const writeJs = async (filepath, rawJavaScript) => {
-    const { code } = await esbuildTransform(rawJavaScript, {
-      minify: true,
-    });
+  const writeJs = async (filepath, rawJavaScript, opts = null) => {
+    opts = opts === null ? { minify: true } : opts;
+    const { code } = await esbuildTransform(rawJavaScript, opts);
     await fs.writeFile(filepath, code);
   };
   const writeTs = async (filepath, rawTypeScript) => {
@@ -119,6 +117,11 @@ const build = async () => {
     '',
   )}`;
   await writeTs(indexDtsFile, rawIndexDts);
+
+  // create a CommonJS SDK file
+  await writeJs(sdkJsFile, await fs.readFile(sdkMjsFile, UTF8), {
+    format: 'cjs',
+  });
 };
 
 build();
diff --git a/scripts/get-filename.js b/scripts/get-filename.js
index 6649c8a2..1b34d91d 100644
--- a/scripts/get-filename.js
+++ b/scripts/get-filename.js
@@ -5,7 +5,7 @@
  * icon SVG filename to standard output.
  */
 
-import { titleToSlug } from './utils.js';
+import { titleToSlug } from '../sdk.mjs';
 
 if (process.argv.length < 3) {
   console.error('Provide a brand name as argument');
diff --git a/scripts/lint/jsonlint.js b/scripts/lint/jsonlint.js
index 920d0bd4..dd223c45 100644
--- a/scripts/lint/jsonlint.js
+++ b/scripts/lint/jsonlint.js
@@ -4,14 +4,10 @@
  * CLI tool to run jsonschema on the simple-icons.json data file.
  */
 
-import { promises as fs } from 'node:fs';
 import path from 'node:path';
 import { Validator } from 'jsonschema';
-import {
-  getDirnameFromImportMeta,
-  getIconsData,
-  getJsonSchemaData,
-} from '../utils.js';
+import { getDirnameFromImportMeta, getIconsData } from '../../sdk.mjs';
+import { getJsonSchemaData } from '../utils.js';
 
 const icons = await getIconsData();
 const __dirname = getDirnameFromImportMeta(import.meta.url);
diff --git a/scripts/lint/ourlint.js b/scripts/lint/ourlint.js
index 3512a55b..dfb346fb 100644
--- a/scripts/lint/ourlint.js
+++ b/scripts/lint/ourlint.js
@@ -6,7 +6,7 @@
  */
 
 import fakeDiff from 'fake-diff';
-import { getIconsDataString, normalizeNewlines, collator } from '../utils.js';
+import { getIconsDataString, normalizeNewlines, collator } from '../../sdk.mjs';
 
 /**
  * Contains our tests so they can be isolated from each other.
diff --git a/scripts/release/bump-version.js b/scripts/release/bump-version.js
index d19b7bb0..fd1acbf0 100644
--- a/scripts/release/bump-version.js
+++ b/scripts/release/bump-version.js
@@ -6,7 +6,7 @@
 
 import fs from 'node:fs';
 import path from 'node:path';
-import { getDirnameFromImportMeta } from '../utils.js';
+import { getDirnameFromImportMeta } from '../../sdk.mjs';
 
 const __dirname = getDirnameFromImportMeta(import.meta.url);
 
diff --git a/scripts/release/update-cdn-urls.js b/scripts/release/update-cdn-urls.js
index ed311a18..38eaaf4b 100644
--- a/scripts/release/update-cdn-urls.js
+++ b/scripts/release/update-cdn-urls.js
@@ -7,7 +7,7 @@
 
 import fs from 'node:fs';
 import path from 'node:path';
-import { getDirnameFromImportMeta } from '../utils.js';
+import { getDirnameFromImportMeta } from '../../sdk.mjs';
 
 const __dirname = getDirnameFromImportMeta(import.meta.url);
 
diff --git a/scripts/release/update-slugs-table.js b/scripts/release/update-slugs-table.js
index 250c96d6..e8ddd952 100644
--- a/scripts/release/update-slugs-table.js
+++ b/scripts/release/update-slugs-table.js
@@ -7,7 +7,7 @@
 import { promises as fs } from 'node:fs';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
-import { getIconsData, getIconSlug } from '../utils.js';
+import { getIconsData, getIconSlug } from '../../sdk.mjs';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
diff --git a/scripts/release/update-svgs-count.js b/scripts/release/update-svgs-count.js
index 4c426254..484c891e 100644
--- a/scripts/release/update-svgs-count.js
+++ b/scripts/release/update-svgs-count.js
@@ -7,7 +7,7 @@
  */
 import { promises as fs } from 'node:fs';
 import path from 'node:path';
-import { getDirnameFromImportMeta, getIconsData } from '../utils.js';
+import { getDirnameFromImportMeta, getIconsData } from '../../sdk.mjs';
 
 const regexMatcher = /Over\s(\d+)\s/;
 const updateRange = 100;
diff --git a/scripts/utils.js b/scripts/utils.js
index 50af0bf6..c0b2c913 100644
--- a/scripts/utils.js
+++ b/scripts/utils.js
@@ -1,110 +1,9 @@
-/**
- * @fileoverview
- * Some common utilities for scripts.
- */
-
 import path from 'node:path';
 import fs from 'node:fs/promises';
-import { fileURLToPath } from 'node:url';
-
-const TITLE_TO_SLUG_REPLACEMENTS = {
-  '+': 'plus',
-  '.': 'dot',
-  '&': 'and',
-  đ: 'd',
-  ħ: 'h',
-  ı: 'i',
-  ĸ: 'k',
-  ŀ: 'l',
-  ł: 'l',
-  ß: 'ss',
-  ŧ: 't',
-};
-
-const TITLE_TO_SLUG_CHARS_REGEX = RegExp(
-  `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`,
-  'g',
-);
-
-const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z0-9]/g;
-
-export const URL_REGEX = /^https:\/\/[^\s]+$/;
-
-/**
- * Get the directory name where this file is located from `import.meta.url`,
- * equivalent to the `__dirname` global variable in CommonJS.
- * @param {String} importMetaUrl import.meta.url
- */
-export const getDirnameFromImportMeta = (importMetaUrl) =>
-  path.dirname(fileURLToPath(importMetaUrl));
+import { getDirnameFromImportMeta, getIconDataPath } from '../sdk.mjs';
 
 const __dirname = getDirnameFromImportMeta(import.meta.url);
 
-/**
- * Get the slug/filename for an icon.
- * @param {Object} icon The icon data as it appears in _data/simple-icons.json
- */
-export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
-
-/**
- * Extract the path from an icon SVG content.
- * @param {Object} svg The icon SVG content
- **/
-export const svgToPath = (svg) => svg.match(/<path\s+d="([^"]*)/)[1];
-
-/**
- * Converts a brand title into a slug/filename.
- * @param {String} title The title to convert
- */
-export const titleToSlug = (title) =>
-  title
-    .toLowerCase()
-    .replace(
-      TITLE_TO_SLUG_CHARS_REGEX,
-      (char) => TITLE_TO_SLUG_REPLACEMENTS[char],
-    )
-    .normalize('NFD')
-    .replace(TITLE_TO_SLUG_RANGE_REGEX, '');
-
-/**
- * Converts a slug into a variable name that can be exported.
- * @param {String} slug The slug to convert
- */
-export const slugToVariableName = (slug) => {
-  const slugFirstLetter = slug[0].toUpperCase();
-  const slugRest = slug.slice(1);
-  return `si${slugFirstLetter}${slugRest}`;
-};
-
-/**
- * Converts a brand title (as it is seen in simple-icons.json) into a brand
- * title in HTML/SVG friendly format.
- * @param {String} brandTitle The title to convert
- */
-export const titleToHtmlFriendly = (brandTitle) =>
-  brandTitle
-    .replace(/&/g, '&amp;')
-    .replace(/"/g, '&quot;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/./g, (char) => {
-      const charCode = char.charCodeAt(0);
-      return charCode > 127 ? `&#${charCode};` : char;
-    });
-
-/**
- * Converts a brand title in HTML/SVG friendly format into a brand title (as
- * it is seen in simple-icons.json)
- * @param {String} htmlFriendlyTitle The title to convert
- */
-export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
-  htmlFriendlyTitle
-    .replace(/&#([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num)))
-    .replace(
-      /&(quot|amp|lt|gt);/g,
-      (_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' }[ref]),
-    );
-
 /**
  * Get JSON schema data.
  * @param {String|undefined} rootDir Path to the root directory of the project.
@@ -112,104 +11,23 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
 export const getJsonSchemaData = async (
   rootDir = path.resolve(__dirname, '..'),
 ) => {
-  const __dirname = getDirnameFromImportMeta(import.meta.url);
   const jsonSchemaPath = path.resolve(rootDir, '.jsonschema.json');
   const jsonSchemaString = await fs.readFile(jsonSchemaPath, 'utf8');
   return JSON.parse(jsonSchemaString);
 };
 
-/**
- * Get path of _data/simpe-icons.json.
- * @param {String|undefined} rootDir Path to the root directory of the project.
- */
-export const getIconDataPath = (
-  rootDir = path.resolve(getDirnameFromImportMeta(import.meta.url), '..'),
-) => {
-  return path.resolve(rootDir, '_data', 'simple-icons.json');
-};
-
-/**
- * Get contents of _data/simple-icons.json.
- * @param {String|undefined} rootDir Path to the root directory of the project.
- */
-export const getIconsDataString = (rootDir) => {
-  return fs.readFile(getIconDataPath(rootDir), 'utf8');
-};
-
-/**
- * Get icons data as object from _data/simple-icons.json.
- * @param {String|undefined} rootDir Path to the root directory of the project.
- */
-export const getIconsData = async (rootDir) => {
-  const fileContents = await getIconsDataString(rootDir);
-  return JSON.parse(fileContents).icons;
-};
-
 /**
  * Write icons data to _data/simple-icons.json.
  * @param {Object} iconsData Icons data object.
  * @param {String|undefined} rootDir Path to the root directory of the project.
  */
-export const writeIconsData = async (iconsData, rootDir) => {
+export const writeIconsData = async (
+  iconsData,
+  rootDir = path.resolve(__dirname, '..'),
+) => {
   return fs.writeFile(
     getIconDataPath(rootDir),
     `${JSON.stringify(iconsData, null, 4)}\n`,
     'utf8',
   );
 };
-
-/**
- * Replace Windows newline characters by Unix ones.
- * @param {String} text The text to replace
- */
-export const normalizeNewlines = (text) => {
-  return text.replace(/\r\n/g, '\n');
-};
-
-/**
- * Convert non-6-digit hex color to 6-digit.
- * @param {String} text The color text
- */
-export const normalizeColor = (text) => {
-  let color = text.replace('#', '').toUpperCase();
-  if (color.length < 6) {
-    color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join('');
-  } else if (color.length > 6) {
-    color = color.slice(0, 6);
-  }
-  return color;
-};
-
-/**
- * Get information about third party extensions.
- * @param {String} readmePath Path to the README file
- */
-export const getThirdPartyExtensions = async (readmePath) =>
-  normalizeNewlines(await fs.readFile(readmePath, 'utf8'))
-    .split('## Third-Party Extensions\n\n')[1]
-    .split('\n\n')[0]
-    .split('\n')
-    .slice(2)
-    .map((line) => {
-      const [module, author] = line.split(' | ');
-      return {
-        module: {
-          name: /\[(.+)\]/.exec(module)[1],
-          url: /\((.+)\)/.exec(module.split('<picture>')[0])[1],
-        },
-        author: {
-          name: /\[(.+)\]/.exec(author)[1],
-          url: /\((.+)\)/.exec(author)[1],
-        },
-      };
-    });
-
-/**
- * `Intl.Collator` object ready to be used for icon titles sorting.
- * @type {Intl.Collator}
- * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator
- **/
-export const collator = new Intl.Collator('en', {
-  usage: 'search',
-  caseFirst: 'upper',
-});
diff --git a/sdk.d.ts b/sdk.d.ts
new file mode 100644
index 00000000..2362a47c
--- /dev/null
+++ b/sdk.d.ts
@@ -0,0 +1,96 @@
+/**
+ * @fileoverview
+ * Types for Simple Icons SDK.
+ */
+
+/**
+ * The data for a third-party extension.
+ *
+ * Includes the module and author of the extension,
+ * both including a name and URL.
+ *
+ * @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions}
+ */
+export type ThirdPartyExtension = {
+  module: ThirdPartyExtensionSubject;
+  author: ThirdPartyExtensionSubject;
+};
+
+type ThirdPartyExtensionSubject = {
+  name: string;
+  url: string;
+};
+
+/**
+ * The license for a Simple Icon.
+ *
+ * Corresponds to the `license` property in the *_data/simple-icons.json* file.
+ *
+ * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data}
+ */
+export type License = SPDXLicense | CustomLicense;
+
+type SPDXLicense = {
+  type: string;
+  url?: string;
+};
+
+type CustomLicense = {
+  type: 'custom';
+  url: string;
+};
+
+/**
+ * The aliases for a Simple Icon.
+ *
+ * Corresponds to the `aliases` property in the *_data/simple-icons.json* file.
+ *
+ * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases}
+ */
+export type Aliases = {
+  aka?: string[];
+  dup?: DuplicateAlias[];
+  loc?: { [key: string]: string };
+};
+
+type DuplicateAlias = {
+  title: string;
+  hex?: string;
+  guidelines?: string;
+};
+
+/**
+ * The data for a Simple Icon.
+ *
+ * Corresponds to the data stored for each icon in the *_data/simple-icons.json* file.
+ *
+ * @see {@link https://github.com/mondeja/simple-icons/blob/utils-entrypoint/CONTRIBUTING.md#7-update-the-json-data-for-simpleiconsorg Update the JSON Data for SimpleIcons.org}
+ */
+export type IconData = {
+  title: string;
+  hex: string;
+  source: string;
+  slug?: string;
+  guidelines?: string;
+  license?: License;
+  aliases?: Aliases;
+};
+
+export const URL_REGEX: RegExp;
+
+export function getDirnameFromImportMeta(importMetaUrl: string): string;
+export function getIconSlug(icon: IconData): string;
+export function svgToPath(svg: string): string;
+export function titleToSlug(title: string): string;
+export function slugToVariableName(slug: string): string;
+export function titleToHtmlFriendly(brandTitle: string): string;
+export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string;
+export function getIconDataPath(rootDir?: string): string;
+export function getIconsDataString(rootDir?: string): string;
+export function getIconsData(rootDir?: string): IconData[];
+export function normalizeNewlines(text: string): string;
+export function normalizeColor(text: string): string;
+export function getThirdPartyExtensions(
+  readmePath?: string,
+): Promise<ThirdPartyExtension[]>;
+export const collator: Intl.Collator;
diff --git a/sdk.mjs b/sdk.mjs
new file mode 100644
index 00000000..7f3d6721
--- /dev/null
+++ b/sdk.mjs
@@ -0,0 +1,222 @@
+/**
+ * @fileoverview
+ * Simple Icons SDK.
+ */
+
+import path from 'node:path';
+import fs from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+
+/**
+ * @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension
+ * @typedef {import("./sdk").IconData} IconData
+ */
+
+const TITLE_TO_SLUG_REPLACEMENTS = {
+  '+': 'plus',
+  '.': 'dot',
+  '&': 'and',
+  đ: 'd',
+  ħ: 'h',
+  ı: 'i',
+  ĸ: 'k',
+  ŀ: 'l',
+  ł: 'l',
+  ß: 'ss',
+  ŧ: 't',
+};
+
+const TITLE_TO_SLUG_CHARS_REGEX = RegExp(
+  `[${Object.keys(TITLE_TO_SLUG_REPLACEMENTS).join('')}]`,
+  'g',
+);
+
+const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z0-9]/g;
+
+/**
+ * Regex to validate HTTPs URLs.
+ */
+export const URL_REGEX = /^https:\/\/[^\s]+$/;
+
+/**
+ * Get the directory name where this file is located from `import.meta.url`,
+ * equivalent to the `__dirname` global variable in CommonJS.
+ * @param {String} importMetaUrl import.meta.url
+ * @returns {String} Directory name in which this file is located
+ */
+export const getDirnameFromImportMeta = (importMetaUrl) =>
+  path.dirname(fileURLToPath(importMetaUrl));
+
+/**
+ * Get the slug/filename for an icon.
+ * @param {IconData} icon The icon data as it appears in *_data/simple-icons.json*
+ * @returns {String} The slug/filename for the icon
+ */
+export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
+
+/**
+ * Extract the path from an icon SVG content.
+ * @param {String} svg The icon SVG content
+ * @returns {String} The path from the icon SVG content
+ **/
+export const svgToPath = (svg) => svg.match(/<path\s+d="([^"]*)/)[1];
+
+/**
+ * Converts a brand title into a slug/filename.
+ * @param {String} title The title to convert
+ * @returns {String} The slug/filename for the title
+ */
+export const titleToSlug = (title) =>
+  title
+    .toLowerCase()
+    .replace(
+      TITLE_TO_SLUG_CHARS_REGEX,
+      (char) => TITLE_TO_SLUG_REPLACEMENTS[char],
+    )
+    .normalize('NFD')
+    .replace(TITLE_TO_SLUG_RANGE_REGEX, '');
+
+/**
+ * Converts a slug into a variable name that can be exported.
+ * @param {String} slug The slug to convert
+ * @returns {String} The variable name for the slug
+ */
+export const slugToVariableName = (slug) => {
+  const slugFirstLetter = slug[0].toUpperCase();
+  const slugRest = slug.slice(1);
+  return `si${slugFirstLetter}${slugRest}`;
+};
+
+/**
+ * Converts a brand title as defined in *_data/simple-icons.json* into a brand
+ * title in HTML/SVG friendly format.
+ * @param {String} brandTitle The title to convert
+ * @returns {String} The brand title in HTML/SVG friendly format
+ */
+export const titleToHtmlFriendly = (brandTitle) =>
+  brandTitle
+    .replace(/&/g, '&amp;')
+    .replace(/"/g, '&quot;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/./g, (char) => {
+      const charCode = char.charCodeAt(0);
+      return charCode > 127 ? `&#${charCode};` : char;
+    });
+
+/**
+ * Converts a brand title in HTML/SVG friendly format into a brand title (as
+ * it is seen in *_data/simple-icons.json*)
+ * @param {String} htmlFriendlyTitle The title to convert
+ * @returns {String} The brand title in HTML/SVG friendly format
+ */
+export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
+  htmlFriendlyTitle
+    .replace(/&#([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num)))
+    .replace(
+      /&(quot|amp|lt|gt);/g,
+      (_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' }[ref]),
+    );
+
+/**
+ * Get path of *_data/simpe-icons.json*.
+ * @param {String|undefined} rootDir Path to the root directory of the project
+ * @returns {String} Path of *_data/simple-icons.json*
+ */
+export const getIconDataPath = (
+  rootDir = getDirnameFromImportMeta(import.meta.url),
+) => {
+  return path.resolve(rootDir, '_data', 'simple-icons.json');
+};
+
+/**
+ * Get contents of *_data/simple-icons.json*.
+ * @param {String|undefined} rootDir Path to the root directory of the project
+ * @returns {String} Content of *_data/simple-icons.json*
+ */
+export const getIconsDataString = (
+  rootDir = getDirnameFromImportMeta(import.meta.url),
+) => {
+  return fs.readFile(getIconDataPath(rootDir), 'utf8');
+};
+
+/**
+ * Get icons data as object from *_data/simple-icons.json*.
+ * @param {String|undefined} rootDir Path to the root directory of the project
+ * @returns {IconData[]} Icons data as array from *_data/simple-icons.json*
+ */
+export const getIconsData = async (
+  rootDir = getDirnameFromImportMeta(import.meta.url),
+) => {
+  const fileContents = await getIconsDataString(rootDir);
+  return JSON.parse(fileContents).icons;
+};
+
+/**
+ * Replace Windows newline characters by Unix ones.
+ * @param {String} text The text to replace
+ * @returns {String} The text with Windows newline characters replaced by Unix ones
+ */
+export const normalizeNewlines = (text) => {
+  return text.replace(/\r\n/g, '\n');
+};
+
+/**
+ * Convert non-6-digit hex color to 6-digit with the character `#` stripped.
+ * @param {String} text The color text
+ * @returns {String} The color text in 6-digit hex format
+ */
+export const normalizeColor = (text) => {
+  let color = text.replace('#', '').toUpperCase();
+  if (color.length < 6) {
+    color = [...color.slice(0, 3)].map((x) => x.repeat(2)).join('');
+  } else if (color.length > 6) {
+    color = color.slice(0, 6);
+  }
+  return color;
+};
+
+/**
+ * Get information about third party extensions from the README table.
+ * @param {String|undefined} readmePath Path to the README file
+ * @returns {Promise<ThirdPartyExtension[]>} Information about third party extensions
+ */
+export const getThirdPartyExtensions = async (
+  readmePath = path.join(
+    getDirnameFromImportMeta(import.meta.url),
+    'README.md',
+  ),
+) =>
+  normalizeNewlines(await fs.readFile(readmePath, 'utf8'))
+    .split('## Third-Party Extensions\n\n')[1]
+    .split('\n\n')[0]
+    .split('\n')
+    .slice(2)
+    .map((line) => {
+      let [module, author] = line.split(' | ');
+
+      // README shipped with package has not Github theme image links
+      module = module.includes('<picture>')
+        ? module.split('<picture>')[0]
+        : module.split('<img src="')[1].split(' ').slice(5).join(' ');
+      return {
+        module: {
+          name: /\[(.+)\]/.exec(module)[1],
+          url: /\((.+)\)/.exec(module)[1],
+        },
+        author: {
+          name: /\[(.+)\]/.exec(author)[1],
+          url: /\((.+)\)/.exec(author)[1],
+        },
+      };
+    });
+
+/**
+ * `Intl.Collator` object ready to be used for icon titles sorting.
+ * @type {Intl.Collator}
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator Intl.Collator}
+ **/
+export const collator = new Intl.Collator('en', {
+  usage: 'search',
+  caseFirst: 'upper',
+});


Test patch:
diff --git a/tests/docs.test.js b/tests/docs.test.js
index b29eb009..1c3fea60 100644
--- a/tests/docs.test.js
+++ b/tests/docs.test.js
@@ -2,11 +2,7 @@ import fs from 'node:fs';
 import path from 'node:path';
 import { describe, test } from 'mocha';
 import { strict as assert } from 'node:assert';
-import {
-  getThirdPartyExtensions,
-  getDirnameFromImportMeta,
-  URL_REGEX,
-} from '../scripts/utils.js';
+import { getThirdPartyExtensions, getDirnameFromImportMeta } from '../sdk.mjs';
 
 const __dirname = getDirnameFromImportMeta(import.meta.url);
 const root = path.dirname(__dirname);
diff --git a/tests/index.test.js b/tests/index.test.js
index 6af68fa9..a8f7c413 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -1,8 +1,4 @@
-import {
-  getIconsData,
-  getIconSlug,
-  slugToVariableName,
-} from '../scripts/utils.js';
+import { getIconsData, getIconSlug, slugToVariableName } from '../sdk.mjs';
 import * as simpleIcons from '../index.mjs';
 import { testIcon } from './test-icon.js';
 
diff --git a/tests/test-icon.js b/tests/test-icon.js
index e6496a97..87861984 100644
--- a/tests/test-icon.js
+++ b/tests/test-icon.js
@@ -2,7 +2,7 @@ import fs from 'node:fs';
 import path from 'node:path';
 import { strict as assert } from 'node:assert';
 import { describe, it } from 'mocha';
-import { URL_REGEX, titleToSlug } from '../scripts/utils.js';
+import { URL_REGEX, titleToSlug } from '../sdk.mjs';
 
 const iconsDir = path.resolve(process.cwd(), 'icons');