@thallylabs/migrate 0.2.22 → 0.2.24

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.js CHANGED
@@ -16,13 +16,13 @@ import {
16
16
  } from "fs";
17
17
  import { createRequire } from "module";
18
18
  import { basename as basename3, dirname as dirname4, extname as extname6, isAbsolute as isAbsolute2, posix as posix3, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
19
- import { parse as parseYaml4 } from "yaml";
19
+ import { parse as parseYaml5 } from "yaml";
20
20
  import * as ts4 from "typescript";
21
21
 
22
22
  // src/components.ts
23
23
  import { createHash } from "crypto";
24
24
  import { existsSync, lstatSync, readFileSync } from "fs";
25
- import { basename, dirname, extname, relative as relative2, resolve as resolve2 } from "path";
25
+ import { basename, dirname, extname as extname2, relative as relative2, resolve as resolve2 } from "path";
26
26
  import postcss from "postcss";
27
27
  import selectorParser from "postcss-selector-parser";
28
28
  import remarkMdx2 from "remark-mdx";
@@ -157,6 +157,177 @@ function isThallyBuiltinComponent(name) {
157
157
  return THALLY_BUILTIN_COMPONENTS.has(name) || THALLY_BUILTIN_COMPONENTS.has(name.split(".")[0]);
158
158
  }
159
159
 
160
+ // src/mintlify-extras.ts
161
+ import { extname } from "path";
162
+
163
+ // src/path.ts
164
+ import { isAbsolute, relative, resolve, sep } from "path";
165
+ var SAFE_SEGMENT = /[^a-z0-9._-]+/gi;
166
+ function trimTrailingSlashes(value) {
167
+ let end = value.length;
168
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
169
+ return value.slice(0, end);
170
+ }
171
+ function trimEdgeSlashes(value) {
172
+ let start = 0;
173
+ let end = value.length;
174
+ while (start < end && value.charCodeAt(start) === 47) start++;
175
+ while (end > start && value.charCodeAt(end - 1) === 47) end--;
176
+ return value.slice(start, end);
177
+ }
178
+ function slugifySegment(value, preserveCase = false) {
179
+ let decoded = value;
180
+ try {
181
+ decoded = decodeURIComponent(value);
182
+ } catch {
183
+ }
184
+ const normalized = preserveCase ? decoded : decoded.toLowerCase();
185
+ return normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
186
+ }
187
+ function pageIdFromReference(value, preserveCase = false) {
188
+ const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
189
+ if (!withoutQuery || withoutQuery.includes("\0")) return null;
190
+ const rawSegments = withoutQuery.split("/").filter(Boolean);
191
+ if (rawSegments.some((segment) => segment === ".." || segment === ".")) return null;
192
+ const last = rawSegments.at(-1)?.replace(/\.(?:mdx?|rst|txt)$/i, "") ?? "";
193
+ const isRootLevelReadme = rawSegments.length === 1 && /^readme$/i.test(last);
194
+ const collapsesToParent = /^index$/i.test(last) || isRootLevelReadme;
195
+ const baseSegments = collapsesToParent ? rawSegments.slice(0, -1) : rawSegments;
196
+ if (!collapsesToParent) {
197
+ baseSegments[baseSegments.length - 1] = last;
198
+ }
199
+ const segments = baseSegments.map((segment) => slugifySegment(segment, preserveCase)).filter(Boolean);
200
+ return segments.join("/") || "introduction";
201
+ }
202
+ function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__ */ new Set()) {
203
+ const reference = value.replace(/^\/+/, "");
204
+ const localePrefix = `${locale.toLowerCase()}/`;
205
+ if (reference.toLowerCase().startsWith(localePrefix)) return reference.slice(localePrefix.length);
206
+ const sectionPrefix = `${locale.toLowerCase()}-`;
207
+ if (reference.toLowerCase().startsWith(sectionPrefix)) {
208
+ const candidate = reference.slice(sectionPrefix.length);
209
+ const candidateId = pageIdFromReference(candidate, true);
210
+ if (candidateId && defaultPageIds.has(candidateId)) return candidate;
211
+ }
212
+ const segments = reference.split("/");
213
+ const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === locale.toLowerCase());
214
+ if (localeIndex > 0) {
215
+ return [...segments.slice(0, localeIndex), ...segments.slice(localeIndex + 1)].join("/");
216
+ }
217
+ return value;
218
+ }
219
+ function resolveWithin(root, candidate) {
220
+ return resolveWithinRoot(root, candidate, root);
221
+ }
222
+ function resolveWithinRoot(baseDir, candidate, confineRoot) {
223
+ if (isAbsolute(candidate) || candidate.includes("\0")) {
224
+ throw new Error(`Unsafe migration path: ${candidate}`);
225
+ }
226
+ const target = resolve(baseDir, candidate);
227
+ const fromRoot = relative(resolve(confineRoot), target);
228
+ if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
229
+ throw new Error(`Migration path escapes its root: ${candidate}`);
230
+ }
231
+ return target;
232
+ }
233
+ function normalizeAssetPath(value) {
234
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
235
+ if (!normalized || normalized.includes("\0")) return null;
236
+ const segments = normalized.split("/").filter(Boolean);
237
+ if (segments.some((segment) => segment === "." || segment === "..")) return null;
238
+ return segments.join("/");
239
+ }
240
+
241
+ // src/mintlify-extras.ts
242
+ function objectValue(value) {
243
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
244
+ }
245
+ var PUBLIC_FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "no", "off", "0"]);
246
+ function isPublicFalse(value) {
247
+ return value === false || value === 0 || typeof value === "string" && PUBLIC_FALSE_STRINGS.has(value.trim().toLowerCase());
248
+ }
249
+ function isPublicTrue(value) {
250
+ return value === true || typeof value === "string" && value.trim().toLowerCase() === "true";
251
+ }
252
+ function frontmatterGateReason(data) {
253
+ const groups = data.groups;
254
+ const hasGroups = Array.isArray(groups) ? groups.length > 0 : typeof groups === "string" ? groups.trim() !== "" : groups !== void 0 && groups !== null && groups !== false;
255
+ if (hasGroups) return "frontmatter `groups` restricts it to authenticated groups";
256
+ if (isPublicFalse(data.public)) return "frontmatter `public: false`";
257
+ return void 0;
258
+ }
259
+ function navigationGateReason(node) {
260
+ const groups = node.groups;
261
+ const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && ["group", "pages", "tab", "groups", "page", "root", "menu", "href"].some((key) => key in entry);
262
+ const restricted = typeof groups === "string" ? groups.trim() !== "" : Array.isArray(groups) && groups.length > 0 && !groups.every(isContainer);
263
+ if (restricted) return "its navigation container sets `groups`";
264
+ if (isPublicFalse(node.public)) return "its navigation container sets `public: false`";
265
+ return void 0;
266
+ }
267
+ var CONFIG_BASENAME = /(?:^|[.-])(?:config|rc)\.[cm]?js$|^(?:tailwind|postcss|next|eslint|prettier|babel|webpack|vite|vitest|jest|rollup|gulpfile|gruntfile|stylelint|commitlint|lint-staged|svgo|metro)[.-]/i;
268
+ var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "coverage"]);
269
+ var FONT_EXTENSIONS = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
270
+ function isMintlifyServedScriptOrStyle(relativePath) {
271
+ const extension = extname(relativePath).toLowerCase();
272
+ if (extension !== ".css" && extension !== ".js") return false;
273
+ const segments = relativePath.split("/");
274
+ if (segments.slice(0, -1).some((segment) => BUILD_OUTPUT_SEGMENTS.has(segment.toLowerCase()))) return false;
275
+ return extension === ".css" || !CONFIG_BASENAME.test(segments[segments.length - 1]);
276
+ }
277
+ function mintlifyFontSources(config) {
278
+ const fonts = objectValue(config?.fonts);
279
+ if (!fonts) return [];
280
+ const found = [];
281
+ for (const entry of [fonts, objectValue(fonts.heading), objectValue(fonts.body)]) {
282
+ if (!entry || typeof entry.source !== "string" || !entry.source.trim()) continue;
283
+ const source = entry.source.trim();
284
+ const family = typeof entry.family === "string" ? entry.family : "a font";
285
+ const remote = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(source);
286
+ const path = remote || /^[a-z]:[\\/]/i.test(source) ? null : normalizeAssetPath(source);
287
+ found.push({ family, source, path: path && FONT_EXTENSIONS.has(extname(path).toLowerCase()) ? path : null, remote });
288
+ }
289
+ return found;
290
+ }
291
+ function mintlifyAppearance(config, warnings) {
292
+ const appearance = objectValue(config.appearance);
293
+ const legacy = objectValue(config.modeToggle);
294
+ const result = {};
295
+ const isMode = (value) => value === "system" || value === "light" || value === "dark";
296
+ const skip = (field, value) => {
297
+ warnings.push({
298
+ code: "unsupported-config",
299
+ message: `Ignored invalid ${field} value ${JSON.stringify(value)?.slice(0, 40)}; the appearance default was left unchanged.`
300
+ });
301
+ };
302
+ if (appearance?.default !== void 0) {
303
+ if (isMode(appearance.default)) result.default = appearance.default;
304
+ else skip("appearance.default", appearance.default);
305
+ } else if (legacy?.default !== void 0) {
306
+ if (isMode(legacy.default)) result.default = legacy.default;
307
+ else skip("modeToggle.default", legacy.default);
308
+ }
309
+ if (appearance?.strict !== void 0) {
310
+ if (typeof appearance.strict === "boolean") {
311
+ if (appearance.strict) result.showToggle = false;
312
+ } else skip("appearance.strict", appearance.strict);
313
+ } else if (legacy?.isHidden !== void 0) {
314
+ if (typeof legacy.isHidden === "boolean") {
315
+ if (legacy.isHidden) result.showToggle = false;
316
+ } else skip("modeToggle.isHidden", legacy.isHidden);
317
+ }
318
+ return result;
319
+ }
320
+ function playgroundDisplay(value, where, warn) {
321
+ const mode = typeof value === "string" ? value.trim().toLowerCase() : "";
322
+ if (mode === "interactive" || mode === "simple" || mode === "none") return mode;
323
+ if (mode === "auth") {
324
+ warn?.(`${where} "auth" requires reader sign-in, which Thally does not support. Migrated as "simple" (no Try it). Set it to "interactive" to allow everyone.`);
325
+ return "simple";
326
+ }
327
+ warn?.(`${where} ${JSON.stringify(value)} is not one of interactive, simple, none, auth and was dropped.`);
328
+ return void 0;
329
+ }
330
+
160
331
  // src/mdx.ts
161
332
  function titleFromId(id) {
162
333
  const value = id.split("/").at(-1) ?? id;
@@ -265,7 +436,7 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
265
436
  return isMdxCodeBlock ? body : output.join("\n");
266
437
  }
267
438
  var HTML_ID = /^[^\s"'<>`{}&]+$/u;
268
- function normalizeExplicitHeadingIds(raw, warn) {
439
+ function normalizeExplicitHeadingIds(raw, warn, options = {}) {
269
440
  const { front, body } = splitFrontmatterBlock(raw);
270
441
  return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
271
442
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
@@ -276,13 +447,15 @@ function normalizeExplicitHeadingIds(raw, warn) {
276
447
  const id = trimmed.slice(marker + 3, -1);
277
448
  const heading = trimmed.slice(0, marker).trimEnd();
278
449
  if (/[{}]/.test(id)) return line;
279
- if (HTML_ID.test(id)) return `<a id="${id}"></a>
450
+ const usable = options.headingMarkers ? id && !/\s/.test(id) && !id.includes("*/") : HTML_ID.test(id);
451
+ if (usable && options.headingMarkers) return `${heading} ${headingIdMarker(id)}`;
452
+ if (usable) return `<a id="${id}"></a>
280
453
  ${heading}`;
281
- warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
454
+ warn?.(`Heading anchor {#${id}} ${options.headingMarkers ? 'contains whitespace or "*/"' : "is not a valid HTML id"} and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
282
455
  return heading;
283
456
  }
284
457
  }
285
- if (trimmed.endsWith("*/}")) {
458
+ if (!options.headingMarkers && !options.keepIdComments && trimmed.endsWith("*/}")) {
286
459
  const marker = trimmed.lastIndexOf(" {/*");
287
460
  if (marker >= 0) {
288
461
  const comment = trimmed.slice(marker + 4, -3).trim();
@@ -296,6 +469,44 @@ ${trimmed.slice(0, marker).trimEnd()}`;
296
469
  return line;
297
470
  }).join("\n"));
298
471
  }
472
+ function headingIdMarker(id) {
473
+ return `{/* #${id} */}`;
474
+ }
475
+ function mintlifyHeadingSlug(text2) {
476
+ return text2.replace(/\u200b/g, "").normalize("NFC").trim().toLowerCase().replace(/[.\s]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").replace(/[^\p{L}\p{M}\p{N}_\-+&/\u2019\u201c\u201d\u2014\u2013\u2192]/gu, "");
477
+ }
478
+ function thallyHeadingSlug(text2) {
479
+ return text2.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
480
+ }
481
+ function typesetQuotes(text2) {
482
+ return text2.split(/(`[^`]*`|<[^>]*>|\]\([^)]*\))/).map((part, index) => {
483
+ if (index % 2) return part;
484
+ return part.replace(/(^|[\s([{\u2014\u2013])"/g, "$1\u201C").replace(/"/g, "\u201D").replace(/(^|[\s([{\u2014\u2013\u201c])'(?=\S)/g, "$1\u2018").replace(/'/g, "\u2019");
485
+ }).join("");
486
+ }
487
+ function plainHeadingText(source) {
488
+ return source.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
489
+ }
490
+ function markMintlifyHeadings(body) {
491
+ let fence;
492
+ return body.split("\n").map((line) => {
493
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
494
+ if (marker) {
495
+ if (!fence) fence = marker[1];
496
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = void 0;
497
+ return line;
498
+ }
499
+ if (fence) return line;
500
+ const heading = line.match(/^( {0,3}#{2,6}[ \t]+)(.+?)(?:[ \t]+#+)?[ \t]*$/);
501
+ if (!heading) return line;
502
+ const explicit = heading[2].match(/^(.*?)([ \t]*\{\/\*[ \t]*#\S+[ \t]*\*\/\})$/);
503
+ const source = typesetQuotes(explicit ? explicit[1] : heading[2]);
504
+ if (explicit) return `${heading[1]}${source}${explicit[2]}`;
505
+ const text2 = plainHeadingText(source);
506
+ const id = mintlifyHeadingSlug(text2);
507
+ return `${heading[1]}${source}${id && id !== thallyHeadingSlug(text2) ? ` ${headingIdMarker(id)}` : ""}`;
508
+ }).join("\n");
509
+ }
299
510
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
300
511
  "Tabs",
301
512
  "TabItem",
@@ -1257,7 +1468,12 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1257
1468
  const runMintlify = platform === "mintlify";
1258
1469
  const runDocusaurus = platform === "docusaurus";
1259
1470
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1260
- let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernFileTrees(normalizeFernCallouts(normalizeNestedCodeFences(sourceBody))) : normalizeNestedCodeFences(sourceBody)));
1471
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(
1472
+ runFern ? normalizeFernFileTrees(normalizeFernCallouts(normalizeNestedCodeFences(sourceBody))) : normalizeNestedCodeFences(sourceBody),
1473
+ void 0,
1474
+ { headingMarkers: runMintlify }
1475
+ ));
1476
+ if (runMintlify) rewritten = markMintlifyHeadings(rewritten);
1261
1477
  if (runDocusaurus) {
1262
1478
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1263
1479
  /:([+\w-]+):/g,
@@ -1324,16 +1540,25 @@ function parseMarkdownPage(input) {
1324
1540
  body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
1325
1541
  }
1326
1542
  const navTitle = typeof parsed.data.sidebarTitle === "string" && parsed.data.sidebarTitle.trim() ? parsed.data.sidebarTitle.trim() : typeof parsed.data.navTitle === "string" && parsed.data.navTitle.trim() ? parsed.data.navTitle.trim() : void 0;
1543
+ const icon = typeof parsed.data.icon === "string" && parsed.data.icon.trim() ? parsed.data.icon.trim() : void 0;
1544
+ const iconType = icon && ["regular", "solid", "outline", "brands"].includes(String(parsed.data.iconType)) ? parsed.data.iconType : void 0;
1545
+ const meta = {};
1546
+ for (const [source, field] of PAGE_META_FIELDS) {
1547
+ const value = parsed.data[source];
1548
+ if (typeof value === "string" && value.trim()) meta[field] = value.trim();
1549
+ }
1327
1550
  const badge = typeof parsed.data.tag === "string" && parsed.data.tag.trim() ? parsed.data.tag.trim() : typeof parsed.data.badge === "string" && parsed.data.badge.trim() ? parsed.data.badge.trim() : void 0;
1328
1551
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
1329
1552
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
1330
- const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : firstParagraph(body);
1553
+ const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : typeof parsed.data.openapi === "string" && parsed.data.openapi.trim() ? "" : firstParagraph(body);
1331
1554
  return {
1332
1555
  id: identity.id,
1333
1556
  navigationId: identity.navigationId,
1334
1557
  locale: identity.locale,
1335
1558
  title,
1336
1559
  navTitle,
1560
+ icon,
1561
+ iconType,
1337
1562
  description,
1338
1563
  ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
1339
1564
  badge,
@@ -1341,6 +1566,7 @@ function parseMarkdownPage(input) {
1341
1566
  mode,
1342
1567
  hidden: parsed.data.hidden === true ? true : void 0,
1343
1568
  noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
1569
+ ...Object.keys(meta).length > 0 ? { meta } : {},
1344
1570
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
1345
1571
  ...apiFrontmatter(parsed.data, input.warn),
1346
1572
  body,
@@ -1348,9 +1574,21 @@ function parseMarkdownPage(input) {
1348
1574
  ...parsed.error ? { frontmatterError: parsed.error } : {}
1349
1575
  };
1350
1576
  }
1577
+ var PAGE_META_FIELDS = [
1578
+ ["og:title", "ogTitle"],
1579
+ ["og:description", "ogDescription"],
1580
+ ["og:image", "ogImage"],
1581
+ ["twitter:title", "twitterTitle"],
1582
+ ["twitter:description", "twitterDescription"],
1583
+ ["twitter:image", "twitterImage"]
1584
+ ];
1351
1585
  var AUTH_METHODS = /* @__PURE__ */ new Set(["bearer", "basic", "key", "none"]);
1352
1586
  function apiFrontmatter(data, warn) {
1353
1587
  const result = {};
1588
+ if (data.playground !== void 0 && data.playground !== null) {
1589
+ const display = playgroundDisplay(data.playground, `The page's "playground" frontmatter`, warn);
1590
+ if (display) result.playground = display;
1591
+ }
1354
1592
  if (data.api !== void 0 && data.api !== null) {
1355
1593
  if (typeof data.api === "string" && data.api.trim()) result.api = data.api.trim();
1356
1594
  else warn?.(`The page's "api" frontmatter is not a "METHOD url-or-path" string and was dropped.`);
@@ -1363,84 +1601,6 @@ function apiFrontmatter(data, warn) {
1363
1601
  return result;
1364
1602
  }
1365
1603
 
1366
- // src/path.ts
1367
- import { isAbsolute, relative, resolve, sep } from "path";
1368
- var SAFE_SEGMENT = /[^a-z0-9._-]+/gi;
1369
- function trimTrailingSlashes(value) {
1370
- let end = value.length;
1371
- while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
1372
- return value.slice(0, end);
1373
- }
1374
- function trimEdgeSlashes(value) {
1375
- let start = 0;
1376
- let end = value.length;
1377
- while (start < end && value.charCodeAt(start) === 47) start++;
1378
- while (end > start && value.charCodeAt(end - 1) === 47) end--;
1379
- return value.slice(start, end);
1380
- }
1381
- function slugifySegment(value, preserveCase = false) {
1382
- let decoded = value;
1383
- try {
1384
- decoded = decodeURIComponent(value);
1385
- } catch {
1386
- }
1387
- const normalized = preserveCase ? decoded : decoded.toLowerCase();
1388
- return normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
1389
- }
1390
- function pageIdFromReference(value, preserveCase = false) {
1391
- const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
1392
- if (!withoutQuery || withoutQuery.includes("\0")) return null;
1393
- const rawSegments = withoutQuery.split("/").filter(Boolean);
1394
- if (rawSegments.some((segment) => segment === ".." || segment === ".")) return null;
1395
- const last = rawSegments.at(-1)?.replace(/\.(?:mdx?|rst|txt)$/i, "") ?? "";
1396
- const isRootLevelReadme = rawSegments.length === 1 && /^readme$/i.test(last);
1397
- const collapsesToParent = /^index$/i.test(last) || isRootLevelReadme;
1398
- const baseSegments = collapsesToParent ? rawSegments.slice(0, -1) : rawSegments;
1399
- if (!collapsesToParent) {
1400
- baseSegments[baseSegments.length - 1] = last;
1401
- }
1402
- const segments = baseSegments.map((segment) => slugifySegment(segment, preserveCase)).filter(Boolean);
1403
- return segments.join("/") || "introduction";
1404
- }
1405
- function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__ */ new Set()) {
1406
- const reference = value.replace(/^\/+/, "");
1407
- const localePrefix = `${locale.toLowerCase()}/`;
1408
- if (reference.toLowerCase().startsWith(localePrefix)) return reference.slice(localePrefix.length);
1409
- const sectionPrefix = `${locale.toLowerCase()}-`;
1410
- if (reference.toLowerCase().startsWith(sectionPrefix)) {
1411
- const candidate = reference.slice(sectionPrefix.length);
1412
- const candidateId = pageIdFromReference(candidate, true);
1413
- if (candidateId && defaultPageIds.has(candidateId)) return candidate;
1414
- }
1415
- const segments = reference.split("/");
1416
- const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === locale.toLowerCase());
1417
- if (localeIndex > 0) {
1418
- return [...segments.slice(0, localeIndex), ...segments.slice(localeIndex + 1)].join("/");
1419
- }
1420
- return value;
1421
- }
1422
- function resolveWithin(root, candidate) {
1423
- return resolveWithinRoot(root, candidate, root);
1424
- }
1425
- function resolveWithinRoot(baseDir, candidate, confineRoot) {
1426
- if (isAbsolute(candidate) || candidate.includes("\0")) {
1427
- throw new Error(`Unsafe migration path: ${candidate}`);
1428
- }
1429
- const target = resolve(baseDir, candidate);
1430
- const fromRoot = relative(resolve(confineRoot), target);
1431
- if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
1432
- throw new Error(`Migration path escapes its root: ${candidate}`);
1433
- }
1434
- return target;
1435
- }
1436
- function normalizeAssetPath(value) {
1437
- const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
1438
- if (!normalized || normalized.includes("\0")) return null;
1439
- const segments = normalized.split("/").filter(Boolean);
1440
- if (segments.some((segment) => segment === "." || segment === "..")) return null;
1441
- return segments.join("/");
1442
- }
1443
-
1444
1604
  // src/inline-extraction.ts
1445
1605
  import ts from "typescript";
1446
1606
  var SERVER_ONLY_IDENTIFIERS = /* @__PURE__ */ new Set(["process", "require", "module", "exports", "__dirname", "__filename", "Buffer"]);
@@ -2468,10 +2628,10 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2468
2628
  }
2469
2629
  function expandCandidates(candidate) {
2470
2630
  const candidates = [candidate];
2471
- if (!extname(candidate)) {
2631
+ if (!extname2(candidate)) {
2472
2632
  candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json"].map((extension) => candidate + extension));
2473
2633
  candidates.push(...["index.tsx", "index.jsx", "index.ts", "index.js"].map((name) => resolve2(candidate, name)));
2474
- } else if (extname(candidate) === ".js") {
2634
+ } else if (extname2(candidate) === ".js") {
2475
2635
  candidates.push(candidate.slice(0, -3) + ".ts", candidate.slice(0, -3) + ".tsx");
2476
2636
  }
2477
2637
  return candidates;
@@ -2512,7 +2672,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2512
2672
  checkedFile(path);
2513
2673
  const size = lstatSync(path).size;
2514
2674
  if (size > MAX_FILE_BYTES || copiedBytes + stagedBytes + size > MAX_COMPONENT_BYTES || copied.size + staged.size >= MAX_COMPONENT_FILES) throw new Error("component migration budget exceeded");
2515
- const extension = extname(path).toLowerCase();
2675
+ const extension = extname2(path).toLowerCase();
2516
2676
  if (!CODE_EXTENSIONS.has(extension) && !DATA_EXTENSIONS.has(extension)) throw new Error(`unsupported dependency file type ${extension}`);
2517
2677
  stagedBytes += size;
2518
2678
  const content = readFileSync(path);
@@ -2608,7 +2768,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2608
2768
  if (size > MAX_FILE_BYTES || copiedBytes + size > MAX_COMPONENT_BYTES || copied.size >= MAX_COMPONENT_FILES) {
2609
2769
  throw new Error("component migration budget exceeded");
2610
2770
  }
2611
- const publicName = `migrated-${hash(relative2(confined, path))}${extname(path).toLowerCase()}`;
2771
+ const publicName = `migrated-${hash(relative2(confined, path))}${extname2(path).toLowerCase()}`;
2612
2772
  const destination = `public/${publicName}`;
2613
2773
  if (!copied.has(destination)) {
2614
2774
  copied.set(destination, { path: destination, content: readFileSync(path) });
@@ -2643,7 +2803,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2643
2803
  function transform(raw, currentFile) {
2644
2804
  const parsedFrontmatter = parseFrontmatter(raw).content;
2645
2805
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
2646
- const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)));
2806
+ const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)), void 0, { keepIdComments: true });
2647
2807
  let tree;
2648
2808
  try {
2649
2809
  tree = parser.parse(content);
@@ -2761,7 +2921,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2761
2921
  }
2762
2922
  continue;
2763
2923
  }
2764
- if (bindings.length && RESCUABLE_ASSET_EXTENSIONS.has(extname(specifier).toLowerCase()) && !bindings.some((binding) => usedAsJsxTagName(binding.local))) {
2924
+ if (bindings.length && RESCUABLE_ASSET_EXTENSIONS.has(extname2(specifier).toLowerCase()) && !bindings.some((binding) => usedAsJsxTagName(binding.local))) {
2765
2925
  try {
2766
2926
  const href = copyPublicAsset(resolveDependency(specifier, currentFile));
2767
2927
  edits.push({
@@ -2792,7 +2952,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2792
2952
  try {
2793
2953
  const sourcePath = resolveDependency(specifier, currentFile);
2794
2954
  const path = copyGraph(sourcePath);
2795
- const isSvgUsedAsTag = extname(path).toLowerCase() === ".svg" && bindings.some((binding) => usedAsJsxTagName(binding.local));
2955
+ const isSvgUsedAsTag = extname2(path).toLowerCase() === ".svg" && bindings.some((binding) => usedAsJsxTagName(binding.local));
2796
2956
  for (const binding of bindings) {
2797
2957
  const registerPath = isSvgUsedAsTag ? wrapSvgAsComponent(path) : path;
2798
2958
  const registerImported = isSvgUsedAsTag ? "default" : binding.imported;
@@ -2831,7 +2991,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2831
2991
  const importStart = node.position.start.offset + statement.getStart(ast);
2832
2992
  const importEnd = node.position.start.offset + statement.end;
2833
2993
  let rescuedAssetHref;
2834
- if (RESCUABLE_ASSET_EXTENSIONS.has(extname(specifier).toLowerCase())) {
2994
+ if (RESCUABLE_ASSET_EXTENSIONS.has(extname2(specifier).toLowerCase())) {
2835
2995
  const usedAsJsxTag = bindings.some((binding) => hasExpressionReference(/* @__PURE__ */ new Set([binding.local]), { includeDirectTags: true }));
2836
2996
  if (!usedAsJsxTag) {
2837
2997
  try {
@@ -3206,6 +3366,21 @@ function withoutFencedCode(content) {
3206
3366
  return line;
3207
3367
  }).join("\n");
3208
3368
  }
3369
+ function navbarLinkButtons(css) {
3370
+ const buttons = [];
3371
+ postcss2.parse(css).walkRules((rule) => {
3372
+ const match = /^(?:li)?\.navbar-link\s+a\[href(\*)?=["']([^"']+)["']\]$/.exec(rule.selector.trim());
3373
+ if (!match) return;
3374
+ let background;
3375
+ let color;
3376
+ rule.walkDecls((decl) => {
3377
+ if (decl.prop === "background-color" || decl.prop === "background") background = decl.value;
3378
+ else if (decl.prop === "color") color = decl.value;
3379
+ });
3380
+ if (background) buttons.push({ href: match[2], exact: !match[1], background, ...color ? { color } : {} });
3381
+ });
3382
+ return buttons;
3383
+ }
3209
3384
  function authoredStyleNames(contents) {
3210
3385
  const classes = /* @__PURE__ */ new Set();
3211
3386
  const ids = /* @__PURE__ */ new Set();
@@ -3295,89 +3470,6 @@ import { parse as parseYaml2 } from "yaml";
3295
3470
  // src/navigation.ts
3296
3471
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
3297
3472
  import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
3298
-
3299
- // src/mintlify-extras.ts
3300
- import { extname as extname2 } from "path";
3301
- function objectValue(value) {
3302
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3303
- }
3304
- var PUBLIC_FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "no", "off", "0"]);
3305
- function isPublicFalse(value) {
3306
- return value === false || value === 0 || typeof value === "string" && PUBLIC_FALSE_STRINGS.has(value.trim().toLowerCase());
3307
- }
3308
- function isPublicTrue(value) {
3309
- return value === true || typeof value === "string" && value.trim().toLowerCase() === "true";
3310
- }
3311
- function frontmatterGateReason(data) {
3312
- const groups = data.groups;
3313
- const hasGroups = Array.isArray(groups) ? groups.length > 0 : typeof groups === "string" ? groups.trim() !== "" : groups !== void 0 && groups !== null && groups !== false;
3314
- if (hasGroups) return "frontmatter `groups` restricts it to authenticated groups";
3315
- if (isPublicFalse(data.public)) return "frontmatter `public: false`";
3316
- return void 0;
3317
- }
3318
- function navigationGateReason(node) {
3319
- const groups = node.groups;
3320
- const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && ["group", "pages", "tab", "groups", "page", "root", "menu", "href"].some((key) => key in entry);
3321
- const restricted = typeof groups === "string" ? groups.trim() !== "" : Array.isArray(groups) && groups.length > 0 && !groups.every(isContainer);
3322
- if (restricted) return "its navigation container sets `groups`";
3323
- if (isPublicFalse(node.public)) return "its navigation container sets `public: false`";
3324
- return void 0;
3325
- }
3326
- var CONFIG_BASENAME = /(?:^|[.-])(?:config|rc)\.[cm]?js$|^(?:tailwind|postcss|next|eslint|prettier|babel|webpack|vite|vitest|jest|rollup|gulpfile|gruntfile|stylelint|commitlint|lint-staged|svgo|metro)[.-]/i;
3327
- var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "coverage"]);
3328
- var FONT_EXTENSIONS = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
3329
- function isMintlifyServedScriptOrStyle(relativePath) {
3330
- const extension = extname2(relativePath).toLowerCase();
3331
- if (extension !== ".css" && extension !== ".js") return false;
3332
- const segments = relativePath.split("/");
3333
- if (segments.slice(0, -1).some((segment) => BUILD_OUTPUT_SEGMENTS.has(segment.toLowerCase()))) return false;
3334
- return extension === ".css" || !CONFIG_BASENAME.test(segments[segments.length - 1]);
3335
- }
3336
- function mintlifyFontSources(config) {
3337
- const fonts = objectValue(config?.fonts);
3338
- if (!fonts) return [];
3339
- const found = [];
3340
- for (const entry of [fonts, objectValue(fonts.heading), objectValue(fonts.body)]) {
3341
- if (!entry || typeof entry.source !== "string" || !entry.source.trim()) continue;
3342
- const source = entry.source.trim();
3343
- const family = typeof entry.family === "string" ? entry.family : "a font";
3344
- const remote = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(source);
3345
- const path = remote || /^[a-z]:[\\/]/i.test(source) ? null : normalizeAssetPath(source);
3346
- found.push({ family, source, path: path && FONT_EXTENSIONS.has(extname2(path).toLowerCase()) ? path : null, remote });
3347
- }
3348
- return found;
3349
- }
3350
- function mintlifyAppearance(config, warnings) {
3351
- const appearance = objectValue(config.appearance);
3352
- const legacy = objectValue(config.modeToggle);
3353
- const result = {};
3354
- const isMode = (value) => value === "system" || value === "light" || value === "dark";
3355
- const skip = (field, value) => {
3356
- warnings.push({
3357
- code: "unsupported-config",
3358
- message: `Ignored invalid ${field} value ${JSON.stringify(value)?.slice(0, 40)}; the appearance default was left unchanged.`
3359
- });
3360
- };
3361
- if (appearance?.default !== void 0) {
3362
- if (isMode(appearance.default)) result.default = appearance.default;
3363
- else skip("appearance.default", appearance.default);
3364
- } else if (legacy?.default !== void 0) {
3365
- if (isMode(legacy.default)) result.default = legacy.default;
3366
- else skip("modeToggle.default", legacy.default);
3367
- }
3368
- if (appearance?.strict !== void 0) {
3369
- if (typeof appearance.strict === "boolean") {
3370
- if (appearance.strict) result.showToggle = false;
3371
- } else skip("appearance.strict", appearance.strict);
3372
- } else if (legacy?.isHidden !== void 0) {
3373
- if (typeof legacy.isHidden === "boolean") {
3374
- if (legacy.isHidden) result.showToggle = false;
3375
- } else skip("modeToggle.isHidden", legacy.isHidden);
3376
- }
3377
- return result;
3378
- }
3379
-
3380
- // src/navigation.ts
3381
3473
  var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
3382
3474
  function addMintlifyDirectoryRedirects(config, pages) {
3383
3475
  const pageIds = new Set(pages.map((page) => page.id));
@@ -3448,34 +3540,37 @@ function addMintlifyHomepageRedirects(config, pages) {
3448
3540
  }
3449
3541
  }
3450
3542
  const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
3451
- if (!homepage || homepage === "introduction") return config;
3543
+ if (!homepage) return config;
3452
3544
  const redirects = [...config.redirects ?? []];
3453
3545
  const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
3454
- const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).map((locale) => locale.code)];
3546
+ const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).filter((locale) => homepage !== "introduction" || pages.some((page) => page.locale === locale.code && page.navigationId === homepage)).map((locale) => locale.code)];
3455
3547
  for (const locale of roots) {
3456
3548
  const source = locale ? `/${locale}` : "/";
3457
3549
  if (sources.has(source)) continue;
3458
- redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: false });
3550
+ redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
3459
3551
  }
3460
3552
  return { ...config, redirects };
3461
3553
  }
3462
3554
  var LANGUAGE_LABELS = {
3463
- ar: "Arabic",
3464
- de: "German",
3555
+ ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629",
3556
+ de: "Deutsch",
3465
3557
  en: "English",
3466
- es: "Spanish",
3467
- fr: "French",
3468
- hi: "Hindi",
3469
- it: "Italian",
3470
- ja: "Japanese",
3471
- ko: "Korean",
3472
- nl: "Dutch",
3473
- pl: "Polish",
3474
- pt: "Portuguese",
3475
- ru: "Russian",
3476
- tr: "Turkish",
3477
- uk: "Ukrainian",
3478
- zh: "Chinese"
3558
+ es: "Espa\xF1ol",
3559
+ fr: "Fran\xE7ais",
3560
+ hi: "\u0939\u093F\u0928\u094D\u0926\u0940",
3561
+ it: "Italiano",
3562
+ ja: "\u65E5\u672C\u8A9E",
3563
+ ko: "\uD55C\uAD6D\uC5B4",
3564
+ nl: "Nederlands",
3565
+ pl: "Polski",
3566
+ pt: "Portugu\xEAs",
3567
+ "pt-BR": "Portugu\xEAs (BR)",
3568
+ ru: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439",
3569
+ tr: "T\xFCrk\xE7e",
3570
+ uk: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430",
3571
+ zh: "\u7B80\u4F53\u4E2D\u6587",
3572
+ "zh-Hans": "\u7B80\u4F53\u4E2D\u6587",
3573
+ "zh-Hant": "\u7E41\u9AD4\u4E2D\u6587"
3479
3574
  };
3480
3575
  function objectValue2(value) {
3481
3576
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -3940,11 +4035,35 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
3940
4035
  }] : [];
3941
4036
  }
3942
4037
  function projectedTheme(value) {
3943
- if (value === "maple") return "maple";
3944
- if (["aspen", "luma", "sequoia"].includes(String(value))) return "sharp";
4038
+ if (value === "maple" || value === "aspen") return "maple";
4039
+ if (["luma", "sequoia"].includes(String(value))) return "sharp";
3945
4040
  if (["almond", "palm"].includes(String(value))) return "minimal";
3946
4041
  return typeof value === "string" ? "default" : void 0;
3947
4042
  }
4043
+ function projectedColors(value, warnings) {
4044
+ const colors = objectValue2(value);
4045
+ if (!colors) {
4046
+ if (value !== void 0) warnings.push({ code: "unsupported-config", message: "colors is not an object with a primary hex colour and was dropped.", source: "docs.json" });
4047
+ return void 0;
4048
+ }
4049
+ const hex = (entry, key) => {
4050
+ const match = typeof entry === "string" ? /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(entry.trim()) : null;
4051
+ if (!match) {
4052
+ if (entry !== void 0) warnings.push({ code: "unsupported-config", message: `colors.${key} ${JSON.stringify(entry)} is not a 3- or 6-digit hex colour (for example #16a34a) and was dropped.`, source: "docs.json" });
4053
+ return void 0;
4054
+ }
4055
+ const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
4056
+ return `#${digits.toLowerCase()}`;
4057
+ };
4058
+ const primary = hex(colors.primary, "primary");
4059
+ const light = hex(colors.light, "light") ?? primary;
4060
+ const dark = hex(colors.dark, "dark") ?? primary;
4061
+ if (!primary) {
4062
+ if (colors.primary === void 0) warnings.push({ code: "unsupported-config", message: "colors has no primary hex colour, so none of its colours were migrated.", source: "docs.json" });
4063
+ return void 0;
4064
+ }
4065
+ return { light: { accent: primary, primary: dark ?? primary }, dark: { accent: light ?? primary, primary: light ?? primary } };
4066
+ }
3948
4067
  function projectedNavbar(value) {
3949
4068
  const navbar = objectValue2(value);
3950
4069
  if (!navbar) return void 0;
@@ -3960,7 +4079,8 @@ function projectedNavbar(value) {
3960
4079
  const primaryHref = projectedHref(primaryValue?.href);
3961
4080
  const primary = primaryValue && primaryHref ? {
3962
4081
  label: typeof primaryValue.label === "string" ? primaryValue.label : primaryValue.type === "github" ? "GitHub" : "Get started",
3963
- href: primaryHref
4082
+ href: primaryHref,
4083
+ ...primaryValue.type === "github" ? { type: "github" } : {}
3964
4084
  } : void 0;
3965
4085
  return links.length > 0 || primary ? { ...links.length > 0 ? { links } : {}, ...primary ? { primary } : {} } : void 0;
3966
4086
  }
@@ -4040,8 +4160,24 @@ function projectedCompatibleConfig(config, warnings) {
4040
4160
  const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
4041
4161
  const feedback = objectValue2(config.feedback);
4042
4162
  const seo = objectValue2(config.seo);
4163
+ const contextualOptions = Array.isArray(objectValue2(config.contextual)?.options) ? objectValue2(config.contextual).options : [];
4164
+ const projectedContextual = contextualOptions.filter((option) => typeof option === "string" && ["copy", "view", "chatgpt", "claude", "perplexity"].includes(option));
4165
+ const droppedContextual = contextualOptions.filter((option) => !projectedContextual.includes(option));
4166
+ if (droppedContextual.length > 0) {
4167
+ warnings.push({ code: "unsupported-config", message: `contextual.options entries not supported by the page menu were dropped: ${droppedContextual.map((option) => typeof option === "string" ? option : "custom entry").join(", ")}.`, source: "docs.json" });
4168
+ }
4169
+ const rawMetatags = Object.entries(objectValue2(seo?.metatags) ?? {});
4170
+ const httpEquivNames = /* @__PURE__ */ new Set(["refresh", "set-cookie", "location", "content-security-policy", "content-security-policy-report-only", "content-type", "default-style", "x-ua-compatible"]);
4171
+ const validMetatag = (entry) => /^[\w:.-]+$/.test(entry[0]) && !entry[0].toLowerCase().startsWith("http-equiv") && !httpEquivNames.has(entry[0].toLowerCase()) && typeof entry[1] === "string" && entry[1].length <= 1e3 && ![...entry[1]].some((character) => character.charCodeAt(0) < 32);
4172
+ const metatagEntries = rawMetatags.filter(validMetatag);
4173
+ const droppedMetatags = rawMetatags.filter((entry) => !validMetatag(entry)).map(([key]) => key);
4174
+ if (droppedMetatags.length > 0) {
4175
+ warnings.push({ code: "unsupported-config", message: `seo.metatags entries were dropped because the name is not a plain meta name, is an http-equiv directive, or the value is not a string of at most 1000 characters: ${droppedMetatags.join(", ")}.`, source: "docs.json" });
4176
+ }
4177
+ const projectedMetatags = metatagEntries.length > 0 ? Object.fromEntries(metatagEntries) : void 0;
4178
+ const projectedColorsValue = projectedColors(config.colors, warnings);
4043
4179
  const iconLibrary = objectValue2(config.icons)?.library;
4044
- const projectedIconLibrary = ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
4180
+ const projectedIconLibrary = iconLibrary === void 0 ? "fontawesome" : ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
4045
4181
  const docsNavbar = projectedNavbar(config.navbar);
4046
4182
  const legacyRaw = legacyTopbarNavbar(config);
4047
4183
  const legacyNavbar = projectedNavbar(legacyRaw);
@@ -4069,6 +4205,8 @@ function projectedCompatibleConfig(config, warnings) {
4069
4205
  const projectedNavigation = navbarLinks.length > 0 || navbar?.primary ? { ...navbarLinks.length > 0 ? { links: navbarLinks } : {}, ...navbar?.primary ? { primary: navbar.primary } : {} } : void 0;
4070
4206
  return {
4071
4207
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
4208
+ ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
4209
+ ...projectedColorsValue ? { colors: projectedColorsValue } : {},
4072
4210
  ...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
4073
4211
  ...bannerContent ? {
4074
4212
  banner: {
@@ -4086,10 +4224,19 @@ function projectedCompatibleConfig(config, warnings) {
4086
4224
  ...projectedNavigation ? { navbar: projectedNavigation } : {},
4087
4225
  ...projectedFooter(config.footer) ? { footer: projectedFooter(config.footer) } : {},
4088
4226
  ...bodyFont || headingFont ? { fonts: { ...bodyFont ? { body: bodyFont } : {}, ...headingFont ? { heading: headingFont } : {} } } : {},
4089
- ...seo?.indexing === "all" ? { seo: { indexing: "all" } } : {},
4227
+ // Mintlify titles pages "<title> - <site name>" unless `og:title` overrides them,
4228
+ // and its sitemap lists navigation pages only.
4229
+ seo: { ...seo?.indexing === "all" ? { indexing: "all" } : {}, titleSeparator: " - ", sitemap: "navigable", ...projectedMetatags ? { metatags: projectedMetatags } : {} },
4090
4230
  ...typeof feedback?.thumbsRating === "boolean" ? { feedback: { thumbsRating: feedback.thumbsRating } } : {}
4091
4231
  };
4092
4232
  }
4233
+ function projectedApi(config, warnings) {
4234
+ const mdx = projectedApiMdx(config, warnings).api?.mdx;
4235
+ const raw = objectValue2(objectValue2(config.api)?.playground)?.display;
4236
+ const display = raw === void 0 ? void 0 : playgroundDisplay(raw, "api.playground.display", (message) => warnings.push({ code: "unsupported-config", message }));
4237
+ if (!mdx && !display) return {};
4238
+ return { api: { ...mdx ? { mdx } : {}, ...display ? { playground: { display } } : {} } };
4239
+ }
4093
4240
  function projectedApiMdx(config, warnings) {
4094
4241
  const mdx = objectValue2(objectValue2(config.api)?.mdx);
4095
4242
  if (!mdx) return {};
@@ -4319,7 +4466,7 @@ function projectMintlifyNavigation(config, options = {}) {
4319
4466
  if (!landing) return [];
4320
4467
  const firstSegment = landing.split("/")[0];
4321
4468
  const prefix = firstSegment === label ? label : "";
4322
- return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {} }];
4469
+ return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {}, ...entry.hidden === true ? { hidden: true } : {} }];
4323
4470
  }) : [];
4324
4471
  if (versions.length > 1) {
4325
4472
  tabs = annotateVersions(tabs);
@@ -4355,7 +4502,7 @@ function projectMintlifyNavigation(config, options = {}) {
4355
4502
  ...shortcuts.length > 0 ? { shortcuts } : {}
4356
4503
  } } : {},
4357
4504
  ...projectedCompatibleConfig(config, warnings),
4358
- ...projectedApiMdx(config, warnings),
4505
+ ...projectedApi(config, warnings),
4359
4506
  ...i18n ? { i18n } : {},
4360
4507
  ...redirects.length > 0 ? { redirects } : {}
4361
4508
  },
@@ -5945,6 +6092,68 @@ function withSpecRef(ref, specRef) {
5945
6092
  return `${specRef} ${ref.operation}`;
5946
6093
  }
5947
6094
 
6095
+ // src/spec-exclude.ts
6096
+ import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
6097
+ function specRefMatches(ref, specPath, specFilename, isDefault) {
6098
+ const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
6099
+ if (!prefix) return isDefault;
6100
+ const wanted = prefix.replace(/^\/+/, "").toLowerCase();
6101
+ const lowerPath = specPath.toLowerCase();
6102
+ return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
6103
+ }
6104
+ function operationKey(operation) {
6105
+ const [first, ...rest] = operation.trim().split(/\s+/);
6106
+ return `${first.toLowerCase()} ${rest.join(" ")}`;
6107
+ }
6108
+ function keysFor(spec, refs) {
6109
+ return [...new Set(
6110
+ refs.filter((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true)).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
6111
+ )];
6112
+ }
6113
+ function withheldOperationKeys(spec, withheldRefs, keptRefs) {
6114
+ const kept = keysFor(spec, keptRefs);
6115
+ return { withheld: keysFor(spec, withheldRefs).filter((key) => !kept.includes(key)), kept };
6116
+ }
6117
+ function markExcluded(content, operationKeys, keptKeys = []) {
6118
+ const untouched = { content, excluded: [] };
6119
+ if (operationKeys.length === 0) return untouched;
6120
+ const text2 = content.toString("utf8");
6121
+ let doc;
6122
+ try {
6123
+ doc = parseYaml4(text2);
6124
+ } catch {
6125
+ return untouched;
6126
+ }
6127
+ if (!doc || typeof doc !== "object") return untouched;
6128
+ const excluded = [];
6129
+ for (const key of operationKeys) {
6130
+ const [method, ...rest] = key.split(" ");
6131
+ const name = rest.join(" ");
6132
+ if (method === "webhook") {
6133
+ const entry2 = doc.webhooks?.[name] ?? doc["x-webhooks"]?.[name];
6134
+ if (entry2 && typeof entry2 === "object") {
6135
+ entry2["x-excluded"] = true;
6136
+ excluded.push(`WEBHOOK ${name}`);
6137
+ }
6138
+ continue;
6139
+ }
6140
+ const entry = doc.paths?.[name];
6141
+ if (!entry || typeof entry !== "object") continue;
6142
+ const target = typeof entry.$ref === "string" ? keptKeys.some((k) => k.slice(k.indexOf(" ") + 1) === name) ? void 0 : entry : entry[method];
6143
+ if (target && typeof target === "object") {
6144
+ target["x-excluded"] = true;
6145
+ excluded.push(`${method.toUpperCase()} ${name}`);
6146
+ }
6147
+ }
6148
+ if (excluded.length === 0) return untouched;
6149
+ const out = text2.trimStart().startsWith("{") ? `${JSON.stringify(doc, null, 2)}
6150
+ ` : stringifyYaml(doc);
6151
+ return { content: Buffer.from(out), excluded };
6152
+ }
6153
+ function sharedSpecMessage(path, excluded = []) {
6154
+ return excluded.length ? `OpenAPI spec ${path} is shared with access-restricted pages and is published, but ${excluded.length} operation(s) documented only on those pages were withheld from it (${excluded.join(", ")}). Review the rest of the spec before publishing.` : `OpenAPI spec ${path} is shared with access-restricted pages and is published; it may describe restricted endpoints. Review it before publishing.`;
6155
+ }
6156
+
5948
6157
  // src/analytics.ts
5949
6158
  var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
5950
6159
  var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
@@ -6926,12 +7135,23 @@ function normalizedReferenceKey(value) {
6926
7135
  function exactReferenceKey(value) {
6927
7136
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
6928
7137
  }
6929
- function specRefMatches(ref, specPath, specFilename, isDefault) {
6930
- const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
6931
- if (!prefix) return isDefault;
6932
- const wanted = prefix.replace(/^\/+/, "").toLowerCase();
6933
- const lowerPath = specPath.toLowerCase();
6934
- return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
7138
+ function navigationOperationRefs(config) {
7139
+ const refs = [];
7140
+ const visit = (node) => {
7141
+ if (typeof node === "string") {
7142
+ if (splitOpenApiRef(node)) refs.push(node);
7143
+ } else if (Array.isArray(node)) node.forEach(visit);
7144
+ else if (node && typeof node === "object") {
7145
+ if (navigationGateReason(node)) return;
7146
+ Object.values(node).forEach(visit);
7147
+ }
7148
+ };
7149
+ visit(config?.navigation);
7150
+ return refs;
7151
+ }
7152
+ function excludeWithheldOperations(spec, withheldRefs, keptRefs) {
7153
+ const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs);
7154
+ return markExcluded(spec.content, withheld, kept);
6935
7155
  }
6936
7156
  var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
6937
7157
  var PATH_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\`*!?#&]+/;
@@ -7004,12 +7224,22 @@ function mintlifyTopLevelApiReferences(config) {
7004
7224
  }
7005
7225
  return references;
7006
7226
  }
7007
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7227
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, pages = []) {
7008
7228
  if (!mintlifyConfig) return [];
7009
7229
  const references = [
7010
7230
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
7011
7231
  ...mintlifyNavigationApiReferences(mintlifyConfig)
7012
7232
  ];
7233
+ const specKey = (value) => value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
7234
+ const known = new Set(references.map((reference) => specKey(reference.value)));
7235
+ for (const page of pages) {
7236
+ const specRef = page.openapi ? splitOpenApiRef(page.openapi)?.specRef : void 0;
7237
+ if (!specRef || /^[a-z][a-z0-9+.-]*:/i.test(specRef)) continue;
7238
+ const key = specKey(specRef);
7239
+ if (known.has(key) || !files.some((file) => file.relativePath === key)) continue;
7240
+ known.add(key);
7241
+ references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
7242
+ }
7013
7243
  const seen = /* @__PURE__ */ new Set();
7014
7244
  const specs = [];
7015
7245
  for (const reference of references) {
@@ -7059,6 +7289,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7059
7289
  parentTab: reference.parentTab,
7060
7290
  icon: reference.icon,
7061
7291
  hidden: reference.hidden,
7292
+ ...reference.pageOnly ? { pageOnly: true } : {},
7062
7293
  sourcePath: match.relativePath,
7063
7294
  directory: reference.directory
7064
7295
  });
@@ -7080,7 +7311,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7080
7311
  }
7081
7312
  return specs;
7082
7313
  }
7083
- function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
7314
+ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explainMissing = () => "no such file was found") {
7084
7315
  for (const page of pages) {
7085
7316
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
7086
7317
  if (!ref?.specRef) continue;
@@ -7097,7 +7328,7 @@ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
7097
7328
  if (match) {
7098
7329
  page.openapi = withSpecRef(ref, specAssetPath(match.filename));
7099
7330
  } else {
7100
- warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which is not referenced from docs.json and was not migrated; the page will not render an endpoint. Add the spec to a tab's api setting.`, source: page.source });
7331
+ warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which was not migrated (${explainMissing(key)}); the page will not render an endpoint.`, source: page.source });
7101
7332
  }
7102
7333
  }
7103
7334
  }
@@ -7122,7 +7353,7 @@ function slugifyApiSpecId(value) {
7122
7353
  function parseOpenApiOperationsForLinking(content) {
7123
7354
  let document;
7124
7355
  try {
7125
- document = parseYaml4(content.toString("utf8"));
7356
+ document = parseYaml5(content.toString("utf8"));
7126
7357
  } catch {
7127
7358
  return [];
7128
7359
  }
@@ -7254,7 +7485,7 @@ function fernGeneratorsUnsupportedSpecPaths(config) {
7254
7485
  }
7255
7486
  function readFernGeneratorsConfig(path) {
7256
7487
  if (!existsSync5(path) || !lstatSync5(path).isFile() || lstatSync5(path).size > MAX_FERN_GENERATORS_BYTES) return null;
7257
- const parsed = parseYaml4(readFileSync5(path, "utf8"));
7488
+ const parsed = parseYaml5(readFileSync5(path, "utf8"));
7258
7489
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7259
7490
  }
7260
7491
  function fernOpenApiCandidateDirs(fernRoot, apiName, apiNameExplicit) {
@@ -7568,11 +7799,12 @@ function preserveLinkedAnchors(pages) {
7568
7799
  const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
7569
7800
  if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
7570
7801
  const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
7571
- return match ? { index, text: match[1] } : null;
7802
+ return match ? { index, text: match[1].replace(/\s*\{\/\*\s*#\S+?\s*\*\/\}$/, "") } : null;
7572
7803
  }).filter((entry) => entry !== null);
7573
7804
  const existing = /* @__PURE__ */ new Set();
7574
7805
  replaceOutsideCode(page.body, (text2) => {
7575
7806
  for (const match of text2.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
7807
+ for (const match of text2.matchAll(/\{\/\*\s*#(\S+?)\s*\*\/\}/g)) existing.add(match[1]);
7576
7808
  return text2;
7577
7809
  });
7578
7810
  const candidateIndexes = /* @__PURE__ */ new Map();
@@ -7700,7 +7932,7 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
7700
7932
  }
7701
7933
  return aliases;
7702
7934
  }
7703
- function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7935
+ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced, onRootFallback) {
7704
7936
  const isBracketed = value.startsWith("<") && value.endsWith(">");
7705
7937
  const raw = isBracketed ? value.slice(1, -1) : value;
7706
7938
  if (!raw || raw.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw)) return null;
@@ -7715,7 +7947,15 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7715
7947
  }
7716
7948
  if (!ASSET_EXTENSIONS.has(extname6(decodedPath).toLowerCase())) return null;
7717
7949
  try {
7718
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
7950
+ let usedRootFallback = false;
7951
+ let candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
7952
+ if (!decodedPath.startsWith("/") && !existsSync5(candidate)) {
7953
+ const fromRoot = resolveWithin(siteRoot, decodedPath.replace(/^(?:\.\/)+/, ""));
7954
+ if (existsSync5(fromRoot)) {
7955
+ candidate = fromRoot;
7956
+ usedRootFallback = true;
7957
+ }
7958
+ }
7719
7959
  const realRoot = realpathSync2(siteRoot);
7720
7960
  const realCandidate = realpathSync2(candidate);
7721
7961
  const realRelative = relative5(realRoot, realCandidate);
@@ -7728,6 +7968,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7728
7968
  const onDiskRelative = relative5(realRoot, realpathSync2.native(candidate)).replace(/\\/g, "/");
7729
7969
  const onDisk = onDiskRelative.toLowerCase() === siteRelative.toLowerCase() ? normalizeAssetPath(onDiskRelative) : null;
7730
7970
  onReferenced?.(normalized, onDisk ?? void 0);
7971
+ if (usedRootFallback) onRootFallback?.(decodedPath, normalized);
7731
7972
  const rewritten = `/${normalized}${suffix}`;
7732
7973
  return isBracketed ? `<${rewritten}>` : rewritten;
7733
7974
  } catch {
@@ -7746,12 +7987,12 @@ function rewriteFernIdRenameLinks(body, renames) {
7746
7987
  return text2.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
7747
7988
  });
7748
7989
  }
7749
- function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced) {
7990
+ function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced, onRootFallback) {
7750
7991
  return body.replace(/(!?\[[^\]]*\]\()(<[^>]+>|[^)\s]+)([^)]*\))/g, (original, opening, destination, closing) => {
7751
- const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced);
7992
+ const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced, onRootFallback);
7752
7993
  return rewritten ? `${opening}${rewritten}${closing}` : original;
7753
7994
  }).replace(/\b(src|img|image|href)=(['"])([^'"]+)\2/g, (original, property, quote, destination) => {
7754
- const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced);
7995
+ const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced, onRootFallback);
7755
7996
  return rewritten ? `${property}=${quote}${rewritten}${quote}` : original;
7756
7997
  });
7757
7998
  }
@@ -8469,7 +8710,11 @@ function migrateRepository(options) {
8469
8710
  const publishedText = trackPublishedRefs ? raw : void 0;
8470
8711
  const pageAssetReferences = [];
8471
8712
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8472
- raw = normalizeExplicitHeadingIds(raw, (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }));
8713
+ raw = normalizeExplicitHeadingIds(
8714
+ raw,
8715
+ (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }),
8716
+ { headingMarkers: platform === "mintlify" }
8717
+ );
8473
8718
  }
8474
8719
  if (componentMigrator) {
8475
8720
  const warningsBeforeTransform = warnings.length;
@@ -8556,6 +8801,8 @@ function migrateRepository(options) {
8556
8801
  if (platform === "mintlify" && mintlifyProjectRoot) {
8557
8802
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath, onDiskSpelling) => {
8558
8803
  pageAssetReferences.push(publicAssetKey(onDiskSpelling ?? assetPath));
8804
+ }, (spelled, used) => {
8805
+ warnings.push({ code: "unsupported-config", message: `"${spelled}" was not found beside this page; the file "${used}" in the site root was used instead and will be published.`, source: file.relativePath });
8559
8806
  });
8560
8807
  }
8561
8808
  if (platform === "fern" && fernProjectRoot) {
@@ -8703,15 +8950,19 @@ function migrateRepository(options) {
8703
8950
  const rootStylesheetPath = platform === "mintlify" && mintlifyProjectRoot ? resolveWithin(mintlifyProjectRoot, "style.css") : void 0;
8704
8951
  const hasRootStylesheet = Boolean(rootStylesheetPath && existsSync5(rootStylesheetPath) && lstatSync5(rootStylesheetPath).isFile() && !lstatSync5(rootStylesheetPath).isSymbolicLink());
8705
8952
  let rootStyleProjection;
8953
+ let rootNavbarButtons = [];
8706
8954
  let rootStyleProblem;
8707
8955
  if (hasRootStylesheet && rootStylesheetPath && lstatSync5(rootStylesheetPath).size <= MAX_ASSET_BYTES) {
8708
8956
  const css = readFileSync5(rootStylesheetPath, "utf8");
8709
8957
  if (/@import\b|url\s*\(\s*['"]?\s*(?:javascript:|file:)/i.test(css)) rootStyleProblem = "unsafe";
8710
8958
  else {
8711
8959
  try {
8960
+ rootNavbarButtons = navbarLinkButtons(css);
8712
8961
  rootStyleProjection = projectAuthoredStyles(css, [
8713
8962
  ...pages.map((page) => page.body),
8714
- ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : "")
8963
+ ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : ""),
8964
+ // Site-wide scripts build markup of their own (a CTA card, say) that the stylesheet is meant for.
8965
+ ...files.filter((file) => isMintlifyServedScriptOrStyle(file.relativePath) && extname6(file.relativePath).toLowerCase() === ".js" && lstatSync5(file.absolutePath).size <= MAX_PAGE_BYTES).map((file) => readFileSync5(file.absolutePath, "utf8"))
8715
8966
  ]);
8716
8967
  } catch {
8717
8968
  rootStyleProblem = "parse";
@@ -8849,6 +9100,18 @@ function migrateRepository(options) {
8849
9100
  } else if (rootStyleProblem === "parse" || !rootStyleProjection) {
8850
9101
  warnings.push({ code: "unsupported-config", message: "Mintlify root style.css could not be parsed and was not imported.", source: "style.css" });
8851
9102
  } else {
9103
+ if (rootNavbarButtons.length > 0 && docsConfig.navbar?.links) {
9104
+ docsConfig = {
9105
+ ...docsConfig,
9106
+ navbar: {
9107
+ ...docsConfig.navbar,
9108
+ links: docsConfig.navbar.links.map((link) => {
9109
+ const rule = rootNavbarButtons.find((entry) => entry.exact ? link.href === entry.href : link.href.includes(entry.href));
9110
+ return rule ? { ...link, button: { background: rule.background, ...rule.color ? { color: rule.color } : {} } } : link;
9111
+ })
9112
+ }
9113
+ };
9114
+ }
8852
9115
  if (rootStyleProjection.omittedSelectors > 0) warnings.push({
8853
9116
  code: "unsupported-config",
8854
9117
  source: "style.css",
@@ -8992,19 +9255,31 @@ function migrateRepository(options) {
8992
9255
  rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
8993
9256
  }
8994
9257
  } else if (platform === "mintlify") {
8995
- const warnSharedSpec = (path) => {
8996
- warnings.push({
8997
- code: "gated-page",
8998
- message: `OpenAPI spec ${path} is shared with access-restricted pages and is published; it may describe restricted endpoints. Review it before publishing.`,
8999
- source: path
9000
- });
9258
+ const warnSharedSpec = (path, excluded = []) => {
9259
+ warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
9001
9260
  };
9002
- const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
9261
+ const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
9262
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages);
9263
+ for (const remote of remoteApiSpecs) {
9264
+ const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
9265
+ if (withheld.length > 0) {
9266
+ remote.withheldOperations = withheld;
9267
+ remote.keptOperations = kept;
9268
+ }
9269
+ }
9270
+ const withheldOperations = /* @__PURE__ */ new Map();
9271
+ for (const spec of allSpecs) {
9272
+ const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs);
9273
+ spec.content = result.content;
9274
+ withheldOperations.set(spec, result.excluded);
9275
+ }
9276
+ const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
9277
+ const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
9003
9278
  for (const spec of resolvedSpecs) {
9004
9279
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
9005
9280
  assets.push(specAsset(spec.filename, spec.content));
9006
9281
  }
9007
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
9282
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
9008
9283
  }
9009
9284
  let pageSpecs = resolvedSpecs;
9010
9285
  if (resolvedSpecs.length > 0) {
@@ -9023,16 +9298,35 @@ function migrateRepository(options) {
9023
9298
  quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: readFileSync5(fallback.absolutePath) });
9024
9299
  withheldAssetCount++;
9025
9300
  } else {
9301
+ const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: readFileSync5(fallback.absolutePath) }, withheldSpecRefs, keptSpecRefs);
9026
9302
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
9027
- assets.push(specAsset(filename, readFileSync5(fallback.absolutePath)));
9303
+ assets.push(specAsset(filename, pruned.content));
9028
9304
  }
9029
9305
  docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
9030
9306
  pageSpecs = [{ filename, sourcePath: fallback.relativePath }];
9031
- if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath);
9307
+ if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath, pruned.excluded);
9032
9308
  }
9033
9309
  }
9034
9310
  }
9035
- rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
9311
+ for (const spec of pageOnlySpecs) {
9312
+ if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
9313
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
9314
+ }
9315
+ if (pageOnlySpecs.length > 0) {
9316
+ docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
9317
+ pageSpecs = [...pageSpecs, ...pageOnlySpecs];
9318
+ }
9319
+ const explainMissingSpec = (key) => {
9320
+ const sameName = scannedFiles.find((file) => file.relativePath.toLowerCase() === key.toLowerCase());
9321
+ if (sameName && sameName.relativePath !== key) return `paths are case-sensitive; the file is at "${sameName.relativePath}"`;
9322
+ if (sameName && mintignoreMatcher?.ignores(sameName.relativePath)) return "the file is excluded by .mintignore";
9323
+ try {
9324
+ if (lstatSync5(resolvePath(repositoryDir, key)).isSymbolicLink()) return "the file is a symbolic link, which is never followed";
9325
+ } catch {
9326
+ }
9327
+ return sameName ? "the file could not be read as an OpenAPI document" : `no file exists at "${key}"`;
9328
+ };
9329
+ rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
9036
9330
  }
9037
9331
  if (platform === "mintlify") {
9038
9332
  const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
@@ -9447,6 +9741,8 @@ function renderPage(bundle, page) {
9447
9741
  "---",
9448
9742
  `title: ${yamlString(page.title)}`,
9449
9743
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
9744
+ page.icon ? `icon: ${yamlString(page.icon)}` : null,
9745
+ page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
9450
9746
  `description: ${yamlString(page.description)}`,
9451
9747
  page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
9452
9748
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
@@ -9454,9 +9750,11 @@ function renderPage(bundle, page) {
9454
9750
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
9455
9751
  page.hidden ? "hidden: true" : null,
9456
9752
  page.noindex ? "noindex: true" : null,
9753
+ ...Object.entries(page.meta ?? {}).map(([field, value]) => `${field}: ${yamlString(value)}`),
9457
9754
  page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
9458
9755
  page.api ? `api: ${yamlString(page.api)}` : null,
9459
9756
  page.authMethod ? `authMethod: ${yamlString(page.authMethod)}` : null,
9757
+ page.playground ? `playground: ${yamlString(page.playground)}` : null,
9460
9758
  bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
9461
9759
  "---",
9462
9760
  "",
@@ -9570,7 +9868,7 @@ import remarkMdx3 from "remark-mdx";
9570
9868
  import remarkParse3 from "remark-parse";
9571
9869
  import TurndownService from "turndown";
9572
9870
  import { unified as unified3 } from "unified";
9573
- import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
9871
+ import { parse as parseYaml6, stringify as stringifyYaml2 } from "yaml";
9574
9872
  var DEFAULT_MAX_PAGES = 1e3;
9575
9873
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
9576
9874
  var MAX_DISCOVERED_URLS = 5e3;
@@ -10295,7 +10593,7 @@ function extractEmbeddedOpenApi(content) {
10295
10593
  const match = fencePattern.exec(content);
10296
10594
  if (!match) return { content };
10297
10595
  try {
10298
- const parsed = parseYaml5(match[5], { maxAliasCount: 50 });
10596
+ const parsed = parseYaml6(match[5], { maxAliasCount: 50 });
10299
10597
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
10300
10598
  const document = parsed;
10301
10599
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -10409,7 +10707,7 @@ function mergeEmbeddedOpenApi(fragments) {
10409
10707
  if (Object.keys(combinedPaths).length === 0) return null;
10410
10708
  if (Object.keys(combinedComponents).length === 0) delete combined.components;
10411
10709
  if (tags.size > 0) combined.tags = [...tags.values()];
10412
- return new TextEncoder().encode(stringifyYaml(combined));
10710
+ return new TextEncoder().encode(stringifyYaml2(combined));
10413
10711
  }
10414
10712
  function htmlPage(document, id) {
10415
10713
  const $ = load2(document.body);
@@ -11148,7 +11446,7 @@ import { createHash as createHash3 } from "crypto";
11148
11446
  import { lookup } from "dns/promises";
11149
11447
  import { request } from "https";
11150
11448
  import { isIP } from "net";
11151
- import { parse as parseYaml6 } from "yaml";
11449
+ import { parse as parseYaml7 } from "yaml";
11152
11450
  var MAX_SPEC_BYTES = 25e6;
11153
11451
  var MAX_REDIRECTS = 3;
11154
11452
  function isPublicAddress(address, family) {
@@ -11244,7 +11542,7 @@ async function downloadSpec(value, fetcher) {
11244
11542
  }
11245
11543
  function parseOpenApi(content, extension) {
11246
11544
  const text2 = content.toString("utf8");
11247
- const parsed = extension === "yaml" || !text2.trimStart().startsWith("{") ? parseYaml6(text2) : JSON.parse(text2);
11545
+ const parsed = extension === "yaml" || !text2.trimStart().startsWith("{") ? parseYaml7(text2) : JSON.parse(text2);
11248
11546
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
11249
11547
  const document = parsed;
11250
11548
  if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
@@ -11302,7 +11600,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
11302
11600
  try {
11303
11601
  const url = validatedSpecUrl(reference.url);
11304
11602
  const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
11305
- const body = await downloadSpec(reference.url, fetcher);
11603
+ const downloaded = await downloadSpec(reference.url, fetcher);
11604
+ const pruned = markExcluded(downloaded, reference.withheldOperations ?? [], reference.keptOperations);
11605
+ const body = pruned.content;
11606
+ if (reference.withheldOperations?.length) warnings.push({ code: "gated-page", message: sharedSpecMessage(reference.url, pruned.excluded), source: reference.url });
11306
11607
  const operations = parseOpenApi(body, extension);
11307
11608
  const filename = `openapi-${createHash3("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11308
11609
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });