@mintlify/common 1.0.1263 → 1.0.1265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -45,3 +45,5 @@ export * from './live-preview.js';
45
45
  export * from './paths/isEqualIgnoringLeadingSlash.js';
46
46
  export * from './sdk/index.js';
47
47
  export * from './seo/resolveSeoMetatags.js';
48
+ export * from './mintExportAssets.js';
49
+ export * from './snippetPresets.js';
package/dist/index.js CHANGED
@@ -45,3 +45,5 @@ export * from './live-preview.js';
45
45
  export * from './paths/isEqualIgnoringLeadingSlash.js';
46
46
  export * from './sdk/index.js';
47
47
  export * from './seo/resolveSeoMetatags.js';
48
+ export * from './mintExportAssets.js';
49
+ export * from './snippetPresets.js';
@@ -0,0 +1,3 @@
1
+ export declare const MINT_EXPORT_ASSETS_PREFIX = "/_mintlify/export-assets";
2
+ export declare const toMintExportAssetPath: (url: string, extension?: string) => string;
3
+ export declare const fromMintExportAssetPath: (assetPath: string) => string | undefined;
@@ -0,0 +1,58 @@
1
+ export const MINT_EXPORT_ASSETS_PREFIX = '/_mintlify/export-assets';
2
+ const QUERY_SEGMENT = '_q';
3
+ const HTTP_SEGMENT = '_http';
4
+ const PORT_SEPARATOR = '~';
5
+ const toBase64Url = (value) => btoa(String.fromCharCode(...new TextEncoder().encode(value)))
6
+ .replace(/\+/g, '-')
7
+ .replace(/\//g, '_')
8
+ .replace(/=+$/, '');
9
+ const fromBase64Url = (value) => {
10
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
11
+ const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
12
+ return new TextDecoder().decode(bytes);
13
+ };
14
+ export const toMintExportAssetPath = (url, extension) => {
15
+ var _a, _b;
16
+ let parsed;
17
+ try {
18
+ parsed = new URL(url);
19
+ }
20
+ catch (_c) {
21
+ return url;
22
+ }
23
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
24
+ return url;
25
+ const scheme = parsed.protocol === 'http:' ? `${HTTP_SEGMENT}/` : '';
26
+ const host = parsed.port ? `${parsed.hostname}${PORT_SEPARATOR}${parsed.port}` : parsed.hostname;
27
+ const base = `${MINT_EXPORT_ASSETS_PREFIX}/${scheme}${host}${parsed.pathname}`;
28
+ if (!parsed.search)
29
+ return base;
30
+ const resolvedExtension = extension !== null && extension !== void 0 ? extension : (_b = (_a = /\.([a-z0-9]+)$/i.exec(parsed.pathname)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
31
+ const suffix = resolvedExtension ? `.${resolvedExtension}` : '';
32
+ return `${base.replace(/\/$/, '')}/${QUERY_SEGMENT}/${toBase64Url(parsed.search.slice(1))}${suffix}`;
33
+ };
34
+ export const fromMintExportAssetPath = (assetPath) => {
35
+ if (!assetPath.startsWith(`${MINT_EXPORT_ASSETS_PREFIX}/`))
36
+ return undefined;
37
+ const unprefixed = assetPath.slice(MINT_EXPORT_ASSETS_PREFIX.length + 1);
38
+ const isHttp = unprefixed.startsWith(`${HTTP_SEGMENT}/`);
39
+ const protocol = isHttp ? 'http' : 'https';
40
+ const rest = isHttp ? unprefixed.slice(HTTP_SEGMENT.length + 1) : unprefixed;
41
+ const slashIndex = rest.indexOf('/');
42
+ const encodedHost = slashIndex === -1 ? rest : rest.slice(0, slashIndex);
43
+ const pathname = slashIndex === -1 ? '/' : rest.slice(slashIndex);
44
+ if (!encodedHost || encodedHost.startsWith('.') || encodedHost.includes('..'))
45
+ return undefined;
46
+ const host = encodedHost.replace(PORT_SEPARATOR, ':');
47
+ const queryMarker = `/${QUERY_SEGMENT}/`;
48
+ const queryIndex = pathname.lastIndexOf(queryMarker);
49
+ if (queryIndex === -1)
50
+ return `${protocol}://${host}${pathname}`;
51
+ const encodedQuery = pathname.slice(queryIndex + queryMarker.length).replace(/\.[a-z0-9]+$/i, '');
52
+ try {
53
+ return `${protocol}://${host}${pathname.slice(0, queryIndex)}?${fromBase64Url(encodedQuery)}`;
54
+ }
55
+ catch (_a) {
56
+ return undefined;
57
+ }
58
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { fromMintExportAssetPath, toMintExportAssetPath } from './mintExportAssets.js';
3
+ describe('toMintExportAssetPath', () => {
4
+ it('maps an absolute url to a local asset path', () => {
5
+ expect(toMintExportAssetPath('https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/house.svg')).toBe('/_mintlify/export-assets/d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/house.svg');
6
+ });
7
+ it('encodes the query string into the path with the given extension', () => {
8
+ const assetPath = toMintExportAssetPath('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;1,700&display=swap', 'css');
9
+ expect(assetPath).toMatch(/^\/_mintlify\/export-assets\/fonts\.googleapis\.com\/css2\/_q\/[\w-]+\.css$/);
10
+ });
11
+ it('keeps the source extension for urls with a query', () => {
12
+ expect(toMintExportAssetPath('https://cdn.example.com/style.css?v=1')).toMatch(/^\/_mintlify\/export-assets\/cdn\.example\.com\/style\.css\/_q\/[\w-]+\.css$/);
13
+ });
14
+ it('encodes ports without a colon so the path is valid on every file system', () => {
15
+ expect(toMintExportAssetPath('https://fonts.example.com:8443/custom.woff2')).toBe('/_mintlify/export-assets/fonts.example.com~8443/custom.woff2');
16
+ });
17
+ it('keeps plain http assets distinct from https ones', () => {
18
+ expect(toMintExportAssetPath('http://example.com/a.woff2')).toBe('/_mintlify/export-assets/_http/example.com/a.woff2');
19
+ });
20
+ it('leaves relative and non-http urls untouched', () => {
21
+ expect(toMintExportAssetPath('/images/logo.svg')).toBe('/images/logo.svg');
22
+ expect(toMintExportAssetPath('data:image/svg+xml,abc')).toBe('data:image/svg+xml,abc');
23
+ });
24
+ });
25
+ describe('fromMintExportAssetPath', () => {
26
+ it.each([
27
+ 'https://d3gk2c5xim1je2.cloudfront.net/flags/US.svg',
28
+ 'https://cdn.jsdelivr.net/npm/@fontsource/inter@5/files/inter-latin-400-normal.woff2',
29
+ 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;1,700&display=swap',
30
+ 'https://example.com/font.woff2?v=1&name=%C3%BC',
31
+ 'http://fonts.example.com/inter.woff2',
32
+ 'http://example.com/style.css?v=2',
33
+ 'https://example.com/font.WOFF2?v=1',
34
+ 'https://fonts.example.com:8443/custom.woff2',
35
+ 'http://localhost:8080/a.css?v=1',
36
+ ])('round-trips %s', (url) => {
37
+ expect(fromMintExportAssetPath(toMintExportAssetPath(url, 'css'))).toBe(url);
38
+ });
39
+ it('rejects paths outside the asset prefix or with traversal hosts', () => {
40
+ expect(fromMintExportAssetPath('/images/logo.svg')).toBeUndefined();
41
+ expect(fromMintExportAssetPath('/_mintlify/export-assets/../etc/passwd')).toBeUndefined();
42
+ });
43
+ });
@@ -10,6 +10,7 @@ export type FilterDivisionsParams = {
10
10
  userGroups: Set<string>;
11
11
  isPreview?: boolean;
12
12
  shouldUseDivisionMatch: boolean;
13
+ skipDivisionTargets?: boolean;
13
14
  cache: {
14
15
  dropdownCache: Map<string, string>;
15
16
  };
@@ -54,7 +55,7 @@ export type NavigationDivisions = {
54
55
  products: Array<ProductsResult>;
55
56
  menu: Array<MenuResult>;
56
57
  };
57
- export declare const filterDivisions: ({ currentPath, currentVersion, currentLanguage, currentProduct, decoratedNav, userGroups, shouldUseDivisionMatch, cache, isPreview, }: FilterDivisionsParams) => FilterDivisionsResult;
58
+ export declare const filterDivisions: ({ currentPath, currentVersion, currentLanguage, currentProduct, decoratedNav, userGroups, shouldUseDivisionMatch, skipDivisionTargets, cache, isPreview, }: FilterDivisionsParams) => FilterDivisionsResult;
58
59
  /**
59
60
  * TODO: we should add cache for versions and languages in the future
60
61
  * Generate a cache key for the dropdowns
@@ -34,7 +34,7 @@ function reorderForCurrentProduct(items, currentProduct) {
34
34
  return items;
35
35
  return [match, ...items.slice(0, idx), ...items.slice(idx + 1)];
36
36
  }
37
- export const filterDivisions = ({ currentPath, currentVersion, currentLanguage, currentProduct, decoratedNav, userGroups, shouldUseDivisionMatch = true, cache, isPreview = false, }) => {
37
+ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage, currentProduct, decoratedNav, userGroups, shouldUseDivisionMatch = true, skipDivisionTargets = false, cache, isPreview = false, }) => {
38
38
  var _a, _b, _c;
39
39
  let navigationDivisions = {
40
40
  tabs: [],
@@ -204,7 +204,7 @@ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage,
204
204
  }
205
205
  if (filteredGroupsOrPages && filteredGroupsOrPages.length)
206
206
  groups = filteredGroupsOrPages;
207
- if (navigationDivisions.versions.length) {
207
+ if (!skipDivisionTargets && navigationDivisions.versions.length) {
208
208
  updateFirstHrefInDivision({
209
209
  type: 'version',
210
210
  currentPath,
@@ -221,7 +221,7 @@ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage,
221
221
  activeDivisionOwner: activeVersionsOwner,
222
222
  });
223
223
  }
224
- if (navigationDivisions.languages.length) {
224
+ if (!skipDivisionTargets && navigationDivisions.languages.length) {
225
225
  updateFirstHrefInDivision({
226
226
  type: 'language',
227
227
  currentPath,
@@ -13,15 +13,16 @@ export function isCommonPage(entry, path) {
13
13
  isCommonPageRecursive(entry, path, occurrences);
14
14
  return occurrences.value > 1;
15
15
  }
16
+ const CHILD_KEYS = [...divisions, 'groups', 'pages'];
16
17
  function isCommonPageRecursive(entry, path, occurrences) {
17
- if (typeof entry !== 'object')
18
+ if (typeof entry !== 'object' || occurrences.value > 1)
18
19
  return;
19
20
  if (isPage(entry)) {
20
21
  if (isEqualIgnoringLeadingSlash(entry.href, path)) {
21
22
  occurrences.value++;
22
23
  }
23
24
  }
24
- for (const key of [...divisions, 'groups', 'pages']) {
25
+ for (const key of CHILD_KEYS) {
25
26
  if (key === 'pages' && 'root' in entry && typeof entry.root === 'object') {
26
27
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- root is a DecoratedNavigationPage
27
28
  isCommonPageRecursive(entry.root, path, occurrences);
@@ -149,3 +149,11 @@ describe('pathsDifferByOneSegment', () => {
149
149
  });
150
150
  });
151
151
  });
152
+ describe('pathsDifferByOneSegment configured languages', () => {
153
+ it('sees languages added to the same array in place', () => {
154
+ const languages = ['zz'];
155
+ expect(pathsDifferByOneSegment('/tlh/guide', '/guide', 'language', languages)).toBe(false);
156
+ languages.push('tlh');
157
+ expect(pathsDifferByOneSegment('/tlh/guide', '/guide', 'language', languages)).toBe(true);
158
+ });
159
+ });
@@ -592,6 +592,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
592
592
  currentLanguage: undefined,
593
593
  currentProduct,
594
594
  shouldUseDivisionMatch: false,
595
+ skipDivisionTargets: true,
595
596
  cache: { dropdownCache: new Map() },
596
597
  isPreview,
597
598
  });
@@ -107,7 +107,7 @@ export function updateFirstHrefInDivision({ type, currentPath, entry, parentGrou
107
107
  });
108
108
  }
109
109
  }
110
- for (const key of [...divisions, 'groups']) {
110
+ for (const key of divisions) {
111
111
  const subDivisions = readObjectArrayProperty(entry, key);
112
112
  if (!subDivisions)
113
113
  continue;
@@ -165,10 +165,7 @@ export function updateFirstHrefInDivision({ type, currentPath, entry, parentGrou
165
165
  type,
166
166
  currentPath,
167
167
  entry: subDivision,
168
- parentGroups: key === 'groups' && 'groups' in entry
169
- ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
170
- entry.groups
171
- : parentGroups,
168
+ parentGroups,
172
169
  nearestDivisionValue: divisionValue !== null && divisionValue !== void 0 ? divisionValue : nearestDivisionValue,
173
170
  isActiveGroup,
174
171
  inActiveDivision: inActiveDivision,
@@ -220,14 +217,37 @@ function findActiveGroupNames(entry, currentPath, activeGroupNames = new Set())
220
217
  }
221
218
  return activeGroupNames;
222
219
  }
220
+ const NO_LANGUAGES = [];
221
+ let lastLanguageSet;
222
+ function getLowerCaseLanguageSet(languages) {
223
+ const cached = lastLanguageSet;
224
+ if ((cached === null || cached === void 0 ? void 0 : cached.languages.length) === languages.length &&
225
+ cached.languages.every((language, i) => language === languages[i])) {
226
+ return cached.set;
227
+ }
228
+ const set = new Set(languages.map((language) => language.toLowerCase()));
229
+ lastLanguageSet = { languages: [...languages], set };
230
+ return set;
231
+ }
223
232
  const isLanguageSegment = (segment, languages) => {
224
233
  if (segment === undefined)
225
234
  return false;
226
- if (isKnownLocale(segment))
227
- return true;
228
- const lowerCaseSegment = segment.toLowerCase();
229
- return languages.some((language) => language.toLowerCase() === lowerCaseSegment);
235
+ return isKnownLocale(segment) || languages.has(segment.toLowerCase());
230
236
  };
237
+ const toSegments = (path) => path.split('/').filter(Boolean);
238
+ let lastTarget;
239
+ function getTarget(path, languages) {
240
+ if ((lastTarget === null || lastTarget === void 0 ? void 0 : lastTarget.path) !== path || lastTarget.languages !== languages) {
241
+ const segments = toSegments(path);
242
+ lastTarget = {
243
+ path,
244
+ languages,
245
+ segments,
246
+ languagePath: parseLanguagePath(segments, languages),
247
+ };
248
+ }
249
+ return lastTarget;
250
+ }
231
251
  function parseLanguagePath(segments, languages) {
232
252
  let index = 0;
233
253
  let locale;
@@ -340,19 +360,21 @@ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup,
340
360
  }
341
361
  }
342
362
  }
343
- export function pathsDifferByOneSegment(a, b, type, languages = []) {
363
+ export function pathsDifferByOneSegment(a, b, type, languages = NO_LANGUAGES) {
344
364
  var _a, _b;
345
- const aSegments = a.split('/').filter(Boolean);
346
- const bSegments = b.split('/').filter(Boolean);
365
+ const languageSet = getLowerCaseLanguageSet(languages);
366
+ const aSegments = toSegments(a);
367
+ const target = getTarget(b, languageSet);
368
+ const bSegments = target.segments;
347
369
  // For language switching, we need to be more strict - paths should be identical except for the language segment
348
370
  // once normalized, content segments should be identical and only locale prefix may differ.
349
371
  if (type === 'language') {
350
- const aPath = parseLanguagePath(aSegments, languages);
351
- const bPath = parseLanguagePath(bSegments, languages);
372
+ const aPath = parseLanguagePath(aSegments, languageSet);
373
+ const bPath = target.languagePath;
352
374
  if (aPath.hasDocsBase !== bPath.hasDocsBase)
353
375
  return false;
354
376
  if (!aPath.locale && !bPath.locale) {
355
- return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments, languages);
377
+ return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments, languageSet);
356
378
  }
357
379
  // For language switching, the content parts (including version segments) must be identical.
358
380
  if (aPath.contentSegments.length !== bPath.contentSegments.length)
@@ -0,0 +1,8 @@
1
+ import dns from 'dns';
2
+ export declare class PrivateIPError extends Error {
3
+ readonly hostname: string;
4
+ readonly ip: string;
5
+ constructor(hostname: string, ip: string);
6
+ }
7
+ export declare function isPrivateIP(ip: string): boolean;
8
+ export declare function safeLookup(hostname: string, options: dns.LookupOptions, callback: (err: NodeJS.ErrnoException | null, address: string | dns.LookupAddress[], family?: number) => void): void;
@@ -0,0 +1,108 @@
1
+ import dns from 'dns';
2
+ import { BlockList, isIP } from 'net';
3
+ export class PrivateIPError extends Error {
4
+ constructor(hostname, ip) {
5
+ super(`Blocked: ${hostname} resolves to private IP ${ip}`);
6
+ this.hostname = hostname;
7
+ this.ip = ip;
8
+ this.name = 'PrivateIPError';
9
+ }
10
+ }
11
+ const privateIPBlockList = new BlockList();
12
+ for (const [network, prefix] of [
13
+ ['0.0.0.0', 8],
14
+ ['10.0.0.0', 8],
15
+ ['100.64.0.0', 10],
16
+ ['127.0.0.0', 8],
17
+ ['169.254.0.0', 16],
18
+ ['172.16.0.0', 12],
19
+ ['192.0.0.0', 24],
20
+ ['192.0.2.0', 24],
21
+ ['192.88.99.0', 24],
22
+ ['192.168.0.0', 16],
23
+ ['198.18.0.0', 15],
24
+ ['198.51.100.0', 24],
25
+ ['203.0.113.0', 24],
26
+ ['224.0.0.0', 4],
27
+ ['240.0.0.0', 4],
28
+ ]) {
29
+ privateIPBlockList.addSubnet(network, prefix, 'ipv4');
30
+ }
31
+ // ::/96 covers ::, ::1 and the deprecated IPv4-compatible ::a.b.c.d form; the NAT64 and
32
+ // 6to4 prefixes embed an arbitrary IPv4 address, so they are blocked wholesale rather
33
+ // than unwrapped - they are transition mechanisms, never a legitimate API destination
34
+ for (const [network, prefix] of [
35
+ ['::', 96],
36
+ ['64:ff9b::', 96],
37
+ ['64:ff9b:1::', 48],
38
+ ['100::', 64],
39
+ ['2001::', 32],
40
+ ['2001:2::', 48],
41
+ ['2001:10::', 28],
42
+ ['2001:20::', 28],
43
+ ['2001:db8::', 32],
44
+ ['2002::', 16],
45
+ ['3fff::', 20],
46
+ ['5f00::', 16],
47
+ ['fc00::', 7],
48
+ ['fe80::', 10],
49
+ ['fec0::', 10],
50
+ ['ff00::', 8],
51
+ ]) {
52
+ privateIPBlockList.addSubnet(network, prefix, 'ipv6');
53
+ }
54
+ export function isPrivateIP(ip) {
55
+ const ipType = isIP(ip);
56
+ if (ipType === 0)
57
+ return true;
58
+ // handle IPv4-mapped IPv6 (::ffff:x.x.x.x) - extract IPv4 and check that
59
+ const lower = ip.toLowerCase();
60
+ if (lower.startsWith('::ffff:') && ip.includes('.')) {
61
+ const ipv4 = ip.slice(7);
62
+ if (isIP(ipv4) === 4) {
63
+ return privateIPBlockList.check(ipv4, 'ipv4');
64
+ }
65
+ return true; // malformed IPv4 part = block
66
+ }
67
+ return privateIPBlockList.check(ip, ipType === 4 ? 'ipv4' : 'ipv6');
68
+ }
69
+ // custom lookup that validates resolved IPs before returning them
70
+ // the IP returned here is the EXACT IP the socket will connect to (no TOCTOU)
71
+ // resolves all addresses so dual-stack fallback keeps working; rejects if any is private
72
+ export function safeLookup(hostname, options, callback) {
73
+ dns.lookup(hostname, Object.assign(Object.assign({}, options), { all: true }), (err, addresses) => {
74
+ const fail = (e) => {
75
+ if (options.all) {
76
+ callback(e, []);
77
+ }
78
+ else {
79
+ callback(e, '', 0);
80
+ }
81
+ };
82
+ if (err) {
83
+ fail(err);
84
+ return;
85
+ }
86
+ if (!Array.isArray(addresses)) {
87
+ fail(new Error('unexpected DNS lookup result'));
88
+ return;
89
+ }
90
+ const [first] = addresses;
91
+ if (!first) {
92
+ fail(new Error('unexpected DNS lookup result'));
93
+ return;
94
+ }
95
+ for (const { address } of addresses) {
96
+ if (typeof address !== 'string' || isPrivateIP(address)) {
97
+ fail(new PrivateIPError(hostname, address));
98
+ return;
99
+ }
100
+ }
101
+ if (options.all) {
102
+ callback(null, addresses);
103
+ }
104
+ else {
105
+ callback(null, first.address, first.family);
106
+ }
107
+ });
108
+ }
@@ -0,0 +1,26 @@
1
+ export declare const MINTLIFY_ICONS_CDN_URL = "https://d3gk2c5xim1je2.cloudfront.net";
2
+ export type SnippetPreset = {
3
+ /** Canonical identifier (e.g., 'python', 'bash') */
4
+ key: string;
5
+ /** Alternative names that map to this preset (e.g., ['py'] for python) */
6
+ aliases?: string[];
7
+ /** Human-readable name (e.g., "Python", "cURL") */
8
+ displayName: string;
9
+ /** Devicon SVG name if different from key (e.g., 'dot-net' for '.NET') */
10
+ iconKey?: string;
11
+ /** Shiki language for syntax highlighting */
12
+ shikiLanguage: string;
13
+ /** httpsnippet config for code generation */
14
+ httpSnippet: {
15
+ target: string;
16
+ client?: string;
17
+ };
18
+ };
19
+ /**
20
+ * Central config for API playground language options.
21
+ * Each preset defines: display info, icon, syntax highlighting, and code generation.
22
+ */
23
+ export declare const SNIPPET_PRESETS: SnippetPreset[];
24
+ export declare const getPreset: (lang: string) => SnippetPreset | undefined;
25
+ export declare const getIconKey: (lang: string) => string | undefined;
26
+ export declare const getLanguageIconUrl: (language: string) => string | null;
@@ -0,0 +1,149 @@
1
+ var _a;
2
+ export const MINTLIFY_ICONS_CDN_URL = 'https://d3gk2c5xim1je2.cloudfront.net';
3
+ /**
4
+ * Central config for API playground language options.
5
+ * Each preset defines: display info, icon, syntax highlighting, and code generation.
6
+ */
7
+ export const SNIPPET_PRESETS = [
8
+ {
9
+ key: 'bash',
10
+ aliases: ['curl', 'sh', 'shell'],
11
+ displayName: 'cURL',
12
+ shikiLanguage: 'bash',
13
+ httpSnippet: { target: 'shell' },
14
+ },
15
+ {
16
+ key: 'python',
17
+ aliases: ['py'],
18
+ displayName: 'Python',
19
+ shikiLanguage: 'python',
20
+ httpSnippet: { target: 'python', client: 'requests' },
21
+ },
22
+ {
23
+ key: 'javascript',
24
+ aliases: ['js'],
25
+ displayName: 'JavaScript',
26
+ shikiLanguage: 'javascript',
27
+ httpSnippet: { target: 'javascript', client: 'fetch' },
28
+ },
29
+ {
30
+ key: 'node',
31
+ aliases: ['nodejs', 'node.js'],
32
+ displayName: 'Node.js',
33
+ iconKey: 'node',
34
+ shikiLanguage: 'javascript',
35
+ httpSnippet: { target: 'node', client: 'fetch' },
36
+ },
37
+ {
38
+ key: 'php',
39
+ displayName: 'PHP',
40
+ shikiLanguage: 'php',
41
+ httpSnippet: { target: 'php', client: 'curl' },
42
+ },
43
+ {
44
+ key: 'go',
45
+ aliases: ['golang'],
46
+ displayName: 'Go',
47
+ shikiLanguage: 'go',
48
+ httpSnippet: { target: 'go' },
49
+ },
50
+ {
51
+ key: 'java',
52
+ displayName: 'Java',
53
+ shikiLanguage: 'java',
54
+ httpSnippet: { target: 'java' },
55
+ },
56
+ {
57
+ key: 'ruby',
58
+ aliases: ['rb'],
59
+ displayName: 'Ruby',
60
+ shikiLanguage: 'ruby',
61
+ httpSnippet: { target: 'ruby' },
62
+ },
63
+ {
64
+ key: 'powershell',
65
+ displayName: 'PowerShell',
66
+ shikiLanguage: 'bash', // shiki doesn't have powershell in our LANGS
67
+ httpSnippet: { target: 'powershell' },
68
+ },
69
+ {
70
+ key: 'swift',
71
+ displayName: 'Swift',
72
+ shikiLanguage: 'swift',
73
+ httpSnippet: { target: 'swift' },
74
+ },
75
+ {
76
+ key: 'csharp',
77
+ aliases: ['c#'],
78
+ displayName: 'C#',
79
+ shikiLanguage: 'csharp',
80
+ httpSnippet: { target: 'csharp', client: 'restsharp' },
81
+ },
82
+ {
83
+ key: 'dotnet',
84
+ aliases: ['.net', '.NET', 'dotnet', 'dot-net'],
85
+ displayName: '.NET',
86
+ iconKey: 'dot-net',
87
+ shikiLanguage: 'csharp',
88
+ httpSnippet: { target: 'csharp', client: 'restsharp' },
89
+ },
90
+ {
91
+ key: 'typescript',
92
+ aliases: ['ts'],
93
+ displayName: 'TypeScript',
94
+ shikiLanguage: 'typescript',
95
+ httpSnippet: { target: 'javascript', client: 'fetch' },
96
+ },
97
+ {
98
+ key: 'c',
99
+ displayName: 'C',
100
+ shikiLanguage: 'c',
101
+ httpSnippet: { target: 'c' },
102
+ },
103
+ {
104
+ key: 'c++',
105
+ aliases: ['cpp'],
106
+ displayName: 'C++',
107
+ shikiLanguage: 'c++',
108
+ iconKey: 'cplusplus',
109
+ httpSnippet: { target: 'c' },
110
+ },
111
+ {
112
+ key: 'kotlin',
113
+ aliases: ['kt'],
114
+ displayName: 'Kotlin',
115
+ shikiLanguage: 'kotlin',
116
+ httpSnippet: { target: 'kotlin' },
117
+ },
118
+ {
119
+ key: 'rust',
120
+ aliases: ['rs'],
121
+ displayName: 'Rust',
122
+ shikiLanguage: 'rust',
123
+ httpSnippet: { target: 'rust' },
124
+ },
125
+ {
126
+ key: 'dart',
127
+ aliases: ['flutter'],
128
+ displayName: 'Dart',
129
+ shikiLanguage: 'dart',
130
+ httpSnippet: { target: 'dart' },
131
+ },
132
+ ];
133
+ const presetLookup = new Map();
134
+ for (const preset of SNIPPET_PRESETS) {
135
+ presetLookup.set(preset.key.toLowerCase(), preset);
136
+ for (const alias of (_a = preset.aliases) !== null && _a !== void 0 ? _a : []) {
137
+ presetLookup.set(alias.toLowerCase(), preset);
138
+ }
139
+ }
140
+ export const getPreset = (lang) => presetLookup.get(lang.toLowerCase());
141
+ export const getIconKey = (lang) => {
142
+ var _a;
143
+ const preset = getPreset(lang);
144
+ return preset ? ((_a = preset.iconKey) !== null && _a !== void 0 ? _a : preset.key) : lang.toLowerCase();
145
+ };
146
+ export const getLanguageIconUrl = (language) => {
147
+ const iconKey = getIconKey(language);
148
+ return iconKey ? `${MINTLIFY_ICONS_CDN_URL}/devicon/${iconKey}.svg` : null;
149
+ };