@thallylabs/migrate 0.2.4 → 0.2.6

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,18 +1,44 @@
1
1
  // src/repository.ts
2
2
  import { spawn } from "child_process";
3
3
  import {
4
- existsSync as existsSync3,
5
- lstatSync as lstatSync3,
6
- readFileSync as readFileSync3,
4
+ existsSync as existsSync4,
5
+ lstatSync as lstatSync4,
6
+ readFileSync as readFileSync4,
7
7
  readdirSync
8
8
  } from "fs";
9
- import { basename, dirname as dirname2, extname as extname3, relative as relative3, resolve as resolvePath } from "path";
9
+ import { basename, dirname as dirname3, extname as extname4, relative as relative4, resolve as resolvePath } from "path";
10
10
 
11
- // src/docusaurus.ts
11
+ // src/components.ts
12
+ import { createHash } from "crypto";
12
13
  import { existsSync, lstatSync, readFileSync } from "fs";
13
- import { extname, posix } from "path";
14
- import JSON5 from "json5";
14
+ import { dirname, extname, relative as relative2, resolve as resolve2 } from "path";
15
+ import remarkMdx from "remark-mdx";
16
+ import remarkParse from "remark-parse";
17
+ import ts from "typescript";
18
+ import { unified } from "unified";
19
+
20
+ // src/frontmatter.ts
15
21
  import { parse as parseYaml } from "yaml";
22
+ function parseFrontmatter(raw) {
23
+ const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
24
+ const opening = /^---([^\r\n]*)\r?\n/.exec(source);
25
+ if (!opening || opening[1].startsWith("-")) return { content: source, data: {} };
26
+ const language = opening[1].trim().toLowerCase();
27
+ const remainder = source.slice(opening[0].length);
28
+ const closing = /^---[ \t]*\r?$/m.exec(remainder);
29
+ const matter = closing ? remainder.slice(0, closing.index) : remainder;
30
+ let content = closing ? remainder.slice(closing.index + closing[0].length) : "";
31
+ if (content.startsWith("\r\n")) content = content.slice(2);
32
+ else if (content.startsWith("\n")) content = content.slice(1);
33
+ if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
34
+ return { content, data: {} };
35
+ }
36
+ const parsed = parseYaml(matter);
37
+ return {
38
+ content,
39
+ data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
40
+ };
41
+ }
16
42
 
17
43
  // src/path.ts
18
44
  import { isAbsolute, relative, resolve, sep } from "path";
@@ -51,6 +77,18 @@ function pageIdFromReference(value, preserveCase = false) {
51
77
  const segments = baseSegments.map((segment) => slugifySegment(segment, preserveCase)).filter(Boolean);
52
78
  return segments.join("/") || "introduction";
53
79
  }
80
+ function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__ */ new Set()) {
81
+ const reference = value.replace(/^\/+/, "");
82
+ const localePrefix = `${locale.toLowerCase()}/`;
83
+ if (reference.toLowerCase().startsWith(localePrefix)) return reference.slice(localePrefix.length);
84
+ const sectionPrefix = `${locale.toLowerCase()}-`;
85
+ if (reference.toLowerCase().startsWith(sectionPrefix)) {
86
+ const candidate = reference.slice(sectionPrefix.length);
87
+ const candidateId = pageIdFromReference(candidate, true);
88
+ if (candidateId && defaultPageIds.has(candidateId)) return candidate;
89
+ }
90
+ return value;
91
+ }
54
92
  function resolveWithin(root, candidate) {
55
93
  if (isAbsolute(candidate) || candidate.includes("\0")) {
56
94
  throw new Error(`Unsafe migration path: ${candidate}`);
@@ -70,7 +108,471 @@ function normalizeAssetPath(value) {
70
108
  return segments.join("/");
71
109
  }
72
110
 
111
+ // src/components.ts
112
+ var parser = unified().use(remarkParse).use(remarkMdx);
113
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs"]);
114
+ var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".css", ".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".woff", ".woff2"]);
115
+ var SHARED_IMPORTS = /* @__PURE__ */ new Set(["react", "react/jsx-runtime", "react/jsx-dev-runtime"]);
116
+ var REACT_GLOBALS = /* @__PURE__ */ new Set([
117
+ "useState",
118
+ "useEffect",
119
+ "useLayoutEffect",
120
+ "useMemo",
121
+ "useCallback",
122
+ "useRef",
123
+ "useReducer",
124
+ "useContext",
125
+ "useId",
126
+ "useTransition",
127
+ "useDeferredValue",
128
+ "useImperativeHandle",
129
+ "useSyncExternalStore",
130
+ "useInsertionEffect",
131
+ "useOptimistic",
132
+ "useActionState",
133
+ "use",
134
+ "createContext",
135
+ "forwardRef",
136
+ "memo"
137
+ ]);
138
+ var MAX_COMPONENT_FILES = 300;
139
+ var MAX_COMPONENT_BYTES = 2e7;
140
+ var MAX_FILE_BYTES = 2e6;
141
+ function applyReplacements(source, replacements) {
142
+ return replacements.sort((a, b) => b.start - a.start).reduce(
143
+ (text, edit) => text.slice(0, edit.start) + edit.value + text.slice(edit.end),
144
+ source
145
+ );
146
+ }
147
+ function hash(value) {
148
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
149
+ }
150
+ function portableSpecifier(path) {
151
+ return path.replace(/\.tsx?$/, "");
152
+ }
153
+ function sourceFile(source, filename) {
154
+ return ts.createSourceFile(
155
+ filename,
156
+ source,
157
+ ts.ScriptTarget.Latest,
158
+ true,
159
+ /\.[cm]?ts$/.test(filename) ? ts.ScriptKind.TS : ts.ScriptKind.TSX
160
+ );
161
+ }
162
+ function walk(node, visitor) {
163
+ visitor(node);
164
+ for (const child of node.children ?? []) walk(child, visitor);
165
+ }
166
+ function implicitReactImports(source) {
167
+ const bindings = /* @__PURE__ */ new Set();
168
+ const references = /* @__PURE__ */ new Set();
169
+ function bind(name) {
170
+ if (ts.isIdentifier(name)) bindings.add(name.text);
171
+ else for (const element of name.elements) if (ts.isBindingElement(element)) bind(element.name);
172
+ }
173
+ for (const statement of source.statements) {
174
+ if (ts.isImportDeclaration(statement) && statement.importClause) {
175
+ const clause = statement.importClause;
176
+ if (clause.name) bindings.add(clause.name.text);
177
+ if (clause.namedBindings) {
178
+ if (ts.isNamespaceImport(clause.namedBindings)) bindings.add(clause.namedBindings.name.text);
179
+ else for (const element of clause.namedBindings.elements) bindings.add(element.name.text);
180
+ }
181
+ }
182
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) bind(declaration.name);
183
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) bindings.add(statement.name.text);
184
+ }
185
+ function visit(node) {
186
+ if (ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts.isPropertyAssignment(node.parent) && node.parent.name === node)) references.add(node.text);
187
+ ts.forEachChild(node, visit);
188
+ }
189
+ visit(source);
190
+ const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
191
+ return [
192
+ references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
193
+ hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : ""
194
+ ].filter(Boolean).join("\n");
195
+ }
196
+ function imports(statement) {
197
+ if (!ts.isStringLiteral(statement.moduleSpecifier) || !statement.importClause) return [];
198
+ const clause = statement.importClause;
199
+ if (clause.isTypeOnly) return [];
200
+ const source = statement.moduleSpecifier.text;
201
+ const bindings = clause.name ? [{ local: clause.name.text, imported: "default", source }] : [];
202
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
203
+ for (const element of clause.namedBindings.elements) {
204
+ if (!element.isTypeOnly) bindings.push({ local: element.name.text, imported: element.propertyName?.text ?? element.name.text, source });
205
+ }
206
+ }
207
+ return bindings;
208
+ }
209
+ function createComponentMigrator(siteRoot, warnings, sourceIdentity) {
210
+ const root = resolve2(siteRoot);
211
+ const destinationRoot = `src/mdx/migrated/${hash(sourceIdentity)}`;
212
+ const copied = /* @__PURE__ */ new Map();
213
+ const registrations = /* @__PURE__ */ new Map();
214
+ let copiedBytes = 0;
215
+ function warn(message, source) {
216
+ warnings.push({ code: "unsupported-config", message, source: relative2(root, source).replace(/\\/g, "/") });
217
+ }
218
+ function checkedFile(path) {
219
+ const local = relative2(root, path);
220
+ resolveWithin(root, local);
221
+ let current = root;
222
+ if (lstatSync(current).isSymbolicLink()) throw new Error("symbolic links are not imported");
223
+ for (const segment of local.split(/[\\/]/)) {
224
+ current = resolveWithin(current, segment);
225
+ if (lstatSync(current).isSymbolicLink()) throw new Error("symbolic links are not imported");
226
+ }
227
+ if (!lstatSync(path).isFile()) throw new Error("dependency is not a regular file");
228
+ return path;
229
+ }
230
+ function resolveDependency(specifier, importer) {
231
+ if (specifier.includes("\\") || specifier.includes("\0") || /[?#]/.test(specifier)) throw new Error("unsupported component dependency path");
232
+ const candidate = specifier.startsWith("/") ? resolveWithin(root, specifier.slice(1)) : resolveWithin(root, relative2(root, resolve2(dirname(importer), specifier)));
233
+ const candidates = [candidate];
234
+ if (!extname(candidate)) {
235
+ candidates.push(...[".tsx", ".jsx", ".ts", ".js", ".mjs", ".json"].map((extension) => candidate + extension));
236
+ candidates.push(...["index.tsx", "index.jsx", "index.ts", "index.js"].map((name) => resolve2(candidate, name)));
237
+ } else if (extname(candidate) === ".js") {
238
+ candidates.push(candidate.slice(0, -3) + ".ts", candidate.slice(0, -3) + ".tsx");
239
+ }
240
+ for (const path of candidates) {
241
+ if (existsSync(path) && lstatSync(path).isFile()) return checkedFile(path);
242
+ }
243
+ throw new Error(`component dependency ${specifier} was not found`);
244
+ }
245
+ function outputPath(path) {
246
+ return `${destinationRoot}/source/${relative2(root, path).replace(/\\/g, "/")}`;
247
+ }
248
+ function copyGraph(entry) {
249
+ const staged = /* @__PURE__ */ new Map();
250
+ let stagedBytes = 0;
251
+ function visit(path) {
252
+ const destination = outputPath(path);
253
+ if (copied.has(destination) || staged.has(destination)) return;
254
+ checkedFile(path);
255
+ const size = lstatSync(path).size;
256
+ if (size > MAX_FILE_BYTES || copiedBytes + stagedBytes + size > MAX_COMPONENT_BYTES || copied.size + staged.size >= MAX_COMPONENT_FILES) throw new Error("component migration budget exceeded");
257
+ const extension = extname(path).toLowerCase();
258
+ if (!CODE_EXTENSIONS.has(extension) && !DATA_EXTENSIONS.has(extension)) throw new Error(`unsupported dependency file type ${extension}`);
259
+ stagedBytes += size;
260
+ const content = readFileSync(path);
261
+ staged.set(destination, { path: destination, content });
262
+ if (!CODE_EXTENSIONS.has(extension)) {
263
+ if (extension === ".css" && /@import\b|url\s*\(/i.test(content.toString("utf8"))) throw new Error("CSS resource dependencies require manual migration");
264
+ return;
265
+ }
266
+ const text = content.toString("utf8");
267
+ const syntax = ts.transpileModule(text, {
268
+ fileName: path,
269
+ reportDiagnostics: true,
270
+ compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext }
271
+ });
272
+ if (syntax.diagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) throw new Error("component source contains unsupported JavaScript or TypeScript syntax");
273
+ const ast = sourceFile(text, path);
274
+ if (ast.statements.some((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression) && statement.expression.text === "use server")) throw new Error("server-only component modules require manual migration");
275
+ const edits = [];
276
+ function dependency(literal) {
277
+ const specifier = literal.text;
278
+ if (SHARED_IMPORTS.has(specifier)) return;
279
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
280
+ throw new Error(`external package ${specifier} requires manual installation and review`);
281
+ }
282
+ const target = resolveDependency(specifier, path);
283
+ visit(target);
284
+ const nextPath = relative2(dirname(destination), outputPath(target)).replace(/\\/g, "/");
285
+ edits.push({ start: literal.getStart(ast), end: literal.end, value: JSON.stringify(portableSpecifier(nextPath.startsWith(".") ? nextPath : `./${nextPath}`)) });
286
+ }
287
+ function inspect(node) {
288
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) dependency(node.moduleSpecifier);
289
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require")) {
290
+ const argument = node.arguments[0];
291
+ if (node.arguments.length !== 1 || !argument || !ts.isStringLiteralLike(argument)) throw new Error("computed component imports require manual migration");
292
+ dependency(argument);
293
+ }
294
+ ts.forEachChild(node, inspect);
295
+ }
296
+ inspect(ast);
297
+ staged.set(destination, { path: destination, content: `'use client';
298
+
299
+ ${implicitReactImports(ast)}
300
+ ${applyReplacements(text, edits)}` });
301
+ }
302
+ visit(entry);
303
+ for (const [path, file] of staged) copied.set(path, file);
304
+ copiedBytes += stagedBytes;
305
+ return outputPath(entry);
306
+ }
307
+ function register(path, imported) {
308
+ const name = `Migrated${hash(`${path}:${imported}`)}`;
309
+ registrations.set(name, { path, imported });
310
+ return name;
311
+ }
312
+ function transform(raw, currentFile) {
313
+ const content = parseFrontmatter(raw).content;
314
+ const frontmatter = raw.slice(0, raw.length - content.length);
315
+ let tree;
316
+ try {
317
+ tree = parser.parse(content);
318
+ } catch {
319
+ warn("Custom component analysis could not parse this MDX; its source was preserved for manual migration.", currentFile);
320
+ return raw;
321
+ }
322
+ const aliases = /* @__PURE__ */ new Map();
323
+ const edits = [];
324
+ const declarations = [];
325
+ const moduleImports = [];
326
+ const sharedImportEdits = [];
327
+ let hasUnsupportedImports = false;
328
+ function hasExpressionReference(names, options = {}) {
329
+ let found = false;
330
+ function inspect2(node) {
331
+ if (ts.isIdentifier(node) && names.has(node.text)) found = true;
332
+ ts.forEachChild(node, inspect2);
333
+ }
334
+ walk(tree, (node) => {
335
+ if (options.excludedNodes?.some((excluded) => node.position?.start.offset !== void 0 && node.position.end.offset !== void 0 && node.position.start.offset >= excluded.position.start.offset && node.position.end.offset <= excluded.position.end.offset)) return;
336
+ if (node.name && [...names].some((name) => node.name.startsWith(`${name}.`) || options.includeDirectTags && node.name === name)) found = true;
337
+ if (options.includeDeclarations !== false && node.type === "mdxjsEsm" && node.value) {
338
+ for (const statement of sourceFile(node.value, "expression.tsx").statements) {
339
+ if (!ts.isImportDeclaration(statement)) inspect2(statement);
340
+ }
341
+ }
342
+ const expressions = [
343
+ ...["mdxFlowExpression", "mdxTextExpression"].includes(node.type) ? [node.value ?? ""] : [],
344
+ ...(node.attributes ?? []).flatMap((attribute) => {
345
+ if (attribute.type === "mdxJsxExpressionAttribute" && typeof attribute.value === "string") return [attribute.value];
346
+ return attribute.value && typeof attribute.value === "object" ? [attribute.value.value ?? ""] : [];
347
+ })
348
+ ];
349
+ for (const expression of expressions) inspect2(sourceFile(expression, "expression.tsx"));
350
+ });
351
+ return found;
352
+ }
353
+ for (const node of tree.children ?? []) {
354
+ if (node.type !== "mdxjsEsm" || node.value === void 0 || node.position?.start.offset === void 0) continue;
355
+ const ast = sourceFile(node.value, "inline.tsx");
356
+ for (const statement of ast.statements) {
357
+ if (!ts.isImportDeclaration(statement)) {
358
+ let inspect2 = function(node2) {
359
+ if (ts.isCallExpression(node2) && (node2.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node2.expression) && node2.expression.text === "require")) unsupportedDependency = true;
360
+ ts.forEachChild(node2, inspect2);
361
+ };
362
+ var inspect = inspect2;
363
+ let unsupportedDependency = ts.isExportDeclaration(statement) && !!statement.moduleSpecifier;
364
+ inspect2(statement);
365
+ if (unsupportedDependency) {
366
+ hasUnsupportedImports = true;
367
+ warn("Inline MDX module dependencies require manual extraction; source was preserved.", currentFile);
368
+ }
369
+ declarations.push({
370
+ start: node.position.start.offset + statement.getStart(ast),
371
+ end: node.position.start.offset + statement.end,
372
+ source: statement.getText(ast)
373
+ });
374
+ continue;
375
+ }
376
+ const bindings = imports(statement);
377
+ const specifier = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
378
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
379
+ if (SHARED_IMPORTS.has(specifier)) {
380
+ moduleImports.push(statement.getText(ast));
381
+ sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
382
+ } else {
383
+ hasUnsupportedImports = true;
384
+ warn(`MDX import ${specifier} requires manual package installation or registration; the import was preserved.`, currentFile);
385
+ }
386
+ continue;
387
+ }
388
+ if (/\.mdx?$/.test(specifier)) continue;
389
+ if (!bindings.length || statement.importClause?.namedBindings && ts.isNamespaceImport(statement.importClause.namedBindings)) {
390
+ warn("Namespace or side-effect MDX imports require manual registration; the import was preserved.", currentFile);
391
+ hasUnsupportedImports = true;
392
+ continue;
393
+ }
394
+ if (bindings.some((binding) => !/^[A-Z]/.test(binding.local))) {
395
+ warn("MDX imports containing non-component values require manual migration; the import was preserved.", currentFile);
396
+ hasUnsupportedImports = true;
397
+ continue;
398
+ }
399
+ if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
400
+ warn("Imported components used in MDX expressions, component props, or member tags require manual migration; the import was preserved.", currentFile);
401
+ hasUnsupportedImports = true;
402
+ continue;
403
+ }
404
+ try {
405
+ const path = copyGraph(resolveDependency(specifier, currentFile));
406
+ for (const binding of bindings) {
407
+ const name = register(path, binding.imported);
408
+ aliases.set(binding.local, name);
409
+ moduleImports.push(`import { ${binding.imported} as ${binding.local} } from ${JSON.stringify(portableSpecifier(`./${relative2(destinationRoot, path).replace(/\\/g, "/")}`))};`);
410
+ }
411
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
412
+ } catch (error) {
413
+ hasUnsupportedImports = true;
414
+ warn(`Custom component was preserved for manual migration: ${error instanceof Error ? error.message : "unsupported dependency"}.`, currentFile);
415
+ }
416
+ }
417
+ }
418
+ const extracted = [];
419
+ for (const candidate of tree.children ?? []) {
420
+ const node = candidate.type === "paragraph" && candidate.children?.length === 1 ? candidate.children[0] : candidate;
421
+ if (!["mdxJsxFlowElement", "mdxJsxTextElement"].includes(node.type) || node.position?.start.offset === void 0 || node.position.end.offset === void 0) continue;
422
+ let interactive = false;
423
+ let supported = true;
424
+ walk(node, (child) => {
425
+ if (child.attributes?.some((attribute) => /^on[A-Z]/.test(attribute.name ?? ""))) interactive = true;
426
+ if (child.name && /^[A-Z]/.test(child.name) && !aliases.has(child.name)) supported = false;
427
+ if (!["mdxJsxFlowElement", "mdxJsxTextElement", "mdxFlowExpression", "mdxTextExpression", "text", "paragraph"].includes(child.type)) supported = false;
428
+ });
429
+ if (!interactive) continue;
430
+ if (!supported || hasUnsupportedImports) {
431
+ warn("Interactive MDX containing Markdown or unregistered components requires manual extraction; source was preserved.", currentFile);
432
+ continue;
433
+ }
434
+ const jsx = content.slice(node.position.start.offset, node.position.end.offset);
435
+ const result = ts.transpileModule(`export default function Inline() { return (${jsx}); }`, {
436
+ fileName: "inline.jsx",
437
+ compilerOptions: { jsx: ts.JsxEmit.Preserve },
438
+ reportDiagnostics: true
439
+ });
440
+ if (result.diagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
441
+ warn("Interactive MDX could not be safely extracted as JSX; source was preserved.", currentFile);
442
+ continue;
443
+ }
444
+ extracted.push({ node, name: `Inline${extracted.length}`, jsx });
445
+ }
446
+ if (extracted.length && declarations.length) {
447
+ let bind2 = function(name) {
448
+ if (ts.isIdentifier(name)) declared.add(name.text);
449
+ else for (const element of name.elements) if (ts.isBindingElement(element)) bind2(element.name);
450
+ };
451
+ var bind = bind2;
452
+ const declared = /* @__PURE__ */ new Set();
453
+ for (const declaration of declarations) {
454
+ for (const statement of sourceFile(declaration.source, "declaration.tsx").statements) {
455
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) declared.add(statement.name.text);
456
+ if (ts.isVariableStatement(statement)) for (const variable of statement.declarationList.declarations) bind2(variable.name);
457
+ }
458
+ }
459
+ const hasSharedDeclarations = hasExpressionReference(declared, {
460
+ excludedNodes: extracted.map((entry) => entry.node),
461
+ includeDeclarations: false,
462
+ includeDirectTags: true
463
+ });
464
+ if (hasSharedDeclarations) {
465
+ warn("Interactive MDX shares declarations with remaining page content; source was preserved for manual extraction.", currentFile);
466
+ extracted.length = 0;
467
+ }
468
+ }
469
+ if (extracted.length && (copied.size >= MAX_COMPONENT_FILES || Buffer.byteLength(content) > MAX_FILE_BYTES || copiedBytes + Buffer.byteLength(content) > MAX_COMPONENT_BYTES)) {
470
+ warn("Interactive MDX exceeded the component migration budget; source was preserved.", currentFile);
471
+ extracted.length = 0;
472
+ }
473
+ if (extracted.length) {
474
+ const path = `${destinationRoot}/inline-${hash(relative2(root, currentFile))}.jsx`;
475
+ let declarationSource = [...new Set(declarations.map((entry) => entry.source))].join("\n\n");
476
+ declarationSource = declarationSource.replace(
477
+ /document\.getElementById\(\s*(['"])search-bar-entry\1\s*\)\.click\(\s*\)/g,
478
+ `document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }))`
479
+ );
480
+ const inlineSource = [
481
+ "'use client';",
482
+ "",
483
+ ...moduleImports,
484
+ "",
485
+ declarationSource,
486
+ "",
487
+ ...extracted.map(({ name, jsx }) => `export function ${name}() {
488
+ return (${jsx});
489
+ }`),
490
+ ""
491
+ ].join("\n");
492
+ copied.set(path, { path, content: `'use client';
493
+ ${implicitReactImports(sourceFile(inlineSource, "inline.jsx"))}
494
+ ${inlineSource}` });
495
+ copiedBytes += Buffer.byteLength(inlineSource);
496
+ for (const { node, name } of extracted) {
497
+ edits.push({ start: node.position.start.offset, end: node.position.end.offset, value: `<${register(path, name)} />` });
498
+ }
499
+ edits.push(...declarations.map(({ start, end }) => ({ start, end, value: "" })), ...sharedImportEdits);
500
+ }
501
+ walk(tree, (node) => {
502
+ const replacement = node.name ? aliases.get(node.name) : void 0;
503
+ if (!replacement || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
504
+ const start = node.position.start.offset;
505
+ const end = node.position.end.offset;
506
+ if (extracted.some((item) => start >= item.node.position.start.offset && end <= item.node.position.end.offset)) return;
507
+ const text = content.slice(start, end);
508
+ const opening = text.indexOf(`<${node.name}`);
509
+ if (opening >= 0) edits.push({ start: start + opening + 1, end: start + opening + 1 + node.name.length, value: replacement });
510
+ const closing = text.lastIndexOf(`</${node.name}`);
511
+ if (closing >= 0) edits.push({ start: start + closing + 2, end: start + closing + 2 + node.name.length, value: replacement });
512
+ });
513
+ return frontmatter + applyReplacements(content, edits);
514
+ }
515
+ function files() {
516
+ if (!registrations.size) return [];
517
+ const entries = [...registrations.entries()].sort(([a], [b]) => a.localeCompare(b));
518
+ return [...copied.values(), {
519
+ path: "src/mdx/custom-components.tsx",
520
+ content: [
521
+ "/** Repository components preserved by migration; this registry is customer-owned. */",
522
+ "import type { MDXComponents } from 'mdx/types'",
523
+ ...entries.map(([name, binding]) => {
524
+ const path = portableSpecifier(`./${relative2("src/mdx", binding.path).replace(/\\/g, "/")}`);
525
+ return `import { ${binding.imported} as ${name} } from ${JSON.stringify(path)}`;
526
+ }),
527
+ "",
528
+ "export const customComponents: MDXComponents = {",
529
+ ...entries.map(([name]) => ` ${name},`),
530
+ "}",
531
+ ""
532
+ ].join("\n")
533
+ }];
534
+ }
535
+ return { transform, files };
536
+ }
537
+ function mergeComponentRegistry(existing, incoming) {
538
+ const fingerprint = hash(incoming);
539
+ const name = `MigratedRegistry${fingerprint}`;
540
+ const registryPath = `./migrated-components-${fingerprint}`;
541
+ const source = sourceFile(existing, "custom-components.tsx");
542
+ const alreadyImported = source.statements.some((statement) => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === registryPath);
543
+ let initializer;
544
+ for (const statement of source.statements) {
545
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
546
+ for (const declaration of statement.declarationList.declarations) {
547
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === "customComponents") initializer = declaration.initializer;
548
+ }
549
+ }
550
+ while (initializer && (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer) || ts.isParenthesizedExpression(initializer))) initializer = initializer.expression;
551
+ if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
552
+ throw new Error("Cannot safely merge src/mdx/custom-components.tsx: export customComponents as an object literal before importing components. The existing registry was preserved.");
553
+ }
554
+ const content = alreadyImported ? existing : applyReplacements(existing, [
555
+ {
556
+ start: source.statements.filter((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
557
+ end: source.statements.filter((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)).at(-1)?.end ?? 0,
558
+ value: `
559
+ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
560
+ `
561
+ },
562
+ { start: initializer.getStart(source) + 1, end: initializer.getStart(source) + 1, value: `
563
+ ...${name},` }
564
+ ]);
565
+ return [
566
+ { path: `src/mdx/migrated-components-${fingerprint}.tsx`, content: incoming },
567
+ { path: "src/mdx/custom-components.tsx", content }
568
+ ];
569
+ }
570
+
73
571
  // src/docusaurus.ts
572
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
573
+ import { extname as extname2, posix } from "path";
574
+ import JSON5 from "json5";
575
+ import { parse as parseYaml2 } from "yaml";
74
576
  var MAX_CONFIG_BYTES = 1e6;
75
577
  var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
76
578
  var SIDEBAR_FILENAMES = [
@@ -184,10 +686,10 @@ function rewriteDocusaurusLinks(body, current, descriptors) {
184
686
  }).join("\n");
185
687
  }
186
688
  function readBoundedText(path) {
187
- if (lstatSync(path).size > MAX_CONFIG_BYTES) {
689
+ if (lstatSync2(path).size > MAX_CONFIG_BYTES) {
188
690
  throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
189
691
  }
190
- return readFileSync(path, "utf8");
692
+ return readFileSync2(path, "utf8");
191
693
  }
192
694
  function matchingObjectLiteral(source, start) {
193
695
  let depth = 0;
@@ -342,33 +844,33 @@ function matchingArrayLiteral(source, start) {
342
844
  function configuredSidebarPath(repositoryRoot) {
343
845
  for (const filename of ["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"]) {
344
846
  const path = resolveWithin(repositoryRoot, filename);
345
- if (!existsSync(path)) continue;
847
+ if (!existsSync2(path)) continue;
346
848
  const source = readBoundedText(path);
347
849
  const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
348
850
  if (!match) continue;
349
851
  const candidate = match[2].replace(/^\.\//, "");
350
852
  const resolved = resolveWithin(repositoryRoot, candidate);
351
- if (existsSync(resolved) && lstatSync(resolved).isFile()) return candidate;
853
+ if (existsSync2(resolved) && lstatSync2(resolved).isFile()) return candidate;
352
854
  }
353
855
  return null;
354
856
  }
355
857
  function readDocusaurusSidebars(repositoryRoot) {
356
858
  const configured = configuredSidebarPath(repositoryRoot);
357
- const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync(resolveWithin(repositoryRoot, candidate)));
859
+ const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync2(resolveWithin(repositoryRoot, candidate)));
358
860
  if (!sourcePath) return null;
359
861
  const absolutePath = resolveWithin(repositoryRoot, sourcePath);
360
862
  const source = readBoundedText(absolutePath);
361
- const parsed = extname(sourcePath).toLowerCase() === ".json" ? objectValue(JSON5.parse(source)) : parseStaticSidebarModule(source);
863
+ const parsed = extname2(sourcePath).toLowerCase() === ".json" ? objectValue(JSON5.parse(source)) : parseStaticSidebarModule(source);
362
864
  if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
363
865
  return { config: parsed, sourcePath };
364
866
  }
365
867
  function readCategoryMetadata(contentRoot, directory) {
366
868
  for (const filename of CATEGORY_FILENAMES) {
367
869
  const path = resolveWithin(contentRoot, posix.join(directory, filename));
368
- if (!existsSync(path) || !lstatSync(path).isFile()) continue;
870
+ if (!existsSync2(path) || !lstatSync2(path).isFile()) continue;
369
871
  try {
370
872
  const raw = readBoundedText(path);
371
- const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml(raw);
873
+ const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml2(raw);
372
874
  const object = objectValue(parsed);
373
875
  if (!object) return {};
374
876
  return {
@@ -533,42 +1035,39 @@ function projectDocusaurusNavigation(input) {
533
1035
  };
534
1036
  }
535
1037
 
536
- // src/frontmatter.ts
537
- import { parse as parseYaml2 } from "yaml";
538
- function parseFrontmatter(raw) {
539
- const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
540
- const opening = /^---([^\r\n]*)\r?\n/.exec(source);
541
- if (!opening || opening[1].startsWith("-")) return { content: source, data: {} };
542
- const language = opening[1].trim().toLowerCase();
543
- const remainder = source.slice(opening[0].length);
544
- const closing = /^---[ \t]*\r?$/m.exec(remainder);
545
- const matter = closing ? remainder.slice(0, closing.index) : remainder;
546
- let content = closing ? remainder.slice(closing.index + closing[0].length) : "";
547
- if (content.startsWith("\r\n")) content = content.slice(2);
548
- else if (content.startsWith("\n")) content = content.slice(1);
549
- if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
550
- return { content, data: {} };
551
- }
552
- const parsed = parseYaml2(matter);
553
- return {
554
- content,
555
- data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
556
- };
557
- }
558
-
559
1038
  // src/mdx.ts
1039
+ import remarkMdx2 from "remark-mdx";
1040
+ import remarkParse2 from "remark-parse";
1041
+ import { unified as unified2 } from "unified";
560
1042
  function titleFromId(id) {
561
1043
  const value = id.split("/").at(-1) ?? id;
562
1044
  if (value === "introduction") return "Introduction";
563
1045
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
564
1046
  }
1047
+ var descriptionParser = unified2().use(remarkParse2).use(remarkMdx2);
565
1048
  function firstParagraph(content) {
566
- for (const block of content.split(/\n\s*\n/)) {
567
- const value = block.replace(/\s+/g, " ").trim();
568
- if (!value || /^(?:#|```|:::|<|import\s|export\s)/.test(value)) continue;
569
- return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 240);
1049
+ function plainText(node) {
1050
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
1051
+ if (node.type === "image") return node.alt ?? "";
1052
+ if (node.type === "break") return " ";
1053
+ return (node.children ?? []).map(plainText).join("");
1054
+ }
1055
+ function findParagraph(node) {
1056
+ if (node.type === "paragraph") {
1057
+ const value = plainText(node).replace(/\s+/g, " ").trim();
1058
+ if (value) return value.slice(0, 240);
1059
+ }
1060
+ for (const child of node.children ?? []) {
1061
+ const value = findParagraph(child);
1062
+ if (value) return value;
1063
+ }
1064
+ return "";
1065
+ }
1066
+ try {
1067
+ return findParagraph(descriptionParser.parse(content));
1068
+ } catch {
1069
+ return "";
570
1070
  }
571
- return "";
572
1071
  }
573
1072
  function docusaurusAdmonitionTag(kind) {
574
1073
  if (kind === "danger") return "Error";
@@ -605,8 +1104,30 @@ function normalizeDocusaurusAdmonitions(body) {
605
1104
  return line;
606
1105
  }).join("\n");
607
1106
  }
1107
+ var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
1108
+ "Tabs",
1109
+ "TabItem",
1110
+ "Link",
1111
+ "DocCardList",
1112
+ "TOCInline"
1113
+ ]);
1114
+ function isGlobalDocusaurusImport(line) {
1115
+ const trimmed = line.trim().replace(/;$/, "").trimEnd();
1116
+ if (!trimmed.startsWith("import ")) return false;
1117
+ const separator = trimmed.indexOf(" from ", "import ".length);
1118
+ if (separator < 0) return false;
1119
+ const component = trimmed.slice("import ".length, separator).trim();
1120
+ if (!GLOBAL_DOCUSARUS_COMPONENTS.has(component)) return false;
1121
+ const source = trimmed.slice(separator + " from ".length);
1122
+ if (source.length < 3) return false;
1123
+ const quote = source[0];
1124
+ if (quote !== "'" && quote !== '"' || source.at(-1) !== quote) return false;
1125
+ const moduleName = source.slice(1, -1);
1126
+ return moduleName.startsWith("@theme/") || moduleName.startsWith("@docusaurus/");
1127
+ }
608
1128
  function normalizeMdx(body) {
609
- 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) => {
1129
+ const withoutGlobalImports = body.split("\n").filter((line) => !isGlobalDocusaurusImport(line)).join("\n");
1130
+ return normalizeDocusaurusAdmonitions(withoutGlobalImports).replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
610
1131
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
611
1132
  return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
612
1133
  }).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(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").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>");
@@ -646,24 +1167,94 @@ function parseMarkdownPage(input) {
646
1167
  }
647
1168
 
648
1169
  // src/navigation.ts
649
- import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
650
- import { dirname, extname as extname2, relative as relative2 } from "path";
1170
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, realpathSync } from "fs";
1171
+ import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
651
1172
  var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
652
1173
  function addMintlifyDirectoryRedirects(config, pages) {
653
- const pageIds = new Set(pages.filter((page) => !page.locale).map((page) => page.navigationId));
1174
+ const pageIds = new Set(pages.map((page) => page.id));
654
1175
  const redirects = [...config.redirects ?? []];
655
1176
  const redirectSources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
1177
+ function hasAuthoredRedirect(source) {
1178
+ if (redirectSources.has(source)) return true;
1179
+ return (config.redirects ?? []).some((redirect) => {
1180
+ const match = redirect.source.match(/^(.*)\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/);
1181
+ return match ? source === match[1] || source.startsWith(`${match[1]}/`) : false;
1182
+ });
1183
+ }
656
1184
  for (const page of pages) {
657
- if (page.locale || !/(?:^|\/)(?:overview|introduction)$/.test(page.navigationId)) continue;
658
- const parent = page.navigationId.replace(/\/(?:overview|introduction)$/, "");
1185
+ if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
1186
+ const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
659
1187
  if (!parent || pageIds.has(parent)) continue;
660
1188
  const source = `/${parent}`;
661
- if (redirectSources.has(source)) continue;
662
- redirects.push({ source, destination: `/${page.navigationId}`, permanent: false });
1189
+ if (hasAuthoredRedirect(source)) continue;
1190
+ redirects.push({ source, destination: `/${page.id}`, permanent: false });
1191
+ redirectSources.add(source);
1192
+ }
1193
+ const navigationOrder = [];
1194
+ function collectPages(nodes) {
1195
+ for (const node of nodes) {
1196
+ if (typeof node === "string") navigationOrder.push(node);
1197
+ else if (!node.hidden) collectPages(node.pages);
1198
+ }
1199
+ }
1200
+ for (const tab of config.tabs) {
1201
+ if (!tab.hidden) collectPages([...tab.pages ?? [], ...tab.groups ?? []]);
1202
+ }
1203
+ const pagesByNavigationId = /* @__PURE__ */ new Map();
1204
+ for (const page of pages) {
1205
+ if (page.hidden) continue;
1206
+ const variants = pagesByNavigationId.get(page.navigationId) ?? [];
1207
+ variants.push(page);
1208
+ pagesByNavigationId.set(page.navigationId, variants);
1209
+ }
1210
+ const firstDescendantByDirectory = /* @__PURE__ */ new Map();
1211
+ for (const navigationId of navigationOrder) {
1212
+ for (const page of pagesByNavigationId.get(navigationId) ?? []) {
1213
+ const segments = page.id.split("/");
1214
+ for (let length = 1; length < segments.length; length++) {
1215
+ const directory = segments.slice(0, length).join("/");
1216
+ if (!firstDescendantByDirectory.has(directory)) firstDescendantByDirectory.set(directory, page);
1217
+ }
1218
+ }
1219
+ }
1220
+ for (const redirect of config.redirects ?? []) {
1221
+ const destination = redirect.destination.split(/[?#]/, 1)[0].replace(/\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/, "");
1222
+ if (!destination.startsWith("/") || destination.startsWith("//") || /[:*()[\]{}]/.test(destination)) continue;
1223
+ const directory = trimEdgeSlashes(destination);
1224
+ if (!directory || pageIds.has(directory)) continue;
1225
+ const source = `/${directory}`;
1226
+ if (hasAuthoredRedirect(source)) continue;
1227
+ const landing = firstDescendantByDirectory.get(directory);
1228
+ if (!landing) continue;
1229
+ redirects.push({ source, destination: `/${landing.id}`, permanent: false });
663
1230
  redirectSources.add(source);
664
1231
  }
665
1232
  return redirects.length > 0 ? { ...config, redirects } : config;
666
1233
  }
1234
+ function addMintlifyHomepageRedirects(config, pages) {
1235
+ const defaultPages = new Set(pages.filter((page) => !page.locale || page.locale === config.i18n?.defaultLocale).map((page) => page.navigationId));
1236
+ function firstPage(nodes) {
1237
+ for (const node of nodes) {
1238
+ if (typeof node === "string") {
1239
+ if (defaultPages.has(node)) return node;
1240
+ } else if (!node.hidden) {
1241
+ const page = firstPage(node.pages);
1242
+ if (page) return page;
1243
+ }
1244
+ }
1245
+ }
1246
+ const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
1247
+ if (!homepage || homepage === "introduction") return config;
1248
+ const redirects = [...config.redirects ?? []];
1249
+ const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
1250
+ const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).map((locale) => locale.code)];
1251
+ for (const locale of roots) {
1252
+ const source = locale ? `/${locale}` : "/";
1253
+ if (sources.has(source)) continue;
1254
+ redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: false });
1255
+ }
1256
+ return { ...config, redirects };
1257
+ }
667
1258
  var LANGUAGE_LABELS = {
668
1259
  ar: "Arabic",
669
1260
  de: "German",
@@ -717,16 +1308,16 @@ function jsonPointer(root, pointer) {
717
1308
  }, root);
718
1309
  }
719
1310
  function readMintlifyJson(path, repositoryRoot) {
720
- if (!existsSync2(path) || !lstatSync2(path).isFile()) {
721
- throw new Error(`Mintlify config reference is not a regular file: ${relative2(repositoryRoot, path)}`);
1311
+ if (!existsSync3(path) || !lstatSync3(path).isFile()) {
1312
+ throw new Error(`Mintlify config reference is not a regular file: ${relative3(repositoryRoot, path)}`);
722
1313
  }
723
1314
  const realPath = realpathSync(path);
724
1315
  const realRoot = realpathSync(repositoryRoot);
725
- resolveWithin(realRoot, relative2(realRoot, realPath));
726
- if (lstatSync2(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
727
- throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative2(repositoryRoot, path)}`);
1316
+ resolveWithin(realRoot, relative3(realRoot, realPath));
1317
+ if (lstatSync3(realPath).size > MAX_MINTLIFY_CONFIG_BYTES) {
1318
+ throw new Error(`Mintlify config reference exceeded ${MAX_MINTLIFY_CONFIG_BYTES / 1e6} MB: ${relative3(repositoryRoot, path)}`);
728
1319
  }
729
- return JSON.parse(readFileSync2(realPath, "utf8"));
1320
+ return JSON.parse(readFileSync3(realPath, "utf8"));
730
1321
  }
731
1322
  function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
732
1323
  if (Array.isArray(value)) {
@@ -736,8 +1327,8 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
736
1327
  if (!object) return value;
737
1328
  if (typeof object.$ref === "string") {
738
1329
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
739
- const referencedFile = filePart ? resolveWithin(dirname(currentFile), filePart) : currentFile;
740
- const relativePath = relative2(repositoryRoot, referencedFile);
1330
+ const referencedFile = filePart ? resolveWithin(dirname2(currentFile), filePart) : currentFile;
1331
+ const relativePath = relative3(repositoryRoot, referencedFile);
741
1332
  resolveWithin(repositoryRoot, relativePath);
742
1333
  const stackKey = `${referencedFile}#${pointer}`;
743
1334
  if (stack.has(stackKey)) throw new Error(`Circular Mintlify $ref: ${object.$ref}`);
@@ -762,7 +1353,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
762
1353
  );
763
1354
  }
764
1355
  function readMintlifyConfig(repositoryRoot) {
765
- const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync2);
1356
+ const configPath = ["docs.json", "mint.json"].map((filename) => resolveWithin(repositoryRoot, filename)).find(existsSync3);
766
1357
  if (!configPath) return null;
767
1358
  const raw = readMintlifyJson(configPath, repositoryRoot);
768
1359
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
@@ -794,8 +1385,7 @@ function containerPresentation(value) {
794
1385
  };
795
1386
  }
796
1387
  function registerReference(value, context) {
797
- const localePrefix = context.locale ? `${context.locale}/` : "";
798
- const localizedValue = localePrefix && value.replace(/^\/+/, "").startsWith(localePrefix) ? value.replace(/^\/+/, "").slice(localePrefix.length) : value;
1388
+ const localizedValue = context.locale ? mintlifyLocalizedReference(value, context.locale, context.defaultPageIds) : value;
799
1389
  const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
800
1390
  if (!navigationId) return null;
801
1391
  const key = `${context.locale ?? ""}:${value}`;
@@ -1003,6 +1593,8 @@ function projectedCompatibleConfig(config) {
1003
1593
  const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
1004
1594
  const feedback = objectValue2(config.feedback);
1005
1595
  const seo = objectValue2(config.seo);
1596
+ const iconLibrary = objectValue2(config.icons)?.library;
1597
+ const projectedIconLibrary = ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
1006
1598
  const navbar = projectedNavbar(config.navbar);
1007
1599
  const globalLinks = projectedGlobalNavigationLinks(objectValue2(config.navigation)?.global);
1008
1600
  const navbarLinks = [...new Map(
@@ -1011,6 +1603,7 @@ function projectedCompatibleConfig(config) {
1011
1603
  const projectedNavigation = navbarLinks.length > 0 || navbar?.primary ? { ...navbarLinks.length > 0 ? { links: navbarLinks } : {}, ...navbar?.primary ? { primary: navbar.primary } : {} } : void 0;
1012
1604
  return {
1013
1605
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
1606
+ ...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
1014
1607
  ...bannerContent ? {
1015
1608
  banner: {
1016
1609
  content: bannerContent,
@@ -1037,15 +1630,25 @@ function projectMintlifyNavigation(config, options = {}) {
1037
1630
  const seenReferences = /* @__PURE__ */ new Set();
1038
1631
  const warningKeys = /* @__PURE__ */ new Set();
1039
1632
  const navigation = objectValue2(config.navigation) ?? config;
1040
- const languages = Array.isArray(navigation.languages) ? navigation.languages.map(objectValue2).filter((value) => Boolean(value)) : [];
1633
+ const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
1634
+ const language = objectValue2(value);
1635
+ if (!language) return [];
1636
+ try {
1637
+ const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
1638
+ return [{ ...language, language: code }];
1639
+ } catch {
1640
+ warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
1641
+ return [];
1642
+ }
1643
+ }) : [];
1041
1644
  let tabs = [];
1042
1645
  let i18n;
1043
1646
  const projectionTrace = {};
1044
1647
  if (languages.length > 0) {
1045
1648
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
1046
- const defaultLocale = String(defaultLanguage.language ?? defaultLanguage.locale ?? "en");
1649
+ const defaultLocale = String(defaultLanguage.language);
1047
1650
  const locales = languages.map((entry) => {
1048
- const code = String(entry.language ?? entry.locale ?? "en");
1651
+ const code = String(entry.language);
1049
1652
  let label = LANGUAGE_LABELS[code];
1050
1653
  try {
1051
1654
  label ??= new Intl.DisplayNames(["en"], { type: "language" }).of(code) ?? void 0;
@@ -1054,10 +1657,12 @@ function projectMintlifyNavigation(config, options = {}) {
1054
1657
  return { code, label: label ?? code.toUpperCase() };
1055
1658
  });
1056
1659
  i18n = { defaultLocale, locales };
1057
- for (const language of languages) {
1058
- const locale = String(language.language ?? language.locale ?? defaultLocale);
1660
+ const defaultPageIds = /* @__PURE__ */ new Set();
1661
+ for (const language of [defaultLanguage, ...languages.filter((entry) => entry !== defaultLanguage)]) {
1662
+ const locale = String(language.language);
1059
1663
  const context = {
1060
1664
  locale,
1665
+ defaultPageIds,
1061
1666
  pathPrefix: options.pathPrefix,
1062
1667
  references,
1063
1668
  seenReferences,
@@ -1070,7 +1675,10 @@ function projectMintlifyNavigation(config, options = {}) {
1070
1675
  "Documentation",
1071
1676
  language === defaultLanguage ? projectionTrace : void 0
1072
1677
  );
1073
- if (language === defaultLanguage) tabs = languageTabs;
1678
+ if (language === defaultLanguage) {
1679
+ tabs = languageTabs;
1680
+ for (const reference of references) defaultPageIds.add(reference.navigationId);
1681
+ }
1074
1682
  }
1075
1683
  } else {
1076
1684
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
@@ -1133,8 +1741,8 @@ function topLevelLabel(segment) {
1133
1741
  function groupsWithinSection(section, pageIds, sectionLabel = topLevelLabel(section)) {
1134
1742
  const groups = /* @__PURE__ */ new Map();
1135
1743
  for (const id of pageIds) {
1136
- const relative4 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
1137
- const nestedSegment = relative4.includes("/") ? relative4.split("/", 1)[0] : "overview";
1744
+ const relative5 = id === "introduction" ? "" : id.startsWith(`${section}/`) ? id.slice(section.length + 1) : id;
1745
+ const nestedSegment = relative5.includes("/") ? relative5.split("/", 1)[0] : "overview";
1138
1746
  const group = groups.get(nestedSegment) ?? [];
1139
1747
  group.push(id);
1140
1748
  groups.set(nestedSegment, group);
@@ -1218,7 +1826,7 @@ function buildNavigationFromPages(pages, options = {}) {
1218
1826
  return { tabs: [{ tab: "Documentation", groups }] };
1219
1827
  }
1220
1828
  function isDocumentationExtension(filename) {
1221
- return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
1829
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname3(filename).toLowerCase());
1222
1830
  }
1223
1831
 
1224
1832
  // src/repository.ts
@@ -1280,16 +1888,16 @@ var DOCUSAURUS_CONFIG_FILENAMES = [
1280
1888
  function hasDocusaurusConfig(directory) {
1281
1889
  return DOCUSAURUS_CONFIG_FILENAMES.some((filename) => {
1282
1890
  const path = resolveWithin(directory, filename);
1283
- return existsSync3(path) && lstatSync3(path).isFile();
1891
+ return existsSync4(path) && lstatSync4(path).isFile();
1284
1892
  });
1285
1893
  }
1286
1894
  function hasMintlifyConfig(directory) {
1287
1895
  return ["docs.json", "mint.json"].some((filename) => {
1288
1896
  const path = resolveWithin(directory, filename);
1289
- if (!existsSync3(path) || !lstatSync3(path).isFile()) return false;
1897
+ if (!existsSync4(path) || !lstatSync4(path).isFile()) return false;
1290
1898
  if (filename === "mint.json") return true;
1291
1899
  try {
1292
- const config = JSON.parse(readFileSync3(path, "utf8"));
1900
+ const config = JSON.parse(readFileSync4(path, "utf8"));
1293
1901
  return String(config.$schema ?? "").includes("mintlify") || "navigation" in config;
1294
1902
  } catch {
1295
1903
  return false;
@@ -1303,7 +1911,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir) {
1303
1911
  const path = resolveWithin(repositoryDir, candidate || ".");
1304
1912
  if (hasMintlifyConfig(path)) return path;
1305
1913
  if (!candidate) break;
1306
- const parent = dirname2(candidate);
1914
+ const parent = dirname3(candidate);
1307
1915
  candidate = parent === "." ? "" : parent;
1308
1916
  }
1309
1917
  }
@@ -1328,7 +1936,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1328
1936
  const path = resolveWithin(repositoryDir, candidate || ".");
1329
1937
  if (hasDocusaurusConfig(path)) return path;
1330
1938
  if (!candidate) break;
1331
- const parent = dirname2(candidate);
1939
+ const parent = dirname3(candidate);
1332
1940
  candidate = parent === "." ? "" : parent;
1333
1941
  }
1334
1942
  }
@@ -1350,9 +1958,9 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir) {
1350
1958
  return null;
1351
1959
  }
1352
1960
  function readDocusaurusConfigSource(projectRoot) {
1353
- const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync3(candidate) && lstatSync3(candidate).isFile());
1354
- if (!path || lstatSync3(path).size > 1e6) return "";
1355
- return readFileSync3(path, "utf8");
1961
+ const path = DOCUSAURUS_CONFIG_FILENAMES.map((filename) => resolveWithin(projectRoot, filename)).find((candidate) => existsSync4(candidate) && lstatSync4(candidate).isFile());
1962
+ if (!path || lstatSync4(path).size > 1e6) return "";
1963
+ return readFileSync4(path, "utf8");
1356
1964
  }
1357
1965
  function primaryDocusaurusDocsDirectory(projectRoot) {
1358
1966
  const source = readDocusaurusConfigSource(projectRoot);
@@ -1361,7 +1969,7 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
1361
1969
  }
1362
1970
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1363
1971
  const source = readDocusaurusConfigSource(projectRoot);
1364
- const projectRelative = relative3(repositoryDir, projectRoot).replace(/\\/g, "/");
1972
+ const projectRelative = relative4(repositoryDir, projectRoot).replace(/\\/g, "/");
1365
1973
  const plugins = [];
1366
1974
  const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
1367
1975
  for (const match of source.matchAll(matcher)) {
@@ -1370,7 +1978,7 @@ function additionalDocusaurusPluginRoots(repositoryDir, projectRoot) {
1370
1978
  if (!localPath || !routePrefix) continue;
1371
1979
  const docsDir = [projectRelative, localPath].filter(Boolean).join("/");
1372
1980
  const absolute = resolveWithin(repositoryDir, docsDir);
1373
- if (!existsSync3(absolute) || !lstatSync3(absolute).isDirectory()) continue;
1981
+ if (!existsSync4(absolute) || !lstatSync4(absolute).isDirectory()) continue;
1374
1982
  plugins.push({ docsDir, routePrefix });
1375
1983
  }
1376
1984
  return plugins;
@@ -1413,7 +2021,7 @@ async function cloneGitHubRepository(source, targetDir) {
1413
2021
  const args = ["clone", "--depth", "1", "--single-branch"];
1414
2022
  if (source.branch !== "HEAD") args.push("--branch", source.branch);
1415
2023
  args.push("--", source.cloneUrl, targetDir);
1416
- await new Promise((resolve2, reject) => {
2024
+ await new Promise((resolve3, reject) => {
1417
2025
  const child = spawn("git", args, { stdio: ["ignore", "ignore", "pipe"] });
1418
2026
  let stderr = "";
1419
2027
  child.stderr.setEncoding("utf8");
@@ -1422,7 +2030,7 @@ async function cloneGitHubRepository(source, targetDir) {
1422
2030
  });
1423
2031
  child.on("error", reject);
1424
2032
  child.on("close", (code) => {
1425
- if (code === 0) resolve2();
2033
+ if (code === 0) resolve3();
1426
2034
  else reject(new Error(`Failed to clone ${source.owner}/${source.repo}: ${stderr.trim() || `git exited ${code}`}`));
1427
2035
  });
1428
2036
  });
@@ -1431,19 +2039,19 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
1431
2039
  if (findMintlifyProjectRoot(repositoryDir, docsDir)) return "mintlify";
1432
2040
  const selectedRoot = docsDir === void 0 ? repositoryDir : resolveWithin(repositoryDir, docsDir || ".");
1433
2041
  const docsJson = resolveWithin(selectedRoot, "docs.json");
1434
- if (existsSync3(docsJson)) {
2042
+ if (existsSync4(docsJson)) {
1435
2043
  try {
1436
- const config = JSON.parse(readFileSync3(docsJson, "utf8"));
2044
+ const config = JSON.parse(readFileSync4(docsJson, "utf8"));
1437
2045
  if (Array.isArray(config.tabs)) return "thally";
1438
2046
  } catch {
1439
2047
  }
1440
2048
  }
1441
- if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "docusaurus";
2049
+ if (["docusaurus.config.js", "docusaurus.config.ts", "docusaurus.config.mjs"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "docusaurus";
1442
2050
  if (findDocusaurusProjectRoot(repositoryDir)) return "docusaurus";
1443
- if (existsSync3(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
1444
- if (existsSync3(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
1445
- if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "starlight";
1446
- if (["pages/_meta.json", "_meta.json"].some((name) => existsSync3(resolveWithin(repositoryDir, name)))) return "nextra";
2051
+ if (existsSync4(resolveWithin(repositoryDir, "SUMMARY.md"))) return "gitbook";
2052
+ if (existsSync4(resolveWithin(repositoryDir, ".vitepress"))) return "vitepress";
2053
+ if (["astro.config.mjs", "astro.config.ts"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "starlight";
2054
+ if (["pages/_meta.json", "_meta.json"].some((name) => existsSync4(resolveWithin(repositoryDir, name)))) return "nextra";
1447
2055
  return "unknown";
1448
2056
  }
1449
2057
  function containsMarkdown(directory, depth = 0) {
@@ -1451,7 +2059,7 @@ function containsMarkdown(directory, depth = 0) {
1451
2059
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1452
2060
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1453
2061
  const path = resolveWithin(directory, entry.name);
1454
- if (entry.isFile() && [".md", ".mdx"].includes(extname3(entry.name).toLowerCase())) return true;
2062
+ if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) return true;
1455
2063
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
1456
2064
  }
1457
2065
  return false;
@@ -1459,7 +2067,7 @@ function containsMarkdown(directory, depth = 0) {
1459
2067
  function detectRepositoryDocsDir(repositoryDir) {
1460
2068
  for (const candidate of ["docs", "documentation", "content", "pages", "src/content", "src/pages", "guide", "guides", ""]) {
1461
2069
  const path = resolveWithin(repositoryDir, candidate);
1462
- if (existsSync3(path) && lstatSync3(path).isDirectory() && containsMarkdown(path)) return candidate;
2070
+ if (existsSync4(path) && lstatSync4(path).isDirectory() && containsMarkdown(path)) return candidate;
1463
2071
  }
1464
2072
  return "";
1465
2073
  }
@@ -1472,7 +2080,7 @@ function scanFiles(root) {
1472
2080
  if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
1473
2081
  const path = resolveWithin(directory, entry.name);
1474
2082
  if (entry.isDirectory()) visit(path);
1475
- else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative3(root, path).replace(/\\/g, "/") });
2083
+ else if (entry.isFile()) files.push({ absolutePath: path, relativePath: relative4(root, path).replace(/\\/g, "/") });
1476
2084
  }
1477
2085
  }
1478
2086
  visit(root);
@@ -1527,9 +2135,9 @@ function interpolateSnippet(snippet, attributes) {
1527
2135
  }).join("\n");
1528
2136
  }
1529
2137
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
1530
- const repositoryRelative = sourcePath.startsWith("@site/") ? relative3(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative3(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative3(repositoryRoot, resolvePath(dirname2(currentFile), sourcePath)).replace(/\\/g, "/");
2138
+ const repositoryRelative = sourcePath.startsWith("@site/") ? relative4(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative4(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative4(repositoryRoot, resolvePath(dirname3(currentFile), sourcePath)).replace(/\\/g, "/");
1531
2139
  const candidate = resolveWithin(repositoryRoot, repositoryRelative);
1532
- resolveWithin(repositoryRoot, relative3(repositoryRoot, candidate));
2140
+ resolveWithin(repositoryRoot, relative4(repositoryRoot, candidate));
1533
2141
  return candidate;
1534
2142
  }
1535
2143
  function globalSnippetAliases(files, repositoryRoot, siteRoot) {
@@ -1543,12 +2151,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
1543
2151
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
1544
2152
  }
1545
2153
  for (const file of files) {
1546
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) continue;
1547
- const raw = readFileSync3(file.absolutePath, "utf8");
2154
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
2155
+ const raw = readFileSync4(file.absolutePath, "utf8");
1548
2156
  for (const match of raw.matchAll(matcher)) {
1549
2157
  try {
1550
2158
  const candidate = resolveSnippetPath(match[2], file.absolutePath, repositoryRoot, siteRoot);
1551
- if (existsSync3(candidate) && lstatSync3(candidate).isFile()) {
2159
+ if (existsSync4(candidate) && lstatSync4(candidate).isFile()) {
1552
2160
  aliases.set(match[1], candidate);
1553
2161
  }
1554
2162
  } catch {
@@ -1570,12 +2178,12 @@ function repositoryAssetHref(value, currentFile, siteRoot) {
1570
2178
  } catch {
1571
2179
  return null;
1572
2180
  }
1573
- if (!ASSET_EXTENSIONS.has(extname3(decodedPath).toLowerCase())) return null;
2181
+ if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
1574
2182
  try {
1575
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname2(currentFile), decodedPath);
1576
- const siteRelative = relative3(siteRoot, candidate).replace(/\\/g, "/");
2183
+ const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname3(currentFile), decodedPath);
2184
+ const siteRelative = relative4(siteRoot, candidate).replace(/\\/g, "/");
1577
2185
  resolveWithin(siteRoot, siteRelative);
1578
- if (!existsSync3(candidate) || !lstatSync3(candidate).isFile()) return null;
2186
+ if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) return null;
1579
2187
  const normalized = normalizeAssetPath(siteRelative);
1580
2188
  if (!normalized) return null;
1581
2189
  const rewritten = `/${normalized}${suffix}`;
@@ -1601,9 +2209,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1601
2209
  (_statement, componentName, sourcePath) => {
1602
2210
  try {
1603
2211
  const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
1604
- if (!existsSync3(candidate) || !lstatSync3(candidate).isFile()) throw new Error("file not found");
2212
+ if (!existsSync4(candidate) || !lstatSync4(candidate).isFile()) throw new Error("file not found");
1605
2213
  const nested = inlineMdxSnippets(
1606
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2214
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1607
2215
  candidate,
1608
2216
  repositoryRoot,
1609
2217
  warnings,
@@ -1617,7 +2225,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1617
2225
  warnings.push({
1618
2226
  code: "missing-page",
1619
2227
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
1620
- source: relative3(repositoryRoot, currentFile).replace(/\\/g, "/")
2228
+ source: relative4(repositoryRoot, currentFile).replace(/\\/g, "/")
1621
2229
  });
1622
2230
  snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
1623
2231
  return "";
@@ -1627,7 +2235,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
1627
2235
  for (const [componentName, candidate] of globalAliases) {
1628
2236
  if (snippets.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
1629
2237
  const nested = inlineMdxSnippets(
1630
- withoutFrontmatter(readFileSync3(candidate, "utf8")),
2238
+ withoutFrontmatter(readFileSync4(candidate, "utf8")),
1631
2239
  candidate,
1632
2240
  repositoryRoot,
1633
2241
  warnings,
@@ -1654,6 +2262,11 @@ function injectOpenApi(config, filename) {
1654
2262
  else tabs.push({ tab: "API Reference", api: { source: `/${filename}` } });
1655
2263
  return { ...config, tabs };
1656
2264
  }
2265
+ function componentSourceIdentity(sourceUrl, repositoryDir, siteRoot) {
2266
+ const url = new URL(sourceUrl);
2267
+ const repository = url.hostname.toLowerCase() === "github.com" ? `https://github.com/${url.pathname.split("/").filter(Boolean).slice(0, 2).join("/").replace(/\.git$/i, "").toLowerCase()}` : `${url.origin}${trimTrailingSlashes(url.pathname)}`;
2268
+ return JSON.stringify([repository, relative4(repositoryDir, siteRoot).replace(/\\/g, "/")]);
2269
+ }
1657
2270
  function migrateRepository(options) {
1658
2271
  const repositoryDir = options.repositoryDir;
1659
2272
  const platform = options.platform ?? detectRepositoryPlatform(repositoryDir, options.docsDir);
@@ -1663,21 +2276,23 @@ function migrateRepository(options) {
1663
2276
  if (options.docsDir !== void 0) {
1664
2277
  const selected = resolveWithin(repositoryDir, options.docsDir || ".");
1665
2278
  if (platform === "docusaurus" && hasDocusaurusConfig(selected)) {
1666
- const projectRelative = relative3(repositoryDir, selected).replace(/\\/g, "/");
2279
+ const projectRelative = relative4(repositoryDir, selected).replace(/\\/g, "/");
1667
2280
  return [projectRelative, primaryDocusaurusDocsDirectory(selected)].filter(Boolean).join("/");
1668
2281
  }
1669
2282
  return options.docsDir;
1670
2283
  }
1671
2284
  if (docusaurusProjectRoot) {
1672
- const projectRelative = relative3(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
2285
+ const projectRelative = relative4(repositoryDir, docusaurusProjectRoot).replace(/\\/g, "/");
1673
2286
  return [projectRelative, primaryDocusaurusDocsDirectory(docusaurusProjectRoot)].filter(Boolean).join("/");
1674
2287
  }
1675
2288
  if (platform === "mintlify" && mintlifyProjectRoot) {
1676
- return relative3(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
2289
+ return relative4(repositoryDir, mintlifyProjectRoot).replace(/\\/g, "/");
1677
2290
  }
1678
2291
  return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
1679
2292
  })();
1680
2293
  const warnings = [];
2294
+ const componentRoot = mintlifyProjectRoot ?? repositoryDir;
2295
+ const componentMigrator = platform === "mintlify" ? createComponentMigrator(componentRoot, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
1681
2296
  let docsConfig = { tabs: [] };
1682
2297
  const referenceMap = /* @__PURE__ */ new Map();
1683
2298
  const exactReferenceMap = /* @__PURE__ */ new Map();
@@ -1694,9 +2309,10 @@ function migrateRepository(options) {
1694
2309
  warnings.push(...projected.warnings);
1695
2310
  for (const [index, reference] of projected.pageReferences.entries()) {
1696
2311
  const key = normalizedReferenceKey(reference.ref);
1697
- referenceMap.set(key, reference);
1698
- exactReferenceMap.set(exactReferenceKey(reference.ref), reference);
1699
- referenceOrder.set(key, index);
2312
+ if (!referenceMap.has(key)) referenceMap.set(key, reference);
2313
+ const exactKey = exactReferenceKey(reference.ref);
2314
+ if (!exactReferenceMap.has(exactKey)) exactReferenceMap.set(exactKey, reference);
2315
+ if (!referenceOrder.has(key)) referenceOrder.set(key, index);
1700
2316
  }
1701
2317
  }
1702
2318
  } catch (error) {
@@ -1717,7 +2333,7 @@ function migrateRepository(options) {
1717
2333
  }
1718
2334
  }
1719
2335
  const contentRoot = resolveWithin(repositoryDir, configuredDocsDir);
1720
- if (!existsSync3(contentRoot) || !lstatSync3(contentRoot).isDirectory()) {
2336
+ if (!existsSync4(contentRoot) || !lstatSync4(contentRoot).isDirectory()) {
1721
2337
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
1722
2338
  }
1723
2339
  const files = scanFiles(contentRoot);
@@ -1728,6 +2344,8 @@ function migrateRepository(options) {
1728
2344
  let skipped = 0;
1729
2345
  let discovered = files.length;
1730
2346
  const localeConfig = docsConfig.i18n;
2347
+ const defaultPageIds = new Set([...referenceMap.values()].filter((reference) => !reference.locale || reference.locale === localeConfig?.defaultLocale).map((reference) => reference.navigationId));
2348
+ const routeAliases = [];
1731
2349
  const pageFiles = [...files].sort((left, right) => {
1732
2350
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
1733
2351
  const rightOrder = referenceOrder.get(normalizedReferenceKey(right.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -1746,12 +2364,12 @@ function migrateRepository(options) {
1746
2364
  skipped++;
1747
2365
  continue;
1748
2366
  }
1749
- if (![".md", ".mdx"].includes(extname3(file.relativePath).toLowerCase())) {
2367
+ if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
1750
2368
  skipped++;
1751
2369
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
1752
2370
  continue;
1753
2371
  }
1754
- const size = lstatSync3(file.absolutePath).size;
2372
+ const size = lstatSync4(file.absolutePath).size;
1755
2373
  if (size > MAX_PAGE_BYTES) {
1756
2374
  skipped++;
1757
2375
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
@@ -1762,10 +2380,12 @@ function migrateRepository(options) {
1762
2380
  let locale = referenced?.locale;
1763
2381
  let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
1764
2382
  if (!referenced && localeConfig) {
1765
- const prefix = file.relativePath.split("/", 1)[0];
1766
- if (localeConfig.locales.some((entry) => entry.code === prefix)) {
1767
- locale = prefix;
1768
- navigationId = pageIdFromReference(file.relativePath.slice(prefix.length + 1), platform === "mintlify");
2383
+ for (const entry of localeConfig.locales) {
2384
+ const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
2385
+ if (localized === file.relativePath) continue;
2386
+ locale = entry.code;
2387
+ navigationId = pageIdFromReference(localized, platform === "mintlify");
2388
+ break;
1769
2389
  }
1770
2390
  }
1771
2391
  if (!navigationId) {
@@ -1775,8 +2395,8 @@ function migrateRepository(options) {
1775
2395
  }
1776
2396
  const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
1777
2397
  const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
1778
- const raw = inlineMdxSnippets(
1779
- readFileSync3(file.absolutePath, "utf8"),
2398
+ let raw = inlineMdxSnippets(
2399
+ readFileSync4(file.absolutePath, "utf8"),
1780
2400
  file.absolutePath,
1781
2401
  repositoryDir,
1782
2402
  warnings,
@@ -1784,13 +2404,14 @@ function migrateRepository(options) {
1784
2404
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? repositoryDir,
1785
2405
  snippetAliases
1786
2406
  );
2407
+ if (componentMigrator) raw = componentMigrator.transform(raw, file.absolutePath);
1787
2408
  let docusaurusDescriptor;
1788
2409
  const page = parseMarkdownPage({
1789
2410
  id,
1790
2411
  navigationId,
1791
2412
  ...locale ? { locale } : {},
1792
2413
  raw,
1793
- source: `${options.sourceUrl}#${relative3(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
2414
+ source: `${options.sourceUrl}#${relative4(repositoryDir, file.absolutePath).replace(/\\/g, "/")}`,
1794
2415
  ...platform === "docusaurus" ? {
1795
2416
  resolveIdentity: (frontmatter, fallback) => {
1796
2417
  const resolved = resolveDocusaurusPageIdentity(file.relativePath, frontmatter, fallback);
@@ -1827,6 +2448,12 @@ function migrateRepository(options) {
1827
2448
  }
1828
2449
  seenPageIds.add(page.id);
1829
2450
  pages.push(page);
2451
+ if (platform === "mintlify") {
2452
+ const sourcePath = exactReferenceKey(file.relativePath);
2453
+ if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
2454
+ routeAliases.push({ source: `/${sourcePath}`, destination: `/${page.id}`, permanent: false });
2455
+ }
2456
+ }
1830
2457
  if (docusaurusDescriptor) {
1831
2458
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
1832
2459
  }
@@ -1840,7 +2467,7 @@ function migrateRepository(options) {
1840
2467
  const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
1841
2468
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
1842
2469
  const root = resolveWithin(docusaurusAssetRoot, directory);
1843
- if (!existsSync3(root) || !lstatSync3(root).isDirectory()) return [];
2470
+ if (!existsSync4(root) || !lstatSync4(root).isDirectory()) return [];
1844
2471
  return scanFiles(root).map((file) => ({
1845
2472
  ...file,
1846
2473
  relativePath: `${directory}/${file.relativePath}`
@@ -1849,9 +2476,9 @@ function migrateRepository(options) {
1849
2476
  let totalAssetBytes = 0;
1850
2477
  for (const file of [...files, ...repositoryAssets]) {
1851
2478
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
1852
- if (!ASSET_EXTENSIONS.has(extname3(file.relativePath).toLowerCase())) continue;
2479
+ if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
1853
2480
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
1854
- const size = lstatSync3(file.absolutePath).size;
2481
+ const size = lstatSync4(file.absolutePath).size;
1855
2482
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
1856
2483
  warnings.push({ code: "limit-reached", message: "An asset was skipped because the migration asset budget was exhausted.", source: file.relativePath });
1857
2484
  continue;
@@ -1859,7 +2486,7 @@ function migrateRepository(options) {
1859
2486
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
1860
2487
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
1861
2488
  if (!assetPath) continue;
1862
- assets.push({ path: assetPath, content: readFileSync3(file.absolutePath) });
2489
+ assets.push({ path: assetPath, content: readFileSync4(file.absolutePath) });
1863
2490
  totalAssetBytes += size;
1864
2491
  }
1865
2492
  if (files.length >= MAX_SOURCE_FILES) {
@@ -1893,11 +2520,16 @@ function migrateRepository(options) {
1893
2520
  if (openApi) {
1894
2521
  const filename = basename(openApi.relativePath);
1895
2522
  if (!assets.some((asset) => asset.path === filename)) {
1896
- assets.push({ path: filename, content: readFileSync3(openApi.absolutePath) });
2523
+ assets.push({ path: filename, content: readFileSync4(openApi.absolutePath) });
1897
2524
  }
1898
2525
  docsConfig = injectOpenApi(docsConfig, filename);
1899
2526
  }
1900
- if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
2527
+ if (platform === "mintlify") {
2528
+ const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
2529
+ const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
2530
+ if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
2531
+ docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
2532
+ }
1901
2533
  if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
1902
2534
  for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot)) {
1903
2535
  if (plugin.docsDir === configuredDocsDir) continue;
@@ -1938,6 +2570,7 @@ function migrateRepository(options) {
1938
2570
  platform,
1939
2571
  pages,
1940
2572
  assets,
2573
+ ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
1941
2574
  docsConfig,
1942
2575
  ...mintlifyConfig ? {
1943
2576
  site: {
@@ -2054,6 +2687,7 @@ function renderMigrationFiles(bundle, options = {}) {
2054
2687
  content: renderPage(bundle, page)
2055
2688
  })),
2056
2689
  ...bundle.assets.map((asset) => ({ path: `public/${asset.path}`, content: asset.content })),
2690
+ ...(bundle.componentFiles ?? []).flatMap((file) => file.path === "src/mdx/custom-components.tsx" && options.existingComponentRegistry !== void 0 && typeof file.content === "string" ? mergeComponentRegistry(options.existingComponentRegistry, file.content) : [file]),
2057
2691
  { path: "docs.json", content: `${JSON.stringify(config, null, 2)}
2058
2692
  ` }
2059
2693
  ];
@@ -2061,7 +2695,10 @@ function renderMigrationFiles(bundle, options = {}) {
2061
2695
 
2062
2696
  // src/url.ts
2063
2697
  import { load } from "cheerio";
2698
+ import remarkMdx3 from "remark-mdx";
2699
+ import remarkParse3 from "remark-parse";
2064
2700
  import TurndownService from "turndown";
2701
+ import { unified as unified3 } from "unified";
2065
2702
  import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
2066
2703
  var DEFAULT_MAX_PAGES = 1e3;
2067
2704
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
@@ -2164,6 +2801,8 @@ var LOCALE_CODES = /* @__PURE__ */ new Set([
2164
2801
  "zh-hant"
2165
2802
  ]);
2166
2803
  var PORTABLE_URL_PROPS = "href|src|img|primaryHref|secondaryHref";
2804
+ var PORTABLE_URL_PROP_NAMES = new Set(PORTABLE_URL_PROPS.split("|"));
2805
+ var remoteMarkdownParser = unified3().use(remarkParse3).use(remarkMdx3);
2167
2806
  function validateMigrationUrl(value) {
2168
2807
  let url;
2169
2808
  try {
@@ -2293,7 +2932,7 @@ function detectUrlPlatform(document) {
2293
2932
  const value = `${document.body.slice(0, 2e5)} ${headers}`.toLowerCase();
2294
2933
  if (value.includes("__mintlify") || value.includes("/mintlify-assets/") || value.includes("x-mintlify-") || value.includes("/_mintlify/")) return "mintlify";
2295
2934
  if (value.includes("docusaurus") || value.includes("__docusaurus")) return "docusaurus";
2296
- if (value.includes("gitbook") || value.includes("gitbook.io")) return "gitbook";
2935
+ if (value.includes("gitbook")) return "gitbook";
2297
2936
  if (value.includes("nextra")) return "nextra";
2298
2937
  if (value.includes("vitepress")) return "vitepress";
2299
2938
  if (value.includes("starlight")) return "starlight";
@@ -2432,19 +3071,184 @@ function stripEventHandlerProps(line) {
2432
3071
  }
2433
3072
  return cursor === 0 ? line : `${result}${line.slice(cursor)}`;
2434
3073
  }
2435
- function hasUnsafeUrlScheme(value) {
2436
- const normalized = value.trim().replace(/[\u0000-\u0020]/g, "").replace(/&(?:#x0*3a|#0*58|colon);/gi, ":");
2437
- return /^(?:javascript|data|vbscript):/i.test(normalized);
3074
+ function isSafePortableUrl(value) {
3075
+ try {
3076
+ const parsed = new URL(value, "https://relative.thally.invalid");
3077
+ return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
3078
+ } catch {
3079
+ return false;
3080
+ }
3081
+ }
3082
+ function decodeHtmlAttribute(value) {
3083
+ const document = load(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
3084
+ return document("i").attr("data-thally-value") ?? "";
3085
+ }
3086
+ function escapeMarkdownText(value, escapeTableSeparator = false) {
3087
+ let escaped = "";
3088
+ for (const character of value) {
3089
+ if (character === "\\") escaped += "\\\\";
3090
+ else if (character === "[" || character === "]") escaped += `\\${character}`;
3091
+ else if (character === "<") escaped += "&lt;";
3092
+ else if (character === ">") escaped += "&gt;";
3093
+ else if (character === "{") escaped += "&#123;";
3094
+ else if (character === "}") escaped += "&#125;";
3095
+ else if (character === "|" && escapeTableSeparator) escaped += "\\|";
3096
+ else escaped += character;
3097
+ }
3098
+ return escaped;
3099
+ }
3100
+ function markdownLinkDestination(value) {
3101
+ try {
3102
+ return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/&/g, "&amp;");
3103
+ } catch {
3104
+ return null;
3105
+ }
3106
+ }
3107
+ function htmlOpeningTagEnd(value, start) {
3108
+ let quote = "";
3109
+ for (let index = start; index < value.length; index += 1) {
3110
+ const character = value[index];
3111
+ if (quote) {
3112
+ if (character === quote) quote = "";
3113
+ continue;
3114
+ }
3115
+ if (character === '"' || character === "'") {
3116
+ quote = character;
3117
+ continue;
3118
+ }
3119
+ if (character === ">") return index + 1;
3120
+ }
3121
+ return null;
3122
+ }
3123
+ function replacePortableHtmlImages(value) {
3124
+ const matcher = /<img\b/gi;
3125
+ let cursor = 0;
3126
+ let output = "";
3127
+ for (const match of value.matchAll(matcher)) {
3128
+ const start = match.index;
3129
+ if (start < cursor) continue;
3130
+ const end = htmlOpeningTagEnd(value, start + match[0].length);
3131
+ if (end === null) continue;
3132
+ const fragment = load(value.slice(start, end));
3133
+ const image = fragment("img").first();
3134
+ const source = image.attr("src");
3135
+ const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
3136
+ const alternative = escapeMarkdownText(image.attr("alt") ?? "");
3137
+ output += value.slice(cursor, start);
3138
+ output += destination ? `![${alternative}](${destination})` : alternative;
3139
+ cursor = end;
3140
+ }
3141
+ return cursor === 0 ? value : `${output}${value.slice(cursor)}`;
3142
+ }
3143
+ function replacePortableHtmlAnchors(value) {
3144
+ const matcher = /<a\b/gi;
3145
+ let cursor = 0;
3146
+ let output = "";
3147
+ for (const match of value.matchAll(matcher)) {
3148
+ const start = match.index;
3149
+ if (start < cursor) continue;
3150
+ const openingEnd = htmlOpeningTagEnd(value, start + match[0].length);
3151
+ if (openingEnd === null) continue;
3152
+ const closingMatch = /<\/a\s*>/gi;
3153
+ closingMatch.lastIndex = openingEnd;
3154
+ const closing = closingMatch.exec(value);
3155
+ if (!closing) continue;
3156
+ const end = closing.index + closing[0].length;
3157
+ const fragment = load(value.slice(start, end));
3158
+ const anchor = fragment("a").first();
3159
+ const href = anchor.attr("href");
3160
+ const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
3161
+ const destination = href && isSafePortableUrl(href) ? markdownLinkDestination(href) : null;
3162
+ output += value.slice(cursor, start);
3163
+ output += destination ? `[${label}](${destination})` : label;
3164
+ cursor = end;
3165
+ }
3166
+ return cursor === 0 ? value : `${output}${value.slice(cursor)}`;
3167
+ }
3168
+ function markdownNodeText(node) {
3169
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
3170
+ if (node.type === "image" || node.type === "imageReference") return node.alt ?? "";
3171
+ return (node.children ?? []).map(markdownNodeText).join("");
3172
+ }
3173
+ function sanitizeMarkdownUrls(value) {
3174
+ let root;
3175
+ try {
3176
+ root = remoteMarkdownParser.parse(value);
3177
+ } catch {
3178
+ return null;
3179
+ }
3180
+ let hasUnsafeMdxSyntax = false;
3181
+ const validateMdxSyntax = (node) => {
3182
+ if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" || node.type === "mdxjsEsm") {
3183
+ hasUnsafeMdxSyntax = true;
3184
+ return;
3185
+ }
3186
+ if (node.type === "mdxJsxTextElement" || node.type === "mdxJsxFlowElement") {
3187
+ for (const attribute of node.attributes ?? []) {
3188
+ if (attribute.type !== "mdxJsxAttribute" || !attribute.name || /^on[A-Z]/.test(attribute.name)) {
3189
+ hasUnsafeMdxSyntax = true;
3190
+ return;
3191
+ }
3192
+ let staticValue = attribute.value;
3193
+ if (attribute.value && typeof attribute.value === "object") {
3194
+ if (attribute.value.type !== "mdxJsxAttributeValueExpression") {
3195
+ hasUnsafeMdxSyntax = true;
3196
+ return;
3197
+ }
3198
+ try {
3199
+ staticValue = JSON.parse(attribute.value.value);
3200
+ } catch {
3201
+ hasUnsafeMdxSyntax = true;
3202
+ return;
3203
+ }
3204
+ }
3205
+ if (PORTABLE_URL_PROP_NAMES.has(attribute.name) && (typeof staticValue !== "string" || !isSafePortableUrl(decodeHtmlAttribute(staticValue)))) {
3206
+ hasUnsafeMdxSyntax = true;
3207
+ return;
3208
+ }
3209
+ }
3210
+ }
3211
+ for (const child of node.children ?? []) validateMdxSyntax(child);
3212
+ };
3213
+ validateMdxSyntax(root);
3214
+ if (hasUnsafeMdxSyntax) return null;
3215
+ const unsafeDefinitions = /* @__PURE__ */ new Set();
3216
+ const visitDefinitions = (node) => {
3217
+ if (node.type === "definition" && node.identifier && node.url && !isSafePortableUrl(node.url)) unsafeDefinitions.add(node.identifier.toLowerCase());
3218
+ for (const child of node.children ?? []) visitDefinitions(child);
3219
+ };
3220
+ visitDefinitions(root);
3221
+ const replacements = [];
3222
+ const replaceNode = (node, replacement) => {
3223
+ const start = node.position?.start.offset;
3224
+ const end = node.position?.end.offset;
3225
+ if (start === void 0 || end === void 0) return false;
3226
+ replacements.push({ start, end, value: replacement });
3227
+ return true;
3228
+ };
3229
+ const visitUrls = (node) => {
3230
+ const hasUnsafeDirectUrl = (node.type === "link" || node.type === "image") && typeof node.url === "string" && !isSafePortableUrl(node.url);
3231
+ const hasUnsafeReference = (node.type === "linkReference" || node.type === "imageReference") && typeof node.identifier === "string" && unsafeDefinitions.has(node.identifier.toLowerCase());
3232
+ if (hasUnsafeDirectUrl || hasUnsafeReference) {
3233
+ if (replaceNode(node, escapeMarkdownText(markdownNodeText(node)))) return;
3234
+ }
3235
+ if (node.type === "definition" && node.identifier && unsafeDefinitions.has(node.identifier.toLowerCase())) {
3236
+ if (replaceNode(node, "")) return;
3237
+ }
3238
+ for (const child of node.children ?? []) visitUrls(child);
3239
+ };
3240
+ visitUrls(root);
3241
+ return replacements.sort((left, right) => right.start - left.start).reduce((result, replacement) => `${result.slice(0, replacement.start)}${replacement.value}${result.slice(replacement.end)}`, value);
2438
3242
  }
2439
3243
  function hasUnsafePortableUrlProp(value) {
2440
3244
  const quotedProps = new RegExp(`\\b(?:${PORTABLE_URL_PROPS})\\s*=\\s*(['"])([^'"\\r\\n]*)\\1`, "gi");
2441
3245
  for (const match of value.matchAll(quotedProps)) {
2442
- if (hasUnsafeUrlScheme(match[2])) return true;
3246
+ if (!isSafePortableUrl(decodeHtmlAttribute(match[2]))) return true;
2443
3247
  }
2444
3248
  const expressionProps = new RegExp(`\\b(?:${PORTABLE_URL_PROPS})\\s*=\\s*\\{\\s*("(?:\\\\.|[^"\\\\])*")\\s*\\}`, "gi");
2445
3249
  for (const match of value.matchAll(expressionProps)) {
2446
3250
  try {
2447
- if (hasUnsafeUrlScheme(JSON.parse(match[1]))) return true;
3251
+ if (!isSafePortableUrl(decodeHtmlAttribute(JSON.parse(match[1])))) return true;
2448
3252
  } catch {
2449
3253
  return true;
2450
3254
  }
@@ -2452,12 +3256,7 @@ function hasUnsafePortableUrlProp(value) {
2452
3256
  return false;
2453
3257
  }
2454
3258
  function portableHtmlToMarkdown(value) {
2455
- return value.replace(/<span\b[^>]*>|<\/span>/gi, "").replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (_original, attributes, label) => {
2456
- const href = attributes.match(/\bhref\s*=\s*(['"])(.*?)\1/i)?.[2];
2457
- const plainLabel = label.replace(/<[^>]+>/g, "").trim();
2458
- if (!href || hasUnsafeUrlScheme(href)) return plainLabel;
2459
- return `[${plainLabel.replace(/]/g, "\\]")}](${href})`;
2460
- }).replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_original, level, content) => {
3259
+ return replacePortableHtmlAnchors(replacePortableHtmlImages(value)).replace(/<span\b[^>]*>|<\/span>/gi, "").replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_original, level, content) => {
2461
3260
  return `
2462
3261
  ${"#".repeat(Number(level))} ${content.trim()}
2463
3262
  `;
@@ -2514,16 +3313,11 @@ function sanitizeRemoteMarkdown(body) {
2514
3313
  return line;
2515
3314
  }
2516
3315
  if (fenceCharacter) return line;
2517
- return portableHtmlToMarkdown(stripEventHandlerProps(line.replace(/!\[([^\]]*)\]\(\s*([^)]+)\)/g, (original, alternative, destination) => {
2518
- return hasUnsafeUrlScheme(destination.replace(/^<|>$/g, "")) ? alternative : original;
2519
- }).replace(/<img\s+([^>]*?)\/?\s*>/gi, (original, attributes) => {
2520
- const source = attributes.match(/\bsrc\s*=\s*(['"])(.*?)\1/i)?.[2];
2521
- const alternative = attributes.match(/\balt\s*=\s*(['"])(.*?)\1/i)?.[2] ?? "";
2522
- if (!source || hasUnsafeUrlScheme(source)) return "";
2523
- return `![${alternative.replace(/]/g, "\\]")}](${source})`;
2524
- })));
3316
+ return portableHtmlToMarkdown(stripEventHandlerProps(line.replace(/!\[([^\]]*)\]\(\s*([^)]+)\)/g, (original, alternative, destination) => isSafePortableUrl(decodeHtmlAttribute(destination.replace(/^<|>$/g, ""))) ? original : escapeMarkdownText(alternative))));
2525
3317
  }).join("\n");
2526
- const executable = withoutFencedCode(sanitized).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
3318
+ const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
3319
+ if (urlSafeMarkdown === null) return null;
3320
+ const executable = withoutFencedCode(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
2527
3321
  if (/^\s*(?:import|export)\s/m.test(executable)) return null;
2528
3322
  let hasUnsafeExpression = false;
2529
3323
  const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
@@ -2540,7 +3334,7 @@ function sanitizeRemoteMarkdown(body) {
2540
3334
  const component = match[1].split(".", 1)[0];
2541
3335
  if (!PORTABLE_MDX_COMPONENTS.has(component)) return null;
2542
3336
  }
2543
- return sanitized;
3337
+ return urlSafeMarkdown;
2544
3338
  }
2545
3339
  function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHomePageId) {
2546
3340
  if (/^(?:javascript|data|vbscript):/i.test(href)) return "#";
@@ -2827,7 +3621,10 @@ ${tabs.join("\n\n")}
2827
3621
  filter: "table",
2828
3622
  replacement: (_content, node) => {
2829
3623
  const fragment = load(node.outerHTML);
2830
- 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);
3624
+ const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
3625
+ fragment(cell).text().replace(/\s+/g, " ").trim(),
3626
+ true
3627
+ ))).filter((row) => row.length > 0);
2831
3628
  if (rows.length === 0) return "";
2832
3629
  const width = Math.max(...rows.map((row) => row.length));
2833
3630
  const normalized = rows.map((row) => [...row, ...Array(width - row.length).fill("")]);