@thallylabs/migrate 0.1.2 → 0.2.0

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,553 @@ 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/mdx.ts
524
+ import matter from "gray-matter";
525
+ function titleFromId(id) {
526
+ const value = id.split("/").at(-1) ?? id;
527
+ if (value === "introduction") return "Introduction";
528
+ return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
529
+ }
530
+ function firstParagraph(content) {
531
+ for (const block of content.split(/\n\s*\n/)) {
532
+ const value = block.replace(/\s+/g, " ").trim();
533
+ if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
534
+ return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
535
+ }
536
+ return "";
537
+ }
538
+ function docusaurusAdmonitionTag(kind) {
539
+ if (kind === "danger") return "Error";
540
+ if (kind === "info") return "Info";
541
+ if (kind === "caution" || kind === "warning") return "Warning";
542
+ return "Note";
543
+ }
544
+ function normalizeDocusaurusAdmonitions(body) {
545
+ const lines = body.split("\n");
546
+ const openAdmonitions = [];
547
+ let codeFence = null;
548
+ return lines.map((line) => {
549
+ const codeMatch = line.match(/^\s*(`{3,}|~{3,})/);
550
+ if (codeMatch) {
551
+ if (!codeFence) codeFence = codeMatch[1][0];
552
+ else if (codeMatch[1][0] === codeFence) codeFence = null;
553
+ return line;
554
+ }
555
+ if (codeFence) return line;
556
+ const opening = line.match(/^\s*(:{3,})(note|tip|info|warning|caution|danger)(?:\[([^\]]+)\]|\s+(.+))?\s*$/i);
557
+ if (opening) {
558
+ const tag = docusaurusAdmonitionTag(opening[2].toLowerCase());
559
+ openAdmonitions.push({ delimiter: opening[1], tag });
560
+ const title = (opening[3] ?? opening[4])?.trim();
561
+ return title ? `<${tag}>
562
+ **${title}**` : `<${tag}>`;
563
+ }
564
+ const closing = line.match(/^\s*(:{3,})\s*$/);
565
+ const current = openAdmonitions.at(-1);
566
+ if (closing && current?.delimiter === closing[1]) {
567
+ openAdmonitions.pop();
568
+ return `</${current.tag}>`;
569
+ }
570
+ return line;
571
+ }).join("\n");
572
+ }
573
+ function normalizeMdx(body) {
574
+ 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) => {
575
+ const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
576
+ return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
577
+ }).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>");
578
+ }
579
+ function parseMarkdownPage(input) {
580
+ const parsed = matter(input.raw);
581
+ const fallbackIdentity = {
582
+ id: input.id,
583
+ navigationId: input.navigationId ?? input.id,
584
+ ...input.locale ? { locale: input.locale } : {}
585
+ };
586
+ const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
587
+ const body = normalizeMdx(parsed.content).trim();
588
+ const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
589
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(identity.navigationId);
590
+ const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : firstParagraph(body);
591
+ return {
592
+ id: identity.id,
593
+ navigationId: identity.navigationId,
594
+ locale: identity.locale,
595
+ title,
596
+ description,
597
+ keywords,
598
+ openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
599
+ body,
600
+ source: input.source
601
+ };
602
+ }
603
+
95
604
  // src/navigation.ts
605
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
606
+ import { dirname, extname as extname2, relative as relative2 } from "path";
96
607
  var LANGUAGE_LABELS = {
97
608
  ar: "Arabic",
98
609
  de: "German",
@@ -111,7 +622,7 @@ var LANGUAGE_LABELS = {
111
622
  uk: "Ukrainian",
112
623
  zh: "Chinese"
113
624
  };
114
- function objectValue(value) {
625
+ function objectValue2(value) {
115
626
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
116
627
  }
117
628
  function labelFor(value, fallback) {
@@ -127,7 +638,7 @@ function jsonPointer(root, pointer) {
127
638
  if (!pointer || pointer === "#") return root;
128
639
  const tokens = pointer.replace(/^#\/?/, "").split("/").filter(Boolean);
129
640
  return tokens.reduce((current, token) => {
130
- const object = objectValue(current);
641
+ const object = objectValue2(current);
131
642
  if (!object) return void 0;
132
643
  return object[token.replace(/~1/g, "/").replace(/~0/g, "~")];
133
644
  }, root);
@@ -136,7 +647,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
136
647
  if (Array.isArray(value)) {
137
648
  return value.map((entry) => resolveJsonReferences(entry, currentFile, repositoryRoot, stack));
138
649
  }
139
- const object = objectValue(value);
650
+ const object = objectValue2(value);
140
651
  if (!object) return value;
141
652
  if (typeof object.$ref === "string") {
142
653
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
@@ -146,7 +657,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
146
657
  const stackKey = `${referencedFile}#${pointer}`;
147
658
  if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
148
659
  stack.add(stackKey);
149
- const referencedRoot = JSON.parse(readFileSync(referencedFile, "utf8"));
660
+ const referencedRoot = JSON.parse(readFileSync2(referencedFile, "utf8"));
150
661
  const referenced = jsonPointer(referencedRoot, pointer ? `#${pointer}` : "");
151
662
  const resolved = resolveJsonReferences(referenced, referencedFile, repositoryRoot, stack);
152
663
  stack.delete(stackKey);
@@ -160,9 +671,9 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
160
671
  );
161
672
  }
162
673
  function readMintlifyConfig(repositoryRoot) {
163
- const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync);
674
+ const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync2);
164
675
  if (!configPath) return null;
165
- const raw = JSON.parse(readFileSync(configPath, "utf8"));
676
+ const raw = JSON.parse(readFileSync2(configPath, "utf8"));
166
677
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
167
678
  }
168
679
  function normalizePageRef(value) {
@@ -184,7 +695,7 @@ function registerReference(value, context) {
184
695
  }
185
696
  function convertPage(value, context) {
186
697
  if (typeof value === "string") return registerReference(value, context);
187
- const object = objectValue(value);
698
+ const object = objectValue2(value);
188
699
  if (!object) return null;
189
700
  if (typeof object.page === "string") return registerReference(object.page, context);
190
701
  const pages = Array.isArray(object.pages) ? object.pages : [];
@@ -220,12 +731,12 @@ function convertGroups(values, context, fallbackGroup) {
220
731
  return groups;
221
732
  }
222
733
  function convertContainerToTabs(containerValue, context, fallbackTab) {
223
- const container = objectValue(containerValue);
734
+ const container = objectValue2(containerValue);
224
735
  if (!container) return [];
225
736
  for (const key of ["tabs", "anchors", "products", "dropdowns", "versions", "menus"]) {
226
737
  if (!Array.isArray(container[key])) continue;
227
738
  const tabs = container[key].flatMap((value, index) => {
228
- const object = objectValue(value);
739
+ const object = objectValue2(value);
229
740
  if (!object) return [];
230
741
  const tab = labelFor(object, `${fallbackTab} ${index + 1}`);
231
742
  if (typeof object.href === "string" && !object.pages && !object.groups) {
@@ -252,8 +763,8 @@ function projectMintlifyNavigation(config) {
252
763
  const warnings = [];
253
764
  const references = [];
254
765
  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)) : [];
766
+ const navigation = objectValue2(config.navigation) ?? config;
767
+ const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue2).filter((value) => Boolean(value)) : [];
257
768
  let tabs = [];
258
769
  let i18n;
259
770
  if (languages.length > 0) {
@@ -291,7 +802,7 @@ function projectMintlifyNavigation(config) {
291
802
  tabs.push({ tab: "Changelog", href: "/changelog" });
292
803
  }
293
804
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
294
- const redirect = objectValue(value);
805
+ const redirect = objectValue2(value);
295
806
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
296
807
  return [{
297
808
  source: redirect.source,
@@ -305,7 +816,7 @@ function projectMintlifyNavigation(config) {
305
816
  warnings
306
817
  };
307
818
  }
308
- function titleCase(value) {
819
+ function titleCase2(value) {
309
820
  return value.split(/[-_]/).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
310
821
  }
311
822
  var TOP_LEVEL_LABELS = {
@@ -317,7 +828,7 @@ var TOP_LEVEL_LABELS = {
317
828
  sdk: "SDK"
318
829
  };
319
830
  function topLevelLabel(segment) {
320
- return TOP_LEVEL_LABELS[segment] ?? titleCase(segment);
831
+ return TOP_LEVEL_LABELS[segment] ?? titleCase2(segment);
321
832
  }
322
833
  function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
323
834
  const groups = /* @__PURE__ */ new Map();
@@ -329,7 +840,7 @@ function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(sect
329
840
  groups.set(nestedSegment, group);
330
841
  }
331
842
  return [...groups].map(([segment, pages]) => ({
332
- group: segment === "overview" ? sectionLabel : titleCase(segment),
843
+ group: segment === "overview" ? sectionLabel : titleCase2(segment),
333
844
  pages
334
845
  }));
335
846
  }
@@ -401,20 +912,20 @@ function buildNavigationFromPages(pages, options = {}) {
401
912
  buckets.set(segment, bucket);
402
913
  }
403
914
  const groups = [...buckets].map(([segment, pageIds]) => ({
404
- group: segment === "overview" ? "Overview" : titleCase(segment),
915
+ group: segment === "overview" ? "Overview" : titleCase2(segment),
405
916
  pages: pageIds
406
917
  }));
407
918
  return { tabs: [{ tab: "Documentation", groups }] };
408
919
  }
409
920
  function isDocumentationExtension(filename) {
410
- return [".md", ".mdx", ".rst", ".txt"].includes(extname(filename).toLowerCase());
921
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
411
922
  }
412
923
 
413
924
  // src/repository.ts
414
925
  var MAX_SOURCE_FILES = 5e3;
415
926
  var MAX_PAGE_BYTES = 2e6;
416
927
  var MAX_ASSET_BYTES = 25e6;
417
- var MAX_TOTAL_ASSET_BYTES = 1e8;
928
+ var MAX_TOTAL_ASSET_BYTES = 5e8;
418
929
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
419
930
  ".git",
420
931
  ".github",
@@ -461,6 +972,71 @@ var OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
461
972
  "swagger.yaml",
462
973
  "swagger.yml"
463
974
  ]);
975
+ var DOCUSAURUS_CONFIG_FILENAMES = [
976
+ "docusaurus.config.js",
977
+ "docusaurus.config.ts",
978
+ "docusaurus.config.mjs"
979
+ ];
980
+ function hasDocusaurusConfig(directory) {
981
+ return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
982
+ const path = resolveWithin(directory, filename);
983
+ return existsSync3(path) && lstatSync2(path).isFile();
984
+ });
985
+ }
986
+ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
987
+ if (docsDir !== void 0) {
988
+ let candidate = docsDir.replace(/\/+$/, "");
989
+ while (true) {
990
+ const path = resolveWithin(repositoryDir, candidate || ".");
991
+ if (hasDocusaurusConfig(path)) return path;
992
+ if (!candidate) break;
993
+ const parent = dirname2(candidate);
994
+ candidate = parent === "." ? "" : parent;
995
+ }
996
+ }
997
+ const queue = [{ directory: repositoryDir, depth: 0 }];
998
+ let visited = 0;
999
+ while (queue.length > 0 && visited < 500) {
1000
+ const current = queue.shift();
1001
+ visited++;
1002
+ if (hasDocusaurusConfig(current.directory)) return current.directory;
1003
+ if (current.depth >= 4) continue;
1004
+ for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
1005
+ if (!entry.isDirectory() || entry.isSymbolicLink() || IGNORED_DIRECTORIES.has(entry.name)) continue;
1006
+ queue.push({
1007
+ directory: resolveWithin(current.directory, entry.name),
1008
+ depth: current.depth + 1
1009
+ });
1010
+ }
1011
+ }
1012
+ return null;
1013
+ }
1014
+ function readDocusaurusConfigSource(projectRoot) {
1015
+ const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync2(candidate).isFile());
1016
+ if (!path || lstatSync2(path).size > 1e6) return "";
1017
+ return readFileSync3(path, "utf8");
1018
+ }
1019
+ function primaryDocusaurusDocsDirectory(projectRoot) {
1020
+ const source = readDocusaurusConfigSource(projectRoot);
1021
+ const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
1022
+ return configured?.replace(/^\.\//, "").replace(/\/+$/, "") || "docs";
1023
+ }
1024
+ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1025
+ const source = readDocusaurusConfigSource(projectRoot);
1026
+ const projectRelative = relative3(repositoryDir, projectRoot).replace(/\\/g, "/");
1027
+ const plugins = [];
1028
+ const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
1029
+ for (const match of source.matchAll(matcher)) {
1030
+ const localPath = match[2].replace(/^\.\//, "").replace(/\/+$/, "");
1031
+ const routePrefix = match[4].replace(/^\/+|\/+$/g, "");
1032
+ if (!localPath || !routePrefix) continue;
1033
+ const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
1034
+ const absolute = resolveWithin(repositoryDir, docsDir);
1035
+ if (!existsSync3(absolute) || !lstatSync2(absolute).isDirectory()) continue;
1036
+ plugins.push({ docsDir, routePrefix });
1037
+ }
1038
+ return plugins;
1039
+ }
464
1040
  function parseGitHubRepositoryUrl(rawUrl) {
465
1041
  let url;
466
1042
  try {
@@ -514,21 +1090,22 @@ async function cloneGitHubRepository(source, targetDir) {
514
1090
  });
515
1091
  }
516
1092
  function detectRepositoryPlatform(repositoryDir) {
517
- if (existsSync2(resolveWithin(repositoryDir, "mint.json"))) return "mintlify";
1093
+ if (existsSync3(resolveWithin(repositoryDir, "mint.json"))) return "mintlify";
518
1094
  const docsJson = resolveWithin(repositoryDir, "docs.json");
519
- if (existsSync2(docsJson)) {
1095
+ if (existsSync3(docsJson)) {
520
1096
  try {
521
- const config = JSON.parse(readFileSync2(docsJson, "utf8"));
1097
+ const config = JSON.parse(readFileSync3(docsJson, "utf8"));
522
1098
  if (String(config.$schema ?? "").includes("mintlify") || "navigation" in config) return "mintlify";
523
1099
  if (Array.isArray(config.tabs)) return "thally";
524
1100
  } catch {
525
1101
  }
526
1102
  }
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";
1103
+ if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "docusaurus";
1104
+ if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
1105
+ if (existsSync3(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
1106
+ if (existsSync3(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
1107
+ if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "starlight";
1108
+ if (["pages/_meta.json", "_meta.json"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "nextra";
532
1109
  return "unknown";
533
1110
  }
534
1111
  function containsMarkdown(directory, depth = 0) {
@@ -536,7 +1113,7 @@ function containsMarkdown(directory, depth = 0) {
536
1113
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
537
1114
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
538
1115
  const path = resolveWithin(directory, entry.name);
539
- if (entry.isFile() && [".md", ".mdx"].includes(extname2(entry.name).toLowerCase())) return true;
1116
+ if (entry.isFile() && [".md", ".mdx"].includes(extname3(entry.name).toLowerCase())) return true;
540
1117
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
541
1118
  }
542
1119
  return false;
@@ -544,7 +1121,7 @@ function containsMarkdown(directory, depth = 0) {
544
1121
  function detectRepositoryDocsDir(repositoryDir) {
545
1122
  for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
546
1123
  const path = resolveWithin(repositoryDir, candidate);
547
- if (existsSync2(path) && lstatSync(path).isDirectory() && containsMarkdown(path)) return candidate;
1124
+ if (existsSync3(path) && lstatSync2(path).isDirectory() && containsMarkdown(path)) return candidate;
548
1125
  }
549
1126
  return "";
550
1127
  }
@@ -572,22 +1149,24 @@ function findOpenApi(files) {
572
1149
  function withoutFrontmatter(value) {
573
1150
  return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
574
1151
  }
575
- function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0) {
1152
+ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot) {
576
1153
  if (depth >= 8) return raw;
577
1154
  const snippets = /* @__PURE__ */ new Map();
578
1155
  const withoutImports = raw.replace(
579
- /^import\s+([A-Z][A-Za-z0-9_]*)\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?\s*$/gm,
1156
+ /^import\s+(?:\{\s*default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}?\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?(?:\s*\/\/.*)?$/gm,
580
1157
  (_statement, componentName, sourcePath) => {
581
1158
  try {
582
- const candidate = sourcePath.startsWith("/") ? resolveWithin(repositoryRoot, sourcePath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), sourcePath);
1159
+ 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, "/");
1160
+ const candidate = resolveWithin(repositoryRoot, repositoryRelative);
583
1161
  resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
584
- if (!existsSync2(candidate) || !lstatSync(candidate).isFile()) throw new Error("file not found");
1162
+ if (!existsSync3(candidate) || !lstatSync2(candidate).isFile()) throw new Error("file not found");
585
1163
  const nested = inlineMdxSnippets(
586
- withoutFrontmatter(readFileSync2(candidate, "utf8")),
1164
+ withoutFrontmatter(readFileSync3(candidate, "utf8")),
587
1165
  candidate,
588
1166
  repositoryRoot,
589
1167
  warnings,
590
- depth + 1
1168
+ depth + 1,
1169
+ siteRoot
591
1170
  );
592
1171
  snippets.set(componentName, nested);
593
1172
  return "";
@@ -622,10 +1201,27 @@ function injectOpenApi(config, filename) {
622
1201
  function migrateRepository(options) {
623
1202
  const repositoryDir = options.repositoryDir;
624
1203
  const platform = options.platform ?? detectRepositoryPlatform(repositoryDir);
1204
+ const docusaurusProjectRoot = platform === "docusaurus" ? findDocusaurusProjectRoot(repositoryDir, options.docsDir) : null;
1205
+ const configuredDocsDir = (() => {
1206
+ if (options.docsDir !== void 0) {
1207
+ const selected = resolveWithin(repositoryDir, options.docsDir || ".");
1208
+ if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
1209
+ const projectRelative = relative3(repositoryDir, selected).replace(/\\/g, "/");
1210
+ return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
1211
+ }
1212
+ return options.docsDir;
1213
+ }
1214
+ if (docusaurusProjectRoot) {
1215
+ const projectRelative = relative3(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
1216
+ return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
1217
+ }
1218
+ return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
1219
+ })();
625
1220
  const warnings = [];
626
1221
  let docsConfig = { tabs: [] };
627
1222
  const referenceMap = /* @__PURE__ */ new Map();
628
1223
  const referenceOrder = /* @__PURE__ */ new Map();
1224
+ let docusaurusSidebars = null;
629
1225
  if (platform === "mintlify") {
630
1226
  try {
631
1227
  const config = readMintlifyConfig(repositoryDir);
@@ -646,16 +1242,27 @@ function migrateRepository(options) {
646
1242
  });
647
1243
  }
648
1244
  }
649
- const configuredDocsDir = options.docsDir ?? (platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir));
1245
+ if (platform === "docusaurus") {
1246
+ try {
1247
+ docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot);
1248
+ } catch (error) {
1249
+ warnings.push({
1250
+ code: "unsupported-config",
1251
+ message: `Docusaurus sidebar could not be read safely: ${error instanceof Error ? error.message : String(error)} Generated navigation will be used.`
1252
+ });
1253
+ }
1254
+ }
650
1255
  const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
651
- if (!existsSync2(contentRoot) || !lstatSync(contentRoot).isDirectory()) {
1256
+ if (!existsSync3(contentRoot) || !lstatSync2(contentRoot).isDirectory()) {
652
1257
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
653
1258
  }
654
1259
  const files = scanFiles(contentRoot);
655
1260
  const pages = [];
656
1261
  const assets = [];
1262
+ const docusaurusDescriptors = [];
657
1263
  const seenPageIds = /* @__PURE__ */ new Set();
658
1264
  let skipped = 0;
1265
+ let discovered = files.length;
659
1266
  const localeConfig = docsConfig.i18n;
660
1267
  const pageFiles = [...files].sort((left, right) => {
661
1268
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -664,21 +1271,22 @@ function migrateRepository(options) {
664
1271
  });
665
1272
  for (const file of pageFiles) {
666
1273
  if (!isDocumentationExtension(file.relativePath)) continue;
667
- if (file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()))) {
1274
+ const sourceSegments = file.relativePath.split("/");
1275
+ if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename(file.relativePath).startsWith("_")) {
668
1276
  skipped++;
669
1277
  continue;
670
1278
  }
671
1279
  const rootFilename = !file.relativePath.includes("/") ? basename(file.relativePath).toLowerCase() : "";
672
- if (REPOSITORY_ONLY_DOCUMENTS.has(rootFilename)) {
1280
+ if (!configuredDocsDir && REPOSITORY_ONLY_DOCUMENTS.has(rootFilename)) {
673
1281
  skipped++;
674
1282
  continue;
675
1283
  }
676
- if (![".md", ".mdx"].includes(extname2(file.relativePath).toLowerCase())) {
1284
+ if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) {
677
1285
  skipped++;
678
1286
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
679
1287
  continue;
680
1288
  }
681
- const size = lstatSync(file.absolutePath).size;
1289
+ const size = lstatSync2(file.absolutePath).size;
682
1290
  if (size > MAX_PAGE_BYTES) {
683
1291
  skipped++;
684
1292
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
@@ -702,30 +1310,57 @@ function migrateRepository(options) {
702
1310
  }
703
1311
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
704
1312
  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
1313
  const raw = inlineMdxSnippets(
711
- readFileSync2(file.absolutePath, "utf8"),
1314
+ readFileSync3(file.absolutePath, "utf8"),
712
1315
  file.absolutePath,
713
1316
  repositoryDir,
714
- warnings
1317
+ warnings,
1318
+ 0,
1319
+ docusaurusProjectRoot ?? repositoryDir
715
1320
  );
1321
+ let docusaurusDescriptor;
716
1322
  const page = parseMarkdownPage({
717
1323
  id,
718
1324
  navigationId,
719
1325
  ...locale ? { locale } : {},
720
1326
  raw,
721
- source: `${options.sourceUrl}#${file.relativePath}`
1327
+ source: `${options.sourceUrl}#${file.relativePath}`,
1328
+ ...platform === "docusaurus" ? {
1329
+ resolveIdentity: (frontmatter, fallback) => {
1330
+ const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
1331
+ const routePrefix = options.docusaurusRoutePrefix?.replace(/^\/+|\/+$/g, "");
1332
+ if (!routePrefix) {
1333
+ docusaurusDescriptor = resolved.descriptor;
1334
+ return resolved.identity;
1335
+ }
1336
+ const prefixedNavigationId = resolved.identity.navigationId === "introduction" ? routePrefix : `${routePrefix}/${resolved.identity.navigationId}`;
1337
+ const prefixedStorageId = resolved.identity.id === "introduction" ? routePrefix : `${routePrefix}/${resolved.identity.id}`;
1338
+ docusaurusDescriptor = {
1339
+ ...resolved.descriptor,
1340
+ navigationId: prefixedNavigationId
1341
+ };
1342
+ return {
1343
+ ...resolved.identity,
1344
+ id: prefixedStorageId,
1345
+ navigationId: prefixedNavigationId
1346
+ };
1347
+ }
1348
+ } : {}
722
1349
  });
723
1350
  if (!page) {
724
1351
  skipped++;
725
1352
  continue;
726
1353
  }
727
- seenPageIds.add(id);
1354
+ if (seenPageIds.has(page.id)) {
1355
+ skipped++;
1356
+ warnings.push({ code: "collision", message: `Multiple source files map to ${page.id}; the first file was kept.`, source: file.relativePath });
1357
+ continue;
1358
+ }
1359
+ seenPageIds.add(page.id);
728
1360
  pages.push(page);
1361
+ if (docusaurusDescriptor) {
1362
+ docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
1363
+ }
729
1364
  }
730
1365
  const discoveredReferenceKeys = new Set(files.map((file) => normalizedReferenceKey(file.relativePath)));
731
1366
  for (const [key] of referenceMap) {
@@ -733,32 +1368,98 @@ function migrateRepository(options) {
733
1368
  warnings.push({ code: "missing-page", message: "A navigation entry did not resolve to a source page.", source: key });
734
1369
  }
735
1370
  }
1371
+ const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
1372
+ const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
1373
+ const root = resolveWithin(docusaurusAssetRoot, directory);
1374
+ if (!existsSync3(root) || !lstatSync2(root).isDirectory()) return [];
1375
+ return scanFiles(root).map((file) => ({
1376
+ ...file,
1377
+ relativePath: `${directory}/${file.relativePath}`
1378
+ }));
1379
+ }) : [];
736
1380
  let totalAssetBytes = 0;
737
- for (const file of files) {
1381
+ for (const file of [...files, ...repositoryAssets]) {
738
1382
  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;
1383
+ if (!ASSET_DIRECTORIES.has(firstSegment) || !ASSET_EXTENSIONS.has(extname3(file.relativePath).toLowerCase())) continue;
1384
+ const size = lstatSync2(file.absolutePath).size;
741
1385
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
742
1386
  warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
743
1387
  continue;
744
1388
  }
745
- const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : file.relativePath);
1389
+ const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
1390
+ const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
746
1391
  if (!assetPath) continue;
747
- assets.push({ path: assetPath, content: readFileSync2(file.absolutePath) });
1392
+ assets.push({ path: assetPath, content: readFileSync3(file.absolutePath) });
748
1393
  totalAssetBytes += size;
749
1394
  }
750
1395
  if (files.length >= MAX_SOURCE_FILES) {
751
1396
  warnings.push({ code: "limit-reached", message: `Repository discovery stopped at ${MAX_SOURCE_FILES} files.` });
752
1397
  }
1398
+ if (platform === "docusaurus") {
1399
+ const projected = projectDocusaurusNavigation({
1400
+ sidebars: docusaurusSidebars,
1401
+ descriptors: docusaurusDescriptors,
1402
+ contentRoot,
1403
+ sourceUrl: options.sourceUrl,
1404
+ routePrefix: options.docusaurusRoutePrefix
1405
+ });
1406
+ docsConfig = projected.docsConfig;
1407
+ warnings.push(...projected.warnings);
1408
+ for (const page of projected.generatedPages) {
1409
+ if (seenPageIds.has(page.id)) continue;
1410
+ seenPageIds.add(page.id);
1411
+ pages.push(page);
1412
+ }
1413
+ const descriptorByNavigationId = new Map(
1414
+ docusaurusDescriptors.map((descriptor) => [descriptor.navigationId, descriptor])
1415
+ );
1416
+ for (const page of pages) {
1417
+ const descriptor = descriptorByNavigationId.get(page.navigationId);
1418
+ if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors);
1419
+ }
1420
+ }
753
1421
  if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
754
1422
  const openApi = findOpenApi(files);
755
1423
  if (openApi) {
756
1424
  const filename = basename(openApi.relativePath);
757
1425
  if (!assets.some((asset) => asset.path === filename)) {
758
- assets.push({ path: filename, content: readFileSync2(openApi.absolutePath) });
1426
+ assets.push({ path: filename, content: readFileSync3(openApi.absolutePath) });
759
1427
  }
760
1428
  docsConfig = injectOpenApi(docsConfig, filename);
761
1429
  }
1430
+ if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
1431
+ for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
1432
+ if (plugin.docsDir === configuredDocsDir) continue;
1433
+ const pluginBundle = migrateRepository({
1434
+ ...options,
1435
+ docsDir: plugin.docsDir,
1436
+ platform: "docusaurus",
1437
+ docusaurusRoutePrefix: plugin.routePrefix,
1438
+ docusaurusSkipPlugins: true,
1439
+ docusaurusSkipSidebar: true,
1440
+ docusaurusSkipAssets: true
1441
+ });
1442
+ for (const page of pluginBundle.pages) {
1443
+ if (seenPageIds.has(page.id)) continue;
1444
+ seenPageIds.add(page.id);
1445
+ pages.push(page);
1446
+ }
1447
+ const assetPaths = new Set(assets.map((asset) => asset.path));
1448
+ for (const asset of pluginBundle.assets) {
1449
+ if (assetPaths.has(asset.path)) continue;
1450
+ assetPaths.add(asset.path);
1451
+ assets.push(asset);
1452
+ }
1453
+ const pluginLabel = plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1454
+ docsConfig.tabs.push(...pluginBundle.docsConfig.tabs.map((tab, index) => ({
1455
+ ...tab,
1456
+ tab: index === 0 ? pluginLabel : `${pluginLabel}: ${tab.tab}`
1457
+ })));
1458
+ warnings.push(...pluginBundle.warnings);
1459
+ discovered += pluginBundle.stats.discovered;
1460
+ skipped += pluginBundle.stats.skipped;
1461
+ }
1462
+ }
762
1463
  if (pages.length === 0) throw new Error("No importable Markdown or MDX pages were found in the repository.");
763
1464
  return {
764
1465
  sourceUrl: options.sourceUrl,
@@ -768,7 +1469,7 @@ function migrateRepository(options) {
768
1469
  assets,
769
1470
  docsConfig,
770
1471
  warnings,
771
- stats: { discovered: files.length, imported: pages.length, skipped }
1472
+ stats: { discovered, imported: pages.length, skipped }
772
1473
  };
773
1474
  }
774
1475
 
@@ -861,7 +1562,7 @@ function renderMigrationFiles(bundle, options = {}) {
861
1562
  // src/url.ts
862
1563
  import { load } from "cheerio";
863
1564
  import TurndownService from "turndown";
864
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1565
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
865
1566
  var DEFAULT_MAX_PAGES = 1e3;
866
1567
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
867
1568
  var MAX_DISCOVERED_URLS = 5e3;
@@ -1336,7 +2037,7 @@ function extractEmbeddedOpenApi(content) {
1336
2037
  const match = fencePattern.exec(content);
1337
2038
  if (!match) return { content };
1338
2039
  try {
1339
- const parsed = parseYaml(match[5], { maxAliasCount: 50 });
2040
+ const parsed = parseYaml2(match[5], { maxAliasCount: 50 });
1340
2041
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
1341
2042
  const document = parsed;
1342
2043
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -1452,6 +2153,7 @@ function htmlPage(document, id) {
1452
2153
  const content = $('main article, article, main, [role="main"]').first();
1453
2154
  const root = content.length ? content : $("body");
1454
2155
  root.find("script,style,noscript,nav,header,footer,aside,form,button,svg,iframe,object,embed,link,meta").remove();
2156
+ root.find(".theme-doc-version-badge,.theme-doc-breadcrumbs,.theme-doc-toc-mobile,.table-of-contents,.hash-link").remove();
1455
2157
  root.find("a[href]").each((_index, element) => {
1456
2158
  const href = $(element).attr("href");
1457
2159
  if (!href) return;
@@ -1482,12 +2184,78 @@ function htmlPage(document, id) {
1482
2184
  });
1483
2185
  const turndown = new TurndownService({ codeBlockStyle: "fenced", emDelimiter: "_", headingStyle: "atx" });
1484
2186
  turndown.remove(["script", "style", "noscript"]);
2187
+ turndown.addRule("docusaurus-admonition", {
2188
+ filter: (node) => node.nodeName === "DIV" && node.classList.contains("theme-admonition"),
2189
+ replacement: (_content, node) => {
2190
+ const element = node;
2191
+ const className = element.className;
2192
+ const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
2193
+ const fragment = load(element.outerHTML);
2194
+ const clone = fragment(".theme-admonition").first();
2195
+ const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
2196
+ const titleText = title2.text().trim();
2197
+ title2.remove();
2198
+ const markdown = turndown.turndown(clone.html() ?? "").trim();
2199
+ return `
2200
+
2201
+ <${tag}>
2202
+ ${titleText ? `**${titleText}**
2203
+
2204
+ ` : ""}${markdown}
2205
+ </${tag}>
2206
+
2207
+ `;
2208
+ }
2209
+ });
2210
+ turndown.addRule("docusaurus-tabs", {
2211
+ filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
2212
+ replacement: (_content, node) => {
2213
+ const element = node;
2214
+ const fragment = load(element.outerHTML);
2215
+ const container = fragment(".tabs-container").first();
2216
+ const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
2217
+ const panels = container.children('div[role="tabpanel"]').toArray();
2218
+ if (panels.length === 0) return _content;
2219
+ const tabs = panels.map((panel, index) => {
2220
+ const title2 = (labels[index] ?? `Tab ${index + 1}`).replace(/"/g, "&quot;");
2221
+ return `<Tab title="${title2}">
2222
+ ${turndown.turndown(fragment(panel).html() ?? "").trim()}
2223
+ </Tab>`;
2224
+ });
2225
+ return `
2226
+
2227
+ <Tabs>
2228
+ ${tabs.join("\n\n")}
2229
+ </Tabs>
2230
+
2231
+ `;
2232
+ }
2233
+ });
2234
+ turndown.addRule("gfm-table", {
2235
+ filter: "table",
2236
+ replacement: (_content, node) => {
2237
+ const fragment = load(node.outerHTML);
2238
+ 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);
2239
+ if (rows.length === 0) return "";
2240
+ const width = Math.max(...rows.map((row) => row.length));
2241
+ const normalized = rows.map((row) => [...row, ...Array(width - row.length).fill("")]);
2242
+ return `
2243
+
2244
+ | ${normalized[0].join(" | ")} |
2245
+ | ${Array(width).fill("---").join(" | ")} |
2246
+ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
2247
+
2248
+ `;
2249
+ }
2250
+ });
1485
2251
  turndown.addRule("migration-safe-fenced-code", {
1486
2252
  filter: (node) => node.nodeName === "PRE",
1487
2253
  replacement: (_content, node) => {
1488
2254
  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] ?? "";
2255
+ const fragment = load(element.outerHTML);
2256
+ const tokenLines = fragment(".token-line").toArray();
2257
+ const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
2258
+ const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
1491
2259
  const longestFence = Math.max(0, ...[...code.matchAll(/`+/g)].map((match) => match[0].length));
1492
2260
  const fence = "`".repeat(Math.max(3, longestFence + 1));
1493
2261
  return `
@@ -1502,7 +2270,7 @@ ${fence}
1502
2270
  const escape = turndown.escape.bind(turndown);
1503
2271
  turndown.escape = (text) => escape(text).replace(/[<{]/g, (match) => `\\${match}`);
1504
2272
  const body = turndown.turndown(root.html() ?? "").trim();
1505
- if (!title || body.length < 40) return { page: null, links };
2273
+ if (!title || body.length < 40 && $("article").length === 0) return { page: null, links };
1506
2274
  return {
1507
2275
  page: {
1508
2276
  id,
@@ -1520,6 +2288,176 @@ function htmlNavigationLinks(document) {
1520
2288
  const $ = load(document.body);
1521
2289
  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
2290
  }
2291
+ function normalizedLinkLabel(value) {
2292
+ return value.replace(/\s+/g, " ").trim();
2293
+ }
2294
+ function readDocusaurusSidebar(document) {
2295
+ if (!/html/i.test(document.contentType)) return null;
2296
+ const $ = load(document.body);
2297
+ const sidebar = $("ul.theme-doc-sidebar-menu").first();
2298
+ if (sidebar.length === 0) return null;
2299
+ function readList(list) {
2300
+ return $(list).children("li").toArray().flatMap((item) => {
2301
+ const directLink = $(item).children("a.menu__link").first();
2302
+ const collapsibleLink = $(item).children(".menu__list-item-collapsible").children("a.menu__link").first();
2303
+ const link = directLink.length > 0 ? directLink : collapsibleLink;
2304
+ const label = normalizedLinkLabel(link.text());
2305
+ const href = link.attr("href");
2306
+ const nested = $(item).children("ul.menu__list").first();
2307
+ const children = nested.length > 0 ? readList(nested.get(0)) : [];
2308
+ const isCategory = $(item).hasClass("theme-doc-sidebar-item-category") || link.hasClass("menu__link--sublist");
2309
+ if (!label && children.length === 0) return [];
2310
+ return [{
2311
+ label: label || "Documentation",
2312
+ ...href ? { href: new URL(href, document.finalUrl).toString() } : {},
2313
+ ...children.length > 0 ? { items: children } : {},
2314
+ ...isCategory ? { isCategory: true } : {}
2315
+ }];
2316
+ });
2317
+ }
2318
+ return { items: readList(sidebar.get(0)) };
2319
+ }
2320
+ function docusaurusSidebarHrefs(snapshot) {
2321
+ const hrefs = [];
2322
+ const visit = (items) => {
2323
+ for (const item of items) {
2324
+ if (item.href) hrefs.push(item.href);
2325
+ if (item.items) visit(item.items);
2326
+ }
2327
+ };
2328
+ if (snapshot) visit(snapshot.items);
2329
+ return hrefs;
2330
+ }
2331
+ function deriveDocusaurusScope(source, document, submittedScopePath) {
2332
+ const $ = load(document.body);
2333
+ const sidebar = readDocusaurusSidebar(document);
2334
+ const internalPaths = docusaurusSidebarHrefs(sidebar).flatMap((href) => {
2335
+ try {
2336
+ const url = new URL(href);
2337
+ return url.origin === source.origin ? [url.pathname.replace(/\/+$/, "") || "/"] : [];
2338
+ } catch {
2339
+ return [];
2340
+ }
2341
+ });
2342
+ const counts = /* @__PURE__ */ new Map();
2343
+ for (const path of internalPaths) {
2344
+ const segments = path.split("/").filter(Boolean);
2345
+ for (let length = 1; length < segments.length; length++) {
2346
+ const prefix = `/${segments.slice(0, length).join("/")}`;
2347
+ counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
2348
+ }
2349
+ }
2350
+ const threshold = Math.max(2, Math.ceil(internalPaths.length * 0.6));
2351
+ 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;
2352
+ const excludedVersionPrefixes = $(".dropdown__menu a[href]").toArray().flatMap((element) => {
2353
+ const href = $(element).attr("href");
2354
+ if (!href) return [];
2355
+ try {
2356
+ const target = new URL(href, document.finalUrl);
2357
+ if (target.origin !== source.origin || !isInScope(target, source, scopePath)) return [];
2358
+ const relativePath = target.pathname.slice(scopePath.length).replace(/^\/+/, "");
2359
+ const version = relativePath.split("/", 1)[0];
2360
+ if (!/^(?:legacy|next|v\d[\w.-]*)$/i.test(version)) return [];
2361
+ return [`${scopePath}/${version}`];
2362
+ } catch {
2363
+ return [];
2364
+ }
2365
+ });
2366
+ return {
2367
+ scopePath,
2368
+ excludedVersionPrefixes: [...new Set(excludedVersionPrefixes)],
2369
+ sidebar
2370
+ };
2371
+ }
2372
+ function docusaurusRouteIdForUrl(url, scopePath) {
2373
+ let path = url.pathname.replace(/\/+$/, "");
2374
+ if (scopePath === "/") path = path.replace(/^\/+/, "");
2375
+ else if (path === scopePath) path = "";
2376
+ else if (path.startsWith(`${scopePath}/`)) path = path.slice(scopePath.length + 1);
2377
+ else return null;
2378
+ const segments = path.split("/").filter(Boolean).map((segment) => {
2379
+ let decoded = segment;
2380
+ try {
2381
+ decoded = decodeURIComponent(segment);
2382
+ } catch {
2383
+ }
2384
+ return decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
2385
+ }).filter(Boolean);
2386
+ return segments.join("/") || "introduction";
2387
+ }
2388
+ function storageIdForDocusaurusRoute(routeId) {
2389
+ return /(?:^|\/)(?:index|readme)$/i.test(routeId) ? `${routeId}/index` : routeId;
2390
+ }
2391
+ function docusaurusNavigationFromSnapshots(snapshots, pages, scopePath) {
2392
+ const imported = new Set(pages.map((page) => page.navigationId));
2393
+ const claimed = /* @__PURE__ */ new Set();
2394
+ const uniqueSnapshots = [...new Map(snapshots.map((snapshot) => [
2395
+ JSON.stringify(snapshot.items),
2396
+ snapshot
2397
+ ])).values()];
2398
+ const mergedSnapshots = [];
2399
+ function mergeItems(target, incoming) {
2400
+ for (const item of incoming) {
2401
+ const existing = target.find((candidate) => candidate.label === item.label && (candidate.items || item.items)) ?? target.find((candidate) => candidate.label === item.label && candidate.href === item.href);
2402
+ if (!existing) {
2403
+ target.push(structuredClone(item));
2404
+ continue;
2405
+ }
2406
+ if (!existing.href && item.href) existing.href = item.href;
2407
+ if (item.items) {
2408
+ existing.items ??= [];
2409
+ mergeItems(existing.items, item.items);
2410
+ }
2411
+ }
2412
+ }
2413
+ for (const snapshot of uniqueSnapshots) {
2414
+ const incomingLabels = new Set(snapshot.items.map((item) => item.label));
2415
+ const cluster = mergedSnapshots.find((candidate) => {
2416
+ const candidateLabels = new Set(candidate.items.map((item) => item.label));
2417
+ const overlap = [...incomingLabels].filter((label) => candidateLabels.has(label)).length;
2418
+ return overlap >= Math.max(2, Math.ceil(Math.min(incomingLabels.size, candidateLabels.size) * 0.5));
2419
+ });
2420
+ if (cluster) mergeItems(cluster.items, snapshot.items);
2421
+ else mergedSnapshots.push(structuredClone(snapshot));
2422
+ }
2423
+ const pageIdsFor = (snapshot) => docusaurusSidebarHrefs(snapshot).flatMap((href) => {
2424
+ const id = docusaurusRouteIdForUrl(new URL(href), scopePath);
2425
+ return id && imported.has(id) ? [id] : [];
2426
+ });
2427
+ mergedSnapshots.sort((left, right) => pageIdsFor(right).length - pageIdsFor(left).length);
2428
+ const tabs = mergedSnapshots.flatMap((snapshot, snapshotIndex) => {
2429
+ function convert(items) {
2430
+ const converted2 = [];
2431
+ for (const item of items) {
2432
+ const routeId = item.href ? docusaurusRouteIdForUrl(new URL(item.href), scopePath) : null;
2433
+ const page = routeId && imported.has(routeId) && !claimed.has(routeId) ? routeId : null;
2434
+ if (page) claimed.add(page);
2435
+ const children = item.items ? convert(item.items) : [];
2436
+ if (children.length > 0 || item.isCategory) {
2437
+ converted2.push({ group: item.label, pages: page ? [page, ...children] : children });
2438
+ continue;
2439
+ }
2440
+ if (page) converted2.push(page);
2441
+ }
2442
+ return converted2;
2443
+ }
2444
+ const converted = convert(snapshot.items);
2445
+ const loosePages = converted.filter((item) => typeof item === "string");
2446
+ const groups = converted.filter((item) => typeof item !== "string");
2447
+ if (loosePages.length > 0) groups.unshift({ group: "Overview", pages: loosePages });
2448
+ if (groups.length === 0) return [];
2449
+ const snapshotPageIds = pageIdsFor(snapshot);
2450
+ const firstSegments = new Set(snapshotPageIds.map((id) => id.split("/", 1)[0]));
2451
+ const tab = snapshotIndex > 0 && firstSegments.size === 1 ? normalizedLinkLabel([...firstSegments][0].replace(/[-_]/g, " ")).replace(/\b\w/g, (letter) => letter.toUpperCase()) : "Documentation";
2452
+ return [{ tab, groups }];
2453
+ });
2454
+ const additional = pages.map((page) => page.navigationId).filter((id) => !claimed.has(id));
2455
+ if (additional.length > 0) {
2456
+ if (tabs.length === 0) return buildNavigationFromPages(pages);
2457
+ tabs[0].groups?.push({ group: "Additional", pages: additional });
2458
+ }
2459
+ return { tabs };
2460
+ }
1523
2461
  function htmlTopLevelNavigation(document, source, scopePath) {
1524
2462
  const $ = load(document.body);
1525
2463
  const collectCandidates = (selector) => {
@@ -1598,24 +2536,54 @@ async function migrateUrl(options) {
1598
2536
  }
1599
2537
  const htmlProbe = /html/i.test(initial.contentType) ? initial : await safeFetch(fetcher, source, "text/html,application/xhtml+xml");
1600
2538
  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;
2539
+ const platform = options.platform ?? detectUrlPlatform(scopedHtmlProbe ?? initial);
2540
+ let docusaurusHtmlProbe = platform === "docusaurus" ? scopedHtmlProbe : null;
2541
+ if (docusaurusHtmlProbe && !readDocusaurusSidebar(docusaurusHtmlProbe)) {
2542
+ const $ = load(docusaurusHtmlProbe.body);
2543
+ const docsHref = $("nav a[href]").toArray().map((element) => ({
2544
+ href: $(element).attr("href"),
2545
+ label: normalizedLinkLabel($(element).text())
2546
+ })).find((link) => /^docs(?:umentation)?$/i.test(link.label))?.href;
2547
+ if (docsHref) {
2548
+ try {
2549
+ const docsUrl = new URL(docsHref, docusaurusHtmlProbe.finalUrl);
2550
+ if (docsUrl.origin === source.origin && isInScope(docsUrl, source, submittedScopePath)) {
2551
+ const probe = await safeFetch(fetcher, docsUrl, "text/html,application/xhtml+xml");
2552
+ if (probe && probe.finalUrl.origin === source.origin && /html/i.test(probe.contentType)) {
2553
+ docusaurusHtmlProbe = probe;
2554
+ }
2555
+ }
2556
+ } catch {
2557
+ }
2558
+ }
2559
+ }
2560
+ const docusaurusDiscovery = platform === "docusaurus" && docusaurusHtmlProbe ? deriveDocusaurusScope(source, docusaurusHtmlProbe, submittedScopePath) : null;
2561
+ const scopePath = platform === "mintlify" ? machineIndexScopePath(source, [initial, scopedHtmlProbe]) ?? submittedScopePath : docusaurusDiscovery?.scopePath ?? submittedScopePath;
1603
2562
  const sourceTopLevelNavigation = platform === "mintlify" && scopedHtmlProbe && /html/i.test(scopedHtmlProbe.contentType) ? htmlTopLevelNavigation(scopedHtmlProbe, source, scopePath) : void 0;
1604
2563
  const sourceHomePageId = sourceTopLevelNavigation?.[0]?.pageId ?? (platform === "mintlify" && scopePath === "/" ? canonicalLocalizedPageId(pageIdForUrl(source, scopePath) ?? "") || void 0 : void 0);
1605
2564
  const cache = /* @__PURE__ */ new Map([[source.toString(), initial]]);
2565
+ if (docusaurusHtmlProbe) cache.set(docusaurusHtmlProbe.finalUrl.toString(), docusaurusHtmlProbe);
1606
2566
  const queue = [source];
1607
2567
  const queued = /* @__PURE__ */ new Set([candidateIdentity(source)]);
1608
2568
  const warnings = [];
1609
2569
  const fetchedSitemaps = /* @__PURE__ */ new Set();
2570
+ const docusaurusSidebars = docusaurusDiscovery?.sidebar ? [docusaurusDiscovery.sidebar] : [];
2571
+ const docusaurusRedirects = [];
1610
2572
  function enqueue(value, base) {
1611
2573
  if (queued.size >= MAX_DISCOVERED_URLS) return;
1612
2574
  const url = normalizeCandidate(value, base, source, scopePath);
1613
2575
  if (!url || queued.has(candidateIdentity(url))) return;
2576
+ if (platform === "docusaurus" && url.pathname.replace(/\/+$/, "") === `${scopePath}/search`) return;
2577
+ if (docusaurusDiscovery?.excludedVersionPrefixes.some((prefix) => {
2578
+ const path = url.pathname.replace(/\/+$/, "");
2579
+ return path === prefix || path.startsWith(`${prefix}/`);
2580
+ })) return;
1614
2581
  queued.add(candidateIdentity(url));
1615
2582
  queue.push(url);
1616
2583
  }
1617
- if (scopedHtmlProbe && /html/i.test(scopedHtmlProbe.contentType)) {
1618
- for (const link of htmlNavigationLinks(scopedHtmlProbe)) enqueue(link, scopedHtmlProbe.finalUrl);
2584
+ const navigationProbe = docusaurusHtmlProbe ?? scopedHtmlProbe;
2585
+ if (navigationProbe && /html/i.test(navigationProbe.contentType)) {
2586
+ for (const link of htmlNavigationLinks(navigationProbe)) enqueue(link, navigationProbe.finalUrl);
1619
2587
  }
1620
2588
  async function discoverDocumentLinks(document, depth = 0) {
1621
2589
  const links = sitemapLinks(document.body);
@@ -1734,11 +2702,13 @@ async function migrateUrl(options) {
1734
2702
  return { candidate, page: null, links: [], failure: false, budgetExceeded: true };
1735
2703
  }
1736
2704
  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 };
2705
+ const discoveredNavigationId = platform === "docusaurus" ? docusaurusRouteIdForUrl(document.finalUrl, scopePath) ?? docusaurusRouteIdForUrl(candidate, scopePath) : pageIdForUrl(document.finalUrl, scopePath) ?? pageIdForUrl(candidate, scopePath);
2706
+ const navigationId = discoveredNavigationId && canonicalLocalizedPageId(discoveredNavigationId) === sourceHomePageId ? "introduction" : discoveredNavigationId;
2707
+ if (!navigationId) return { candidate, page: null, links: [], failure: false };
2708
+ const id = platform === "docusaurus" ? storageIdForDocusaurusRoute(navigationId) : navigationId;
1740
2709
  if (/(?:markdown|text\/plain)/i.test(document.contentType) || /\.mdx?$/i.test(document.finalUrl.pathname)) {
1741
2710
  const markdown = markdownPage(document, id);
2711
+ if (markdown.page) markdown.page.navigationId = navigationId;
1742
2712
  return {
1743
2713
  candidate,
1744
2714
  page: markdown.page,
@@ -1749,7 +2719,9 @@ async function migrateUrl(options) {
1749
2719
  };
1750
2720
  }
1751
2721
  const extracted = htmlPage(document, id);
1752
- return { candidate, page: extracted.page, links: extracted.links, base: document.finalUrl, failure: false };
2722
+ if (extracted.page) extracted.page.navigationId = navigationId;
2723
+ const sidebar = platform === "docusaurus" ? readDocusaurusSidebar(document) : null;
2724
+ return { candidate, page: extracted.page, links: extracted.links, base: document.finalUrl, sidebar, failure: false };
1753
2725
  }));
1754
2726
  for (const result of results) {
1755
2727
  if (result.failure) {
@@ -1757,7 +2729,10 @@ async function migrateUrl(options) {
1757
2729
  continue;
1758
2730
  }
1759
2731
  if (result.budgetExceeded) continue;
1760
- if (result.page) result.page.id = canonicalLocalizedPageId(result.page.id);
2732
+ if (result.page && platform !== "docusaurus") {
2733
+ result.page.id = canonicalLocalizedPageId(result.page.id);
2734
+ result.page.navigationId = canonicalLocalizedPageId(result.page.navigationId);
2735
+ }
1761
2736
  const sourceIdentity = result.page ? candidateIdentity(new URL(result.page.source)) : null;
1762
2737
  if (result.page && sourceIdentity && !seenSources.has(sourceIdentity) && !seenIds.has(result.page.id) && pages.length < maxPages) {
1763
2738
  pages.push(result.page);
@@ -1767,6 +2742,16 @@ async function migrateUrl(options) {
1767
2742
  seenIds.add(result.page.id);
1768
2743
  seenSources.add(sourceIdentity);
1769
2744
  }
2745
+ if (platform === "docusaurus" && result.page) {
2746
+ const candidateRoute = docusaurusRouteIdForUrl(result.candidate, scopePath);
2747
+ if (candidateRoute && candidateRoute !== result.page.navigationId) {
2748
+ docusaurusRedirects.push({
2749
+ source: candidateRoute === "introduction" ? "/" : `/${candidateRoute}`,
2750
+ destination: result.page.navigationId === "introduction" ? "/" : `/${result.page.navigationId}`
2751
+ });
2752
+ }
2753
+ }
2754
+ if ("sidebar" in result && result.sidebar) docusaurusSidebars.push(result.sidebar);
1770
2755
  for (const link of result.links) enqueue(link, result.base ?? result.candidate);
1771
2756
  }
1772
2757
  }
@@ -1779,7 +2764,7 @@ async function migrateUrl(options) {
1779
2764
  page.navigationId = navigationSegments.join("/") || "introduction";
1780
2765
  }
1781
2766
  }
1782
- const importedIds = new Set(pages.map((page) => page.id));
2767
+ const importedIds = new Set(pages.map((page) => page.navigationId));
1783
2768
  for (const page of pages) {
1784
2769
  page.body = rewriteInternalLinks(
1785
2770
  page.body,
@@ -1799,10 +2784,16 @@ async function migrateUrl(options) {
1799
2784
  ...entry,
1800
2785
  pageId: entry.pageId === sourceHomePageId ? "introduction" : entry.pageId
1801
2786
  }));
1802
- const docsConfig = buildNavigationFromPages(pages, {
2787
+ const docsConfig = platform === "docusaurus" ? docusaurusNavigationFromSnapshots(docusaurusSidebars, pages, scopePath) : buildNavigationFromPages(pages, {
1803
2788
  topLevelTabs: platform === "mintlify",
1804
2789
  topLevelNavigation
1805
2790
  });
2791
+ if (docusaurusRedirects.length > 0) {
2792
+ docsConfig.redirects = [...new Map(docusaurusRedirects.map((redirect) => [
2793
+ `${redirect.source}:${redirect.destination}`,
2794
+ redirect
2795
+ ])).values()];
2796
+ }
1806
2797
  const openApiAsset = mergeEmbeddedOpenApi(openApiFragments);
1807
2798
  if (openApiAsset) {
1808
2799
  const operationPageIds = new Set(pages.filter((page) => page.openapi).map((page) => page.navigationId));