@thallylabs/migrate 0.2.21 → 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
@@ -1,5 +1,6 @@
1
1
  // src/repository.ts
2
2
  import { compileSync as compileSync2 } from "@mdx-js/mdx";
3
+ import { createHash as createHash2 } from "crypto";
3
4
  import { spawn } from "child_process";
4
5
  import {
5
6
  closeSync,
@@ -16,12 +17,12 @@ import {
16
17
  import { createRequire } from "module";
17
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";
18
19
  import { parse as parseYaml4 } from "yaml";
19
- import * as ts3 from "typescript";
20
+ import * as ts4 from "typescript";
20
21
 
21
22
  // src/components.ts
22
23
  import { createHash } from "crypto";
23
24
  import { existsSync, lstatSync, readFileSync } from "fs";
24
- 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";
25
26
  import postcss from "postcss";
26
27
  import selectorParser from "postcss-selector-parser";
27
28
  import remarkMdx2 from "remark-mdx";
@@ -156,6 +157,177 @@ function isThallyBuiltinComponent(name) {
156
157
  return THALLY_BUILTIN_COMPONENTS.has(name) || THALLY_BUILTIN_COMPONENTS.has(name.split(".")[0]);
157
158
  }
158
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
+
159
331
  // src/mdx.ts
160
332
  function titleFromId(id) {
161
333
  const value = id.split("/").at(-1) ?? id;
@@ -264,7 +436,7 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
264
436
  return isMdxCodeBlock ? body : output.join("\n");
265
437
  }
266
438
  var HTML_ID = /^[^\s"'<>`{}&]+$/u;
267
- function normalizeExplicitHeadingIds(raw, warn) {
439
+ function normalizeExplicitHeadingIds(raw, warn, options = {}) {
268
440
  const { front, body } = splitFrontmatterBlock(raw);
269
441
  return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
270
442
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
@@ -275,13 +447,15 @@ function normalizeExplicitHeadingIds(raw, warn) {
275
447
  const id = trimmed.slice(marker + 3, -1);
276
448
  const heading = trimmed.slice(0, marker).trimEnd();
277
449
  if (/[{}]/.test(id)) return line;
278
- 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>
279
453
  ${heading}`;
280
454
  warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
281
455
  return heading;
282
456
  }
283
457
  }
284
- if (trimmed.endsWith("*/}")) {
458
+ if (!options.headingMarkers && !options.keepIdComments && trimmed.endsWith("*/}")) {
285
459
  const marker = trimmed.lastIndexOf(" {/*");
286
460
  if (marker >= 0) {
287
461
  const comment = trimmed.slice(marker + 4, -3).trim();
@@ -295,6 +469,44 @@ ${trimmed.slice(0, marker).trimEnd()}`;
295
469
  return line;
296
470
  }).join("\n"));
297
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
+ }
298
510
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
299
511
  "Tabs",
300
512
  "TabItem",
@@ -428,6 +640,9 @@ function bracketDelta(line) {
428
640
  return delta;
429
641
  }
430
642
  var ESM_OPEN = /^(?:import|export)\b/;
643
+ function latexElement(source, block = false) {
644
+ return `<Latex${block ? " block" : ""}>{${JSON.stringify(source)}}</Latex>`;
645
+ }
431
646
  function protectMathBlocks(raw) {
432
647
  const { front, body } = splitFrontmatterBlock(raw);
433
648
  const lines = body.split(/\r\n|\r|\n/);
@@ -442,7 +657,7 @@ function protectMathBlocks(raw) {
442
657
  const trimmed = line.trim();
443
658
  if (mathBlockDelimiter) {
444
659
  if (trimmed === mathBlockDelimiter) {
445
- output.push("```math", ...mathLines, "```");
660
+ output.push(latexElement(mathLines.join("\n"), true));
446
661
  mathBlockDelimiter = null;
447
662
  mathLines = [];
448
663
  converted = true;
@@ -484,11 +699,11 @@ function protectMathBlocks(raw) {
484
699
  (whole, block, inline) => {
485
700
  if (block !== void 0) {
486
701
  converted = true;
487
- return `\`$$${block}$$\``;
702
+ return latexElement(unmask(block));
488
703
  }
489
704
  if (inline !== void 0 && hasTexSignal(unmask(inline))) {
490
705
  converted = true;
491
- return `\`$${inline}$\``;
706
+ return latexElement(unmask(inline));
492
707
  }
493
708
  return whole;
494
709
  }
@@ -1222,17 +1437,55 @@ function injectReactHookImports(body) {
1222
1437
 
1223
1438
  ${body}` : body;
1224
1439
  }
1440
+ function normalizeFlowParagraphContainers(body) {
1441
+ if (!body.includes("<p")) return body;
1442
+ let tree;
1443
+ try {
1444
+ tree = descriptionParser.parse(body);
1445
+ } catch {
1446
+ return body;
1447
+ }
1448
+ const edits = [];
1449
+ function visit(node) {
1450
+ if (node.type === "mdxJsxFlowElement" && node.name === "p" && node.children?.some((child) => child.type === "paragraph" || child.type === "list" || child.type === "mdxJsxFlowElement")) {
1451
+ const start = node.position?.start.offset;
1452
+ const end = node.position?.end.offset;
1453
+ if (start !== void 0 && end !== void 0 && body.slice(start, start + 2) === "<p") {
1454
+ const closing = body.slice(start, end).match(/<\/p\s*>\s*$/i);
1455
+ if (closing?.index !== void 0) {
1456
+ edits.push({ offset: start + 1, value: "div" });
1457
+ edits.push({ offset: start + closing.index + 2, value: "div" });
1458
+ }
1459
+ }
1460
+ }
1461
+ for (const child of node.children ?? []) visit(child);
1462
+ }
1463
+ visit(tree);
1464
+ return edits.sort((a, b) => b.offset - a.offset).reduce((text2, edit) => text2.slice(0, edit.offset) + edit.value + text2.slice(edit.offset + 1), body);
1465
+ }
1225
1466
  function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1226
1467
  const runFern = platform === "fern";
1227
1468
  const runMintlify = platform === "mintlify";
1228
1469
  const runDocusaurus = platform === "docusaurus";
1229
1470
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1230
- 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);
1231
1477
  if (runDocusaurus) {
1232
1478
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1233
1479
  /:([+\w-]+):/g,
1234
1480
  (shortcode, name) => nameToEmoji[name] ?? shortcode
1235
1481
  ));
1482
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1483
+ /\b(href|src)=\{\s*require\(\s*(['"])@docusaurus\/useBaseUrl\2\s*\)\.default\(\s*(['"])([^'"]+)\3\s*\)\s*\}/g,
1484
+ (original, attribute, _moduleQuote, _pathQuote, path) => {
1485
+ if (!/^[\w./~%-]+$/.test(path) || path.split("/").includes("..")) return original;
1486
+ return `${attribute}="/${path.replace(/^\.\//, "").replace(/^\/+/, "")}"`;
1487
+ }
1488
+ ));
1236
1489
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1237
1490
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1238
1491
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -1269,7 +1522,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1269
1522
  return normalizeMdx(body, platform, false);
1270
1523
  }
1271
1524
  }
1272
- return normalized;
1525
+ return normalizeFlowParagraphContainers(normalized);
1273
1526
  }
1274
1527
  function parseMarkdownPage(input) {
1275
1528
  const parsed = parseFrontmatter(input.raw);
@@ -1287,16 +1540,25 @@ function parseMarkdownPage(input) {
1287
1540
  body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
1288
1541
  }
1289
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
+ }
1290
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;
1291
1551
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
1292
1552
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
1293
- 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);
1294
1554
  return {
1295
1555
  id: identity.id,
1296
1556
  navigationId: identity.navigationId,
1297
1557
  locale: identity.locale,
1298
1558
  title,
1299
1559
  navTitle,
1560
+ icon,
1561
+ iconType,
1300
1562
  description,
1301
1563
  ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
1302
1564
  badge,
@@ -1304,6 +1566,7 @@ function parseMarkdownPage(input) {
1304
1566
  mode,
1305
1567
  hidden: parsed.data.hidden === true ? true : void 0,
1306
1568
  noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
1569
+ ...Object.keys(meta).length > 0 ? { meta } : {},
1307
1570
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
1308
1571
  ...apiFrontmatter(parsed.data, input.warn),
1309
1572
  body,
@@ -1311,9 +1574,21 @@ function parseMarkdownPage(input) {
1311
1574
  ...parsed.error ? { frontmatterError: parsed.error } : {}
1312
1575
  };
1313
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
+ ];
1314
1585
  var AUTH_METHODS = /* @__PURE__ */ new Set(["bearer", "basic", "key", "none"]);
1315
1586
  function apiFrontmatter(data, warn) {
1316
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
+ }
1317
1592
  if (data.api !== void 0 && data.api !== null) {
1318
1593
  if (typeof data.api === "string" && data.api.trim()) result.api = data.api.trim();
1319
1594
  else warn?.(`The page's "api" frontmatter is not a "METHOD url-or-path" string and was dropped.`);
@@ -1326,84 +1601,6 @@ function apiFrontmatter(data, warn) {
1326
1601
  return result;
1327
1602
  }
1328
1603
 
1329
- // src/path.ts
1330
- import { isAbsolute, relative, resolve, sep } from "path";
1331
- var SAFE_SEGMENT = /[^a-z0-9._-]+/gi;
1332
- function trimTrailingSlashes(value) {
1333
- let end = value.length;
1334
- while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
1335
- return value.slice(0, end);
1336
- }
1337
- function trimEdgeSlashes(value) {
1338
- let start = 0;
1339
- let end = value.length;
1340
- while (start < end && value.charCodeAt(start) === 47) start++;
1341
- while (end > start && value.charCodeAt(end - 1) === 47) end--;
1342
- return value.slice(start, end);
1343
- }
1344
- function slugifySegment(value, preserveCase = false) {
1345
- let decoded = value;
1346
- try {
1347
- decoded = decodeURIComponent(value);
1348
- } catch {
1349
- }
1350
- const normalized = preserveCase ? decoded : decoded.toLowerCase();
1351
- return normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
1352
- }
1353
- function pageIdFromReference(value, preserveCase = false) {
1354
- const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
1355
- if (!withoutQuery || withoutQuery.includes("\0")) return null;
1356
- const rawSegments = withoutQuery.split("/").filter(Boolean);
1357
- if (rawSegments.some((segment) => segment === ".." || segment === ".")) return null;
1358
- const last = rawSegments.at(-1)?.replace(/\.(?:mdx?|rst|txt)$/i, "") ?? "";
1359
- const isRootLevelReadme = rawSegments.length === 1 && /^readme$/i.test(last);
1360
- const collapsesToParent = /^index$/i.test(last) || isRootLevelReadme;
1361
- const baseSegments = collapsesToParent ? rawSegments.slice(0, -1) : rawSegments;
1362
- if (!collapsesToParent) {
1363
- baseSegments[baseSegments.length - 1] = last;
1364
- }
1365
- const segments = baseSegments.map((segment) => slugifySegment(segment, preserveCase)).filter(Boolean);
1366
- return segments.join("/") || "introduction";
1367
- }
1368
- function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__ */ new Set()) {
1369
- const reference = value.replace(/^\/+/, "");
1370
- const localePrefix = `${locale.toLowerCase()}/`;
1371
- if (reference.toLowerCase().startsWith(localePrefix)) return reference.slice(localePrefix.length);
1372
- const sectionPrefix = `${locale.toLowerCase()}-`;
1373
- if (reference.toLowerCase().startsWith(sectionPrefix)) {
1374
- const candidate = reference.slice(sectionPrefix.length);
1375
- const candidateId = pageIdFromReference(candidate, true);
1376
- if (candidateId && defaultPageIds.has(candidateId)) return candidate;
1377
- }
1378
- const segments = reference.split("/");
1379
- const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === locale.toLowerCase());
1380
- if (localeIndex > 0) {
1381
- return [...segments.slice(0, localeIndex), ...segments.slice(localeIndex + 1)].join("/");
1382
- }
1383
- return value;
1384
- }
1385
- function resolveWithin(root, candidate) {
1386
- return resolveWithinRoot(root, candidate, root);
1387
- }
1388
- function resolveWithinRoot(baseDir, candidate, confineRoot) {
1389
- if (isAbsolute(candidate) || candidate.includes("\0")) {
1390
- throw new Error(`Unsafe migration path: ${candidate}`);
1391
- }
1392
- const target = resolve(baseDir, candidate);
1393
- const fromRoot = relative(resolve(confineRoot), target);
1394
- if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
1395
- throw new Error(`Migration path escapes its root: ${candidate}`);
1396
- }
1397
- return target;
1398
- }
1399
- function normalizeAssetPath(value) {
1400
- const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
1401
- if (!normalized || normalized.includes("\0")) return null;
1402
- const segments = normalized.split("/").filter(Boolean);
1403
- if (segments.some((segment) => segment === "." || segment === "..")) return null;
1404
- return segments.join("/");
1405
- }
1406
-
1407
1604
  // src/inline-extraction.ts
1408
1605
  import ts from "typescript";
1409
1606
  var SERVER_ONLY_IDENTIFIERS = /* @__PURE__ */ new Set(["process", "require", "module", "exports", "__dirname", "__filename", "Buffer"]);
@@ -2205,26 +2402,33 @@ function hash(value) {
2205
2402
  function portableSpecifier(path) {
2206
2403
  return path.replace(/\.tsx?$/, "");
2207
2404
  }
2208
- function wrapImpureCssModuleSelectors(css) {
2405
+ function sanitizeCssModuleSelectors(css) {
2209
2406
  const root = postcss.parse(css);
2407
+ let omitted = 0;
2210
2408
  root.walkRules((rule) => {
2211
- rule.selector = selectorParser((selectors) => {
2212
- selectors.each((selector) => {
2213
- let hasClassOrId = false;
2214
- selector.walk((node) => {
2215
- if (node.type === "class" || node.type === "id") hasClassOrId = true;
2216
- });
2217
- if (hasClassOrId) return;
2218
- const nodes = selector.nodes.splice(0, selector.nodes.length);
2219
- const container = selectorParser.selector({ value: "" });
2220
- for (const node of nodes) container.append(node);
2221
- const globalPseudo = selectorParser.pseudo({ value: ":global" });
2222
- globalPseudo.append(container);
2223
- selector.append(globalPseudo);
2409
+ if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
2410
+ if (/^:export$|^:import\(/.test(rule.selector.trim())) return;
2411
+ const selectors = selectorParser().astSync(rule.selector);
2412
+ selectors.each((selector) => {
2413
+ let hasLocalClassOrId = false;
2414
+ selector.walk((node) => {
2415
+ if (node.type !== "class" && node.type !== "id") return;
2416
+ let parent = node.parent;
2417
+ while (parent) {
2418
+ if (parent.type === "pseudo" && parent.value === ":global") return;
2419
+ parent = parent.parent;
2420
+ }
2421
+ hasLocalClassOrId = true;
2224
2422
  });
2225
- }).processSync(rule.selector);
2423
+ if (!hasLocalClassOrId) {
2424
+ selector.remove();
2425
+ omitted++;
2426
+ }
2427
+ });
2428
+ if (selectors.nodes.length === 0) rule.remove();
2429
+ else rule.selector = selectors.toString();
2226
2430
  });
2227
- return root.toString();
2431
+ return { css: root.toString(), omitted };
2228
2432
  }
2229
2433
  function sourceFile(source, filename) {
2230
2434
  return ts2.createSourceFile(
@@ -2424,10 +2628,10 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2424
2628
  }
2425
2629
  function expandCandidates(candidate) {
2426
2630
  const candidates = [candidate];
2427
- if (!extname(candidate)) {
2631
+ if (!extname2(candidate)) {
2428
2632
  candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json"].map((extension) => candidate + extension));
2429
2633
  candidates.push(...["index.tsx", "index.jsx", "index.ts", "index.js"].map((name) => resolve2(candidate, name)));
2430
- } else if (extname(candidate) === ".js") {
2634
+ } else if (extname2(candidate) === ".js") {
2431
2635
  candidates.push(candidate.slice(0, -3) + ".ts", candidate.slice(0, -3) + ".tsx");
2432
2636
  }
2433
2637
  return candidates;
@@ -2455,6 +2659,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2455
2659
  }
2456
2660
  function copyGraph(entry) {
2457
2661
  const staged = /* @__PURE__ */ new Map();
2662
+ const stagedWarnings = [];
2458
2663
  let stagedBytes = 0;
2459
2664
  function stageShim(filename, content) {
2460
2665
  const shimDestination = `${destinationRoot}/shims/${filename}`;
@@ -2467,7 +2672,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2467
2672
  checkedFile(path);
2468
2673
  const size = lstatSync(path).size;
2469
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");
2470
- const extension = extname(path).toLowerCase();
2675
+ const extension = extname2(path).toLowerCase();
2471
2676
  if (!CODE_EXTENSIONS.has(extension) && !DATA_EXTENSIONS.has(extension)) throw new Error(`unsupported dependency file type ${extension}`);
2472
2677
  stagedBytes += size;
2473
2678
  const content = readFileSync(path);
@@ -2475,13 +2680,14 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2475
2680
  if (!CODE_EXTENSIONS.has(extension)) {
2476
2681
  if (extension === ".css" && /@import\b|url\s*\(/i.test(content.toString("utf8"))) throw new Error("CSS resource dependencies require manual migration");
2477
2682
  if (extension === ".css" && path.toLowerCase().endsWith(".module.css")) {
2478
- let wrapped;
2683
+ let sanitized;
2479
2684
  try {
2480
- wrapped = wrapImpureCssModuleSelectors(content.toString("utf8"));
2685
+ sanitized = sanitizeCssModuleSelectors(content.toString("utf8"));
2481
2686
  } catch {
2482
2687
  throw new Error("CSS module could not be parsed to check for Turbopack-safe selectors");
2483
2688
  }
2484
- staged.set(destination, { path: destination, content: wrapped });
2689
+ if (sanitized.omitted > 0) stagedWarnings.push({ path, omitted: sanitized.omitted });
2690
+ staged.set(destination, { path: destination, content: sanitized.css });
2485
2691
  }
2486
2692
  return;
2487
2693
  }
@@ -2550,6 +2756,9 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2550
2756
  }
2551
2757
  visit(entry);
2552
2758
  for (const [path, file] of staged) copied.set(path, file);
2759
+ for (const { path, omitted } of stagedWarnings) {
2760
+ warn(`Site-authored CSS module ${relative2(root, path).replace(/\\/g, "/")} has ${omitted} global-only selector branch(es) that cannot be copied into Thally CSS Modules; those styles were omitted.`, path);
2761
+ }
2553
2762
  copiedBytes += stagedBytes;
2554
2763
  return outputPath(entry);
2555
2764
  }
@@ -2559,7 +2768,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2559
2768
  if (size > MAX_FILE_BYTES || copiedBytes + size > MAX_COMPONENT_BYTES || copied.size >= MAX_COMPONENT_FILES) {
2560
2769
  throw new Error("component migration budget exceeded");
2561
2770
  }
2562
- const publicName = `migrated-${hash(relative2(confined, path))}${extname(path).toLowerCase()}`;
2771
+ const publicName = `migrated-${hash(relative2(confined, path))}${extname2(path).toLowerCase()}`;
2563
2772
  const destination = `public/${publicName}`;
2564
2773
  if (!copied.has(destination)) {
2565
2774
  copied.set(destination, { path: destination, content: readFileSync(path) });
@@ -2594,7 +2803,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2594
2803
  function transform(raw, currentFile) {
2595
2804
  const parsedFrontmatter = parseFrontmatter(raw).content;
2596
2805
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
2597
- const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)));
2806
+ const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)), void 0, { keepIdComments: true });
2598
2807
  let tree;
2599
2808
  try {
2600
2809
  tree = parser.parse(content);
@@ -2712,7 +2921,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2712
2921
  }
2713
2922
  continue;
2714
2923
  }
2715
- 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))) {
2716
2925
  try {
2717
2926
  const href = copyPublicAsset(resolveDependency(specifier, currentFile));
2718
2927
  edits.push({
@@ -2743,7 +2952,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2743
2952
  try {
2744
2953
  const sourcePath = resolveDependency(specifier, currentFile);
2745
2954
  const path = copyGraph(sourcePath);
2746
- 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));
2747
2956
  for (const binding of bindings) {
2748
2957
  const registerPath = isSvgUsedAsTag ? wrapSvgAsComponent(path) : path;
2749
2958
  const registerImported = isSvgUsedAsTag ? "default" : binding.imported;
@@ -2782,7 +2991,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2782
2991
  const importStart = node.position.start.offset + statement.getStart(ast);
2783
2992
  const importEnd = node.position.start.offset + statement.end;
2784
2993
  let rescuedAssetHref;
2785
- if (RESCUABLE_ASSET_EXTENSIONS.has(extname(specifier).toLowerCase())) {
2994
+ if (RESCUABLE_ASSET_EXTENSIONS.has(extname2(specifier).toLowerCase())) {
2786
2995
  const usedAsJsxTag = bindings.some((binding) => hasExpressionReference(/* @__PURE__ */ new Set([binding.local]), { includeDirectTags: true }));
2787
2996
  if (!usedAsJsxTag) {
2788
2997
  try {
@@ -2803,7 +3012,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2803
3012
  warn(`Asset import ${JSON.stringify(rawSpecifier)} could not be copied as a component; it was copied to ${rescuedAssetHref} and bound to that URL instead.`, currentFile);
2804
3013
  } else {
2805
3014
  edits.push({ start: importStart, end: importEnd, value: "" });
2806
- warn(`Component import ${JSON.stringify(rawSpecifier)} could not be copied and was removed: ${error instanceof Error ? error.message : "unsupported dependency"}. Its usage on this page was neutralized.`, currentFile);
3015
+ warn(`Site-authored component import ${JSON.stringify(rawSpecifier)} could not be copied and was removed: ${error instanceof Error ? error.message : "unsupported dependency"}. Its usage on this page was neutralized; review this custom implementation manually.`, currentFile);
2807
3016
  }
2808
3017
  }
2809
3018
  }
@@ -3133,6 +3342,125 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
3133
3342
  ];
3134
3343
  }
3135
3344
 
3345
+ // src/source-styles.ts
3346
+ import postcss2 from "postcss";
3347
+ import selectorParser2 from "postcss-selector-parser";
3348
+ import ts3 from "typescript";
3349
+ var PLATFORM_CLASSES = /* @__PURE__ */ new Set(["navbar-link"]);
3350
+ var PLATFORM_IDS = /* @__PURE__ */ new Set(["footer", "navbar", "sidebar"]);
3351
+ var PLATFORM_CLASS_PREFIX = /^(?:mintlify[-_]|fern[-_]|docusaurus[-_]|theme-doc-|theme-code-block|DocSearch|pagination-nav|menu__)/i;
3352
+ var SHARED_THEME_CLASSES = /* @__PURE__ */ new Set(["dark", "light"]);
3353
+ var UTILITY_CLASS = /^(?:[mp][trblxy]?|w|h|min-w|max-w|min-h|max-h|text|bg|border|rounded|flex|grid|gap|space-[xy]|items|justify|overflow|truncate|hidden|block|inline|relative|absolute|sticky|z|opacity)-/;
3354
+ function withoutFencedCode(content) {
3355
+ let fence;
3356
+ return content.split("\n").map((line) => {
3357
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
3358
+ if (fence) {
3359
+ if (marker?.[0] === fence.marker && marker.length >= fence.length) fence = void 0;
3360
+ return "";
3361
+ }
3362
+ if (marker) {
3363
+ fence = { marker: marker[0], length: marker.length };
3364
+ return "";
3365
+ }
3366
+ return line;
3367
+ }).join("\n");
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
+ }
3384
+ function authoredStyleNames(contents) {
3385
+ const classes = /* @__PURE__ */ new Set();
3386
+ const ids = /* @__PURE__ */ new Set();
3387
+ for (const original of contents) {
3388
+ const content = withoutFencedCode(original);
3389
+ for (const match of content.matchAll(/\b(?:className|class)\s*=\s*["']([^"']+)["']/g)) {
3390
+ for (const name of match[1].split(/\s+/)) if (name) classes.add(name);
3391
+ }
3392
+ for (const match of content.matchAll(/\bid\s*=\s*["']([^"']+)["']/g)) ids.add(match[1]);
3393
+ if (!content.includes("className={")) continue;
3394
+ const source = ts3.createSourceFile("component.tsx", content, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
3395
+ const visit = (node) => {
3396
+ if (ts3.isJsxAttribute(node) && ts3.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
3397
+ const collect = (value) => {
3398
+ if (ts3.isStringLiteral(value) || ts3.isNoSubstitutionTemplateLiteral(value)) {
3399
+ for (const name of value.text.split(/\s+/)) if (name) classes.add(name);
3400
+ } else ts3.forEachChild(value, collect);
3401
+ };
3402
+ collect(node.initializer);
3403
+ }
3404
+ ts3.forEachChild(node, visit);
3405
+ };
3406
+ visit(source);
3407
+ }
3408
+ return { classes, ids };
3409
+ }
3410
+ function projectAuthoredStyles(css, contents) {
3411
+ const names = authoredStyleNames(contents);
3412
+ const root = postcss2.parse(css);
3413
+ let omittedSelectors = 0;
3414
+ let rewrittenSelectors = 0;
3415
+ root.walkRules((rule) => {
3416
+ if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
3417
+ const retained = [];
3418
+ try {
3419
+ selectorParser2((selectors) => {
3420
+ selectors.each((selector) => {
3421
+ let hasAuthoredAnchor = false;
3422
+ let hasPlatformAnchor = false;
3423
+ selector.walk((node) => {
3424
+ let ancestor = node.parent;
3425
+ let isNestedInPseudo = false;
3426
+ while (ancestor && ancestor !== selector) {
3427
+ if (ancestor.type === "pseudo") isNestedInPseudo = true;
3428
+ ancestor = ancestor.parent;
3429
+ }
3430
+ if (node.type === "class") {
3431
+ if (SHARED_THEME_CLASSES.has(node.value)) return;
3432
+ if (PLATFORM_CLASSES.has(node.value) || PLATFORM_CLASS_PREFIX.test(node.value) || UTILITY_CLASS.test(node.value) || !names.classes.has(node.value)) hasPlatformAnchor = true;
3433
+ else if (!isNestedInPseudo) hasAuthoredAnchor = true;
3434
+ } else if (node.type === "id") {
3435
+ if (PLATFORM_IDS.has(node.value) || !names.ids.has(node.value)) hasPlatformAnchor = true;
3436
+ else if (!isNestedInPseudo) hasAuthoredAnchor = true;
3437
+ } else if (node.type === "attribute" && node.attribute === "data-as") {
3438
+ node.remove();
3439
+ rewrittenSelectors += 1;
3440
+ }
3441
+ });
3442
+ if (hasAuthoredAnchor && !hasPlatformAnchor) retained.push(selector.toString());
3443
+ else omittedSelectors += 1;
3444
+ });
3445
+ }).processSync(rule.selector);
3446
+ } catch {
3447
+ omittedSelectors += 1;
3448
+ }
3449
+ if (retained.length) rule.selector = retained.join(", ");
3450
+ else rule.remove();
3451
+ });
3452
+ const hasStyleRule = (nodes) => Boolean(nodes?.some((node) => node.type === "rule" || node.type === "atrule" && hasStyleRule(node.nodes)));
3453
+ root.walkAtRules((rule) => {
3454
+ if (rule.name === "font-face" || rule.name === "property") return;
3455
+ if (!hasStyleRule(rule.nodes)) rule.remove();
3456
+ });
3457
+ if (omittedSelectors === 0 && rewrittenSelectors === 0) return { css, omittedSelectors };
3458
+ root.walkComments((comment) => {
3459
+ comment.remove();
3460
+ });
3461
+ return { css: root.toString(), omittedSelectors };
3462
+ }
3463
+
3136
3464
  // src/docusaurus.ts
3137
3465
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
3138
3466
  import { extname as extname4, posix as posix2 } from "path";
@@ -3142,89 +3470,6 @@ import { parse as parseYaml2 } from "yaml";
3142
3470
  // src/navigation.ts
3143
3471
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
3144
3472
  import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
3145
-
3146
- // src/mintlify-extras.ts
3147
- import { extname as extname2 } from "path";
3148
- function objectValue(value) {
3149
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3150
- }
3151
- var PUBLIC_FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "no", "off", "0"]);
3152
- function isPublicFalse(value) {
3153
- return value === false || value === 0 || typeof value === "string" && PUBLIC_FALSE_STRINGS.has(value.trim().toLowerCase());
3154
- }
3155
- function isPublicTrue(value) {
3156
- return value === true || typeof value === "string" && value.trim().toLowerCase() === "true";
3157
- }
3158
- function frontmatterGateReason(data) {
3159
- const groups = data.groups;
3160
- const hasGroups = Array.isArray(groups) ? groups.length > 0 : typeof groups === "string" ? groups.trim() !== "" : groups !== void 0 && groups !== null && groups !== false;
3161
- if (hasGroups) return "frontmatter `groups` restricts it to authenticated groups";
3162
- if (isPublicFalse(data.public)) return "frontmatter `public: false`";
3163
- return void 0;
3164
- }
3165
- function navigationGateReason(node) {
3166
- const groups = node.groups;
3167
- const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && ["group", "pages", "tab", "groups", "page", "root", "menu", "href"].some((key) => key in entry);
3168
- const restricted = typeof groups === "string" ? groups.trim() !== "" : Array.isArray(groups) && groups.length > 0 && !groups.every(isContainer);
3169
- if (restricted) return "its navigation container sets `groups`";
3170
- if (isPublicFalse(node.public)) return "its navigation container sets `public: false`";
3171
- return void 0;
3172
- }
3173
- 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;
3174
- var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "coverage"]);
3175
- var FONT_EXTENSIONS = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
3176
- function isMintlifyServedScriptOrStyle(relativePath) {
3177
- const extension = extname2(relativePath).toLowerCase();
3178
- if (extension !== ".css" && extension !== ".js") return false;
3179
- const segments = relativePath.split("/");
3180
- if (segments.slice(0, -1).some((segment) => BUILD_OUTPUT_SEGMENTS.has(segment.toLowerCase()))) return false;
3181
- return extension === ".css" || !CONFIG_BASENAME.test(segments[segments.length - 1]);
3182
- }
3183
- function mintlifyFontSources(config) {
3184
- const fonts = objectValue(config?.fonts);
3185
- if (!fonts) return [];
3186
- const found = [];
3187
- for (const entry of [fonts, objectValue(fonts.heading), objectValue(fonts.body)]) {
3188
- if (!entry || typeof entry.source !== "string" || !entry.source.trim()) continue;
3189
- const source = entry.source.trim();
3190
- const family = typeof entry.family === "string" ? entry.family : "a font";
3191
- const remote = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(source);
3192
- const path = remote || /^[a-z]:[\\/]/i.test(source) ? null : normalizeAssetPath(source);
3193
- found.push({ family, source, path: path && FONT_EXTENSIONS.has(extname2(path).toLowerCase()) ? path : null, remote });
3194
- }
3195
- return found;
3196
- }
3197
- function mintlifyAppearance(config, warnings) {
3198
- const appearance = objectValue(config.appearance);
3199
- const legacy = objectValue(config.modeToggle);
3200
- const result = {};
3201
- const isMode = (value) => value === "system" || value === "light" || value === "dark";
3202
- const skip = (field, value) => {
3203
- warnings.push({
3204
- code: "unsupported-config",
3205
- message: `Ignored invalid ${field} value ${JSON.stringify(value)?.slice(0, 40)}; the appearance default was left unchanged.`
3206
- });
3207
- };
3208
- if (appearance?.default !== void 0) {
3209
- if (isMode(appearance.default)) result.default = appearance.default;
3210
- else skip("appearance.default", appearance.default);
3211
- } else if (legacy?.default !== void 0) {
3212
- if (isMode(legacy.default)) result.default = legacy.default;
3213
- else skip("modeToggle.default", legacy.default);
3214
- }
3215
- if (appearance?.strict !== void 0) {
3216
- if (typeof appearance.strict === "boolean") {
3217
- if (appearance.strict) result.showToggle = false;
3218
- } else skip("appearance.strict", appearance.strict);
3219
- } else if (legacy?.isHidden !== void 0) {
3220
- if (typeof legacy.isHidden === "boolean") {
3221
- if (legacy.isHidden) result.showToggle = false;
3222
- } else skip("modeToggle.isHidden", legacy.isHidden);
3223
- }
3224
- return result;
3225
- }
3226
-
3227
- // src/navigation.ts
3228
3473
  var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
3229
3474
  function addMintlifyDirectoryRedirects(config, pages) {
3230
3475
  const pageIds = new Set(pages.map((page) => page.id));
@@ -3295,34 +3540,37 @@ function addMintlifyHomepageRedirects(config, pages) {
3295
3540
  }
3296
3541
  }
3297
3542
  const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
3298
- if (!homepage || homepage === "introduction") return config;
3543
+ if (!homepage) return config;
3299
3544
  const redirects = [...config.redirects ?? []];
3300
3545
  const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
3301
- 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)];
3302
3547
  for (const locale of roots) {
3303
3548
  const source = locale ? `/${locale}` : "/";
3304
3549
  if (sources.has(source)) continue;
3305
- redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: false });
3550
+ redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
3306
3551
  }
3307
3552
  return { ...config, redirects };
3308
3553
  }
3309
3554
  var LANGUAGE_LABELS = {
3310
- ar: "Arabic",
3311
- de: "German",
3555
+ ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629",
3556
+ de: "Deutsch",
3312
3557
  en: "English",
3313
- es: "Spanish",
3314
- fr: "French",
3315
- hi: "Hindi",
3316
- it: "Italian",
3317
- ja: "Japanese",
3318
- ko: "Korean",
3319
- nl: "Dutch",
3320
- pl: "Polish",
3321
- pt: "Portuguese",
3322
- ru: "Russian",
3323
- tr: "Turkish",
3324
- uk: "Ukrainian",
3325
- 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"
3326
3574
  };
3327
3575
  function objectValue2(value) {
3328
3576
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -3787,11 +4035,26 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
3787
4035
  }] : [];
3788
4036
  }
3789
4037
  function projectedTheme(value) {
3790
- if (value === "maple") return "maple";
3791
- 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";
3792
4040
  if (["almond", "palm"].includes(String(value))) return "minimal";
3793
4041
  return typeof value === "string" ? "default" : void 0;
3794
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
+ }
3795
4058
  function projectedNavbar(value) {
3796
4059
  const navbar = objectValue2(value);
3797
4060
  if (!navbar) return void 0;
@@ -3807,7 +4070,8 @@ function projectedNavbar(value) {
3807
4070
  const primaryHref = projectedHref(primaryValue?.href);
3808
4071
  const primary = primaryValue && primaryHref ? {
3809
4072
  label: typeof primaryValue.label === "string" ? primaryValue.label : primaryValue.type === "github" ? "GitHub" : "Get started",
3810
- href: primaryHref
4073
+ href: primaryHref,
4074
+ ...primaryValue.type === "github" ? { type: "github" } : {}
3811
4075
  } : void 0;
3812
4076
  return links.length > 0 || primary ? { ...links.length > 0 ? { links } : {}, ...primary ? { primary } : {} } : void 0;
3813
4077
  }
@@ -3887,8 +4151,16 @@ function projectedCompatibleConfig(config, warnings) {
3887
4151
  const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
3888
4152
  const feedback = objectValue2(config.feedback);
3889
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;
3890
4162
  const iconLibrary = objectValue2(config.icons)?.library;
3891
- 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;
3892
4164
  const docsNavbar = projectedNavbar(config.navbar);
3893
4165
  const legacyRaw = legacyTopbarNavbar(config);
3894
4166
  const legacyNavbar = projectedNavbar(legacyRaw);
@@ -3916,6 +4188,8 @@ function projectedCompatibleConfig(config, warnings) {
3916
4188
  const projectedNavigation = navbarLinks.length > 0 || navbar?.primary ? { ...navbarLinks.length > 0 ? { links: navbarLinks } : {}, ...navbar?.primary ? { primary: navbar.primary } : {} } : void 0;
3917
4189
  return {
3918
4190
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
4191
+ ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
4192
+ ...projectedColors(config.colors) ? { colors: projectedColors(config.colors) } : {},
3919
4193
  ...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
3920
4194
  ...bannerContent ? {
3921
4195
  banner: {
@@ -3933,10 +4207,19 @@ function projectedCompatibleConfig(config, warnings) {
3933
4207
  ...projectedNavigation ? { navbar: projectedNavigation } : {},
3934
4208
  ...projectedFooter(config.footer) ? { footer: projectedFooter(config.footer) } : {},
3935
4209
  ...bodyFont || headingFont ? { fonts: { ...bodyFont ? { body: bodyFont } : {}, ...headingFont ? { heading: headingFont } : {} } } : {},
3936
- ...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 } : {} },
3937
4213
  ...typeof feedback?.thumbsRating === "boolean" ? { feedback: { thumbsRating: feedback.thumbsRating } } : {}
3938
4214
  };
3939
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
+ }
3940
4223
  function projectedApiMdx(config, warnings) {
3941
4224
  const mdx = objectValue2(objectValue2(config.api)?.mdx);
3942
4225
  if (!mdx) return {};
@@ -4131,6 +4414,47 @@ function projectMintlifyNavigation(config, options = {}) {
4131
4414
  message: "Mintlify navigation could not be projected; generated navigation will be used."
4132
4415
  });
4133
4416
  }
4417
+ const versionOwner = languages.length > 0 ? languages.find((entry) => entry.default === true) ?? languages[0] : navigation;
4418
+ const globalNavigation = objectValue2(versionOwner.global) ?? objectValue2(navigation.global);
4419
+ const shortcuts = Array.isArray(globalNavigation?.anchors) ? globalNavigation.anchors.flatMap((value) => {
4420
+ const anchor = objectValue2(value);
4421
+ const label = typeof anchor?.anchor === "string" ? anchor.anchor.trim() : "";
4422
+ const href = projectedHref(anchor?.href);
4423
+ if (!label || !href) return [];
4424
+ return [{ label, href, ...iconName(anchor?.icon) ? { icon: iconName(anchor?.icon) } : {} }];
4425
+ }) : [];
4426
+ const rawVersions = Array.isArray(versionOwner.versions) ? versionOwner.versions.map(objectValue2).filter((value) => value !== null).sort((left, right) => Number(right.default === true) - Number(left.default === true)) : [];
4427
+ const versionNames = rawVersions.map((entry) => typeof entry.version === "string" ? entry.version.trim() : "").filter(Boolean);
4428
+ const annotateVersions = (items) => items.map((item) => {
4429
+ const version = versionNames.find((name) => item.tab.startsWith(`${name}: `));
4430
+ return version ? { ...item, version, displayLabel: item.tab.slice(version.length + 2) } : item;
4431
+ });
4432
+ const firstPage = (item) => {
4433
+ const visit = (entries) => {
4434
+ for (const entry of entries) {
4435
+ if (typeof entry === "string") return entry;
4436
+ const nested = visit(entry.pages);
4437
+ if (nested) return nested;
4438
+ }
4439
+ return void 0;
4440
+ };
4441
+ return visit([...item.pages ?? [], ...item.groups ?? []]);
4442
+ };
4443
+ const defaultVersion = rawVersions.find((entry) => entry.default === true) ?? rawVersions[0];
4444
+ const versions = rawVersions.length > 1 ? rawVersions.flatMap((entry) => {
4445
+ const label = typeof entry.version === "string" ? entry.version.trim() : "";
4446
+ if (!label) return [];
4447
+ const candidates = tabs.filter((item) => item.tab.startsWith(`${label}: `)).map(firstPage).filter((page) => Boolean(page));
4448
+ const landing = candidates.find((page) => page.startsWith(`${label}/`)) ?? candidates[0];
4449
+ if (!landing) return [];
4450
+ const firstSegment = landing.split("/")[0];
4451
+ const prefix = firstSegment === label ? label : "";
4452
+ return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {}, ...entry.hidden === true ? { hidden: true } : {} }];
4453
+ }) : [];
4454
+ if (versions.length > 1) {
4455
+ tabs = annotateVersions(tabs);
4456
+ for (const [locale, items] of Object.entries(localizedNavigation)) localizedNavigation[locale] = annotateVersions(items);
4457
+ }
4134
4458
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
4135
4459
  const redirect = objectValue2(value);
4136
4460
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -4155,9 +4479,13 @@ function projectMintlifyNavigation(config, options = {}) {
4155
4479
  return {
4156
4480
  docsConfig: {
4157
4481
  tabs,
4158
- ...projectionTrace.rootContainerKind === "dropdowns" ? { navigation: { display: "dropdown" } } : {},
4482
+ ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 ? { navigation: {
4483
+ ...projectionTrace.rootContainerKind === "dropdowns" ? { display: "dropdown" } : {},
4484
+ ...versions.length > 1 ? { versions } : {},
4485
+ ...shortcuts.length > 0 ? { shortcuts } : {}
4486
+ } } : {},
4159
4487
  ...projectedCompatibleConfig(config, warnings),
4160
- ...projectedApiMdx(config, warnings),
4488
+ ...projectedApi(config, warnings),
4161
4489
  ...i18n ? { i18n } : {},
4162
4490
  ...redirects.length > 0 ? { redirects } : {}
4163
4491
  },
@@ -4832,9 +5160,10 @@ function descriptorSort(left, right) {
4832
5160
  if (leftPosition !== rightPosition) return leftPosition - rightPosition;
4833
5161
  return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
4834
5162
  }
4835
- function registerDoc(docId, context) {
5163
+ function registerDoc(docId, context, sourceDirectory) {
4836
5164
  const key = normalizeDocId(docId);
4837
- const descriptor = context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
5165
+ const scopedKey = sourceDirectory && !docId.startsWith("/") ? normalizeDocId(posix2.join(sourceDirectory, key)) : void 0;
5166
+ const descriptor = (scopedKey ? context.descriptorByDocId.get(scopedKey) : void 0) ?? context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
4838
5167
  if (!descriptor) {
4839
5168
  context.warnings.push({
4840
5169
  code: "missing-page",
@@ -4846,10 +5175,10 @@ function registerDoc(docId, context) {
4846
5175
  context.referencedNavigationIds.add(descriptor.navigationId);
4847
5176
  return descriptor.navigationId;
4848
5177
  }
4849
- function generatedIndexPage(label, linkValue, context) {
5178
+ function generatedIndexPage(label, linkValue, context, sourceDirectory) {
4850
5179
  const link = objectValue3(linkValue);
4851
5180
  if (!link) return null;
4852
- if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context);
5181
+ if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context, sourceDirectory);
4853
5182
  if (link.type !== "generated-index") return null;
4854
5183
  const fallback = `category/${slugifySegment(label)}`;
4855
5184
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
@@ -5092,7 +5421,7 @@ function autogeneratedItems(dirName, context) {
5092
5421
  };
5093
5422
  }
5094
5423
  }
5095
- const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
5424
+ const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context, directory);
5096
5425
  if (landing && !pages2.includes(landing)) pages2.unshift(landing);
5097
5426
  return {
5098
5427
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
@@ -5926,6 +6255,7 @@ var FRONTMATTER_HEAD_BYTES = 65536;
5926
6255
  var MAX_WITHHELD_SCAN_BYTES = MAX_PAGE_BYTES * 8;
5927
6256
  var MAX_DROPPED_GATED_WARNINGS = 20;
5928
6257
  var MAX_ASSET_BYTES = 25e6;
6258
+ var MAX_REFERENCED_ASSET_BYTES = 1e8;
5929
6259
  var MAX_TOTAL_ASSET_BYTES = 5e8;
5930
6260
  var GIT_LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
5931
6261
  function listAssetPaths(paths) {
@@ -6804,12 +7134,22 @@ function mintlifyTopLevelApiReferences(config) {
6804
7134
  }
6805
7135
  return references;
6806
7136
  }
6807
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
7137
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, pages = []) {
6808
7138
  if (!mintlifyConfig) return [];
6809
7139
  const references = [
6810
7140
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
6811
7141
  ...mintlifyNavigationApiReferences(mintlifyConfig)
6812
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
+ }
6813
7153
  const seen = /* @__PURE__ */ new Set();
6814
7154
  const specs = [];
6815
7155
  for (const reference of references) {
@@ -6859,6 +7199,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6859
7199
  parentTab: reference.parentTab,
6860
7200
  icon: reference.icon,
6861
7201
  hidden: reference.hidden,
7202
+ ...reference.pageOnly ? { pageOnly: true } : {},
6862
7203
  sourcePath: match.relativePath,
6863
7204
  directory: reference.directory
6864
7205
  });
@@ -7186,18 +7527,18 @@ function isolateFences(snippet, source, start, end) {
7186
7527
  return `${fence.test(lines[0]) && /\S/.test(before) ? "\n" : ""}${snippet}${fence.test(lines[lines.length - 1]) && /\S/.test(after) ? "\n" : ""}`;
7187
7528
  }
7188
7529
  function snippetComponentBody(source, componentName) {
7189
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7530
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7190
7531
  for (const statement of parsed.statements) {
7191
- if (!ts3.isVariableStatement(statement)) continue;
7532
+ if (!ts4.isVariableStatement(statement)) continue;
7192
7533
  for (const declaration of statement.declarationList.declarations) {
7193
- if (!ts3.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7534
+ if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7194
7535
  const initializer = declaration.initializer;
7195
- if (!initializer || !ts3.isArrowFunction(initializer)) continue;
7196
- const returned = ts3.isBlock(initializer.body) ? initializer.body.statements.find(ts3.isReturnStatement)?.expression : initializer.body;
7536
+ if (!initializer || !ts4.isArrowFunction(initializer)) continue;
7537
+ const returned = ts4.isBlock(initializer.body) ? initializer.body.statements.find(ts4.isReturnStatement)?.expression : initializer.body;
7197
7538
  if (!returned) continue;
7198
7539
  let expression = returned;
7199
- while (ts3.isParenthesizedExpression(expression)) expression = expression.expression;
7200
- if (ts3.isJsxElement(expression) || ts3.isJsxSelfClosingElement(expression) || ts3.isJsxFragment(expression)) {
7540
+ while (ts4.isParenthesizedExpression(expression)) expression = expression.expression;
7541
+ if (ts4.isJsxElement(expression) || ts4.isJsxSelfClosingElement(expression) || ts4.isJsxFragment(expression)) {
7201
7542
  return expression.getText(parsed);
7202
7543
  }
7203
7544
  }
@@ -7205,31 +7546,31 @@ function snippetComponentBody(source, componentName) {
7205
7546
  return source;
7206
7547
  }
7207
7548
  function statefulSnippetDeclaration(source, componentName) {
7208
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7209
- const isEsm = (statement2) => ts3.isImportDeclaration(statement2) || (ts3.isVariableStatement(statement2) || ts3.isFunctionDeclaration(statement2) || ts3.isClassDeclaration(statement2)) && !!ts3.getModifiers(statement2)?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword);
7549
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7550
+ const isEsm = (statement2) => ts4.isImportDeclaration(statement2) || (ts4.isVariableStatement(statement2) || ts4.isFunctionDeclaration(statement2) || ts4.isClassDeclaration(statement2)) && !!ts4.getModifiers(statement2)?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword);
7210
7551
  if (parsed.statements.length === 0 || !parsed.statements.every(isEsm)) return null;
7211
- const statement = parsed.statements.find((candidate) => ts3.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts3.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
7212
- if (!statement || !ts3.isVariableStatement(statement)) return null;
7552
+ const statement = parsed.statements.find((candidate) => ts4.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts4.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
7553
+ if (!statement || !ts4.isVariableStatement(statement)) return null;
7213
7554
  const declaration = statement.declarationList.declarations[0];
7214
- if (!declaration.initializer || !ts3.isArrowFunction(declaration.initializer) || !ts3.isBlock(declaration.initializer.body)) return null;
7555
+ if (!declaration.initializer || !ts4.isArrowFunction(declaration.initializer) || !ts4.isBlock(declaration.initializer.body)) return null;
7215
7556
  const body = declaration.initializer.body.statements;
7216
- if (body.length < 2 || !body.some(ts3.isReturnStatement)) return null;
7557
+ if (body.length < 2 || !body.some(ts4.isReturnStatement)) return null;
7217
7558
  return source.trim();
7218
7559
  }
7219
7560
  function staticNamedSnippetValues(source) {
7220
7561
  const values = /* @__PURE__ */ new Map();
7221
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7562
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7222
7563
  for (const statement of parsed.statements) {
7223
- if (!ts3.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword)) continue;
7564
+ if (!ts4.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword)) continue;
7224
7565
  for (const declaration of statement.declarationList.declarations) {
7225
- if (!ts3.isIdentifier(declaration.name) || !declaration.initializer) continue;
7566
+ if (!ts4.isIdentifier(declaration.name) || !declaration.initializer) continue;
7226
7567
  const expression = declaration.initializer;
7227
- if (ts3.isStringLiteral(expression) || ts3.isNoSubstitutionTemplateLiteral(expression)) {
7568
+ if (ts4.isStringLiteral(expression) || ts4.isNoSubstitutionTemplateLiteral(expression)) {
7228
7569
  values.set(declaration.name.text, JSON.stringify(expression.text));
7229
- } else if (ts3.isNumericLiteral(expression)) {
7570
+ } else if (ts4.isNumericLiteral(expression)) {
7230
7571
  values.set(declaration.name.text, expression.text);
7231
- } else if (expression.kind === ts3.SyntaxKind.TrueKeyword || expression.kind === ts3.SyntaxKind.FalseKeyword) {
7232
- values.set(declaration.name.text, expression.kind === ts3.SyntaxKind.TrueKeyword ? "true" : "false");
7572
+ } else if (expression.kind === ts4.SyntaxKind.TrueKeyword || expression.kind === ts4.SyntaxKind.FalseKeyword) {
7573
+ values.set(declaration.name.text, expression.kind === ts4.SyntaxKind.TrueKeyword ? "true" : "false");
7233
7574
  }
7234
7575
  }
7235
7576
  }
@@ -7368,11 +7709,12 @@ function preserveLinkedAnchors(pages) {
7368
7709
  const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
7369
7710
  if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
7370
7711
  const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
7371
- return match ? { index, text: match[1] } : null;
7712
+ return match ? { index, text: match[1].replace(/\s*\{\/\*\s*#\S+?\s*\*\/\}$/, "") } : null;
7372
7713
  }).filter((entry) => entry !== null);
7373
7714
  const existing = /* @__PURE__ */ new Set();
7374
7715
  replaceOutsideCode(page.body, (text2) => {
7375
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]);
7376
7718
  return text2;
7377
7719
  });
7378
7720
  const candidateIndexes = /* @__PURE__ */ new Map();
@@ -7515,7 +7857,11 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7515
7857
  }
7516
7858
  if (!ASSET_EXTENSIONS.has(extname6(decodedPath).toLowerCase())) return null;
7517
7859
  try {
7518
- 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
+ }
7519
7865
  const realRoot = realpathSync2(siteRoot);
7520
7866
  const realCandidate = realpathSync2(candidate);
7521
7867
  const realRelative = relative5(realRoot, realCandidate);
@@ -7556,6 +7902,19 @@ function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced)
7556
7902
  });
7557
7903
  }
7558
7904
  var LOCAL_EXPORT_DECLARATION = /^\s*export\s+(?:const|let|var|function|class|async\s+function)\s+([A-Za-z_$][\w$]*)/;
7905
+ function replaceOutsideMdxComments(body, transform) {
7906
+ const comment = /\{\/\*[\s\S]*?\*\/\}/g;
7907
+ let cursor = 0;
7908
+ let output = "";
7909
+ for (const match of body.matchAll(comment)) {
7910
+ output += transform(body.slice(cursor, match.index)) + match[0];
7911
+ cursor = match.index + match[0].length;
7912
+ }
7913
+ return output + transform(body.slice(cursor));
7914
+ }
7915
+ function replaceOutsideCodeAndMdxComments(body, transform) {
7916
+ return replaceOutsideCode(body, (segment) => replaceOutsideMdxComments(segment, transform));
7917
+ }
7559
7918
  function locallyDeclaredNames(raw) {
7560
7919
  const names = /* @__PURE__ */ new Set();
7561
7920
  let codeFence = null;
@@ -7601,7 +7960,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7601
7960
  });
7602
7961
  return `{/* ${verdict.kind === "oversized" ? "Oversized" : "Unreadable"} content not inlined: ${shown} */}`;
7603
7962
  };
7604
- let withoutImports = replaceOutsideCode(raw, (source2) => source2.replace(
7963
+ let withoutImports = replaceOutsideCodeAndMdxComments(raw, (source2) => source2.replace(
7605
7964
  SNIPPET_IMPORT_PATTERN,
7606
7965
  (_statement, namedComponent, defaultComponent, sourcePath) => {
7607
7966
  const componentName = namedComponent ?? defaultComponent;
@@ -7628,6 +7987,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7628
7987
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7629
7988
  return "";
7630
7989
  } catch {
7990
+ let hasLiveUsage = false;
7991
+ replaceOutsideCodeAndMdxComments(raw, (segment) => {
7992
+ if (new RegExp(`<${componentName}(?:\\s|/?>)`).test(segment)) hasLiveUsage = true;
7993
+ return segment;
7994
+ });
7995
+ if (!hasLiveUsage) return "";
7631
7996
  warnings.push({
7632
7997
  code: "missing-page",
7633
7998
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
@@ -7638,7 +8003,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7638
8003
  }
7639
8004
  }
7640
8005
  ));
7641
- withoutImports = replaceOutsideCode(withoutImports, (source2) => source2.replace(
8006
+ withoutImports = replaceOutsideCodeAndMdxComments(withoutImports, (source2) => source2.replace(
7642
8007
  SNIPPET_VALUE_IMPORT_PATTERN,
7643
8008
  (statement, names, sourcePath) => {
7644
8009
  const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
@@ -7682,7 +8047,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7682
8047
  ));
7683
8048
  const localNames = locallyDeclaredNames(withoutImports);
7684
8049
  for (const [componentName, candidate] of globalAliases) {
7685
- if (snippets.has(componentName) || localNames.has(componentName) || gate?.(candidate)?.kind === "gated" || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
8050
+ let hasLiveUsage = false;
8051
+ replaceOutsideCodeAndMdxComments(withoutImports, (source2) => {
8052
+ if (new RegExp(`<${componentName}(?:\\s|/?>)`).test(source2)) hasLiveUsage = true;
8053
+ return source2;
8054
+ });
8055
+ if (snippets.has(componentName) || localNames.has(componentName) || gate?.(candidate)?.kind === "gated" || !hasLiveUsage) continue;
7686
8056
  const blocked = blockInline(candidate, relative5(repositoryRoot, candidate).replace(/\\/g, "/"));
7687
8057
  if (blocked) {
7688
8058
  snippets.set(componentName, blocked);
@@ -7705,9 +8075,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7705
8075
  let result = replaceOutsideCodeAndComments(withoutImports, (segment) => {
7706
8076
  for (const [componentName, snippet] of snippets) {
7707
8077
  segment = segment.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (tag, attributes, offset, source2) => {
7708
- return isolateFences(interpolateSnippet(snippet, attributes), source2, offset, offset + tag.length);
8078
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes)), source2, offset, offset + tag.length);
7709
8079
  }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (tag, attributes, children, offset, source2) => {
7710
- return isolateFences(interpolateSnippet(snippet, attributes, children), source2, offset, offset + tag.length);
8080
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes, children)), source2, offset, offset + tag.length);
7711
8081
  });
7712
8082
  }
7713
8083
  return segment.replace(SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
@@ -7726,7 +8096,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7726
8096
  if (!candidate) throw new Error("file not found");
7727
8097
  const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7728
8098
  if (blocked) return blocked;
7729
- return isolateFences(inlineMdxSnippets(
8099
+ return isolateFences(closeOpenCodeFence(inlineMdxSnippets(
7730
8100
  withoutFrontmatter(readFileSync5(candidate, "utf8")),
7731
8101
  candidate,
7732
8102
  repositoryRoot,
@@ -7735,7 +8105,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7735
8105
  siteRoot,
7736
8106
  globalAliases,
7737
8107
  gate
7738
- ), source2, offset, offset + tag.length);
8108
+ )), source2, offset, offset + tag.length);
7739
8109
  } catch {
7740
8110
  warnings.push({
7741
8111
  code: "missing-page",
@@ -7940,7 +8310,7 @@ function migrateRepository(options) {
7940
8310
  const sourceBudget = options.maxSourceFiles ?? MAX_SOURCE_FILES;
7941
8311
  const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank, sourceBudget);
7942
8312
  const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
7943
- const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > sourceBudget;
8313
+ const isUnreferencedSnippetModule = (file) => file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) && isDocumentationExtension(file.relativePath) && !referenceOrder.has(normalizedReferenceKey(file.relativePath));
7944
8314
  const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes, sourceBudget) : mintignoreFilteredFiles;
7945
8315
  if (platform === "fern" && fernProjectRoot) {
7946
8316
  const discovered2 = new Set(files.map((file) => file.relativePath));
@@ -8019,6 +8389,10 @@ function migrateRepository(options) {
8019
8389
  }
8020
8390
  for (const file of mintignoreFilteredFiles) {
8021
8391
  if (keptFiles.has(file) || !isDocFile(file)) continue;
8392
+ if (isUnreferencedSnippetModule(file)) {
8393
+ if (classifyPageGate(file).reason) withheldDocFiles.push(file);
8394
+ continue;
8395
+ }
8022
8396
  droppedPageCount++;
8023
8397
  if (!isGateCandidate(file)) {
8024
8398
  const snippetReason = classifyPageGate(file).reason;
@@ -8241,7 +8615,11 @@ function migrateRepository(options) {
8241
8615
  const publishedText = trackPublishedRefs ? raw : void 0;
8242
8616
  const pageAssetReferences = [];
8243
8617
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8244
- 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
+ );
8245
8623
  }
8246
8624
  if (componentMigrator) {
8247
8625
  const warningsBeforeTransform = warnings.length;
@@ -8253,13 +8631,7 @@ function migrateRepository(options) {
8253
8631
  }
8254
8632
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8255
8633
  const protectedMath = protectMathBlocks(raw);
8256
- if (protectedMath.converted) {
8257
- warnings.push({
8258
- code: "unsupported-config",
8259
- message: "Math (KaTeX '$$...$$') has no renderer in Thally yet; it was kept as a fenced code block instead of being dropped.",
8260
- source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/")
8261
- });
8262
- } else if (protectedMath.guardTriggered) {
8634
+ if (protectedMath.guardTriggered) {
8263
8635
  warnings.push({
8264
8636
  code: "unsupported-config",
8265
8637
  message: "A suspected math span looked like it needed converting, but doing so broke the page; the page was kept as originally authored instead.",
@@ -8351,7 +8723,7 @@ function migrateRepository(options) {
8351
8723
  page.body = replaceUnknownComponents(page.body, (name) => {
8352
8724
  warnings.push({
8353
8725
  code: "unsupported-config",
8354
- message: `Component <${name}> has no equivalent in Thally and wasn't found on this page; it was replaced with a plain <div> (or removed, if self-closing) so the page still builds. Add a matching component or edit the page.`,
8726
+ message: `Unresolved MDX component <${name}> was replaced with a plain <div> (or removed, if self-closing) so the page still builds. It may be a site-authored or third-party implementation; review its source before adding a Thally equivalent.`,
8355
8727
  source: file.relativePath
8356
8728
  });
8357
8729
  });
@@ -8478,9 +8850,35 @@ function migrateRepository(options) {
8478
8850
  const source = /^src\/mdx\/migrated\/[^/]+\/source\/(.+)$/.exec(file.path)?.[1];
8479
8851
  return source ? [resolvePath(repositoryDir, source)] : [];
8480
8852
  }));
8853
+ const rootStylesheetPath = platform === "mintlify" && mintlifyProjectRoot ? resolveWithin(mintlifyProjectRoot, "style.css") : void 0;
8854
+ const hasRootStylesheet = Boolean(rootStylesheetPath && existsSync5(rootStylesheetPath) && lstatSync5(rootStylesheetPath).isFile() && !lstatSync5(rootStylesheetPath).isSymbolicLink());
8855
+ let rootStyleProjection;
8856
+ let rootNavbarButtons = [];
8857
+ let rootStyleProblem;
8858
+ if (hasRootStylesheet && rootStylesheetPath && lstatSync5(rootStylesheetPath).size <= MAX_ASSET_BYTES) {
8859
+ const css = readFileSync5(rootStylesheetPath, "utf8");
8860
+ if (/@import\b|url\s*\(\s*['"]?\s*(?:javascript:|file:)/i.test(css)) rootStyleProblem = "unsafe";
8861
+ else {
8862
+ try {
8863
+ rootNavbarButtons = navbarLinkButtons(css);
8864
+ rootStyleProjection = projectAuthoredStyles(css, [
8865
+ ...pages.map((page) => page.body),
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"))
8869
+ ]);
8870
+ } catch {
8871
+ rootStyleProblem = "parse";
8872
+ }
8873
+ }
8874
+ }
8875
+ if (trackPublishedRefs && rootStyleProjection?.css) {
8876
+ addPathReferences(rootStyleProjection.css, "", publishedExact, publishedLoose);
8877
+ }
8481
8878
  const siteAssetPaths = /* @__PURE__ */ new Map();
8482
8879
  const fontAssetByRelative = /* @__PURE__ */ new Map();
8483
8880
  for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
8881
+ if (rootStylesheetPath && file.absolutePath === rootStylesheetPath) continue;
8484
8882
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
8485
8883
  const siteKind = platform !== "mintlify" ? void 0 : fontRelativePaths.has(file.relativePath) ? "font" : isMintlifyServedScriptOrStyle(file.relativePath) && !importedComponentSources.has(file.absolutePath) ? extname6(file.relativePath).toLowerCase() === ".css" ? "style" : "script" : void 0;
8486
8884
  if (!siteKind && !ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase())) continue;
@@ -8526,7 +8924,8 @@ function migrateRepository(options) {
8526
8924
  const lfsPointerAssets = [];
8527
8925
  const ambiguouslyNamedAssets = [];
8528
8926
  for (const { file, assetPath, size } of orderedAssetCandidates) {
8529
- if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
8927
+ const individualLimit = referencedAssetPaths.has(assetPath) ? MAX_REFERENCED_ASSET_BYTES : MAX_ASSET_BYTES;
8928
+ if (size > individualLimit || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
8530
8929
  overBudgetAssets.push(file.relativePath);
8531
8930
  if (referencedAssetPaths.has(assetPath)) overBudgetReferenced++;
8532
8931
  continue;
@@ -8568,7 +8967,7 @@ function migrateRepository(options) {
8568
8967
  for (const entry of copiedSite.filter((item) => item.kind === "style")) {
8569
8968
  warnings.push({
8570
8969
  code: "unsupported-config",
8571
- message: `Migration gap: custom stylesheet ${entry.path} is NOT applied. It was copied to public/${entry.path} but no page loads it, Thally cannot load a stylesheet from docs.json, and Mintlify-specific selectors will not match Thally's markup. Port the styles you need by hand into src/app/globals.css.`,
8970
+ message: `Migration gap: custom stylesheet ${entry.path} is NOT applied. It was copied to public/${entry.path} but was not declared in docs.json stylesheets; its selectors may target Mintlify's markup. Review and port its authored rules into a Thally stylesheet.`,
8572
8971
  source: entry.path
8573
8972
  });
8574
8973
  }
@@ -8586,7 +8985,7 @@ function migrateRepository(options) {
8586
8985
  if (overBudgetAssets.length > 0) {
8587
8986
  warnings.push({
8588
8987
  code: "limit-reached",
8589
- message: `${overBudgetAssets.length} asset file${overBudgetAssets.length === 1 ? " was" : "s were"} not copied because files over ${MAX_ASSET_BYTES / 1e6} MB, or beyond ${MAX_TOTAL_ASSET_BYTES / 1e6} MB in total, are skipped: ${listAssetPaths(overBudgetAssets)}. ` + (overBudgetReferenced > 0 ? `${overBudgetReferenced} of them ${overBudgetReferenced === 1 ? "is" : "are"} used by pages, so those images will be broken until you copy ${overBudgetAssets.length === 1 ? "it" : "them"}. ` : "") + `Copy ${overBudgetAssets.length === 1 ? "it" : "them"} into public/ manually.`
8988
+ message: `${overBudgetAssets.length} asset file${overBudgetAssets.length === 1 ? " was" : "s were"} not copied because unreferenced files over ${MAX_ASSET_BYTES / 1e6} MB, referenced files over ${MAX_REFERENCED_ASSET_BYTES / 1e6} MB, or files beyond ${MAX_TOTAL_ASSET_BYTES / 1e6} MB in total, are skipped: ${listAssetPaths(overBudgetAssets)}. ` + (overBudgetReferenced > 0 ? `${overBudgetReferenced} of them ${overBudgetReferenced === 1 ? "is" : "are"} used by pages, so those images will be broken until you copy ${overBudgetAssets.length === 1 ? "it" : "them"}. ` : "") + `Copy ${overBudgetAssets.length === 1 ? "it" : "them"} into public/ manually.`
8590
8989
  });
8591
8990
  }
8592
8991
  if (lfsPointerAssets.length > 0) {
@@ -8595,7 +8994,43 @@ function migrateRepository(options) {
8595
8994
  message: `${lfsPointerAssets.length} asset file${lfsPointerAssets.length === 1 ? " is a Git LFS pointer" : "s are Git LFS pointers"}, not real content, and ${lfsPointerAssets.length === 1 ? "was" : "were"} not copied (Git LFS was skipped during clone because the host has no git-lfs binary): ${listAssetPaths(lfsPointerAssets)}. Install git-lfs and re-run the migration, or copy the real files into public/ manually.`
8596
8995
  });
8597
8996
  }
8598
- if (!discoveryBudgetApplied && files.length >= sourceBudget) {
8997
+ if (hasRootStylesheet && rootStylesheetPath && mintlifyProjectRoot) {
8998
+ const size = lstatSync5(rootStylesheetPath).size;
8999
+ if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
9000
+ warnings.push({ code: "limit-reached", message: "Mintlify root style.css exceeded the asset budget and was not imported.", source: "style.css" });
9001
+ } else if (rootStyleProblem === "unsafe") {
9002
+ warnings.push({ code: "unsupported-config", message: "Mintlify root style.css contains a CSS import or unsafe URL; review and import it manually.", source: "style.css" });
9003
+ } else if (rootStyleProblem === "parse" || !rootStyleProjection) {
9004
+ warnings.push({ code: "unsupported-config", message: "Mintlify root style.css could not be parsed and was not imported.", source: "style.css" });
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
+ }
9018
+ if (rootStyleProjection.omittedSelectors > 0) warnings.push({
9019
+ code: "unsupported-config",
9020
+ source: "style.css",
9021
+ message: `Skipped ${rootStyleProjection.omittedSelectors} stylesheet selector${rootStyleProjection.omittedSelectors === 1 ? "" : "s"} that target platform-owned or unreferenced markup. Recreate any intentional shell customization with Thally configuration.`
9022
+ });
9023
+ if (rootStyleProjection.css.trim()) {
9024
+ const identity = createHash2("sha256").update(componentSourceIdentity(options.sourceUrl, repositoryDir, mintlifyProjectRoot)).digest("hex").slice(0, 12);
9025
+ const assetPath = `migrated/${identity}/style.css`;
9026
+ const content = Buffer.from(rootStyleProjection.css);
9027
+ assets.push({ path: assetPath, content });
9028
+ docsConfig = { ...docsConfig, stylesheets: [...docsConfig.stylesheets ?? [], `/${assetPath}`] };
9029
+ totalAssetBytes += content.length;
9030
+ }
9031
+ }
9032
+ }
9033
+ if (!discoveryRank && files.length >= sourceBudget) {
8599
9034
  warnings.push({ code: "limit-reached", message: `Stopped scanning after ${sourceBudget} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.` });
8600
9035
  }
8601
9036
  if (platform === "docusaurus") {
@@ -8730,7 +9165,9 @@ function migrateRepository(options) {
8730
9165
  source: path
8731
9166
  });
8732
9167
  };
8733
- 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);
8734
9171
  for (const spec of resolvedSpecs) {
8735
9172
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
8736
9173
  assets.push(specAsset(spec.filename, spec.content));
@@ -8763,6 +9200,14 @@ function migrateRepository(options) {
8763
9200
  }
8764
9201
  }
8765
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
+ }
8766
9211
  rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
8767
9212
  }
8768
9213
  if (platform === "mintlify") {
@@ -9178,6 +9623,8 @@ function renderPage(bundle, page) {
9178
9623
  "---",
9179
9624
  `title: ${yamlString(page.title)}`,
9180
9625
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
9626
+ page.icon ? `icon: ${yamlString(page.icon)}` : null,
9627
+ page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
9181
9628
  `description: ${yamlString(page.description)}`,
9182
9629
  page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
9183
9630
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
@@ -9185,9 +9632,11 @@ function renderPage(bundle, page) {
9185
9632
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
9186
9633
  page.hidden ? "hidden: true" : null,
9187
9634
  page.noindex ? "noindex: true" : null,
9635
+ ...Object.entries(page.meta ?? {}).map(([field, value]) => `${field}: ${yamlString(value)}`),
9188
9636
  page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
9189
9637
  page.api ? `api: ${yamlString(page.api)}` : null,
9190
9638
  page.authMethod ? `authMethod: ${yamlString(page.authMethod)}` : null,
9639
+ page.playground ? `playground: ${yamlString(page.playground)}` : null,
9191
9640
  bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
9192
9641
  "---",
9193
9642
  "",
@@ -9268,6 +9717,7 @@ function mergeMigrationConfig(existing, incoming) {
9268
9717
  // Collection presentation belongs to the imported information
9269
9718
  // architecture; starter defaults must not coerce source dropdowns to tabs.
9270
9719
  navigation: incoming.navigation ?? existing.navigation,
9720
+ ...existing.stylesheets?.length || incoming.stylesheets?.length ? { stylesheets: [.../* @__PURE__ */ new Set([...existing.stylesheets ?? [], ...incoming.stylesheets ?? []])] } : {},
9271
9721
  ...i18n ? { i18n } : {},
9272
9722
  // Existing providers win per provider; imported ones fill the gaps.
9273
9723
  // A malformed (string/array) existing value must not spread into index keys.
@@ -9614,7 +10064,7 @@ function embeddedMintlifyConfig(body) {
9614
10064
  function plainDescription(value) {
9615
10065
  return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_>#-]/g, "").replace(/\s+/g, " ").trim().slice(0, 240);
9616
10066
  }
9617
- function withoutFencedCode(body) {
10067
+ function withoutFencedCode2(body) {
9618
10068
  let fenceCharacter = "";
9619
10069
  let fenceLength = 0;
9620
10070
  return body.split("\n").map((line) => {
@@ -9924,7 +10374,7 @@ function sanitizeRemoteMarkdown(body) {
9924
10374
  }).join("\n");
9925
10375
  const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
9926
10376
  if (urlSafeMarkdown === null) return null;
9927
- const executable = withoutFencedCode(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
10377
+ const executable = withoutFencedCode2(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
9928
10378
  if (/^\s*(?:import|export)\s/m.test(executable)) return null;
9929
10379
  let hasUnsafeExpression = false;
9930
10380
  const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
@@ -10874,7 +11324,7 @@ async function migrateUrl(options) {
10874
11324
  }
10875
11325
 
10876
11326
  // src/remote-api.ts
10877
- import { createHash as createHash2 } from "crypto";
11327
+ import { createHash as createHash3 } from "crypto";
10878
11328
  import { lookup } from "dns/promises";
10879
11329
  import { request } from "https";
10880
11330
  import { isIP } from "net";
@@ -11034,7 +11484,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
11034
11484
  const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
11035
11485
  const body = await downloadSpec(reference.url, fetcher);
11036
11486
  const operations = parseOpenApi(body, extension);
11037
- const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11487
+ const filename = `openapi-${createHash3("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11038
11488
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
11039
11489
  for (const page of pages) {
11040
11490
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;