@thallylabs/migrate 0.1.2 → 0.2.1

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,53 +1,18 @@
1
1
  // src/repository.ts
2
2
  import { spawn } from "child_process";
3
3
  import {
4
- existsSync as existsSync2,
5
- lstatSync,
6
- readFileSync as readFileSync2,
4
+ existsSync as existsSync3,
5
+ lstatSync as lstatSync2,
6
+ readFileSync as readFileSync3,
7
7
  readdirSync
8
8
  } from "fs";
9
- import { basename, dirname as dirname2, extname as extname2, relative as relative3 } from "path";
9
+ import { basename, dirname as dirname2, extname as extname3, relative as relative3, resolve as resolvePath } from "path";
10
10
 
11
- // src/mdx.ts
12
- import matter from "gray-matter";
13
- function titleFromId(id) {
14
- const value = id.split("/").at(-1) ?? id;
15
- if (value === "introduction") return "Introduction";
16
- return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
17
- }
18
- function firstParagraph(content) {
19
- for (const block of content.split(/\n\s*\n/)) {
20
- const value = block.replace(/\s+/g, " ").trim();
21
- if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
22
- return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
23
- }
24
- return "";
25
- }
26
- function normalizeMdx(body) {
27
- return body.replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
28
- }
29
- function parseMarkdownPage(input) {
30
- const parsed = matter(input.raw);
31
- const body = normalizeMdx(parsed.content).trim();
32
- const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
33
- const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(input.navigationId ?? input.id);
34
- const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : firstParagraph(body);
35
- return {
36
- id: input.id,
37
- navigationId: input.navigationId ?? input.id,
38
- locale: input.locale,
39
- title,
40
- description,
41
- keywords,
42
- openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
43
- body,
44
- source: input.source
45
- };
46
- }
47
-
48
- // src/navigation.ts
49
- import { existsSync, readFileSync } from "fs";
50
- import { dirname, extname, relative as relative2 } from "path";
11
+ // src/docusaurus.ts
12
+ import { existsSync, lstatSync, readFileSync } from "fs";
13
+ import { extname, posix } from "path";
14
+ import JSON5 from "json5";
15
+ import { parse as parseYaml } from "yaml";
51
16
 
52
17
  // src/path.ts
53
18
  import { isAbsolute, relative, resolve, sep } from "path";
@@ -92,7 +57,564 @@ function normalizeAssetPath(value) {
92
57
  return segments.join("/");
93
58
  }
94
59
 
60
+ // src/docusaurus.ts
61
+ var MAX_CONFIG_BYTES = 1e6;
62
+ var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
63
+ var SIDEBAR_FILENAMES = [
64
+ "sidebars.json",
65
+ "sidebars.js",
66
+ "sidebars.cjs",
67
+ "sidebars.mjs",
68
+ "sidebars.ts"
69
+ ];
70
+ function objectValue(value) {
71
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
72
+ }
73
+ function stripNumberPrefix(value) {
74
+ return value.replace(/^\d+[-_]+/, "");
75
+ }
76
+ function normalizeDocId(value) {
77
+ return value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").split("/").filter(Boolean).map(stripNumberPrefix).join("/");
78
+ }
79
+ function docusaurusSlugifySegment(value) {
80
+ let decoded = value;
81
+ try {
82
+ decoded = decodeURIComponent(value);
83
+ } catch {
84
+ }
85
+ return decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
86
+ }
87
+ function docusaurusRouteId(value) {
88
+ const segments = normalizeDocId(value).split("/").map(docusaurusSlugifySegment).filter(Boolean);
89
+ return segments.join("/") || "introduction";
90
+ }
91
+ function defaultDocusaurusRouteId(value) {
92
+ const segments = normalizeDocId(value).split("/").filter(Boolean);
93
+ if (/^(?:index|readme)$/i.test(segments.at(-1) ?? "")) segments.pop();
94
+ const normalized = segments.map(docusaurusSlugifySegment).filter(Boolean);
95
+ return normalized.join("/") || "introduction";
96
+ }
97
+ function storageIdForRoute(routeId) {
98
+ return /(?:^|\/)(?:index|readme)$/i.test(routeId) ? `${routeId}/index` : routeId;
99
+ }
100
+ function titleCase(value) {
101
+ return stripNumberPrefix(value).replace(/sidebar$/i, "").split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
102
+ }
103
+ function routeFromSlug(slug, fallback, sourceDirectory) {
104
+ if (slug === "/") return "introduction";
105
+ if (slug.startsWith("/")) return docusaurusRouteId(slug);
106
+ const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix.dirname(fallback));
107
+ const resolved = posix.normalize(posix.join(fallbackDirectory, slug));
108
+ if (resolved === ".." || resolved.startsWith("../")) return null;
109
+ return docusaurusRouteId(resolved);
110
+ }
111
+ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
112
+ const sourceDocId = normalizeDocId(sourcePath);
113
+ const sourceDirectory = posix.dirname(sourceDocId);
114
+ const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
115
+ const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix.join(sourceDirectory, configuredId)) : sourceDocId;
116
+ const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
117
+ const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
118
+ const navigationId = configuredSlug ? routeFromSlug(
119
+ configuredSlug,
120
+ defaultNavigationId,
121
+ sourceDirectory === "." ? "" : sourceDirectory
122
+ ) ?? defaultNavigationId : defaultNavigationId;
123
+ const position = typeof frontmatter.sidebar_position === "number" && Number.isFinite(frontmatter.sidebar_position) ? frontmatter.sidebar_position : void 0;
124
+ return {
125
+ identity: { id: storageIdForRoute(navigationId), navigationId },
126
+ descriptor: {
127
+ sourcePath: sourcePath.replace(/\\/g, "/"),
128
+ docId,
129
+ navigationId,
130
+ ...position === void 0 ? {} : { sidebarPosition: position }
131
+ }
132
+ };
133
+ }
134
+ function sourceReferenceKey(value) {
135
+ return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
136
+ }
137
+ function rewriteDocusaurusLinks(body, current, descriptors) {
138
+ const routes = /* @__PURE__ */ new Map();
139
+ for (const descriptor of descriptors) {
140
+ const sourceKey = sourceReferenceKey(descriptor.sourcePath);
141
+ routes.set(sourceKey, descriptor.navigationId);
142
+ routes.set(sourceReferenceKey(descriptor.docId), descriptor.navigationId);
143
+ routes.set(sourceReferenceKey(descriptor.navigationId), descriptor.navigationId);
144
+ const sourceRoute = pageIdFromReference(sourceKey);
145
+ if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
146
+ }
147
+ const currentDirectory = posix.dirname(sourceReferenceKey(current.sourcePath));
148
+ function rewriteTarget(target) {
149
+ if (!target || target.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
150
+ const suffixIndex = target.search(/[?#]/);
151
+ const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
152
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
153
+ const candidates = path.startsWith("/") ? [sourceReferenceKey(path), sourceReferenceKey(path).replace(/^docs\//, "")] : [
154
+ sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
155
+ sourceReferenceKey(path)
156
+ ];
157
+ const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
158
+ if (!route) return target;
159
+ return `${route === "introduction" ? "/" : `/${route}`}${suffix}`;
160
+ }
161
+ let codeFence = null;
162
+ return body.split("\n").map((line) => {
163
+ const fence = line.match(/^\s*(`{3,}|~{3,})/);
164
+ if (fence) {
165
+ if (!codeFence) codeFence = fence[1][0];
166
+ else if (fence[1][0] === codeFence) codeFence = null;
167
+ return line;
168
+ }
169
+ if (codeFence) return line;
170
+ return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`);
171
+ }).join("\n");
172
+ }
173
+ function readBoundedText(path) {
174
+ if (lstatSync(path).size > MAX_CONFIG_BYTES) {
175
+ throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
176
+ }
177
+ return readFileSync(path, "utf8");
178
+ }
179
+ function matchingObjectLiteral(source, start) {
180
+ let depth = 0;
181
+ let quote = null;
182
+ let isEscaped = false;
183
+ let lineComment = false;
184
+ let blockComment = false;
185
+ let objectStart = -1;
186
+ for (let index = start; index < source.length; index++) {
187
+ const char = source[index];
188
+ const next = source[index + 1];
189
+ if (lineComment) {
190
+ if (char === "\n") lineComment = false;
191
+ continue;
192
+ }
193
+ if (blockComment) {
194
+ if (char === "*" && next === "/") {
195
+ blockComment = false;
196
+ index++;
197
+ }
198
+ continue;
199
+ }
200
+ if (quote) {
201
+ if (isEscaped) isEscaped = false;
202
+ else if (char === "\\") isEscaped = true;
203
+ else if (char === quote) quote = null;
204
+ continue;
205
+ }
206
+ if (char === "/" && next === "/") {
207
+ lineComment = true;
208
+ index++;
209
+ continue;
210
+ }
211
+ if (char === "/" && next === "*") {
212
+ blockComment = true;
213
+ index++;
214
+ continue;
215
+ }
216
+ if (char === '"' || char === "'" || char === "`") {
217
+ quote = char;
218
+ continue;
219
+ }
220
+ if (char === "{") {
221
+ if (objectStart < 0) objectStart = index;
222
+ depth++;
223
+ } else if (char === "}" && objectStart >= 0) {
224
+ depth--;
225
+ if (depth === 0) return source.slice(objectStart, index + 1);
226
+ } else if (objectStart < 0 && !/\s/.test(char)) {
227
+ return null;
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+ function parseStaticSidebarModule(source) {
233
+ const bindings = /* @__PURE__ */ new Map();
234
+ const normalizedSource = replaceExternalFbContent(source);
235
+ const parseFailures = [];
236
+ const assignmentPatterns = [
237
+ /\bmodule\.exports\s*=\s*/g,
238
+ /\bexport\s+default\s*/g,
239
+ /\b(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:\s*:\s*[^=;]+)?\s*=\s*/g
240
+ ];
241
+ const candidates = assignmentPatterns.flatMap((pattern) => [...normalizedSource.matchAll(pattern)]).sort((left, right) => (left.index ?? 0) - (right.index ?? 0));
242
+ for (const candidate of candidates) {
243
+ const literal = matchingObjectLiteral(normalizedSource, (candidate.index ?? 0) + candidate[0].length);
244
+ if (!literal) continue;
245
+ try {
246
+ const substituted = [...bindings].reduce(
247
+ (value, [name, binding]) => value.replace(
248
+ new RegExp(`(:\\s*)${name}\\b`, "g"),
249
+ (_match, prefix) => `${prefix}${JSON.stringify(binding)}`
250
+ ),
251
+ literal
252
+ );
253
+ const parsed = JSON5.parse(substituted);
254
+ const object = objectValue(parsed);
255
+ if (!object) continue;
256
+ const bindingName = candidate[0].match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/)?.[1];
257
+ if (bindingName) {
258
+ bindings.set(bindingName, object);
259
+ continue;
260
+ }
261
+ return object;
262
+ } catch (error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ const line = Number(message.match(/at (\d+):/)?.[1] ?? 0);
265
+ const context = line > 0 ? literal.split("\n").slice(Math.max(0, line - 2), line + 1).join(" ") : "";
266
+ parseFailures.push(`${message}${context ? ` near ${context}` : ""}`);
267
+ }
268
+ }
269
+ const exportedBinding = normalizedSource.match(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\b/)?.[1] ?? normalizedSource.match(/\bmodule\.exports\s*=\s*([A-Za-z_$][\w$]*)\b/)?.[1];
270
+ if (exportedBinding && bindings.has(exportedBinding)) return bindings.get(exportedBinding);
271
+ throw new Error(`Sidebar config is executable or outside the supported data-only syntax.${parseFailures[0] ? ` ${parseFailures[0]}` : ""}`);
272
+ }
273
+ function replaceExternalFbContent(source) {
274
+ const marker = "...fbContent(";
275
+ let result = source;
276
+ let searchFrom = 0;
277
+ while (true) {
278
+ const start = result.indexOf(marker, searchFrom);
279
+ if (start < 0) return result.replace(/,\s*,/g, ",");
280
+ const objectStart = result.indexOf("{", start + marker.length);
281
+ if (objectStart < 0) return result;
282
+ const objectLiteral = matchingObjectLiteral(result, objectStart);
283
+ if (!objectLiteral) {
284
+ searchFrom = start + marker.length;
285
+ continue;
286
+ }
287
+ const objectEnd = objectStart + objectLiteral.length;
288
+ const close = result.indexOf(")", objectEnd);
289
+ if (close < 0) return result;
290
+ const externalMatch = /\bexternal\s*:\s*/g.exec(objectLiteral);
291
+ let replacement = "";
292
+ if (externalMatch) {
293
+ const arrayStart = objectLiteral.indexOf("[", externalMatch.index + externalMatch[0].length);
294
+ if (arrayStart >= 0) {
295
+ const array = matchingArrayLiteral(objectLiteral, arrayStart);
296
+ if (array) replacement = array.slice(1, -1);
297
+ }
298
+ }
299
+ let replaceEnd = close + 1;
300
+ if (!replacement) {
301
+ const trailingComma = result.slice(replaceEnd).match(/^\s*,/);
302
+ if (trailingComma) replaceEnd += trailingComma[0].length;
303
+ }
304
+ result = `${result.slice(0, start)}${replacement}${result.slice(replaceEnd)}`;
305
+ searchFrom = start + replacement.length;
306
+ }
307
+ }
308
+ function matchingArrayLiteral(source, start) {
309
+ let depth = 0;
310
+ let quote = "";
311
+ let isEscaped = false;
312
+ for (let index = start; index < source.length; index++) {
313
+ const character = source[index];
314
+ if (quote) {
315
+ if (isEscaped) isEscaped = false;
316
+ else if (character === "\\") isEscaped = true;
317
+ else if (character === quote) quote = "";
318
+ continue;
319
+ }
320
+ if (character === '"' || character === "'" || character === "`") {
321
+ quote = character;
322
+ continue;
323
+ }
324
+ if (character === "[") depth++;
325
+ if (character === "]" && --depth === 0) return source.slice(start, index + 1);
326
+ }
327
+ return null;
328
+ }
329
+ function configuredSidebarPath(repositoryRoot) {
330
+ for (const filename of ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"]) {
331
+ const path = resolveWithin(repositoryRoot, filename);
332
+ if (!existsSync(path)) continue;
333
+ const source = readBoundedText(path);
334
+ const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
335
+ if (!match) continue;
336
+ const candidate = match[2].replace(/^\.\//, "");
337
+ const resolved = resolveWithin(repositoryRoot, candidate);
338
+ if (existsSync(resolved) && lstatSync(resolved).isFile()) return candidate;
339
+ }
340
+ return null;
341
+ }
342
+ function readDocusaurusSidebars(repositoryRoot) {
343
+ const configured = configuredSidebarPath(repositoryRoot);
344
+ const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync(resolveWithin(repositoryRoot, candidate)));
345
+ if (!sourcePath) return null;
346
+ const absolutePath = resolveWithin(repositoryRoot, sourcePath);
347
+ const source = readBoundedText(absolutePath);
348
+ const parsed = extname(sourcePath).toLowerCase() === ".json" ? objectValue(JSON5.parse(source)) : parseStaticSidebarModule(source);
349
+ if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
350
+ return { config: parsed, sourcePath };
351
+ }
352
+ function readCategoryMetadata(contentRoot, directory) {
353
+ for (const filename of CATEGORY_FILENAMES) {
354
+ const path = resolveWithin(contentRoot, posix.join(directory, filename));
355
+ if (!existsSync(path) || !lstatSync(path).isFile()) continue;
356
+ try {
357
+ const raw = readBoundedText(path);
358
+ const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml(raw);
359
+ const object = objectValue(parsed);
360
+ if (!object) return {};
361
+ return {
362
+ ...typeof object.label === "string" ? { label: object.label } : {},
363
+ ...typeof object.position === "number" ? { position: object.position } : {},
364
+ ..."link" in object ? { link: object.link } : {}
365
+ };
366
+ } catch {
367
+ return {};
368
+ }
369
+ }
370
+ return {};
371
+ }
372
+ function descriptorSort(left, right) {
373
+ const leftPosition = left.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
374
+ const rightPosition = right.sidebarPosition ?? Number.MAX_SAFE_INTEGER;
375
+ if (leftPosition !== rightPosition) return leftPosition - rightPosition;
376
+ return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
377
+ }
378
+ function registerDoc(docId, context) {
379
+ const key = normalizeDocId(docId);
380
+ const descriptor = context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
381
+ if (!descriptor) {
382
+ context.warnings.push({
383
+ code: "missing-page",
384
+ message: "A Docusaurus sidebar document did not resolve to an imported page.",
385
+ source: docId
386
+ });
387
+ return null;
388
+ }
389
+ context.referencedNavigationIds.add(descriptor.navigationId);
390
+ return descriptor.navigationId;
391
+ }
392
+ function generatedIndexPage(label, linkValue, context) {
393
+ const link = objectValue(linkValue);
394
+ if (!link) return null;
395
+ if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context);
396
+ if (link.type !== "generated-index") return null;
397
+ const fallback = `category/${slugifySegment(label)}`;
398
+ const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
399
+ const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
400
+ if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
401
+ const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
402
+ context.generatedPages.push({
403
+ id: storageIdForRoute(navigationId),
404
+ navigationId,
405
+ title: typeof link.title === "string" ? link.title : label,
406
+ description,
407
+ keywords: Array.isArray(link.keywords) ? link.keywords.filter((value) => typeof value === "string") : [],
408
+ body: description,
409
+ source: `${context.sourceUrl}#${context.sidebarSource}`
410
+ });
411
+ context.generatedIds.add(navigationId);
412
+ }
413
+ context.referencedNavigationIds.add(navigationId);
414
+ return navigationId;
415
+ }
416
+ function autogeneratedItems(dirName, context) {
417
+ const normalizedDir = dirName === "." ? "" : normalizeDocId(dirName);
418
+ const inDirectory = context.descriptors.filter((descriptor) => {
419
+ const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
420
+ return !normalizedDir || source === normalizedDir || source.startsWith(`${normalizedDir}/`);
421
+ });
422
+ const directPages = [];
423
+ const childDirectories = /* @__PURE__ */ new Set();
424
+ for (const descriptor of inDirectory) {
425
+ const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
426
+ const relativePath = normalizedDir ? posix.relative(normalizedDir, source) : source;
427
+ const segments = relativePath.split("/").filter(Boolean);
428
+ if (segments.length <= 1) directPages.push(descriptor);
429
+ else childDirectories.add(segments[0]);
430
+ }
431
+ const items = directPages.sort(descriptorSort).map((descriptor) => {
432
+ context.referencedNavigationIds.add(descriptor.navigationId);
433
+ return descriptor.navigationId;
434
+ });
435
+ const groups = [...childDirectories].map((segment) => {
436
+ const directory = normalizedDir ? posix.join(normalizedDir, segment) : segment;
437
+ const metadata = readCategoryMetadata(context.contentRoot, directory);
438
+ const pages = autogeneratedItems(directory, context);
439
+ const landing = generatedIndexPage(metadata.label ?? titleCase(segment), metadata.link, context);
440
+ if (landing && !pages.includes(landing)) pages.unshift(landing);
441
+ return {
442
+ position: metadata.position ?? Number.MAX_SAFE_INTEGER,
443
+ segment,
444
+ group: {
445
+ group: metadata.label ?? titleCase(segment),
446
+ pages
447
+ }
448
+ };
449
+ }).sort((left, right) => left.position - right.position || left.segment.localeCompare(right.segment, void 0, { numeric: true }));
450
+ items.push(...groups.map((entry) => entry.group));
451
+ return items;
452
+ }
453
+ function convertItems(value, context) {
454
+ if (typeof value === "string") {
455
+ const page = registerDoc(value, context);
456
+ return page ? [page] : [];
457
+ }
458
+ if (Array.isArray(value)) return value.flatMap((entry) => convertItems(entry, context));
459
+ const object = objectValue(value);
460
+ if (!object) return [];
461
+ if (object.type === "doc" || object.type === "ref") {
462
+ const page = typeof object.id === "string" ? registerDoc(object.id, context) : null;
463
+ return page ? [page] : [];
464
+ }
465
+ if (object.type === "autogenerated") {
466
+ return autogeneratedItems(typeof object.dirName === "string" ? object.dirName : ".", context);
467
+ }
468
+ if (object.type === "category") {
469
+ const label = typeof object.label === "string" ? object.label : "Documentation";
470
+ const pages = convertItems(object.items, context);
471
+ const landing = generatedIndexPage(label, object.link, context);
472
+ if (landing && !pages.includes(landing)) pages.unshift(landing);
473
+ return pages.length > 0 ? [{ group: label, pages }] : [];
474
+ }
475
+ if (object.type === "link" || object.type === "html") return [];
476
+ return Object.entries(object).flatMap(([label, items]) => {
477
+ const pages = convertItems(items, context);
478
+ return pages.length > 0 ? [{ group: label, pages }] : [];
479
+ });
480
+ }
481
+ function projectDocusaurusNavigation(input) {
482
+ const warnings = [];
483
+ const descriptorByDocId = new Map(input.descriptors.map((page) => [page.docId, page]));
484
+ const context = {
485
+ contentRoot: input.contentRoot,
486
+ descriptors: input.descriptors,
487
+ descriptorByDocId,
488
+ generatedPages: [],
489
+ generatedIds: /* @__PURE__ */ new Set(),
490
+ referencedNavigationIds: /* @__PURE__ */ new Set(),
491
+ sourceUrl: input.sourceUrl,
492
+ routePrefix: input.routePrefix ?? "",
493
+ sidebarSource: input.sidebars?.sourcePath ?? "autogenerated sidebar",
494
+ warnings
495
+ };
496
+ const sidebarEntries = input.sidebars ? Object.entries(input.sidebars.config) : [["docs", [
497
+ { type: "autogenerated", dirName: "." }
498
+ ]]];
499
+ const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
500
+ const converted = convertItems(value, context);
501
+ const loosePages = converted.filter((entry) => typeof entry === "string");
502
+ const groups = converted.filter((entry) => typeof entry !== "string");
503
+ if (loosePages.length > 0) groups.unshift({ group: "Overview", pages: loosePages });
504
+ if (groups.length === 0) return [];
505
+ return [{
506
+ tab: sidebarEntries.length === 1 ? "Documentation" : titleCase(sidebarId),
507
+ groups
508
+ }];
509
+ });
510
+ const unreferenced = input.descriptors.filter((page) => !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
511
+ if (unreferenced.length > 0) {
512
+ if (tabs.length === 0) tabs.push({ tab: "Documentation", groups: [] });
513
+ tabs[0].groups?.push({ group: "Additional", pages: unreferenced });
514
+ }
515
+ return {
516
+ docsConfig: { tabs },
517
+ generatedPages: context.generatedPages,
518
+ referencedNavigationIds: context.referencedNavigationIds,
519
+ warnings
520
+ };
521
+ }
522
+
523
+ // src/frontmatter.ts
524
+ import matter from "gray-matter";
525
+ var FRONTMATTER_OPTIONS = {
526
+ engines: {
527
+ javascript: () => ({}),
528
+ js: () => ({})
529
+ }
530
+ };
531
+ function parseFrontmatter(raw) {
532
+ return matter(raw, FRONTMATTER_OPTIONS);
533
+ }
534
+
535
+ // src/mdx.ts
536
+ function titleFromId(id) {
537
+ const value = id.split("/").at(-1) ?? id;
538
+ if (value === "introduction") return "Introduction";
539
+ return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
540
+ }
541
+ function firstParagraph(content) {
542
+ for (const block of content.split(/\n\s*\n/)) {
543
+ const value = block.replace(/\s+/g, " ").trim();
544
+ if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
545
+ return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
546
+ }
547
+ return "";
548
+ }
549
+ function docusaurusAdmonitionTag(kind) {
550
+ if (kind === "danger") return "Error";
551
+ if (kind === "info") return "Info";
552
+ if (kind === "caution" || kind === "warning") return "Warning";
553
+ return "Note";
554
+ }
555
+ function normalizeDocusaurusAdmonitions(body) {
556
+ const lines = body.split("\n");
557
+ const openAdmonitions = [];
558
+ let codeFence = null;
559
+ return lines.map((line) => {
560
+ const codeMatch = line.match(/^\s*(`{3,}|~{3,})/);
561
+ if (codeMatch) {
562
+ if (!codeFence) codeFence = codeMatch[1][0];
563
+ else if (codeMatch[1][0] === codeFence) codeFence = null;
564
+ return line;
565
+ }
566
+ if (codeFence) return line;
567
+ const opening = line.match(/^\s*(:{3,})(note|tip|info|warning|caution|danger)(?:\[([^\]]+)\]|\s+(.+))?\s*$/i);
568
+ if (opening) {
569
+ const tag = docusaurusAdmonitionTag(opening[2].toLowerCase());
570
+ openAdmonitions.push({ delimiter: opening[1], tag });
571
+ const title = (opening[3] ?? opening[4])?.trim();
572
+ return title ? `<${tag}>
573
+ **${title}**` : `<${tag}>`;
574
+ }
575
+ const closing = line.match(/^\s*(:{3,})\s*$/);
576
+ const current = openAdmonitions.at(-1);
577
+ if (closing && current?.delimiter === closing[1]) {
578
+ openAdmonitions.pop();
579
+ return `</${current.tag}>`;
580
+ }
581
+ return line;
582
+ }).join("\n");
583
+ }
584
+ function normalizeMdx(body) {
585
+ return normalizeDocusaurusAdmonitions(body).replace(/^import\s+(?:Tabs|TabItem|Link|DocCardList|TOCInline)\s+from\s+['"]@(?:theme|docusaurus)\/[^'"]+['"]\s*;?\s*$/gm, "").replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
586
+ const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
587
+ return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
588
+ }).replace(/<\/TabItem>/g, "</Tab>").replace(/<Link\b([^>]*)\bto=(?:"([^"]*)"|'([^']*)')([^>]*)>/g, (_match, before, doubleQuoted, singleQuoted, after) => `<a${before}href="${doubleQuoted ?? singleQuoted}"${after}>`).replace(/<\/Link>/g, "</a>").replace(/<(?:DocCardList|TOCInline)\b[^>]*\/>/g, "").replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
589
+ }
590
+ function parseMarkdownPage(input) {
591
+ const parsed = parseFrontmatter(input.raw);
592
+ const fallbackIdentity = {
593
+ id: input.id,
594
+ navigationId: input.navigationId ?? input.id,
595
+ ...input.locale ? { locale: input.locale } : {}
596
+ };
597
+ const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
598
+ const body = normalizeMdx(parsed.content).trim();
599
+ const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
600
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(identity.navigationId);
601
+ const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : firstParagraph(body);
602
+ return {
603
+ id: identity.id,
604
+ navigationId: identity.navigationId,
605
+ locale: identity.locale,
606
+ title,
607
+ description,
608
+ keywords,
609
+ openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
610
+ body,
611
+ source: input.source
612
+ };
613
+ }
614
+
95
615
  // src/navigation.ts
616
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
617
+ import { dirname, extname as extname2, relative as relative2 } from "path";
96
618
  var LANGUAGE_LABELS = {
97
619
  ar: "Arabic",
98
620
  de: "German",
@@ -111,7 +633,7 @@ var LANGUAGE_LABELS = {
111
633
  uk: "Ukrainian",
112
634
  zh: "Chinese"
113
635
  };
114
- function objectValue(value) {
636
+ function objectValue2(value) {
115
637
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
116
638
  }
117
639
  function labelFor(value, fallback) {
@@ -127,7 +649,7 @@ function jsonPointer(root, pointer) {
127
649
  if (!pointer || pointer === "#") return root;
128
650
  const tokens = pointer.replace(/^#\/?/, "").split("/").filter(Boolean);
129
651
  return tokens.reduce((current, token) => {
130
- const object = objectValue(current);
652
+ const object = objectValue2(current);
131
653
  if (!object) return void 0;
132
654
  return object[token.replace(/~1/g, "/").replace(/~0/g, "~")];
133
655
  }, root);
@@ -136,7 +658,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
136
658
  if (Array.isArray(value)) {
137
659
  return value.map((entry) => resolveJsonReferences(entry, currentFile, repositoryRoot, stack));
138
660
  }
139
- const object = objectValue(value);
661
+ const object = objectValue2(value);
140
662
  if (!object) return value;
141
663
  if (typeof object.$ref === "string") {
142
664
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
@@ -146,7 +668,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
146
668
  const stackKey = `${referencedFile}#${pointer}`;
147
669
  if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
148
670
  stack.add(stackKey);
149
- const referencedRoot = JSON.parse(readFileSync(referencedFile, "utf8"));
671
+ const referencedRoot = JSON.parse(readFileSync2(referencedFile, "utf8"));
150
672
  const referenced = jsonPointer(referencedRoot, pointer ? `#${pointer}` : "");
151
673
  const resolved = resolveJsonReferences(referenced, referencedFile, repositoryRoot, stack);
152
674
  stack.delete(stackKey);
@@ -160,9 +682,9 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
160
682
  );
161
683
  }
162
684
  function readMintlifyConfig(repositoryRoot) {
163
- const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync);
685
+ const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync2);
164
686
  if (!configPath) return null;
165
- const raw = JSON.parse(readFileSync(configPath, "utf8"));
687
+ const raw = JSON.parse(readFileSync2(configPath, "utf8"));
166
688
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
167
689
  }
168
690
  function normalizePageRef(value) {
@@ -184,7 +706,7 @@ function registerReference(value, context) {
184
706
  }
185
707
  function convertPage(value, context) {
186
708
  if (typeof value === "string") return registerReference(value, context);
187
- const object = objectValue(value);
709
+ const object = objectValue2(value);
188
710
  if (!object) return null;
189
711
  if (typeof object.page === "string") return registerReference(object.page, context);
190
712
  const pages = Array.isArray(object.pages) ? object.pages : [];
@@ -220,12 +742,12 @@ function convertGroups(values, context, fallbackGroup) {
220
742
  return groups;
221
743
  }
222
744
  function convertContainerToTabs(containerValue, context, fallbackTab) {
223
- const container = objectValue(containerValue);
745
+ const container = objectValue2(containerValue);
224
746
  if (!container) return [];
225
747
  for (const key of ["tabs", "anchors", "products", "dropdowns", "versions", "menus"]) {
226
748
  if (!Array.isArray(container[key])) continue;
227
749
  const tabs = container[key].flatMap((value, index) => {
228
- const object = objectValue(value);
750
+ const object = objectValue2(value);
229
751
  if (!object) return [];
230
752
  const tab = labelFor(object, `${fallbackTab} ${index + 1}`);
231
753
  if (typeof object.href === "string" && !object.pages && !object.groups) {
@@ -252,8 +774,8 @@ function projectMintlifyNavigation(config) {
252
774
  const warnings = [];
253
775
  const references = [];
254
776
  const seenReferences = /* @__PURE__ */ new Set();
255
- const navigation = objectValue(config.navigation) ?? config;
256
- const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue).filter((value) => Boolean(value)) : [];
777
+ const navigation = objectValue2(config.navigation) ?? config;
778
+ const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue2).filter((value) => Boolean(value)) : [];
257
779
  let tabs = [];
258
780
  let i18n;
259
781
  if (languages.length > 0) {
@@ -291,7 +813,7 @@ function projectMintlifyNavigation(config) {
291
813
  tabs.push({ tab: "Changelog", href: "/changelog" });
292
814
  }
293
815
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
294
- const redirect = objectValue(value);
816
+ const redirect = objectValue2(value);
295
817
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
296
818
  return [{
297
819
  source: redirect.source,
@@ -305,7 +827,7 @@ function projectMintlifyNavigation(config) {
305
827
  warnings
306
828
  };
307
829
  }
308
- function titleCase(value) {
830
+ function titleCase2(value) {
309
831
  return value.split(/[-_]/).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
310
832
  }
311
833
  var TOP_LEVEL_LABELS = {
@@ -317,7 +839,7 @@ var TOP_LEVEL_LABELS = {
317
839
  sdk: "SDK"
318
840
  };
319
841
  function topLevelLabel(segment) {
320
- return TOP_LEVEL_LABELS[segment] ?? titleCase(segment);
842
+ return TOP_LEVEL_LABELS[segment] ?? titleCase2(segment);
321
843
  }
322
844
  function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
323
845
  const groups = /* @__PURE__ */ new Map();
@@ -329,7 +851,7 @@ function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(sect
329
851
  groups.set(nestedSegment, group);
330
852
  }
331
853
  return [...groups].map(([segment, pages]) => ({
332
- group: segment === "overview" ? sectionLabel : titleCase(segment),
854
+ group: segment === "overview" ? sectionLabel : titleCase2(segment),
333
855
  pages
334
856
  }));
335
857
  }
@@ -401,20 +923,20 @@ function buildNavigationFromPages(pages, options = {}) {
401
923
  buckets.set(segment, bucket);
402
924
  }
403
925
  const groups = [...buckets].map(([segment, pageIds]) => ({
404
- group: segment === "overview" ? "Overview" : titleCase(segment),
926
+ group: segment === "overview" ? "Overview" : titleCase2(segment),
405
927
  pages: pageIds
406
928
  }));
407
929
  return { tabs: [{ tab: "Documentation", groups }] };
408
930
  }
409
931
  function isDocumentationExtension(filename) {
410
- return [".md", ".mdx", ".rst", ".txt"].includes(extname(filename).toLowerCase());
932
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
411
933
  }
412
934
 
413
935
  // src/repository.ts
414
936
  var MAX_SOURCE_FILES = 5e3;
415
937
  var MAX_PAGE_BYTES = 2e6;
416
938
  var MAX_ASSET_BYTES = 25e6;
417
- var MAX_TOTAL_ASSET_BYTES = 1e8;
939
+ var MAX_TOTAL_ASSET_BYTES = 5e8;
418
940
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
419
941
  ".git",
420
942
  ".github",
@@ -461,6 +983,71 @@ var OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
461
983
  "swagger.yaml",
462
984
  "swagger.yml"
463
985
  ]);
986
+ var DOCUSAURUS_CONFIG_FILENAMES = [
987
+ "docusaurus.config.js",
988
+ "docusaurus.config.ts",
989
+ "docusaurus.config.mjs"
990
+ ];
991
+ function hasDocusaurusConfig(directory) {
992
+ return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
993
+ const path = resolveWithin(directory, filename);
994
+ return existsSync3(path) && lstatSync2(path).isFile();
995
+ });
996
+ }
997
+ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
998
+ if (docsDir !== void 0) {
999
+ let candidate = docsDir.replace(/\/+$/, "");
1000
+ while (true) {
1001
+ const path = resolveWithin(repositoryDir, candidate || ".");
1002
+ if (hasDocusaurusConfig(path)) return path;
1003
+ if (!candidate) break;
1004
+ const parent = dirname2(candidate);
1005
+ candidate = parent === "." ? "" : parent;
1006
+ }
1007
+ }
1008
+ const queue = [{ directory: repositoryDir, depth: 0 }];
1009
+ let visited = 0;
1010
+ while (queue.length > 0 && visited < 500) {
1011
+ const current = queue.shift();
1012
+ visited++;
1013
+ if (hasDocusaurusConfig(current.directory)) return current.directory;
1014
+ if (current.depth >= 4) continue;
1015
+ for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
1016
+ if (!entry.isDirectory() || entry.isSymbolicLink() || IGNORED_DIRECTORIES.has(entry.name)) continue;
1017
+ queue.push({
1018
+ directory: resolveWithin(current.directory, entry.name),
1019
+ depth: current.depth + 1
1020
+ });
1021
+ }
1022
+ }
1023
+ return null;
1024
+ }
1025
+ function readDocusaurusConfigSource(projectRoot) {
1026
+ const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync2(candidate).isFile());
1027
+ if (!path || lstatSync2(path).size > 1e6) return "";
1028
+ return readFileSync3(path, "utf8");
1029
+ }
1030
+ function primaryDocusaurusDocsDirectory(projectRoot) {
1031
+ const source = readDocusaurusConfigSource(projectRoot);
1032
+ const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
1033
+ return configured?.replace(/^\.\//, "").replace(/\/+$/, "") || "docs";
1034
+ }
1035
+ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1036
+ const source = readDocusaurusConfigSource(projectRoot);
1037
+ const projectRelative = relative3(repositoryDir, projectRoot).replace(/\\/g, "/");
1038
+ const plugins = [];
1039
+ const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
1040
+ for (const match of source.matchAll(matcher)) {
1041
+ const localPath = match[2].replace(/^\.\//, "").replace(/\/+$/, "");
1042
+ const routePrefix = match[4].replace(/^\/+|\/+$/g, "");
1043
+ if (!localPath || !routePrefix) continue;
1044
+ const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
1045
+ const absolute = resolveWithin(repositoryDir, docsDir);
1046
+ if (!existsSync3(absolute) || !lstatSync2(absolute).isDirectory()) continue;
1047
+ plugins.push({ docsDir, routePrefix });
1048
+ }
1049
+ return plugins;
1050
+ }
464
1051
  function parseGitHubRepositoryUrl(rawUrl) {
465
1052
  let url;
466
1053
  try {
@@ -514,21 +1101,22 @@ async function cloneGitHubRepository(source, targetDir) {
514
1101
  });
515
1102
  }
516
1103
  function detectRepositoryPlatform(repositoryDir) {
517
- if (existsSync2(resolveWithin(repositoryDir, "mint.json"))) return "mintlify";
1104
+ if (existsSync3(resolveWithin(repositoryDir, "mint.json"))) return "mintlify";
518
1105
  const docsJson = resolveWithin(repositoryDir, "docs.json");
519
- if (existsSync2(docsJson)) {
1106
+ if (existsSync3(docsJson)) {
520
1107
  try {
521
- const config = JSON.parse(readFileSync2(docsJson, "utf8"));
1108
+ const config = JSON.parse(readFileSync3(docsJson, "utf8"));
522
1109
  if (String(config.$schema ?? "").includes("mintlify") || "navigation" in config) return "mintlify";
523
1110
  if (Array.isArray(config.tabs)) return "thally";
524
1111
  } catch {
525
1112
  }
526
1113
  }
527
- if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "docusaurus";
528
- if (existsSync2(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
529
- if (existsSync2(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
530
- if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "starlight";
531
- if (["pages/_meta.json", "_meta.json"].some((name) => existsSync2(resolveWithin(repositoryDir, name)))) return "nextra";
1114
+ if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "docusaurus";
1115
+ if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
1116
+ if (existsSync3(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
1117
+ if (existsSync3(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
1118
+ if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "starlight";
1119
+ if (["pages/_meta.json", "_meta.json"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "nextra";
532
1120
  return "unknown";
533
1121
  }
534
1122
  function containsMarkdown(directory, depth = 0) {
@@ -536,7 +1124,7 @@ function containsMarkdown(directory, depth = 0) {
536
1124
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
537
1125
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
538
1126
  const path = resolveWithin(directory, entry.name);
539
- if (entry.isFile() && [".md", ".mdx"].includes(extname2(entry.name).toLowerCase())) return true;
1127
+ if (entry.isFile() && [".md", ".mdx"].includes(extname3(entry.name).toLowerCase())) return true;
540
1128
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
541
1129
  }
542
1130
  return false;
@@ -544,7 +1132,7 @@ function containsMarkdown(directory, depth = 0) {
544
1132
  function detectRepositoryDocsDir(repositoryDir) {
545
1133
  for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
546
1134
  const path = resolveWithin(repositoryDir, candidate);
547
- if (existsSync2(path) && lstatSync(path).isDirectory() && containsMarkdown(path)) return candidate;
1135
+ if (existsSync3(path) && lstatSync2(path).isDirectory() && containsMarkdown(path)) return candidate;
548
1136
  }
549
1137
  return "";
550
1138
  }
@@ -572,22 +1160,24 @@ function findOpenApi(files) {
572
1160
  function withoutFrontmatter(value) {
573
1161
  return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
574
1162
  }
575
- function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0) {
1163
+ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot) {
576
1164
  if (depth >= 8) return raw;
577
1165
  const snippets = /* @__PURE__ */ new Map();
578
1166
  const withoutImports = raw.replace(
579
- /^import\s+([A-Z][A-Za-z0-9_]*)\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?\s*$/gm,
1167
+ /^import\s+(?:\{\s*default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}?\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?(?:\s*\/\/.*)?$/gm,
580
1168
  (_statement, componentName, sourcePath) => {
581
1169
  try {
582
- const candidate = sourcePath.startsWith("/") ? resolveWithin(repositoryRoot, sourcePath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), sourcePath);
1170
+ const repositoryRelative = sourcePath.startsWith("@site/") ? relative3(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? sourcePath.replace(/^\/+/, "") : relative3(repositoryRoot, resolvePath(dirname2(currentFile), sourcePath)).replace(/\\/g, "/");
1171
+ const candidate = resolveWithin(repositoryRoot, repositoryRelative);
583
1172
  resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
584
- if (!existsSync2(candidate) || !lstatSync(candidate).isFile()) throw new Error("file not found");
1173
+ if (!existsSync3(candidate) || !lstatSync2(candidate).isFile()) throw new Error("file not found");
585
1174
  const nested = inlineMdxSnippets(
586
- withoutFrontmatter(readFileSync2(candidate, "utf8")),
1175
+ withoutFrontmatter(readFileSync3(candidate, "utf8")),
587
1176
  candidate,
588
1177
  repositoryRoot,
589
1178
  warnings,
590
- depth + 1
1179
+ depth + 1,
1180
+ siteRoot
591
1181
  );
592
1182
  snippets.set(componentName, nested);
593
1183
  return "";
@@ -622,10 +1212,27 @@ function injectOpenApi(config, filename) {
622
1212
  function migrateRepository(options) {
623
1213
  const repositoryDir = options.repositoryDir;
624
1214
  const platform = options.platform ?? detectRepositoryPlatform(repositoryDir);
1215
+ const docusaurusProjectRoot = platform === "docusaurus" ? findDocusaurusProjectRoot(repositoryDir, options.docsDir) : null;
1216
+ const configuredDocsDir = (() => {
1217
+ if (options.docsDir !== void 0) {
1218
+ const selected = resolveWithin(repositoryDir, options.docsDir || ".");
1219
+ if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
1220
+ const projectRelative = relative3(repositoryDir, selected).replace(/\\/g, "/");
1221
+ return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
1222
+ }
1223
+ return options.docsDir;
1224
+ }
1225
+ if (docusaurusProjectRoot) {
1226
+ const projectRelative = relative3(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
1227
+ return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
1228
+ }
1229
+ return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
1230
+ })();
625
1231
  const warnings = [];
626
1232
  let docsConfig = { tabs: [] };
627
1233
  const referenceMap = /* @__PURE__ */ new Map();
628
1234
  const referenceOrder = /* @__PURE__ */ new Map();
1235
+ let docusaurusSidebars = null;
629
1236
  if (platform === "mintlify") {
630
1237
  try {
631
1238
  const config = readMintlifyConfig(repositoryDir);
@@ -646,16 +1253,27 @@ function migrateRepository(options) {
646
1253
  });
647
1254
  }
648
1255
  }
649
- const configuredDocsDir = options.docsDir ?? (platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir));
1256
+ if (platform === "docusaurus") {
1257
+ try {
1258
+ docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot);
1259
+ } catch (error) {
1260
+ warnings.push({
1261
+ code: "unsupported-config",
1262
+ message: `Docusaurus sidebar could not be read safely: ${error instanceof Error ? error.message : String(error)} Generated navigation will be used.`
1263
+ });
1264
+ }
1265
+ }
650
1266
  const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
651
- if (!existsSync2(contentRoot) || !lstatSync(contentRoot).isDirectory()) {
1267
+ if (!existsSync3(contentRoot) || !lstatSync2(contentRoot).isDirectory()) {
652
1268
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
653
1269
  }
654
1270
  const files = scanFiles(contentRoot);
655
1271
  const pages = [];
656
1272
  const assets = [];
1273
+ const docusaurusDescriptors = [];
657
1274
  const seenPageIds = /* @__PURE__ */ new Set();
658
1275
  let skipped = 0;
1276
+ let discovered = files.length;
659
1277
  const localeConfig = docsConfig.i18n;
660
1278
  const pageFiles = [...files].sort((left, right) => {
661
1279
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -664,21 +1282,22 @@ function migrateRepository(options) {
664
1282
  });
665
1283
  for (const file of pageFiles) {
666
1284
  if (!isDocumentationExtension(file.relativePath)) continue;
667
- if (file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()))) {
1285
+ const sourceSegments = file.relativePath.split("/");
1286
+ if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename(file.relativePath).startsWith("_")) {
668
1287
  skipped++;
669
1288
  continue;
670
1289
  }
671
1290
  const rootFilename = !file.relativePath.includes("/") ? basename(file.relativePath).toLowerCase() : "";
672
- if (REPOSITORY_ONLY_DOCUMENTS.has(rootFilename)) {
1291
+ if (!configuredDocsDir && REPOSITORY_ONLY_DOCUMENTS.has(rootFilename)) {
673
1292
  skipped++;
674
1293
  continue;
675
1294
  }
676
- if (![".md", ".mdx"].includes(extname2(file.relativePath).toLowerCase())) {
1295
+ if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) {
677
1296
  skipped++;
678
1297
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
679
1298
  continue;
680
1299
  }
681
- const size = lstatSync(file.absolutePath).size;
1300
+ const size = lstatSync2(file.absolutePath).size;
682
1301
  if (size > MAX_PAGE_BYTES) {
683
1302
  skipped++;
684
1303
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
@@ -702,30 +1321,57 @@ function migrateRepository(options) {
702
1321
  }
703
1322
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
704
1323
  const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
705
- if (seenPageIds.has(id)) {
706
- skipped++;
707
- warnings.push({ code: "collision", message: `Multiple source files map to ${id}; the first file was kept.`, source: file.relativePath });
708
- continue;
709
- }
710
1324
  const raw = inlineMdxSnippets(
711
- readFileSync2(file.absolutePath, "utf8"),
1325
+ readFileSync3(file.absolutePath, "utf8"),
712
1326
  file.absolutePath,
713
1327
  repositoryDir,
714
- warnings
1328
+ warnings,
1329
+ 0,
1330
+ docusaurusProjectRoot ?? repositoryDir
715
1331
  );
1332
+ let docusaurusDescriptor;
716
1333
  const page = parseMarkdownPage({
717
1334
  id,
718
1335
  navigationId,
719
1336
  ...locale ? { locale } : {},
720
1337
  raw,
721
- source: `${options.sourceUrl}#${file.relativePath}`
1338
+ source: `${options.sourceUrl}#${file.relativePath}`,
1339
+ ...platform === "docusaurus" ? {
1340
+ resolveIdentity: (frontmatter, fallback) => {
1341
+ const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
1342
+ const routePrefix = options.docusaurusRoutePrefix?.replace(/^\/+|\/+$/g, "");
1343
+ if (!routePrefix) {
1344
+ docusaurusDescriptor = resolved.descriptor;
1345
+ return resolved.identity;
1346
+ }
1347
+ const prefixedNavigationId = resolved.identity.navigationId === "introduction" ? routePrefix : `${routePrefix}/${resolved.identity.navigationId}`;
1348
+ const prefixedStorageId = resolved.identity.id === "introduction" ? routePrefix : `${routePrefix}/${resolved.identity.id}`;
1349
+ docusaurusDescriptor = {
1350
+ ...resolved.descriptor,
1351
+ navigationId: prefixedNavigationId
1352
+ };
1353
+ return {
1354
+ ...resolved.identity,
1355
+ id: prefixedStorageId,
1356
+ navigationId: prefixedNavigationId
1357
+ };
1358
+ }
1359
+ } : {}
722
1360
  });
723
1361
  if (!page) {
724
1362
  skipped++;
725
1363
  continue;
726
1364
  }
727
- seenPageIds.add(id);
1365
+ if (seenPageIds.has(page.id)) {
1366
+ skipped++;
1367
+ warnings.push({ code: "collision", message: `Multiple source files map to ${page.id}; the first file was kept.`, source: file.relativePath });
1368
+ continue;
1369
+ }
1370
+ seenPageIds.add(page.id);
728
1371
  pages.push(page);
1372
+ if (docusaurusDescriptor) {
1373
+ docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
1374
+ }
729
1375
  }
730
1376
  const discoveredReferenceKeys = new Set(files.map((file) => normalizedReferenceKey(file.relativePath)));
731
1377
  for (const [key] of referenceMap) {
@@ -733,32 +1379,98 @@ function migrateRepository(options) {
733
1379
  warnings.push({ code: "missing-page", message: "A navigation entry did not resolve to a source page.", source: key });
734
1380
  }
735
1381
  }
1382
+ const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
1383
+ const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
1384
+ const root = resolveWithin(docusaurusAssetRoot, directory);
1385
+ if (!existsSync3(root) || !lstatSync2(root).isDirectory()) return [];
1386
+ return scanFiles(root).map((file) => ({
1387
+ ...file,
1388
+ relativePath: `${directory}/${file.relativePath}`
1389
+ }));
1390
+ }) : [];
736
1391
  let totalAssetBytes = 0;
737
- for (const file of files) {
1392
+ for (const file of [...files, ...repositoryAssets]) {
738
1393
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
739
- if (!ASSET_DIRECTORIES.has(firstSegment) || !ASSET_EXTENSIONS.has(extname2(file.relativePath).toLowerCase())) continue;
740
- const size = lstatSync(file.absolutePath).size;
1394
+ if (!ASSET_DIRECTORIES.has(firstSegment) || !ASSET_EXTENSIONS.has(extname3(file.relativePath).toLowerCase())) continue;
1395
+ const size = lstatSync2(file.absolutePath).size;
741
1396
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
742
1397
  warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
743
1398
  continue;
744
1399
  }
745
- const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : file.relativePath);
1400
+ const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
1401
+ const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
746
1402
  if (!assetPath) continue;
747
- assets.push({ path: assetPath, content: readFileSync2(file.absolutePath) });
1403
+ assets.push({ path: assetPath, content: readFileSync3(file.absolutePath) });
748
1404
  totalAssetBytes += size;
749
1405
  }
750
1406
  if (files.length >= MAX_SOURCE_FILES) {
751
1407
  warnings.push({ code: "limit-reached", message: `Repository discovery stopped at ${MAX_SOURCE_FILES} files.` });
752
1408
  }
1409
+ if (platform === "docusaurus") {
1410
+ const projected = projectDocusaurusNavigation({
1411
+ sidebars: docusaurusSidebars,
1412
+ descriptors: docusaurusDescriptors,
1413
+ contentRoot,
1414
+ sourceUrl: options.sourceUrl,
1415
+ routePrefix: options.docusaurusRoutePrefix
1416
+ });
1417
+ docsConfig = projected.docsConfig;
1418
+ warnings.push(...projected.warnings);
1419
+ for (const page of projected.generatedPages) {
1420
+ if (seenPageIds.has(page.id)) continue;
1421
+ seenPageIds.add(page.id);
1422
+ pages.push(page);
1423
+ }
1424
+ const descriptorByNavigationId = new Map(
1425
+ docusaurusDescriptors.map((descriptor) => [descriptor.navigationId, descriptor])
1426
+ );
1427
+ for (const page of pages) {
1428
+ const descriptor = descriptorByNavigationId.get(page.navigationId);
1429
+ if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors);
1430
+ }
1431
+ }
753
1432
  if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
754
1433
  const openApi = findOpenApi(files);
755
1434
  if (openApi) {
756
1435
  const filename = basename(openApi.relativePath);
757
1436
  if (!assets.some((asset) => asset.path === filename)) {
758
- assets.push({ path: filename, content: readFileSync2(openApi.absolutePath) });
1437
+ assets.push({ path: filename, content: readFileSync3(openApi.absolutePath) });
759
1438
  }
760
1439
  docsConfig = injectOpenApi(docsConfig, filename);
761
1440
  }
1441
+ if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
1442
+ for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
1443
+ if (plugin.docsDir === configuredDocsDir) continue;
1444
+ const pluginBundle = migrateRepository({
1445
+ ...options,
1446
+ docsDir: plugin.docsDir,
1447
+ platform: "docusaurus",
1448
+ docusaurusRoutePrefix: plugin.routePrefix,
1449
+ docusaurusSkipPlugins: true,
1450
+ docusaurusSkipSidebar: true,
1451
+ docusaurusSkipAssets: true
1452
+ });
1453
+ for (const page of pluginBundle.pages) {
1454
+ if (seenPageIds.has(page.id)) continue;
1455
+ seenPageIds.add(page.id);
1456
+ pages.push(page);
1457
+ }
1458
+ const assetPaths = new Set(assets.map((asset) => asset.path));
1459
+ for (const asset of pluginBundle.assets) {
1460
+ if (assetPaths.has(asset.path)) continue;
1461
+ assetPaths.add(asset.path);
1462
+ assets.push(asset);
1463
+ }
1464
+ const pluginLabel = plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1465
+ docsConfig.tabs.push(...pluginBundle.docsConfig.tabs.map((tab, index) => ({
1466
+ ...tab,
1467
+ tab: index === 0 ? pluginLabel : `${pluginLabel}: ${tab.tab}`
1468
+ })));
1469
+ warnings.push(...pluginBundle.warnings);
1470
+ discovered += pluginBundle.stats.discovered;
1471
+ skipped += pluginBundle.stats.skipped;
1472
+ }
1473
+ }
762
1474
  if (pages.length === 0) throw new Error("No importable Markdown or MDX pages were found in the repository.");
763
1475
  return {
764
1476
  sourceUrl: options.sourceUrl,
@@ -768,7 +1480,7 @@ function migrateRepository(options) {
768
1480
  assets,
769
1481
  docsConfig,
770
1482
  warnings,
771
- stats: { discovered: files.length, imported: pages.length, skipped }
1483
+ stats: { discovered, imported: pages.length, skipped }
772
1484
  };
773
1485
  }
774
1486
 
@@ -861,7 +1573,7 @@ function renderMigrationFiles(bundle, options = {}) {
861
1573
  // src/url.ts
862
1574
  import { load } from "cheerio";
863
1575
  import TurndownService from "turndown";
864
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1576
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
865
1577
  var DEFAULT_MAX_PAGES = 1e3;
866
1578
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
867
1579
  var MAX_DISCOVERED_URLS = 5e3;
@@ -882,13 +1594,22 @@ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
882
1594
  "Danger",
883
1595
  "Error",
884
1596
  "Expandable",
1597
+ "Agent",
1598
+ "Embed",
885
1599
  "File",
886
1600
  "Folder",
887
1601
  "Frame",
1602
+ "GitHub",
1603
+ "Github",
888
1604
  "Hero",
1605
+ "Human",
889
1606
  "Icon",
890
1607
  "Info",
1608
+ "InlinePanel",
1609
+ "InlineRequestExample",
1610
+ "InlineResponseExample",
891
1611
  "Latex",
1612
+ "LegacyView",
892
1613
  "Mermaid",
893
1614
  "Note",
894
1615
  "Panel",
@@ -903,6 +1624,9 @@ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
903
1624
  "Steps",
904
1625
  "Tab",
905
1626
  "Tabs",
1627
+ "Terminal",
1628
+ "TerminalInput",
1629
+ "TerminalOutput",
906
1630
  "Tile",
907
1631
  "TileGroup",
908
1632
  "Tip",
@@ -910,6 +1634,7 @@ var PORTABLE_MDX_COMPONENTS = /* @__PURE__ */ new Set([
910
1634
  "Tree",
911
1635
  "Update",
912
1636
  "View",
1637
+ "Visibility",
913
1638
  "Warning"
914
1639
  ]);
915
1640
  var LOCALE_CODES = /* @__PURE__ */ new Set([
@@ -1336,7 +2061,7 @@ function extractEmbeddedOpenApi(content) {
1336
2061
  const match = fencePattern.exec(content);
1337
2062
  if (!match) return { content };
1338
2063
  try {
1339
- const parsed = parseYaml(match[5], { maxAliasCount: 50 });
2064
+ const parsed = parseYaml2(match[5], { maxAliasCount: 50 });
1340
2065
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
1341
2066
  const document = parsed;
1342
2067
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -1452,6 +2177,7 @@ function htmlPage(document, id) {
1452
2177
  const content = $('main article, article, main, [role="main"]').first();
1453
2178
  const root = content.length ? content : $("body");
1454
2179
  root.find("script,style,noscript,nav,header,footer,aside,form,button,svg,iframe,object,embed,link,meta").remove();
2180
+ root.find(".theme-doc-version-badge,.theme-doc-breadcrumbs,.theme-doc-toc-mobile,.table-of-contents,.hash-link").remove();
1455
2181
  root.find("a[href]").each((_index, element) => {
1456
2182
  const href = $(element).attr("href");
1457
2183
  if (!href) return;
@@ -1482,12 +2208,78 @@ function htmlPage(document, id) {
1482
2208
  });
1483
2209
  const turndown = new TurndownService({ codeBlockStyle: "fenced", emDelimiter: "_", headingStyle: "atx" });
1484
2210
  turndown.remove(["script", "style", "noscript"]);
2211
+ turndown.addRule("docusaurus-admonition", {
2212
+ filter: (node) => node.nodeName === "DIV" && node.classList.contains("theme-admonition"),
2213
+ replacement: (_content, node) => {
2214
+ const element = node;
2215
+ const className = element.className;
2216
+ const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
2217
+ const fragment = load(element.outerHTML);
2218
+ const clone = fragment(".theme-admonition").first();
2219
+ const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
2220
+ const titleText = title2.text().trim();
2221
+ title2.remove();
2222
+ const markdown = turndown.turndown(clone.html() ?? "").trim();
2223
+ return `
2224
+
2225
+ <${tag}>
2226
+ ${titleText ? `**${titleText}**
2227
+
2228
+ ` : ""}${markdown}
2229
+ </${tag}>
2230
+
2231
+ `;
2232
+ }
2233
+ });
2234
+ turndown.addRule("docusaurus-tabs", {
2235
+ filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
2236
+ replacement: (_content, node) => {
2237
+ const element = node;
2238
+ const fragment = load(element.outerHTML);
2239
+ const container = fragment(".tabs-container").first();
2240
+ const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
2241
+ const panels = container.children('div[role="tabpanel"]').toArray();
2242
+ if (panels.length === 0) return _content;
2243
+ const tabs = panels.map((panel, index) => {
2244
+ const title2 = (labels[index] ?? `Tab ${index + 1}`).replace(/"/g, "&quot;");
2245
+ return `<Tab title="${title2}">
2246
+ ${turndown.turndown(fragment(panel).html() ?? "").trim()}
2247
+ </Tab>`;
2248
+ });
2249
+ return `
2250
+
2251
+ <Tabs>
2252
+ ${tabs.join("\n\n")}
2253
+ </Tabs>
2254
+
2255
+ `;
2256
+ }
2257
+ });
2258
+ turndown.addRule("gfm-table", {
2259
+ filter: "table",
2260
+ replacement: (_content, node) => {
2261
+ const fragment = load(node.outerHTML);
2262
+ const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => fragment(cell).text().replace(/\s+/g, " ").replace(/\|/g, "\\|").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\{/g, "&#123;").replace(/\}/g, "&#125;").trim())).filter((row) => row.length > 0);
2263
+ if (rows.length === 0) return "";
2264
+ const width = Math.max(...rows.map((row) => row.length));
2265
+ const normalized = rows.map((row) => [...row, ...Array(width - row.length).fill("")]);
2266
+ return `
2267
+
2268
+ | ${normalized[0].join(" | ")} |
2269
+ | ${Array(width).fill("---").join(" | ")} |
2270
+ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
2271
+
2272
+ `;
2273
+ }
2274
+ });
1485
2275
  turndown.addRule("migration-safe-fenced-code", {
1486
2276
  filter: (node) => node.nodeName === "PRE",
1487
2277
  replacement: (_content, node) => {
1488
2278
  const element = node;
1489
- const code = element.textContent?.replace(/\n$/, "") ?? "";
1490
- const language = element.querySelector("code")?.className.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
2279
+ const fragment = load(element.outerHTML);
2280
+ const tokenLines = fragment(".token-line").toArray();
2281
+ const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
2282
+ const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
1491
2283
  const longestFence = Math.max(0, ...[...code.matchAll(/`+/g)].map((match) => match[0].length));
1492
2284
  const fence = "`".repeat(Math.max(3, longestFence + 1));
1493
2285
  return `
@@ -1502,7 +2294,7 @@ ${fence}
1502
2294
  const escape = turndown.escape.bind(turndown);
1503
2295
  turndown.escape = (text) => escape(text).replace(/[<{]/g, (match) => `\\${match}`);
1504
2296
  const body = turndown.turndown(root.html() ?? "").trim();
1505
- if (!title || body.length < 40) return { page: null, links };
2297
+ if (!title || body.length < 40 && $("article").length === 0) return { page: null, links };
1506
2298
  return {
1507
2299
  page: {
1508
2300
  id,
@@ -1520,6 +2312,176 @@ function htmlNavigationLinks(document) {
1520
2312
  const $ = load(document.body);
1521
2313
  return $('.nav-tabs a[href], nav a[href], aside a[href], [role="navigation"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
1522
2314
  }
2315
+ function normalizedLinkLabel(value) {
2316
+ return value.replace(/\s+/g, " ").trim();
2317
+ }
2318
+ function readDocusaurusSidebar(document) {
2319
+ if (!/html/i.test(document.contentType)) return null;
2320
+ const $ = load(document.body);
2321
+ const sidebar = $("ul.theme-doc-sidebar-menu").first();
2322
+ if (sidebar.length === 0) return null;
2323
+ function readList(list) {
2324
+ return $(list).children("li").toArray().flatMap((item) => {
2325
+ const directLink = $(item).children("a.menu__link").first();
2326
+ const collapsibleLink = $(item).children(".menu__list-item-collapsible").children("a.menu__link").first();
2327
+ const link = directLink.length > 0 ? directLink : collapsibleLink;
2328
+ const label = normalizedLinkLabel(link.text());
2329
+ const href = link.attr("href");
2330
+ const nested = $(item).children("ul.menu__list").first();
2331
+ const children = nested.length > 0 ? readList(nested.get(0)) : [];
2332
+ const isCategory = $(item).hasClass("theme-doc-sidebar-item-category") || link.hasClass("menu__link--sublist");
2333
+ if (!label && children.length === 0) return [];
2334
+ return [{
2335
+ label: label || "Documentation",
2336
+ ...href ? { href: new URL(href, document.finalUrl).toString() } : {},
2337
+ ...children.length > 0 ? { items: children } : {},
2338
+ ...isCategory ? { isCategory: true } : {}
2339
+ }];
2340
+ });
2341
+ }
2342
+ return { items: readList(sidebar.get(0)) };
2343
+ }
2344
+ function docusaurusSidebarHrefs(snapshot) {
2345
+ const hrefs = [];
2346
+ const visit = (items) => {
2347
+ for (const item of items) {
2348
+ if (item.href) hrefs.push(item.href);
2349
+ if (item.items) visit(item.items);
2350
+ }
2351
+ };
2352
+ if (snapshot) visit(snapshot.items);
2353
+ return hrefs;
2354
+ }
2355
+ function deriveDocusaurusScope(source, document, submittedScopePath) {
2356
+ const $ = load(document.body);
2357
+ const sidebar = readDocusaurusSidebar(document);
2358
+ const internalPaths = docusaurusSidebarHrefs(sidebar).flatMap((href) => {
2359
+ try {
2360
+ const url = new URL(href);
2361
+ return url.origin === source.origin ? [url.pathname.replace(/\/+$/, "") || "/"] : [];
2362
+ } catch {
2363
+ return [];
2364
+ }
2365
+ });
2366
+ const counts = /* @__PURE__ */ new Map();
2367
+ for (const path of internalPaths) {
2368
+ const segments = path.split("/").filter(Boolean);
2369
+ for (let length = 1; length < segments.length; length++) {
2370
+ const prefix = `/${segments.slice(0, length).join("/")}`;
2371
+ counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
2372
+ }
2373
+ }
2374
+ const threshold = Math.max(2, Math.ceil(internalPaths.length * 0.6));
2375
+ const scopePath = [...counts].filter(([, count]) => count >= threshold).sort((left, right) => right[0].split("/").length - left[0].split("/").length || right[1] - left[1])[0]?.[0] ?? submittedScopePath;
2376
+ const excludedVersionPrefixes = $(".dropdown__menu a[href]").toArray().flatMap((element) => {
2377
+ const href = $(element).attr("href");
2378
+ if (!href) return [];
2379
+ try {
2380
+ const target = new URL(href, document.finalUrl);
2381
+ if (target.origin !== source.origin || !isInScope(target, source, scopePath)) return [];
2382
+ const relativePath = target.pathname.slice(scopePath.length).replace(/^\/+/, "");
2383
+ const version = relativePath.split("/", 1)[0];
2384
+ if (!/^(?:legacy|next|v\d[\w.-]*)$/i.test(version)) return [];
2385
+ return [`${scopePath}/${version}`];
2386
+ } catch {
2387
+ return [];
2388
+ }
2389
+ });
2390
+ return {
2391
+ scopePath,
2392
+ excludedVersionPrefixes: [...new Set(excludedVersionPrefixes)],
2393
+ sidebar
2394
+ };
2395
+ }
2396
+ function docusaurusRouteIdForUrl(url, scopePath) {
2397
+ let path = url.pathname.replace(/\/+$/, "");
2398
+ if (scopePath === "/") path = path.replace(/^\/+/, "");
2399
+ else if (path === scopePath) path = "";
2400
+ else if (path.startsWith(`${scopePath}/`)) path = path.slice(scopePath.length + 1);
2401
+ else return null;
2402
+ const segments = path.split("/").filter(Boolean).map((segment) => {
2403
+ let decoded = segment;
2404
+ try {
2405
+ decoded = decodeURIComponent(segment);
2406
+ } catch {
2407
+ }
2408
+ return decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
2409
+ }).filter(Boolean);
2410
+ return segments.join("/") || "introduction";
2411
+ }
2412
+ function storageIdForDocusaurusRoute(routeId) {
2413
+ return /(?:^|\/)(?:index|readme)$/i.test(routeId) ? `${routeId}/index` : routeId;
2414
+ }
2415
+ function docusaurusNavigationFromSnapshots(snapshots, pages, scopePath) {
2416
+ const imported = new Set(pages.map((page) => page.navigationId));
2417
+ const claimed = /* @__PURE__ */ new Set();
2418
+ const uniqueSnapshots = [...new Map(snapshots.map((snapshot) => [
2419
+ JSON.stringify(snapshot.items),
2420
+ snapshot
2421
+ ])).values()];
2422
+ const mergedSnapshots = [];
2423
+ function mergeItems(target, incoming) {
2424
+ for (const item of incoming) {
2425
+ const existing = target.find((candidate) => candidate.label === item.label && (candidate.items || item.items)) ?? target.find((candidate) => candidate.label === item.label && candidate.href === item.href);
2426
+ if (!existing) {
2427
+ target.push(structuredClone(item));
2428
+ continue;
2429
+ }
2430
+ if (!existing.href && item.href) existing.href = item.href;
2431
+ if (item.items) {
2432
+ existing.items ??= [];
2433
+ mergeItems(existing.items, item.items);
2434
+ }
2435
+ }
2436
+ }
2437
+ for (const snapshot of uniqueSnapshots) {
2438
+ const incomingLabels = new Set(snapshot.items.map((item) => item.label));
2439
+ const cluster = mergedSnapshots.find((candidate) => {
2440
+ const candidateLabels = new Set(candidate.items.map((item) => item.label));
2441
+ const overlap = [...incomingLabels].filter((label) => candidateLabels.has(label)).length;
2442
+ return overlap >= Math.max(2, Math.ceil(Math.min(incomingLabels.size, candidateLabels.size) * 0.5));
2443
+ });
2444
+ if (cluster) mergeItems(cluster.items, snapshot.items);
2445
+ else mergedSnapshots.push(structuredClone(snapshot));
2446
+ }
2447
+ const pageIdsFor = (snapshot) => docusaurusSidebarHrefs(snapshot).flatMap((href) => {
2448
+ const id = docusaurusRouteIdForUrl(new URL(href), scopePath);
2449
+ return id && imported.has(id) ? [id] : [];
2450
+ });
2451
+ mergedSnapshots.sort((left, right) => pageIdsFor(right).length - pageIdsFor(left).length);
2452
+ const tabs = mergedSnapshots.flatMap((snapshot, snapshotIndex) => {
2453
+ function convert(items) {
2454
+ const converted2 = [];
2455
+ for (const item of items) {
2456
+ const routeId = item.href ? docusaurusRouteIdForUrl(new URL(item.href), scopePath) : null;
2457
+ const page = routeId && imported.has(routeId) && !claimed.has(routeId) ? routeId : null;
2458
+ if (page) claimed.add(page);
2459
+ const children = item.items ? convert(item.items) : [];
2460
+ if (children.length > 0 || item.isCategory) {
2461
+ converted2.push({ group: item.label, pages: page ? [page, ...children] : children });
2462
+ continue;
2463
+ }
2464
+ if (page) converted2.push(page);
2465
+ }
2466
+ return converted2;
2467
+ }
2468
+ const converted = convert(snapshot.items);
2469
+ const loosePages = converted.filter((item) => typeof item === "string");
2470
+ const groups = converted.filter((item) => typeof item !== "string");
2471
+ if (loosePages.length > 0) groups.unshift({ group: "Overview", pages: loosePages });
2472
+ if (groups.length === 0) return [];
2473
+ const snapshotPageIds = pageIdsFor(snapshot);
2474
+ const firstSegments = new Set(snapshotPageIds.map((id) => id.split("/", 1)[0]));
2475
+ const tab = snapshotIndex > 0 && firstSegments.size === 1 ? normalizedLinkLabel([...firstSegments][0].replace(/[-_]/g, " ")).replace(/\b\w/g, (letter) => letter.toUpperCase()) : "Documentation";
2476
+ return [{ tab, groups }];
2477
+ });
2478
+ const additional = pages.map((page) => page.navigationId).filter((id) => !claimed.has(id));
2479
+ if (additional.length > 0) {
2480
+ if (tabs.length === 0) return buildNavigationFromPages(pages);
2481
+ tabs[0].groups?.push({ group: "Additional", pages: additional });
2482
+ }
2483
+ return { tabs };
2484
+ }
1523
2485
  function htmlTopLevelNavigation(document, source, scopePath) {
1524
2486
  const $ = load(document.body);
1525
2487
  const collectCandidates = (selector) => {
@@ -1598,24 +2560,54 @@ async function migrateUrl(options) {
1598
2560
  }
1599
2561
  const htmlProbe = /html/i.test(initial.contentType) ? initial : await safeFetch(fetcher, source, "text/html,application/xhtml+xml");
1600
2562
  const scopedHtmlProbe = htmlProbe && isInScope(htmlProbe.finalUrl, source, submittedScopePath) ? htmlProbe : null;
1601
- const platform = detectUrlPlatform(scopedHtmlProbe ?? initial);
1602
- const scopePath = platform === "mintlify" ? machineIndexScopePath(source, [initial, scopedHtmlProbe]) ?? submittedScopePath : submittedScopePath;
2563
+ const platform = options.platform ?? detectUrlPlatform(scopedHtmlProbe ?? initial);
2564
+ let docusaurusHtmlProbe = platform === "docusaurus" ? scopedHtmlProbe : null;
2565
+ if (docusaurusHtmlProbe && !readDocusaurusSidebar(docusaurusHtmlProbe)) {
2566
+ const $ = load(docusaurusHtmlProbe.body);
2567
+ const docsHref = $("nav a[href]").toArray().map((element) => ({
2568
+ href: $(element).attr("href"),
2569
+ label: normalizedLinkLabel($(element).text())
2570
+ })).find((link) => /^docs(?:umentation)?$/i.test(link.label))?.href;
2571
+ if (docsHref) {
2572
+ try {
2573
+ const docsUrl = new URL(docsHref, docusaurusHtmlProbe.finalUrl);
2574
+ if (docsUrl.origin === source.origin && isInScope(docsUrl, source, submittedScopePath)) {
2575
+ const probe = await safeFetch(fetcher, docsUrl, "text/html,application/xhtml+xml");
2576
+ if (probe && probe.finalUrl.origin === source.origin && /html/i.test(probe.contentType)) {
2577
+ docusaurusHtmlProbe = probe;
2578
+ }
2579
+ }
2580
+ } catch {
2581
+ }
2582
+ }
2583
+ }
2584
+ const docusaurusDiscovery = platform === "docusaurus" && docusaurusHtmlProbe ? deriveDocusaurusScope(source, docusaurusHtmlProbe, submittedScopePath) : null;
2585
+ const scopePath = platform === "mintlify" ? machineIndexScopePath(source, [initial, scopedHtmlProbe]) ?? submittedScopePath : docusaurusDiscovery?.scopePath ?? submittedScopePath;
1603
2586
  const sourceTopLevelNavigation = platform === "mintlify" && scopedHtmlProbe && /html/i.test(scopedHtmlProbe.contentType) ? htmlTopLevelNavigation(scopedHtmlProbe, source, scopePath) : void 0;
1604
2587
  const sourceHomePageId = sourceTopLevelNavigation?.[0]?.pageId ?? (platform === "mintlify" && scopePath === "/" ? canonicalLocalizedPageId(pageIdForUrl(source, scopePath) ?? "") || void 0 : void 0);
1605
2588
  const cache = /* @__PURE__ */ new Map([[source.toString(), initial]]);
2589
+ if (docusaurusHtmlProbe) cache.set(docusaurusHtmlProbe.finalUrl.toString(), docusaurusHtmlProbe);
1606
2590
  const queue = [source];
1607
2591
  const queued = /* @__PURE__ */ new Set([candidateIdentity(source)]);
1608
2592
  const warnings = [];
1609
2593
  const fetchedSitemaps = /* @__PURE__ */ new Set();
2594
+ const docusaurusSidebars = docusaurusDiscovery?.sidebar ? [docusaurusDiscovery.sidebar] : [];
2595
+ const docusaurusRedirects = [];
1610
2596
  function enqueue(value, base) {
1611
2597
  if (queued.size >= MAX_DISCOVERED_URLS) return;
1612
2598
  const url = normalizeCandidate(value, base, source, scopePath);
1613
2599
  if (!url || queued.has(candidateIdentity(url))) return;
2600
+ if (platform === "docusaurus" && url.pathname.replace(/\/+$/, "") === `${scopePath}/search`) return;
2601
+ if (docusaurusDiscovery?.excludedVersionPrefixes.some((prefix) => {
2602
+ const path = url.pathname.replace(/\/+$/, "");
2603
+ return path === prefix || path.startsWith(`${prefix}/`);
2604
+ })) return;
1614
2605
  queued.add(candidateIdentity(url));
1615
2606
  queue.push(url);
1616
2607
  }
1617
- if (scopedHtmlProbe && /html/i.test(scopedHtmlProbe.contentType)) {
1618
- for (const link of htmlNavigationLinks(scopedHtmlProbe)) enqueue(link, scopedHtmlProbe.finalUrl);
2608
+ const navigationProbe = docusaurusHtmlProbe ?? scopedHtmlProbe;
2609
+ if (navigationProbe && /html/i.test(navigationProbe.contentType)) {
2610
+ for (const link of htmlNavigationLinks(navigationProbe)) enqueue(link, navigationProbe.finalUrl);
1619
2611
  }
1620
2612
  async function discoverDocumentLinks(document, depth = 0) {
1621
2613
  const links = sitemapLinks(document.body);
@@ -1734,11 +2726,13 @@ async function migrateUrl(options) {
1734
2726
  return { candidate, page: null, links: [], failure: false, budgetExceeded: true };
1735
2727
  }
1736
2728
  importedBytes += responseBytes;
1737
- const discoveredId = pageIdForUrl(document.finalUrl, scopePath) ?? pageIdForUrl(candidate, scopePath);
1738
- const id = discoveredId && canonicalLocalizedPageId(discoveredId) === sourceHomePageId ? "introduction" : discoveredId;
1739
- if (!id) return { candidate, page: null, links: [], failure: false };
2729
+ const discoveredNavigationId = platform === "docusaurus" ? docusaurusRouteIdForUrl(document.finalUrl, scopePath) ?? docusaurusRouteIdForUrl(candidate, scopePath) : pageIdForUrl(document.finalUrl, scopePath) ?? pageIdForUrl(candidate, scopePath);
2730
+ const navigationId = discoveredNavigationId && canonicalLocalizedPageId(discoveredNavigationId) === sourceHomePageId ? "introduction" : discoveredNavigationId;
2731
+ if (!navigationId) return { candidate, page: null, links: [], failure: false };
2732
+ const id = platform === "docusaurus" ? storageIdForDocusaurusRoute(navigationId) : navigationId;
1740
2733
  if (/(?:markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname)) {
1741
2734
  const markdown = markdownPage(document, id);
2735
+ if (markdown.page) markdown.page.navigationId = navigationId;
1742
2736
  return {
1743
2737
  candidate,
1744
2738
  page: markdown.page,
@@ -1749,7 +2743,9 @@ async function migrateUrl(options) {
1749
2743
  };
1750
2744
  }
1751
2745
  const extracted = htmlPage(document, id);
1752
- return { candidate, page: extracted.page, links: extracted.links, base: document.finalUrl, failure: false };
2746
+ if (extracted.page) extracted.page.navigationId = navigationId;
2747
+ const sidebar = platform === "docusaurus" ? readDocusaurusSidebar(document) : null;
2748
+ return { candidate, page: extracted.page, links: extracted.links, base: document.finalUrl, sidebar, failure: false };
1753
2749
  }));
1754
2750
  for (const result of results) {
1755
2751
  if (result.failure) {
@@ -1757,7 +2753,10 @@ async function migrateUrl(options) {
1757
2753
  continue;
1758
2754
  }
1759
2755
  if (result.budgetExceeded) continue;
1760
- if (result.page) result.page.id = canonicalLocalizedPageId(result.page.id);
2756
+ if (result.page && platform !== "docusaurus") {
2757
+ result.page.id = canonicalLocalizedPageId(result.page.id);
2758
+ result.page.navigationId = canonicalLocalizedPageId(result.page.navigationId);
2759
+ }
1761
2760
  const sourceIdentity = result.page ? candidateIdentity(new URL(result.page.source)) : null;
1762
2761
  if (result.page && sourceIdentity && !seenSources.has(sourceIdentity) && !seenIds.has(result.page.id) && pages.length < maxPages) {
1763
2762
  pages.push(result.page);
@@ -1767,6 +2766,16 @@ async function migrateUrl(options) {
1767
2766
  seenIds.add(result.page.id);
1768
2767
  seenSources.add(sourceIdentity);
1769
2768
  }
2769
+ if (platform === "docusaurus" && result.page) {
2770
+ const candidateRoute = docusaurusRouteIdForUrl(result.candidate, scopePath);
2771
+ if (candidateRoute && candidateRoute !== result.page.navigationId) {
2772
+ docusaurusRedirects.push({
2773
+ source: candidateRoute === "introduction" ? "/" : `/${candidateRoute}`,
2774
+ destination: result.page.navigationId === "introduction" ? "/" : `/${result.page.navigationId}`
2775
+ });
2776
+ }
2777
+ }
2778
+ if ("sidebar" in result && result.sidebar) docusaurusSidebars.push(result.sidebar);
1770
2779
  for (const link of result.links) enqueue(link, result.base ?? result.candidate);
1771
2780
  }
1772
2781
  }
@@ -1779,7 +2788,7 @@ async function migrateUrl(options) {
1779
2788
  page.navigationId = navigationSegments.join("/") || "introduction";
1780
2789
  }
1781
2790
  }
1782
- const importedIds = new Set(pages.map((page) => page.id));
2791
+ const importedIds = new Set(pages.map((page) => page.navigationId));
1783
2792
  for (const page of pages) {
1784
2793
  page.body = rewriteInternalLinks(
1785
2794
  page.body,
@@ -1799,10 +2808,16 @@ async function migrateUrl(options) {
1799
2808
  ...entry,
1800
2809
  pageId: entry.pageId === sourceHomePageId ? "introduction" : entry.pageId
1801
2810
  }));
1802
- const docsConfig = buildNavigationFromPages(pages, {
2811
+ const docsConfig = platform === "docusaurus" ? docusaurusNavigationFromSnapshots(docusaurusSidebars, pages, scopePath) : buildNavigationFromPages(pages, {
1803
2812
  topLevelTabs: platform === "mintlify",
1804
2813
  topLevelNavigation
1805
2814
  });
2815
+ if (docusaurusRedirects.length > 0) {
2816
+ docsConfig.redirects = [...new Map(docusaurusRedirects.map((redirect) => [
2817
+ `${redirect.source}:${redirect.destination}`,
2818
+ redirect
2819
+ ])).values()];
2820
+ }
1806
2821
  const openApiAsset = mergeEmbeddedOpenApi(openApiFragments);
1807
2822
  if (openApiAsset) {
1808
2823
  const operationPageIds = new Set(pages.filter((page) => page.openapi).map((page) => page.navigationId));