@thallylabs/migrate 0.2.22 → 0.2.23

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
@@ -22,7 +22,7 @@ import * as ts4 from "typescript";
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" shows the playground only to signed-in readers, which Thally cannot tell; migrated as "simple" (no playground).`);
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
454
  warn?.(`Heading anchor {#${id}} 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(/!?\[([^\]]*)\]\([^)]*\)/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,26 @@ 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) {
4044
+ const colors = objectValue2(value);
4045
+ if (!colors) return void 0;
4046
+ const hex = (entry) => {
4047
+ const match = typeof entry === "string" ? /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(entry.trim()) : null;
4048
+ if (!match) return void 0;
4049
+ const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
4050
+ return `#${digits.toLowerCase()}`;
4051
+ };
4052
+ const primary = hex(colors.primary);
4053
+ const light = hex(colors.light) ?? primary;
4054
+ const dark = hex(colors.dark) ?? primary;
4055
+ if (!primary) return void 0;
4056
+ return { light: { accent: primary, primary: dark ?? primary }, dark: { accent: light ?? primary, primary: light ?? primary } };
4057
+ }
3948
4058
  function projectedNavbar(value) {
3949
4059
  const navbar = objectValue2(value);
3950
4060
  if (!navbar) return void 0;
@@ -3960,7 +4070,8 @@ function projectedNavbar(value) {
3960
4070
  const primaryHref = projectedHref(primaryValue?.href);
3961
4071
  const primary = primaryValue && primaryHref ? {
3962
4072
  label: typeof primaryValue.label === "string" ? primaryValue.label : primaryValue.type === "github" ? "GitHub" : "Get started",
3963
- href: primaryHref
4073
+ href: primaryHref,
4074
+ ...primaryValue.type === "github" ? { type: "github" } : {}
3964
4075
  } : void 0;
3965
4076
  return links.length > 0 || primary ? { ...links.length > 0 ? { links } : {}, ...primary ? { primary } : {} } : void 0;
3966
4077
  }
@@ -4040,8 +4151,16 @@ function projectedCompatibleConfig(config, warnings) {
4040
4151
  const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
4041
4152
  const feedback = objectValue2(config.feedback);
4042
4153
  const seo = objectValue2(config.seo);
4154
+ const contextualOptions = Array.isArray(objectValue2(config.contextual)?.options) ? objectValue2(config.contextual).options : [];
4155
+ const projectedContextual = contextualOptions.filter((option) => typeof option === "string" && ["copy", "view", "chatgpt", "claude", "perplexity"].includes(option));
4156
+ const droppedContextual = contextualOptions.filter((option) => !projectedContextual.includes(option));
4157
+ if (droppedContextual.length > 0) {
4158
+ 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" });
4159
+ }
4160
+ const metatagEntries = Object.entries(objectValue2(seo?.metatags) ?? {}).filter((entry) => /^[\w:.-]+$/.test(entry[0]) && typeof entry[1] === "string" && entry[1].length <= 1e3);
4161
+ const projectedMetatags = metatagEntries.length > 0 ? Object.fromEntries(metatagEntries) : void 0;
4043
4162
  const iconLibrary = objectValue2(config.icons)?.library;
4044
- const projectedIconLibrary = ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
4163
+ const projectedIconLibrary = iconLibrary === void 0 ? "fontawesome" : ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
4045
4164
  const docsNavbar = projectedNavbar(config.navbar);
4046
4165
  const legacyRaw = legacyTopbarNavbar(config);
4047
4166
  const legacyNavbar = projectedNavbar(legacyRaw);
@@ -4069,6 +4188,8 @@ function projectedCompatibleConfig(config, warnings) {
4069
4188
  const projectedNavigation = navbarLinks.length > 0 || navbar?.primary ? { ...navbarLinks.length > 0 ? { links: navbarLinks } : {}, ...navbar?.primary ? { primary: navbar.primary } : {} } : void 0;
4070
4189
  return {
4071
4190
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
4191
+ ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
4192
+ ...projectedColors(config.colors) ? { colors: projectedColors(config.colors) } : {},
4072
4193
  ...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
4073
4194
  ...bannerContent ? {
4074
4195
  banner: {
@@ -4086,10 +4207,19 @@ function projectedCompatibleConfig(config, warnings) {
4086
4207
  ...projectedNavigation ? { navbar: projectedNavigation } : {},
4087
4208
  ...projectedFooter(config.footer) ? { footer: projectedFooter(config.footer) } : {},
4088
4209
  ...bodyFont || headingFont ? { fonts: { ...bodyFont ? { body: bodyFont } : {}, ...headingFont ? { heading: headingFont } : {} } } : {},
4089
- ...seo?.indexing === "all" ? { seo: { indexing: "all" } } : {},
4210
+ // Mintlify titles pages "<title> - <site name>" unless `og:title` overrides them,
4211
+ // and its sitemap lists navigation pages only.
4212
+ seo: { ...seo?.indexing === "all" ? { indexing: "all" } : {}, titleSeparator: " - ", sitemap: "navigable", ...projectedMetatags ? { metatags: projectedMetatags } : {} },
4090
4213
  ...typeof feedback?.thumbsRating === "boolean" ? { feedback: { thumbsRating: feedback.thumbsRating } } : {}
4091
4214
  };
4092
4215
  }
4216
+ function projectedApi(config, warnings) {
4217
+ const mdx = projectedApiMdx(config, warnings).api?.mdx;
4218
+ const raw = objectValue2(objectValue2(config.api)?.playground)?.display;
4219
+ const display = raw === void 0 ? void 0 : playgroundDisplay(raw, "api.playground.display", (message) => warnings.push({ code: "unsupported-config", message }));
4220
+ if (!mdx && !display) return {};
4221
+ return { api: { ...mdx ? { mdx } : {}, ...display ? { playground: { display } } : {} } };
4222
+ }
4093
4223
  function projectedApiMdx(config, warnings) {
4094
4224
  const mdx = objectValue2(objectValue2(config.api)?.mdx);
4095
4225
  if (!mdx) return {};
@@ -4319,7 +4449,7 @@ function projectMintlifyNavigation(config, options = {}) {
4319
4449
  if (!landing) return [];
4320
4450
  const firstSegment = landing.split("/")[0];
4321
4451
  const prefix = firstSegment === label ? label : "";
4322
- return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {} }];
4452
+ return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {}, ...entry.hidden === true ? { hidden: true } : {} }];
4323
4453
  }) : [];
4324
4454
  if (versions.length > 1) {
4325
4455
  tabs = annotateVersions(tabs);
@@ -4355,7 +4485,7 @@ function projectMintlifyNavigation(config, options = {}) {
4355
4485
  ...shortcuts.length > 0 ? { shortcuts } : {}
4356
4486
  } } : {},
4357
4487
  ...projectedCompatibleConfig(config, warnings),
4358
- ...projectedApiMdx(config, warnings),
4488
+ ...projectedApi(config, warnings),
4359
4489
  ...i18n ? { i18n } : {},
4360
4490
  ...redirects.length > 0 ? { redirects } : {}
4361
4491
  },
@@ -7004,12 +7134,22 @@ function mintlifyTopLevelApiReferences(config) {
7004
7134
  }
7005
7135
  return references;
7006
7136
  }
7007
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7137
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, pages = []) {
7008
7138
  if (!mintlifyConfig) return [];
7009
7139
  const references = [
7010
7140
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
7011
7141
  ...mintlifyNavigationApiReferences(mintlifyConfig)
7012
7142
  ];
7143
+ const specKey = (value) => value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
7144
+ const known = new Set(references.map((reference) => specKey(reference.value)));
7145
+ for (const page of pages) {
7146
+ const specRef = page.openapi ? splitOpenApiRef(page.openapi)?.specRef : void 0;
7147
+ if (!specRef || /^[a-z][a-z0-9+.-]*:/i.test(specRef)) continue;
7148
+ const key = specKey(specRef);
7149
+ if (known.has(key) || !files.some((file) => file.relativePath === key)) continue;
7150
+ known.add(key);
7151
+ references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
7152
+ }
7013
7153
  const seen = /* @__PURE__ */ new Set();
7014
7154
  const specs = [];
7015
7155
  for (const reference of references) {
@@ -7059,6 +7199,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7059
7199
  parentTab: reference.parentTab,
7060
7200
  icon: reference.icon,
7061
7201
  hidden: reference.hidden,
7202
+ ...reference.pageOnly ? { pageOnly: true } : {},
7062
7203
  sourcePath: match.relativePath,
7063
7204
  directory: reference.directory
7064
7205
  });
@@ -7568,11 +7709,12 @@ function preserveLinkedAnchors(pages) {
7568
7709
  const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
7569
7710
  if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
7570
7711
  const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
7571
- return match ? { index, text: match[1] } : null;
7712
+ return match ? { index, text: match[1].replace(/\s*\{\/\*\s*#\S+?\s*\*\/\}$/, "") } : null;
7572
7713
  }).filter((entry) => entry !== null);
7573
7714
  const existing = /* @__PURE__ */ new Set();
7574
7715
  replaceOutsideCode(page.body, (text2) => {
7575
7716
  for (const match of text2.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
7717
+ for (const match of text2.matchAll(/\{\/\*\s*#(\S+?)\s*\*\/\}/g)) existing.add(match[1]);
7576
7718
  return text2;
7577
7719
  });
7578
7720
  const candidateIndexes = /* @__PURE__ */ new Map();
@@ -7715,7 +7857,11 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7715
7857
  }
7716
7858
  if (!ASSET_EXTENSIONS.has(extname6(decodedPath).toLowerCase())) return null;
7717
7859
  try {
7718
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
7860
+ let candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
7861
+ if (!decodedPath.startsWith("/") && !existsSync5(candidate)) {
7862
+ const fromRoot = resolveWithin(siteRoot, decodedPath.replace(/^(?:\.\/)+/, ""));
7863
+ if (existsSync5(fromRoot)) candidate = fromRoot;
7864
+ }
7719
7865
  const realRoot = realpathSync2(siteRoot);
7720
7866
  const realCandidate = realpathSync2(candidate);
7721
7867
  const realRelative = relative5(realRoot, realCandidate);
@@ -8469,7 +8615,11 @@ function migrateRepository(options) {
8469
8615
  const publishedText = trackPublishedRefs ? raw : void 0;
8470
8616
  const pageAssetReferences = [];
8471
8617
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8472
- raw = normalizeExplicitHeadingIds(raw, (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }));
8618
+ raw = normalizeExplicitHeadingIds(
8619
+ raw,
8620
+ (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }),
8621
+ { headingMarkers: platform === "mintlify" }
8622
+ );
8473
8623
  }
8474
8624
  if (componentMigrator) {
8475
8625
  const warningsBeforeTransform = warnings.length;
@@ -8703,15 +8853,19 @@ function migrateRepository(options) {
8703
8853
  const rootStylesheetPath = platform === "mintlify" && mintlifyProjectRoot ? resolveWithin(mintlifyProjectRoot, "style.css") : void 0;
8704
8854
  const hasRootStylesheet = Boolean(rootStylesheetPath && existsSync5(rootStylesheetPath) && lstatSync5(rootStylesheetPath).isFile() && !lstatSync5(rootStylesheetPath).isSymbolicLink());
8705
8855
  let rootStyleProjection;
8856
+ let rootNavbarButtons = [];
8706
8857
  let rootStyleProblem;
8707
8858
  if (hasRootStylesheet && rootStylesheetPath && lstatSync5(rootStylesheetPath).size <= MAX_ASSET_BYTES) {
8708
8859
  const css = readFileSync5(rootStylesheetPath, "utf8");
8709
8860
  if (/@import\b|url\s*\(\s*['"]?\s*(?:javascript:|file:)/i.test(css)) rootStyleProblem = "unsafe";
8710
8861
  else {
8711
8862
  try {
8863
+ rootNavbarButtons = navbarLinkButtons(css);
8712
8864
  rootStyleProjection = projectAuthoredStyles(css, [
8713
8865
  ...pages.map((page) => page.body),
8714
- ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : "")
8866
+ ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : ""),
8867
+ // Site-wide scripts build markup of their own (a CTA card, say) that the stylesheet is meant for.
8868
+ ...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
8869
  ]);
8716
8870
  } catch {
8717
8871
  rootStyleProblem = "parse";
@@ -8849,6 +9003,18 @@ function migrateRepository(options) {
8849
9003
  } else if (rootStyleProblem === "parse" || !rootStyleProjection) {
8850
9004
  warnings.push({ code: "unsupported-config", message: "Mintlify root style.css could not be parsed and was not imported.", source: "style.css" });
8851
9005
  } else {
9006
+ if (rootNavbarButtons.length > 0 && docsConfig.navbar?.links) {
9007
+ docsConfig = {
9008
+ ...docsConfig,
9009
+ navbar: {
9010
+ ...docsConfig.navbar,
9011
+ links: docsConfig.navbar.links.map((link) => {
9012
+ const rule = rootNavbarButtons.find((entry) => entry.exact ? link.href === entry.href : link.href.includes(entry.href));
9013
+ return rule ? { ...link, button: { background: rule.background, ...rule.color ? { color: rule.color } : {} } } : link;
9014
+ })
9015
+ }
9016
+ };
9017
+ }
8852
9018
  if (rootStyleProjection.omittedSelectors > 0) warnings.push({
8853
9019
  code: "unsupported-config",
8854
9020
  source: "style.css",
@@ -8999,7 +9165,9 @@ function migrateRepository(options) {
8999
9165
  source: path
9000
9166
  });
9001
9167
  };
9002
- const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
9168
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages);
9169
+ const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
9170
+ const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
9003
9171
  for (const spec of resolvedSpecs) {
9004
9172
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
9005
9173
  assets.push(specAsset(spec.filename, spec.content));
@@ -9032,6 +9200,14 @@ function migrateRepository(options) {
9032
9200
  }
9033
9201
  }
9034
9202
  }
9203
+ for (const spec of pageOnlySpecs) {
9204
+ if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
9205
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
9206
+ }
9207
+ if (pageOnlySpecs.length > 0) {
9208
+ docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
9209
+ pageSpecs = [...pageSpecs, ...pageOnlySpecs];
9210
+ }
9035
9211
  rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
9036
9212
  }
9037
9213
  if (platform === "mintlify") {
@@ -9447,6 +9623,8 @@ function renderPage(bundle, page) {
9447
9623
  "---",
9448
9624
  `title: ${yamlString(page.title)}`,
9449
9625
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
9626
+ page.icon ? `icon: ${yamlString(page.icon)}` : null,
9627
+ page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
9450
9628
  `description: ${yamlString(page.description)}`,
9451
9629
  page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
9452
9630
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
@@ -9454,9 +9632,11 @@ function renderPage(bundle, page) {
9454
9632
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
9455
9633
  page.hidden ? "hidden: true" : null,
9456
9634
  page.noindex ? "noindex: true" : null,
9635
+ ...Object.entries(page.meta ?? {}).map(([field, value]) => `${field}: ${yamlString(value)}`),
9457
9636
  page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
9458
9637
  page.api ? `api: ${yamlString(page.api)}` : null,
9459
9638
  page.authMethod ? `authMethod: ${yamlString(page.authMethod)}` : null,
9639
+ page.playground ? `playground: ${yamlString(page.playground)}` : null,
9460
9640
  bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
9461
9641
  "---",
9462
9642
  "",