gt 2.17.2 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/api/collectUserEditDiffs.js +28 -5
- package/dist/api/collectUserEditDiffs.js.map +1 -1
- package/dist/cli/commands/translate.js +1 -1
- package/dist/cli/commands/translate.js.map +1 -1
- package/dist/formats/files/aggregateFiles.js +27 -1
- package/dist/formats/files/aggregateFiles.js.map +1 -1
- package/dist/formats/files/supportedFiles.d.ts +3 -1
- package/dist/formats/files/supportedFiles.js +6 -2
- package/dist/formats/files/supportedFiles.js.map +1 -1
- package/dist/formats/files/transformFormat.d.ts +4 -0
- package/dist/formats/files/transformFormat.js +9 -3
- package/dist/formats/files/transformFormat.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/utils/addExplicitAnchorIds.d.ts +21 -1
- package/dist/utils/addExplicitAnchorIds.js +130 -198
- package/dist/utils/addExplicitAnchorIds.js.map +1 -1
- package/dist/utils/localizeRelativeAssets.js +6 -3
- package/dist/utils/localizeRelativeAssets.js.map +1 -1
- package/dist/utils/localizeStaticImports.js +2 -7
- package/dist/utils/localizeStaticImports.js.map +1 -1
- package/dist/utils/localizeStaticUrls.js +6 -4
- package/dist/utils/localizeStaticUrls.js.map +1 -1
- package/dist/utils/mdxAnchorSyntax.d.ts +30 -0
- package/dist/utils/mdxAnchorSyntax.js +113 -0
- package/dist/utils/mdxAnchorSyntax.js.map +1 -0
- package/dist/utils/validateMdx.d.ts +5 -0
- package/dist/utils/validateMdx.js +7 -7
- package/dist/utils/validateMdx.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localizeStaticImports.js","names":["fs","path"],"sources":["../../src/utils/localizeStaticImports.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n AdditionalOptions,\n StaticLocalizationSettings,\n} from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport micromatch from 'micromatch';\nimport { unified } from 'unified';\nimport remarkParse from 'remark-parse';\nimport remarkMdx from 'remark-mdx';\nimport remarkFrontmatter from 'remark-frontmatter';\nimport { visit } from 'unist-util-visit';\nimport type { Root } from 'mdast';\nimport type { MdxjsEsm } from 'mdast-util-mdxjs-esm';\n\nconst { isMatch } = micromatch;\n\nexport type StaticImportSettings = StaticLocalizationSettings;\n\n/**\n * Localizes static imports in content files.\n * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)\n * @param settings - The settings object containing the project configuration.\n * @returns void\n *\n * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before\n * it can be enabled by default.\n *\n * Before this becomes a non-experimental feature, we need to:\n * - Support more file types\n * - Support more complex paths\n */\nexport default async function localizeStaticImports(\n settings: StaticImportSettings,\n includeFiles?: Set<string>\n) {\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return;\n }\n const { resolvedPaths: sourceFiles } = settings.files;\n\n const fileMapping = createFileMapping(\n sourceFiles,\n settings.files.placeholderPaths,\n settings.files.transformPaths ?? {},\n settings.files.transformFormats ?? {},\n settings.locales,\n settings.defaultLocale\n );\n\n // Process all file types at once with a single call\n const processPromises = [];\n\n // First, process default locale files (from source files)\n // This is needed because they might not be in the fileMapping if they're not being translated\n if (!fileMapping[settings.defaultLocale] && !includeFiles) {\n const defaultLocaleFiles: string[] = [];\n\n // Collect all .md and .mdx files from sourceFiles\n if (sourceFiles.md) {\n defaultLocaleFiles.push(...sourceFiles.md);\n }\n if (sourceFiles.mdx) {\n defaultLocaleFiles.push(...sourceFiles.mdx);\n }\n\n if (defaultLocaleFiles.length > 0) {\n const defaultPromise = Promise.all(\n defaultLocaleFiles.map(async (filePath: string) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file using default locale\n const localizedFile = localizeStaticImportsForFile(\n fileContent,\n settings.defaultLocale,\n settings.defaultLocale, // Process as default locale\n settings.options?.docsHideDefaultLocaleImport || false,\n settings.options?.docsImportPattern,\n settings.options?.excludeStaticImports,\n filePath,\n settings.options\n );\n // Write the localized file back to the same path\n await fs.promises.writeFile(filePath, localizedFile);\n })\n );\n processPromises.push(defaultPromise);\n }\n }\n\n // Then process all other locales from fileMapping\n const mappingPromises = Object.entries(fileMapping).map(\n async ([locale, filesMap]) => {\n // Get all files that are md or mdx\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n // Replace the placeholder path with the target path\n await Promise.all(\n targetFiles.map(async (filePath) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file\n const localizedFile = localizeStaticImportsForFile(\n fileContent,\n settings.defaultLocale,\n locale,\n settings.options?.docsHideDefaultLocaleImport || false,\n settings.options?.docsImportPattern,\n settings.options?.excludeStaticImports,\n filePath,\n settings.options\n );\n // Write the localized file to the target path\n await fs.promises.writeFile(filePath, localizedFile);\n })\n );\n }\n );\n processPromises.push(...mappingPromises);\n\n await Promise.all(processPromises);\n}\n\ninterface ImportTransformResult {\n content: string;\n hasChanges: boolean;\n transformedImports: Array<{\n originalPath: string;\n newPath: string;\n }>;\n}\n\n/**\n * Determines if an import path should be processed based on pattern matching\n */\nfunction shouldProcessImportPath(\n importPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): boolean {\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n\n if (targetLocale === defaultLocale) {\n // For default locale processing, check if path contains the pattern\n return importPath.includes(patternWithoutSlash);\n } else {\n // For non-default locales, check if path starts with pattern\n return importPath.includes(patternWithoutSlash);\n }\n}\n\n/**\n * Checks if an import path should be excluded based on exclusion patterns\n */\nfunction isImportPathExcluded(\n importPath: string,\n exclude: string[],\n defaultLocale: string\n): boolean {\n const excludePatterns = exclude.map((p) =>\n p.replace(/\\[locale\\]/g, defaultLocale)\n );\n return excludePatterns.some((pattern) => isMatch(importPath, pattern));\n}\n\n/**\n * Transforms import path for default locale processing\n */\nfunction transformDefaultLocaleImportPath(\n fullPath: string,\n patternHead: string,\n defaultLocale: string,\n hideDefaultLocale: boolean\n): string | null {\n if (hideDefaultLocale) {\n // Remove locale from imports that have it: '/snippets/en/file.mdx' -> '/snippets/file.mdx'\n if (fullPath.includes(`/${defaultLocale}/`)) {\n return fullPath.replace(`/${defaultLocale}/`, '/');\n } else if (fullPath.endsWith(`/${defaultLocale}`)) {\n return fullPath.replace(`/${defaultLocale}`, '');\n }\n return null; // Path doesn't have default locale\n } else {\n // Add locale to imports that don't have it: '/snippets/file.mdx' -> '/snippets/en/file.mdx'\n if (\n fullPath.includes(`/${defaultLocale}/`) ||\n fullPath.endsWith(`/${defaultLocale}`)\n ) {\n return null; // Already has default locale\n }\n\n if (fullPath.startsWith(patternHead)) {\n const pathAfterHead = fullPath.slice(patternHead.length);\n if (pathAfterHead) {\n return `${patternHead}${defaultLocale}/${pathAfterHead}`;\n } else {\n return `${patternHead.replace(/\\/$/, '')}/${defaultLocale}`;\n }\n }\n return null; // Path doesn't match pattern\n }\n}\n\n/**\n * Transforms import path for non-default locale processing with hideDefaultLocale=true\n */\nfunction transformNonDefaultLocaleImportPathWithHidden(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): string | null {\n // Check if already localized\n if (\n fullPath.startsWith(`${patternHead}${targetLocale}/`) ||\n fullPath === `${patternHead}${targetLocale}`\n ) {\n return null;\n }\n\n // Replace default locale with target locale\n const expectedPathWithDefaultLocale = `${patternHead}${defaultLocale}`;\n if (\n fullPath.startsWith(`${expectedPathWithDefaultLocale}/`) ||\n fullPath === expectedPathWithDefaultLocale\n ) {\n return fullPath.replace(\n `${patternHead}${defaultLocale}`,\n `${patternHead}${targetLocale}`\n );\n }\n\n // Handle exact pattern match\n if (fullPath === patternHead.replace(/\\/$/, '')) {\n return `${patternHead.replace(/\\/$/, '')}/${targetLocale}`;\n }\n\n // Add target locale to path without any locale\n const pathAfterHead = fullPath.slice(patternHead.length);\n return pathAfterHead\n ? `${patternHead}${targetLocale}/${pathAfterHead}`\n : `${patternHead}${targetLocale}`;\n}\n\n/**\n * Transforms import path for non-default locale processing with hideDefaultLocale=false\n */\nfunction transformNonDefaultLocaleImportPath(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): string | null {\n // If already localized to target, skip\n const expectedPathWithTarget = `${patternHead}${targetLocale}`;\n if (\n fullPath.startsWith(`${expectedPathWithTarget}/`) ||\n fullPath === expectedPathWithTarget\n ) {\n return null;\n }\n const expectedPathWithLocale = `${patternHead}${defaultLocale}`;\n\n if (\n fullPath.startsWith(`${expectedPathWithLocale}/`) ||\n fullPath === expectedPathWithLocale\n ) {\n // Replace existing default locale with target locale\n return fullPath.replace(\n `${patternHead}${defaultLocale}`,\n `${patternHead}${targetLocale}`\n );\n } else if (fullPath.startsWith(patternHead)) {\n // Add target locale to path that doesn't have any locale\n const pathAfterHead = fullPath.slice(patternHead.length);\n if (pathAfterHead) {\n return `${patternHead}${targetLocale}/${pathAfterHead}`;\n } else {\n return `${patternHead.replace(/\\/$/, '')}/${targetLocale}`;\n }\n }\n\n return null; // Path doesn't match pattern\n}\n\n/**\n * Main import path transformation function that delegates to specific scenarios\n */\nfunction transformImportPath(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n hideDefaultLocale: boolean,\n currentFilePath?: string,\n projectRoot: string = process.cwd(), // fallback if not provided\n rewrites?: Array<{ match: string; replace: string }>\n): string | null {\n // Apply explicit rewrites first (e.g., Docusaurus @site/docs -> @site/i18n/[locale]/...)\n if (rewrites && rewrites.length > 0) {\n const localeMap: Record<string, string> = {\n '[locale]': targetLocale,\n '[defaultLocale]': defaultLocale,\n };\n\n for (const { match, replace } of rewrites) {\n const resolvedMatch = match.replace(\n /\\[locale\\]|\\[defaultLocale\\]/g,\n (token) => localeMap[token] || token\n );\n if (fullPath.startsWith(resolvedMatch)) {\n const remainder = fullPath.slice(resolvedMatch.length);\n const resolvedReplace = replace.replace(\n /\\[locale\\]|\\[defaultLocale\\]/g,\n (token) => localeMap[token] || token\n );\n let newPath: string;\n if (resolvedReplace.endsWith('/')) {\n newPath = `${resolvedReplace}${remainder.replace(/^\\//, '')}`;\n } else if (remainder.startsWith('/')) {\n newPath = `${resolvedReplace}${remainder}`;\n } else {\n newPath = `${resolvedReplace}/${remainder}`;\n }\n return newPath;\n }\n }\n }\n\n let newPath: string | null;\n\n if (targetLocale === defaultLocale) {\n newPath = transformDefaultLocaleImportPath(\n fullPath,\n patternHead,\n defaultLocale,\n hideDefaultLocale\n );\n } else if (hideDefaultLocale) {\n newPath = transformNonDefaultLocaleImportPathWithHidden(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale\n );\n } else {\n newPath = transformNonDefaultLocaleImportPath(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale\n );\n }\n\n if (!newPath) return null;\n\n if (currentFilePath) {\n let resolvedPath: string;\n if (newPath.startsWith('/')) {\n // Interpret as project-root relative\n resolvedPath = path.join(projectRoot, newPath.replace(/^\\//, ''));\n } else {\n // Relative to current file\n const currentDir = path.dirname(currentFilePath);\n resolvedPath = path.resolve(currentDir, newPath);\n }\n\n if (!fs.existsSync(resolvedPath)) {\n return null;\n }\n }\n\n return newPath;\n}\n\n/**\n * AST-based transformation for MDX files using remark-mdx\n */\nfunction transformMdxImports(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): ImportTransformResult {\n const transformedImports: Array<{ originalPath: string; newPath: string }> =\n [];\n\n // Don't auto-prefix relative patterns that start with . or ..\n if (!pattern.startsWith('/') && !pattern.startsWith('.')) {\n pattern = '/' + pattern;\n }\n\n const patternHead = pattern.split('[locale]')[0];\n\n // Quick check: if the file doesn't contain the pattern, skip expensive AST parsing\n // For default locale processing, we also need to check if content might need adjustment\n if (targetLocale === defaultLocale) {\n // For default locale files, we always need to check as we're looking for either:\n // - paths without locale (when hideDefaultLocale=false)\n // - paths with default locale (when hideDefaultLocale=true)\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n if (!mdxContent.includes(patternWithoutSlash)) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedImports: [],\n };\n }\n } else {\n // For non-default locales, use the original logic\n if (!mdxContent.includes(patternHead.replace(/\\/$/, ''))) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedImports: [],\n };\n }\n }\n\n // Parse the MDX content into an AST\n let processedAst: Root;\n try {\n const parseProcessor = unified()\n .use(remarkParse)\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx);\n\n const ast = parseProcessor.parse(mdxContent);\n processedAst = parseProcessor.runSync(ast) as Root;\n } catch {\n return transformImportsStringFallback(\n mdxContent,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n currentFilePath,\n options\n );\n }\n\n let content = mdxContent;\n\n // Visit only mdxjsEsm nodes (import/export statements) to collect replacements\n visit(processedAst, 'mdxjsEsm', (node: MdxjsEsm) => {\n if (node.value && node.value.includes(patternHead.replace(/\\/$/, ''))) {\n // Find import lines that need transformation\n const lines = node.value.split('\\n');\n\n lines.forEach((line: string) => {\n // Only process import lines that match our pattern\n if (!line.trim().startsWith('import ')) {\n return;\n }\n\n // Check if this line should be processed\n if (\n !shouldProcessImportPath(\n line,\n patternHead,\n targetLocale,\n defaultLocale\n )\n ) {\n return;\n }\n\n // Extract the path from the import statement\n const quotes = ['\"', \"'\", '`'];\n\n for (const quote of quotes) {\n // Try both with and without trailing slash\n let startPattern = `${quote}${patternHead}`;\n let startIndex = line.indexOf(startPattern);\n\n // If pattern has trailing slash but path doesn't, try without slash\n if (startIndex === -1 && patternHead.endsWith('/')) {\n const patternWithoutSlash = patternHead.slice(0, -1);\n startPattern = `${quote}${patternWithoutSlash}`;\n startIndex = line.indexOf(startPattern);\n }\n\n if (startIndex === -1) continue;\n\n const pathStart = startIndex + 1; // After the quote\n const pathEnd = line.indexOf(quote, pathStart);\n\n if (pathEnd === -1) continue;\n\n const fullPath = line.slice(pathStart, pathEnd);\n\n // Transform the import path\n const newPath = transformImportPath(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale,\n currentFilePath,\n process.cwd(),\n options?.docsImportRewrites\n );\n\n if (!newPath) {\n continue; // No transformation needed\n }\n\n // Check exclusions\n if (isImportPathExcluded(fullPath, exclude, defaultLocale)) {\n continue;\n }\n\n // Apply the transformation to the original content\n // Simply replace the import path with the new path\n content = content.replace(\n `${quote}${fullPath}${quote}`,\n `${quote}${newPath}${quote}`\n );\n transformedImports.push({ originalPath: fullPath, newPath });\n break;\n }\n });\n }\n });\n\n return {\n content,\n hasChanges: transformedImports.length > 0,\n transformedImports,\n };\n}\n\n/**\n * String-based fallback for import localization when MDX parsing fails (e.g., on .md files)\n */\nfunction transformImportsStringFallback(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): ImportTransformResult {\n const transformedImports: Array<{ originalPath: string; newPath: string }> =\n [];\n\n if (!pattern.startsWith('/') && !pattern.startsWith('.')) {\n pattern = '/' + pattern;\n }\n const patternHead = pattern.split('[locale]')[0];\n\n let content = mdxContent;\n const importRegex = /import\\s+[^'\"]*['\"]([^'\"]+)['\"]\\s*;?/g;\n let match: RegExpExecArray | null;\n\n while ((match = importRegex.exec(mdxContent)) !== null) {\n const fullMatch = match[0];\n const importPath = match[1];\n\n if (\n !shouldProcessImportPath(\n importPath,\n patternHead,\n targetLocale,\n defaultLocale\n )\n ) {\n continue;\n }\n\n if (isImportPathExcluded(importPath, exclude, defaultLocale)) {\n continue;\n }\n\n const newPath = transformImportPath(\n importPath,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale,\n currentFilePath,\n process.cwd(),\n options?.docsImportRewrites\n );\n\n if (!newPath) continue;\n\n const updatedImport = fullMatch.replace(importPath, newPath);\n content = content.replace(fullMatch, updatedImport);\n transformedImports.push({ originalPath: importPath, newPath });\n }\n\n return {\n content,\n hasChanges: transformedImports.length > 0,\n transformedImports,\n };\n}\n\n/**\n * AST-based transformation for MDX files only\n */\nfunction localizeStaticImportsForFile(\n file: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]', // eg /docs/[locale] or /[locale]\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): string {\n // For MDX files, use AST-based transformation\n const result = transformMdxImports(\n file,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n currentFilePath,\n options\n );\n return result.content;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"localizeStaticImports.js","names":["fs","path"],"sources":["../../src/utils/localizeStaticImports.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n AdditionalOptions,\n StaticLocalizationSettings,\n} from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport micromatch from 'micromatch';\nimport { visit } from 'unist-util-visit';\nimport type { Root } from 'mdast';\nimport type { MdxjsEsm } from 'mdast-util-mdxjs-esm';\nimport { parseMdxTolerantly } from './mdxAnchorSyntax.js';\n\nconst { isMatch } = micromatch;\n\nexport type StaticImportSettings = StaticLocalizationSettings;\n\n/**\n * Localizes static imports in content files.\n * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)\n * @param settings - The settings object containing the project configuration.\n * @returns void\n *\n * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before\n * it can be enabled by default.\n *\n * Before this becomes a non-experimental feature, we need to:\n * - Support more file types\n * - Support more complex paths\n */\nexport default async function localizeStaticImports(\n settings: StaticImportSettings,\n includeFiles?: Set<string>\n) {\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return;\n }\n const { resolvedPaths: sourceFiles } = settings.files;\n\n const fileMapping = createFileMapping(\n sourceFiles,\n settings.files.placeholderPaths,\n settings.files.transformPaths ?? {},\n settings.files.transformFormats ?? {},\n settings.locales,\n settings.defaultLocale\n );\n\n // Process all file types at once with a single call\n const processPromises = [];\n\n // First, process default locale files (from source files)\n // This is needed because they might not be in the fileMapping if they're not being translated\n if (!fileMapping[settings.defaultLocale] && !includeFiles) {\n const defaultLocaleFiles: string[] = [];\n\n // Collect all .md and .mdx files from sourceFiles\n if (sourceFiles.md) {\n defaultLocaleFiles.push(...sourceFiles.md);\n }\n if (sourceFiles.mdx) {\n defaultLocaleFiles.push(...sourceFiles.mdx);\n }\n\n if (defaultLocaleFiles.length > 0) {\n const defaultPromise = Promise.all(\n defaultLocaleFiles.map(async (filePath: string) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file using default locale\n const localizedFile = localizeStaticImportsForFile(\n fileContent,\n settings.defaultLocale,\n settings.defaultLocale, // Process as default locale\n settings.options?.docsHideDefaultLocaleImport || false,\n settings.options?.docsImportPattern,\n settings.options?.excludeStaticImports,\n filePath,\n settings.options\n );\n // Write the localized file back to the same path\n await fs.promises.writeFile(filePath, localizedFile);\n })\n );\n processPromises.push(defaultPromise);\n }\n }\n\n // Then process all other locales from fileMapping\n const mappingPromises = Object.entries(fileMapping).map(\n async ([locale, filesMap]) => {\n // Get all files that are md or mdx\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n // Replace the placeholder path with the target path\n await Promise.all(\n targetFiles.map(async (filePath) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file\n const localizedFile = localizeStaticImportsForFile(\n fileContent,\n settings.defaultLocale,\n locale,\n settings.options?.docsHideDefaultLocaleImport || false,\n settings.options?.docsImportPattern,\n settings.options?.excludeStaticImports,\n filePath,\n settings.options\n );\n // Write the localized file to the target path\n await fs.promises.writeFile(filePath, localizedFile);\n })\n );\n }\n );\n processPromises.push(...mappingPromises);\n\n await Promise.all(processPromises);\n}\n\ninterface ImportTransformResult {\n content: string;\n hasChanges: boolean;\n transformedImports: Array<{\n originalPath: string;\n newPath: string;\n }>;\n}\n\n/**\n * Determines if an import path should be processed based on pattern matching\n */\nfunction shouldProcessImportPath(\n importPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): boolean {\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n\n if (targetLocale === defaultLocale) {\n // For default locale processing, check if path contains the pattern\n return importPath.includes(patternWithoutSlash);\n } else {\n // For non-default locales, check if path starts with pattern\n return importPath.includes(patternWithoutSlash);\n }\n}\n\n/**\n * Checks if an import path should be excluded based on exclusion patterns\n */\nfunction isImportPathExcluded(\n importPath: string,\n exclude: string[],\n defaultLocale: string\n): boolean {\n const excludePatterns = exclude.map((p) =>\n p.replace(/\\[locale\\]/g, defaultLocale)\n );\n return excludePatterns.some((pattern) => isMatch(importPath, pattern));\n}\n\n/**\n * Transforms import path for default locale processing\n */\nfunction transformDefaultLocaleImportPath(\n fullPath: string,\n patternHead: string,\n defaultLocale: string,\n hideDefaultLocale: boolean\n): string | null {\n if (hideDefaultLocale) {\n // Remove locale from imports that have it: '/snippets/en/file.mdx' -> '/snippets/file.mdx'\n if (fullPath.includes(`/${defaultLocale}/`)) {\n return fullPath.replace(`/${defaultLocale}/`, '/');\n } else if (fullPath.endsWith(`/${defaultLocale}`)) {\n return fullPath.replace(`/${defaultLocale}`, '');\n }\n return null; // Path doesn't have default locale\n } else {\n // Add locale to imports that don't have it: '/snippets/file.mdx' -> '/snippets/en/file.mdx'\n if (\n fullPath.includes(`/${defaultLocale}/`) ||\n fullPath.endsWith(`/${defaultLocale}`)\n ) {\n return null; // Already has default locale\n }\n\n if (fullPath.startsWith(patternHead)) {\n const pathAfterHead = fullPath.slice(patternHead.length);\n if (pathAfterHead) {\n return `${patternHead}${defaultLocale}/${pathAfterHead}`;\n } else {\n return `${patternHead.replace(/\\/$/, '')}/${defaultLocale}`;\n }\n }\n return null; // Path doesn't match pattern\n }\n}\n\n/**\n * Transforms import path for non-default locale processing with hideDefaultLocale=true\n */\nfunction transformNonDefaultLocaleImportPathWithHidden(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): string | null {\n // Check if already localized\n if (\n fullPath.startsWith(`${patternHead}${targetLocale}/`) ||\n fullPath === `${patternHead}${targetLocale}`\n ) {\n return null;\n }\n\n // Replace default locale with target locale\n const expectedPathWithDefaultLocale = `${patternHead}${defaultLocale}`;\n if (\n fullPath.startsWith(`${expectedPathWithDefaultLocale}/`) ||\n fullPath === expectedPathWithDefaultLocale\n ) {\n return fullPath.replace(\n `${patternHead}${defaultLocale}`,\n `${patternHead}${targetLocale}`\n );\n }\n\n // Handle exact pattern match\n if (fullPath === patternHead.replace(/\\/$/, '')) {\n return `${patternHead.replace(/\\/$/, '')}/${targetLocale}`;\n }\n\n // Add target locale to path without any locale\n const pathAfterHead = fullPath.slice(patternHead.length);\n return pathAfterHead\n ? `${patternHead}${targetLocale}/${pathAfterHead}`\n : `${patternHead}${targetLocale}`;\n}\n\n/**\n * Transforms import path for non-default locale processing with hideDefaultLocale=false\n */\nfunction transformNonDefaultLocaleImportPath(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string\n): string | null {\n // If already localized to target, skip\n const expectedPathWithTarget = `${patternHead}${targetLocale}`;\n if (\n fullPath.startsWith(`${expectedPathWithTarget}/`) ||\n fullPath === expectedPathWithTarget\n ) {\n return null;\n }\n const expectedPathWithLocale = `${patternHead}${defaultLocale}`;\n\n if (\n fullPath.startsWith(`${expectedPathWithLocale}/`) ||\n fullPath === expectedPathWithLocale\n ) {\n // Replace existing default locale with target locale\n return fullPath.replace(\n `${patternHead}${defaultLocale}`,\n `${patternHead}${targetLocale}`\n );\n } else if (fullPath.startsWith(patternHead)) {\n // Add target locale to path that doesn't have any locale\n const pathAfterHead = fullPath.slice(patternHead.length);\n if (pathAfterHead) {\n return `${patternHead}${targetLocale}/${pathAfterHead}`;\n } else {\n return `${patternHead.replace(/\\/$/, '')}/${targetLocale}`;\n }\n }\n\n return null; // Path doesn't match pattern\n}\n\n/**\n * Main import path transformation function that delegates to specific scenarios\n */\nfunction transformImportPath(\n fullPath: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n hideDefaultLocale: boolean,\n currentFilePath?: string,\n projectRoot: string = process.cwd(), // fallback if not provided\n rewrites?: Array<{ match: string; replace: string }>\n): string | null {\n // Apply explicit rewrites first (e.g., Docusaurus @site/docs -> @site/i18n/[locale]/...)\n if (rewrites && rewrites.length > 0) {\n const localeMap: Record<string, string> = {\n '[locale]': targetLocale,\n '[defaultLocale]': defaultLocale,\n };\n\n for (const { match, replace } of rewrites) {\n const resolvedMatch = match.replace(\n /\\[locale\\]|\\[defaultLocale\\]/g,\n (token) => localeMap[token] || token\n );\n if (fullPath.startsWith(resolvedMatch)) {\n const remainder = fullPath.slice(resolvedMatch.length);\n const resolvedReplace = replace.replace(\n /\\[locale\\]|\\[defaultLocale\\]/g,\n (token) => localeMap[token] || token\n );\n let newPath: string;\n if (resolvedReplace.endsWith('/')) {\n newPath = `${resolvedReplace}${remainder.replace(/^\\//, '')}`;\n } else if (remainder.startsWith('/')) {\n newPath = `${resolvedReplace}${remainder}`;\n } else {\n newPath = `${resolvedReplace}/${remainder}`;\n }\n return newPath;\n }\n }\n }\n\n let newPath: string | null;\n\n if (targetLocale === defaultLocale) {\n newPath = transformDefaultLocaleImportPath(\n fullPath,\n patternHead,\n defaultLocale,\n hideDefaultLocale\n );\n } else if (hideDefaultLocale) {\n newPath = transformNonDefaultLocaleImportPathWithHidden(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale\n );\n } else {\n newPath = transformNonDefaultLocaleImportPath(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale\n );\n }\n\n if (!newPath) return null;\n\n if (currentFilePath) {\n let resolvedPath: string;\n if (newPath.startsWith('/')) {\n // Interpret as project-root relative\n resolvedPath = path.join(projectRoot, newPath.replace(/^\\//, ''));\n } else {\n // Relative to current file\n const currentDir = path.dirname(currentFilePath);\n resolvedPath = path.resolve(currentDir, newPath);\n }\n\n if (!fs.existsSync(resolvedPath)) {\n return null;\n }\n }\n\n return newPath;\n}\n\n/**\n * AST-based transformation for MDX files using remark-mdx\n */\nfunction transformMdxImports(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): ImportTransformResult {\n const transformedImports: Array<{ originalPath: string; newPath: string }> =\n [];\n\n // Don't auto-prefix relative patterns that start with . or ..\n if (!pattern.startsWith('/') && !pattern.startsWith('.')) {\n pattern = '/' + pattern;\n }\n\n const patternHead = pattern.split('[locale]')[0];\n\n // Quick check: if the file doesn't contain the pattern, skip expensive AST parsing\n // For default locale processing, we also need to check if content might need adjustment\n if (targetLocale === defaultLocale) {\n // For default locale files, we always need to check as we're looking for either:\n // - paths without locale (when hideDefaultLocale=false)\n // - paths with default locale (when hideDefaultLocale=true)\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n if (!mdxContent.includes(patternWithoutSlash)) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedImports: [],\n };\n }\n } else {\n // For non-default locales, use the original logic\n if (!mdxContent.includes(patternHead.replace(/\\/$/, ''))) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedImports: [],\n };\n }\n }\n\n // Parse the MDX content into an AST\n let processedAst: Root;\n try {\n // Parsed only to locate import nodes; edits below are applied to the\n // original string, so a neutralized parse cannot leak into the output.\n processedAst = parseMdxTolerantly(mdxContent);\n } catch {\n return transformImportsStringFallback(\n mdxContent,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n currentFilePath,\n options\n );\n }\n\n let content = mdxContent;\n\n // Visit only mdxjsEsm nodes (import/export statements) to collect replacements\n visit(processedAst, 'mdxjsEsm', (node: MdxjsEsm) => {\n if (node.value && node.value.includes(patternHead.replace(/\\/$/, ''))) {\n // Find import lines that need transformation\n const lines = node.value.split('\\n');\n\n lines.forEach((line: string) => {\n // Only process import lines that match our pattern\n if (!line.trim().startsWith('import ')) {\n return;\n }\n\n // Check if this line should be processed\n if (\n !shouldProcessImportPath(\n line,\n patternHead,\n targetLocale,\n defaultLocale\n )\n ) {\n return;\n }\n\n // Extract the path from the import statement\n const quotes = ['\"', \"'\", '`'];\n\n for (const quote of quotes) {\n // Try both with and without trailing slash\n let startPattern = `${quote}${patternHead}`;\n let startIndex = line.indexOf(startPattern);\n\n // If pattern has trailing slash but path doesn't, try without slash\n if (startIndex === -1 && patternHead.endsWith('/')) {\n const patternWithoutSlash = patternHead.slice(0, -1);\n startPattern = `${quote}${patternWithoutSlash}`;\n startIndex = line.indexOf(startPattern);\n }\n\n if (startIndex === -1) continue;\n\n const pathStart = startIndex + 1; // After the quote\n const pathEnd = line.indexOf(quote, pathStart);\n\n if (pathEnd === -1) continue;\n\n const fullPath = line.slice(pathStart, pathEnd);\n\n // Transform the import path\n const newPath = transformImportPath(\n fullPath,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale,\n currentFilePath,\n process.cwd(),\n options?.docsImportRewrites\n );\n\n if (!newPath) {\n continue; // No transformation needed\n }\n\n // Check exclusions\n if (isImportPathExcluded(fullPath, exclude, defaultLocale)) {\n continue;\n }\n\n // Apply the transformation to the original content\n // Simply replace the import path with the new path\n content = content.replace(\n `${quote}${fullPath}${quote}`,\n `${quote}${newPath}${quote}`\n );\n transformedImports.push({ originalPath: fullPath, newPath });\n break;\n }\n });\n }\n });\n\n return {\n content,\n hasChanges: transformedImports.length > 0,\n transformedImports,\n };\n}\n\n/**\n * String-based fallback for import localization when MDX parsing fails (e.g., on .md files)\n */\nfunction transformImportsStringFallback(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): ImportTransformResult {\n const transformedImports: Array<{ originalPath: string; newPath: string }> =\n [];\n\n if (!pattern.startsWith('/') && !pattern.startsWith('.')) {\n pattern = '/' + pattern;\n }\n const patternHead = pattern.split('[locale]')[0];\n\n let content = mdxContent;\n const importRegex = /import\\s+[^'\"]*['\"]([^'\"]+)['\"]\\s*;?/g;\n let match: RegExpExecArray | null;\n\n while ((match = importRegex.exec(mdxContent)) !== null) {\n const fullMatch = match[0];\n const importPath = match[1];\n\n if (\n !shouldProcessImportPath(\n importPath,\n patternHead,\n targetLocale,\n defaultLocale\n )\n ) {\n continue;\n }\n\n if (isImportPathExcluded(importPath, exclude, defaultLocale)) {\n continue;\n }\n\n const newPath = transformImportPath(\n importPath,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale,\n currentFilePath,\n process.cwd(),\n options?.docsImportRewrites\n );\n\n if (!newPath) continue;\n\n const updatedImport = fullMatch.replace(importPath, newPath);\n content = content.replace(fullMatch, updatedImport);\n transformedImports.push({ originalPath: importPath, newPath });\n }\n\n return {\n content,\n hasChanges: transformedImports.length > 0,\n transformedImports,\n };\n}\n\n/**\n * AST-based transformation for MDX files only\n */\nfunction localizeStaticImportsForFile(\n file: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]', // eg /docs/[locale] or /[locale]\n exclude: string[] = [],\n currentFilePath?: string,\n options?: AdditionalOptions\n): string {\n // For MDX files, use AST-based transformation\n const result = transformMdxImports(\n file,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n currentFilePath,\n options\n );\n return result.content;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createFileMapping } from "../formats/files/fileMapping.js";
|
|
2
|
+
import { parseMdxForRoundTrip, restoreAnchorIds } from "./mdxAnchorSyntax.js";
|
|
2
3
|
import * as fs$1 from "fs";
|
|
3
4
|
import { parse } from "@babel/parser";
|
|
4
5
|
import micromatch from "micromatch";
|
|
5
6
|
import { unified } from "unified";
|
|
6
|
-
import remarkParse from "remark-parse";
|
|
7
7
|
import remarkMdx from "remark-mdx";
|
|
8
8
|
import remarkFrontmatter from "remark-frontmatter";
|
|
9
9
|
import { visit } from "unist-util-visit";
|
|
@@ -217,10 +217,11 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
|
|
|
217
217
|
transformedUrls: []
|
|
218
218
|
};
|
|
219
219
|
let processedAst;
|
|
220
|
+
let neutralizedAnchors;
|
|
220
221
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const parsed = parseMdxForRoundTrip(mdxContent);
|
|
223
|
+
processedAst = parsed.ast;
|
|
224
|
+
neutralizedAnchors = parsed.neutralized;
|
|
224
225
|
} catch {
|
|
225
226
|
return {
|
|
226
227
|
content: mdxContent,
|
|
@@ -326,6 +327,7 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
|
|
|
326
327
|
} } });
|
|
327
328
|
const outTree = stringifyProcessor.runSync(processedAst);
|
|
328
329
|
content = stringifyProcessor.stringify(outTree);
|
|
330
|
+
if (neutralizedAnchors) content = restoreAnchorIds(content);
|
|
329
331
|
} catch (error) {
|
|
330
332
|
console.warn(`Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
331
333
|
console.warn("Returning original content unchanged due to stringify error.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localizeStaticUrls.js","names":["parseBabel","fs"],"sources":["../../src/utils/localizeStaticUrls.ts"],"sourcesContent":["import * as fs from 'fs';\nimport type { StaticLocalizationSettings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport micromatch from 'micromatch';\nimport { unified } from 'unified';\nimport remarkParse from 'remark-parse';\nimport remarkMdx from 'remark-mdx';\nimport remarkFrontmatter from 'remark-frontmatter';\nimport remarkStringify from 'remark-stringify';\nimport { visit } from 'unist-util-visit';\nimport type { Root, Link, Literal } from 'mdast';\nimport type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx';\nimport { escapeHtmlInTextNodes, normalizeCJKCharacters } from 'gt-remark';\nimport { parse as parseBabel } from '@babel/parser';\n\nconst { isMatch } = micromatch;\n\n/**\n * URL-bearing JSX attributes that we localize. Intentionally limited to link\n * attributes (`href`) — NOT asset attributes like `src`, which usually point at\n * shared, locale-agnostic assets (e.g. `/docs/images/...`) that would 404 if a\n * locale prefix were added. Extend deliberately.\n */\nconst LOCALIZABLE_URL_ATTRIBUTES = new Set(['href']);\n\n/**\n * Localize URL string literals that live inside an MDX expression's raw source.\n *\n * URLs can be nested arbitrarily deep inside `{...}` expressions — e.g. an\n * `<a href>` buried inside a component prop:\n * <ParamField type={<span><a href=\"/docs/x\">Error</a></span>} />\n * Such hrefs never appear as mdast nodes (they live in the expression's embedded\n * JS), so the mdast visitors can't reach them. We re-parse the expression source\n * with Babel — positions are local to `source`, which avoids any document-offset\n * mapping — find JSX url-attribute string literals at any depth, and surgically\n * rewrite them in place.\n *\n * `rootStringIsUrl` covers `href={\"/docs/x\"}`, where the expression itself is a\n * bare string literal that a url-named attribute should treat as a URL.\n *\n * Only static string literals are localized; dynamically-computed URLs\n * (template literals, identifiers, concatenation) are left untouched — a\n * fundamental limit of static localization, not a parser shortcoming.\n */\nfunction localizeUrlsInExpressionSource(\n source: string,\n transformUrl: (url: string, linkType: 'markdown' | 'href') => string | null,\n rootStringIsUrl: boolean = false\n): string {\n if (!source.trim()) return source;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let ast: any;\n try {\n ast = parseBabel(source, {\n sourceType: 'module',\n errorRecovery: true,\n plugins: ['jsx', 'typescript'],\n });\n } catch {\n // Dynamic / unparseable expression — leave it untouched.\n return source;\n }\n\n const replacements: Array<{ start: number; end: number; text: string }> = [];\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pushLiteralReplacement = (literal: any) => {\n if (\n !literal ||\n // A bare string at statement position is parsed by Babel as a\n // DirectiveLiteral (cf. \"use strict\") rather than a StringLiteral.\n (literal.type !== 'StringLiteral' &&\n literal.type !== 'DirectiveLiteral') ||\n typeof literal.value !== 'string' ||\n typeof literal.start !== 'number' ||\n typeof literal.end !== 'number'\n ) {\n return;\n }\n const newUrl = transformUrl(literal.value, 'href');\n if (!newUrl) return;\n // Preserve the original quote style; URLs never contain quote chars.\n const quote = source[literal.start] === \"'\" ? \"'\" : '\"';\n replacements.push({\n start: literal.start,\n end: literal.end,\n text: `${quote}${newUrl}${quote}`,\n });\n };\n\n // `href={\"/docs/x\"}`: the entire expression is the URL string.\n if (rootStringIsUrl) {\n const program = ast.program;\n const body = (program?.body ?? []) as Array<{\n type: string;\n expression?: unknown;\n }>;\n if (body.length === 1 && body[0]?.type === 'ExpressionStatement') {\n pushLiteralReplacement(body[0].expression);\n } else if (body.length === 0 && program?.directives?.length === 1) {\n // Babel parses a lone string literal as a directive, not a statement.\n pushLiteralReplacement(program.directives[0]?.value);\n }\n }\n\n // Walk the AST for JSX url-attributes anywhere in the expression.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const seen = new Set<any>();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const walk = (node: any) => {\n if (!node || typeof node !== 'object') return;\n if (Array.isArray(node)) {\n for (const item of node) walk(item);\n return;\n }\n if (seen.has(node)) return;\n seen.add(node);\n\n if (\n node.type === 'JSXAttribute' &&\n node.name?.type === 'JSXIdentifier' &&\n LOCALIZABLE_URL_ATTRIBUTES.has(node.name.name) &&\n node.value\n ) {\n if (node.value.type === 'StringLiteral') {\n pushLiteralReplacement(node.value);\n } else if (\n node.value.type === 'JSXExpressionContainer' &&\n node.value.expression?.type === 'StringLiteral'\n ) {\n pushLiteralReplacement(node.value.expression);\n }\n }\n\n for (const key in node) {\n if (\n key === 'loc' ||\n key === 'start' ||\n key === 'end' ||\n key === 'range'\n ) {\n continue;\n }\n const child = node[key];\n if (child && typeof child === 'object') walk(child);\n }\n };\n walk(ast.program ?? ast);\n\n if (replacements.length === 0) return source;\n\n // Apply end-to-start so earlier offsets remain valid.\n let result = source;\n replacements\n .sort((a, b) => b.start - a.start)\n .forEach(({ start, end, text }) => {\n result = result.slice(0, start) + text + result.slice(end);\n });\n return result;\n}\n\nexport type StaticUrlSettings = StaticLocalizationSettings;\n\n/**\n * Localizes static urls in content files.\n * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)\n * @param settings - The settings object containing the project configuration.\n * @returns void\n *\n * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before\n * it can be enabled by default.\n *\n * Before this becomes a non-experimental feature, we need to:\n * - Support more file types\n * - Support more complex paths\n */\nexport default async function localizeStaticUrls(\n settings: StaticUrlSettings,\n targetLocales?: string[],\n includeFiles?: Set<string>\n) {\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return;\n }\n const { resolvedPaths: sourceFiles } = settings.files;\n\n // Use filtered locales if provided, otherwise use all locales\n const locales = targetLocales || settings.locales;\n\n const fileMapping = createFileMapping(\n sourceFiles,\n settings.files.placeholderPaths,\n settings.files.transformPaths ?? {},\n settings.files.transformFormats ?? {},\n settings.locales, // Always use all locales for mapping, filter later\n settings.defaultLocale\n );\n\n // Process all file types at once with a single call\n const processPromises = [];\n\n // First, process default locale files (from source files)\n // This is needed because they might not be in the fileMapping if they're not being translated\n // Only process default locale if it's in the target locales filter\n if (\n !fileMapping[settings.defaultLocale] &&\n locales.includes(settings.defaultLocale) &&\n !includeFiles // when filtering, skip default-locale pass\n ) {\n const defaultLocaleFiles: string[] = [];\n\n // Collect all .md and .mdx files from sourceFiles\n if (sourceFiles.md) {\n defaultLocaleFiles.push(...sourceFiles.md);\n }\n if (sourceFiles.mdx) {\n defaultLocaleFiles.push(...sourceFiles.mdx);\n }\n\n if (defaultLocaleFiles.length > 0) {\n const defaultPromise = Promise.all(\n defaultLocaleFiles.map(async (filePath: string) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file using default locale\n const result = localizeStaticUrlsForFile(\n fileContent,\n settings.defaultLocale,\n settings.defaultLocale, // Process as default locale\n settings.options?.experimentalHideDefaultLocale || false,\n settings.options?.docsUrlPattern,\n settings.options?.excludeStaticUrls,\n settings.options?.baseDomain\n );\n // Only write the file if there were changes\n if (result.hasChanges) {\n await fs.promises.writeFile(filePath, result.content);\n }\n })\n );\n processPromises.push(defaultPromise);\n }\n }\n\n // Then process all other locales from fileMapping\n const mappingPromises = Object.entries(fileMapping)\n .filter(([locale]) => locales.includes(locale)) // Filter by target locales\n .map(async ([locale, filesMap]) => {\n // Get all files that are md or mdx\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n // Replace the placeholder path with the target path\n await Promise.all(\n targetFiles.map(async (filePath) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file (handles both URLs and hrefs in single AST pass)\n const result = localizeStaticUrlsForFile(\n fileContent,\n settings.defaultLocale,\n locale,\n settings.options?.experimentalHideDefaultLocale || false,\n settings.options?.docsUrlPattern,\n settings.options?.excludeStaticUrls,\n settings.options?.baseDomain\n );\n // Only write the file if there were changes\n if (result.hasChanges) {\n await fs.promises.writeFile(filePath, result.content);\n }\n })\n );\n });\n processPromises.push(...mappingPromises);\n\n await Promise.all(processPromises);\n}\n\ninterface UrlTransformResult {\n content: string;\n hasChanges: boolean;\n transformedUrls: Array<{\n originalPath: string;\n newPath: string;\n type: 'markdown' | 'href';\n }>;\n}\n\n/**\n * Determines if a URL should be processed based on pattern matching\n */\nfunction shouldProcessUrl(\n originalUrl: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n baseDomain?: string\n): boolean {\n // Check fragment-only URLs like \"#id-name\"\n if (/^\\s*#/.test(originalUrl)) {\n return false;\n }\n\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n\n // Handle absolute URLs with baseDomain\n let urlToCheck = originalUrl;\n if (baseDomain && originalUrl.startsWith(baseDomain)) {\n urlToCheck = originalUrl.substring(baseDomain.length);\n }\n\n if (targetLocale === defaultLocale) {\n // For default locale processing, check if URL contains the pattern\n return urlToCheck.includes(patternWithoutSlash);\n } else {\n // For non-default locales, check if URL starts with pattern\n return urlToCheck.startsWith(patternWithoutSlash);\n }\n}\n\n/**\n * Determines if a URL should be processed based on the base domain\n */\nfunction shouldProcessAbsoluteUrl(\n originalUrl: string,\n baseDomain: string\n): boolean {\n return originalUrl.startsWith(baseDomain);\n}\n\n/**\n * Checks if a URL should be excluded based on exclusion patterns\n */\nfunction isUrlExcluded(\n originalUrl: string,\n exclude: string[],\n defaultLocale: string\n): boolean {\n const excludePatterns = exclude.map((p) =>\n p.replace(/\\[locale\\]/g, defaultLocale)\n );\n return excludePatterns.some((pattern) => isMatch(originalUrl, pattern));\n}\n\n/**\n * Main URL transformation function that delegates to specific scenarios\n */\nexport function transformUrlPath(\n originalUrl: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n hideDefaultLocale: boolean\n): string | null {\n const originalPathArray = originalUrl\n .split('/')\n .filter((path) => path !== '');\n const patternHeadArray = patternHead.split('/').filter((path) => path !== '');\n\n // check if the pattern head matches the original path\n if (!checkIfPathMatchesPattern(originalPathArray, patternHeadArray)) {\n return null;\n }\n\n if (patternHeadArray.length > originalPathArray.length) {\n return null; // Pattern is longer than the URL path\n }\n\n let result = null;\n if (targetLocale === defaultLocale) {\n if (hideDefaultLocale) {\n // check if default locale is already present\n if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {\n return null;\n }\n\n // remove default locale\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n ...originalPathArray.slice(patternHeadArray.length + 1),\n ];\n\n result = newPathArray.join('/');\n } else {\n // check if default locale is already present\n if (originalPathArray?.[patternHeadArray.length] === defaultLocale) {\n return null;\n }\n\n // insert default locale\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n defaultLocale,\n ...originalPathArray.slice(patternHeadArray.length),\n ];\n\n result = newPathArray.join('/');\n }\n } else if (hideDefaultLocale) {\n // Avoid duplicating target locale if already present\n if (originalPathArray?.[patternHeadArray.length] === targetLocale) {\n return null;\n }\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n targetLocale,\n ...originalPathArray.slice(patternHeadArray.length),\n ];\n\n result = newPathArray.join('/');\n } else {\n // check default locale\n if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {\n return null;\n }\n\n // replace default locale with target locale\n const newPathArray = [...originalPathArray];\n newPathArray[patternHeadArray.length] = targetLocale;\n\n result = newPathArray.join('/');\n }\n\n // check for leading and trailing slashes\n if (originalUrl.startsWith('/')) {\n result = '/' + result;\n }\n if (originalUrl.endsWith('/')) {\n result = result + '/';\n }\n\n return result;\n}\n\n/**\n * AST-based transformation for MDX files using remark-mdx\n */\nfunction transformMdxUrls(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n baseDomain?: string\n): UrlTransformResult {\n const transformedUrls: Array<{\n originalPath: string;\n newPath: string;\n type: 'markdown' | 'href';\n }> = [];\n\n if (!pattern.startsWith('/')) {\n pattern = '/' + pattern;\n }\n\n const patternHead = pattern.split('[locale]')[0];\n\n // Quick check: if the file doesn't contain the pattern, skip expensive AST parsing\n // For default locale processing, we also need to check if content might need adjustment\n if (targetLocale === defaultLocale) {\n // For default locale files, we always need to check as we're looking for either:\n // - paths without locale (when hideDefaultLocale=false)\n // - paths with default locale (when hideDefaultLocale=true)\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n if (!mdxContent.includes(patternWithoutSlash)) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n } else {\n // For non-default locales, use the original logic\n if (!mdxContent.includes(patternHead.replace(/\\/$/, ''))) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n }\n\n // Parse the MDX content into an AST\n let processedAst: Root;\n try {\n const parseProcessor = unified()\n .use(remarkParse)\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx);\n\n const ast = parseProcessor.parse(mdxContent);\n processedAst = parseProcessor.runSync(ast) as Root;\n } catch {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls,\n };\n }\n\n // Helper function to transform URL based on pattern\n const transformUrl = (\n originalUrl: string,\n linkType: 'markdown' | 'href'\n ): string | null => {\n // For Markdown links [text](path), only process absolute-root paths starting with '/'\n // Relative markdown links should remain relative to the current page and not be localized.\n if (linkType === 'markdown') {\n const isFragment = /^\\s*#/.test(originalUrl);\n const isAbsoluteRoot = originalUrl.startsWith('/');\n const looksAbsoluteWithDomain = baseDomain\n ? shouldProcessAbsoluteUrl(originalUrl, baseDomain)\n : false;\n if (!isAbsoluteRoot && !looksAbsoluteWithDomain && !isFragment) {\n return null;\n }\n }\n // Check if URL should be processed\n if (\n !shouldProcessUrl(\n originalUrl,\n patternHead,\n targetLocale,\n defaultLocale,\n baseDomain\n )\n ) {\n return null;\n }\n\n // Skip absolute URLs (http://, https://, //, etc.)\n if (baseDomain && shouldProcessAbsoluteUrl(originalUrl, baseDomain)) {\n // Get everything after the base domain\n const afterDomain = originalUrl.substring(baseDomain.length);\n\n const transformedPath = transformUrlPath(\n afterDomain,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale\n );\n if (!transformedPath) {\n return null;\n }\n transformedUrls.push({\n originalPath: originalUrl,\n newPath: transformedPath,\n type: linkType,\n });\n return transformedPath ? baseDomain + transformedPath : null;\n }\n\n // Exclude colon-prefixed URLs (http://, https://, //, etc.)\n if (originalUrl.split('?')[0].includes(':')) {\n return null;\n }\n\n // Transform the URL based on locale and configuration\n const newUrl = transformUrlPath(\n originalUrl,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale\n );\n\n if (!newUrl) {\n return null;\n }\n\n // Check exclusions\n if (isUrlExcluded(originalUrl, exclude, defaultLocale)) {\n return null;\n }\n\n transformedUrls.push({\n originalPath: originalUrl,\n newPath: newUrl,\n type: linkType,\n });\n return newUrl;\n };\n\n // Visit markdown link nodes: [text](url)\n visit(processedAst, 'link', (node: Link) => {\n if (node.url) {\n const newUrl = transformUrl(node.url, 'markdown');\n if (newUrl) {\n node.url = newUrl;\n }\n }\n });\n\n // Visit JSX/HTML elements for href attributes: <a href=\"url\"> or <Card href=\"url\">\n visit(processedAst, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {\n const jsxNode = node as MdxJsxFlowElement | MdxJsxTextElement;\n if (!jsxNode.attributes) return;\n for (const attr of jsxNode.attributes) {\n if (attr.type !== 'mdxJsxAttribute' || !attr.value) continue;\n\n // Plain string attribute value, e.g. <a href=\"/docs/x\">\n if (typeof attr.value === 'string') {\n if (LOCALIZABLE_URL_ATTRIBUTES.has(attr.name)) {\n const newUrl = transformUrl(attr.value, 'href');\n if (newUrl) {\n attr.value = newUrl;\n }\n }\n continue;\n }\n\n // Expression attribute value. Two shapes are handled:\n // href={\"/docs/x\"} (url attr, bare string)\n // type={<span><a href=\"/docs/x\">…</a></span>} (url attr nested deep\n // inside a non-url prop)\n // The nested case never surfaces as an mdast node — it lives in the\n // expression's embedded JS — so it's localized via the source-level walk.\n if (\n typeof attr.value === 'object' &&\n attr.value.type === 'mdxJsxAttributeValueExpression' &&\n typeof attr.value.value === 'string'\n ) {\n const newValue = localizeUrlsInExpressionSource(\n attr.value.value,\n transformUrl,\n LOCALIZABLE_URL_ATTRIBUTES.has(attr.name)\n );\n if (newValue !== attr.value.value) {\n attr.value.value = newValue;\n }\n }\n }\n });\n\n // Visit standalone MDX expressions for URLs inside embedded JSX, e.g.\n // {isBeta && <a href=\"/docs/x\">Beta</a>}\n visit(processedAst, ['mdxFlowExpression', 'mdxTextExpression'], (node) => {\n const exprNode = node as unknown as Literal;\n if (typeof exprNode.value === 'string' && exprNode.value) {\n const newValue = localizeUrlsInExpressionSource(\n exprNode.value,\n transformUrl\n );\n if (newValue !== exprNode.value) {\n exprNode.value = newValue;\n }\n }\n });\n\n // Visit raw JSX nodes for href attributes in JSX strings\n visit(processedAst, 'jsx', (node: Literal) => {\n if (node.value && typeof node.value === 'string') {\n const jsxContent = node.value;\n\n // Use regex to find href attributes in the JSX string\n const hrefRegex = /href\\s*=\\s*[\"']([^\"']+)[\"']/g;\n let match;\n const replacements: Array<{\n start: number;\n end: number;\n oldHrefAttr: string;\n newHrefAttr: string;\n }> = [];\n\n // Reset regex lastIndex to avoid issues with global flag\n hrefRegex.lastIndex = 0;\n\n while ((match = hrefRegex.exec(jsxContent)) !== null) {\n const originalHref = match[1];\n const newUrl = transformUrl(originalHref, 'href');\n\n if (newUrl) {\n // Store replacement info\n const oldHrefAttr = match[0]; // The full match like 'href=\"/quickstart\"'\n const quote = oldHrefAttr.includes('\"') ? '\"' : \"'\";\n const newHrefAttr = `href=${quote}${newUrl}${quote}`;\n\n replacements.push({\n start: match.index!,\n end: match.index! + oldHrefAttr.length,\n oldHrefAttr,\n newHrefAttr,\n });\n }\n }\n\n // Apply replacements in reverse order (from end to start) to avoid position shifts\n if (replacements.length > 0) {\n let newJsxContent = jsxContent;\n replacements\n .sort((a, b) => b.start - a.start)\n .forEach(({ start, end, newHrefAttr }) => {\n newJsxContent =\n newJsxContent.slice(0, start) +\n newHrefAttr +\n newJsxContent.slice(end);\n });\n\n node.value = newJsxContent;\n }\n }\n });\n\n // Convert the modified AST back to MDX string\n let content: string;\n try {\n const stringifyProcessor = unified()\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx)\n .use(normalizeCJKCharacters)\n .use(escapeHtmlInTextNodes)\n .use(remarkStringify, {\n handlers: {\n // Handler to prevent escaping (avoids '<' -> '\\<')\n text(node: Literal) {\n return node.value;\n },\n },\n });\n\n const outTree = stringifyProcessor.runSync(processedAst) as Root;\n content = stringifyProcessor.stringify(outTree);\n } catch (error) {\n console.warn(\n `Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`\n );\n console.warn(\n 'Returning original content unchanged due to stringify error.'\n );\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n\n // Handle newline formatting to match original input\n if (content.endsWith('\\n') && !mdxContent.endsWith('\\n')) {\n content = content.slice(0, -1);\n }\n\n // Preserve leading newlines from original content\n if (mdxContent.startsWith('\\n') && !content.startsWith('\\n')) {\n content = '\\n' + content;\n }\n\n return {\n content,\n hasChanges: transformedUrls.length > 0,\n transformedUrls,\n };\n}\n\n// AST-based transformation for MDX files using remark\nfunction localizeStaticUrlsForFile(\n file: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]', // eg /docs/[locale] or /[locale]\n exclude: string[] = [],\n baseDomain?: string\n): UrlTransformResult {\n // Use AST-based transformation for MDX files\n return transformMdxUrls(\n file,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n baseDomain || ''\n );\n}\n\nfunction checkIfPathMatchesPattern(\n originalUrlArray: string[],\n patternHeadArray: string[]\n): boolean {\n // check if the pattern head matches the original path\n for (let i = 0; i < patternHeadArray.length; i++) {\n if (patternHeadArray[i] !== originalUrlArray?.[i]) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"localizeStaticUrls.js","names":["parseBabel","fs"],"sources":["../../src/utils/localizeStaticUrls.ts"],"sourcesContent":["import * as fs from 'fs';\nimport type { StaticLocalizationSettings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\nimport micromatch from 'micromatch';\nimport { unified } from 'unified';\nimport remarkMdx from 'remark-mdx';\nimport remarkFrontmatter from 'remark-frontmatter';\nimport remarkStringify from 'remark-stringify';\nimport { visit } from 'unist-util-visit';\nimport type { Root, Link, Literal } from 'mdast';\nimport type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx';\nimport { escapeHtmlInTextNodes, normalizeCJKCharacters } from 'gt-remark';\nimport { parse as parseBabel } from '@babel/parser';\nimport { parseMdxForRoundTrip, restoreAnchorIds } from './mdxAnchorSyntax.js';\n\nconst { isMatch } = micromatch;\n\n/**\n * URL-bearing JSX attributes that we localize. Intentionally limited to link\n * attributes (`href`) — NOT asset attributes like `src`, which usually point at\n * shared, locale-agnostic assets (e.g. `/docs/images/...`) that would 404 if a\n * locale prefix were added. Extend deliberately.\n */\nconst LOCALIZABLE_URL_ATTRIBUTES = new Set(['href']);\n\n/**\n * Localize URL string literals that live inside an MDX expression's raw source.\n *\n * URLs can be nested arbitrarily deep inside `{...}` expressions — e.g. an\n * `<a href>` buried inside a component prop:\n * <ParamField type={<span><a href=\"/docs/x\">Error</a></span>} />\n * Such hrefs never appear as mdast nodes (they live in the expression's embedded\n * JS), so the mdast visitors can't reach them. We re-parse the expression source\n * with Babel — positions are local to `source`, which avoids any document-offset\n * mapping — find JSX url-attribute string literals at any depth, and surgically\n * rewrite them in place.\n *\n * `rootStringIsUrl` covers `href={\"/docs/x\"}`, where the expression itself is a\n * bare string literal that a url-named attribute should treat as a URL.\n *\n * Only static string literals are localized; dynamically-computed URLs\n * (template literals, identifiers, concatenation) are left untouched — a\n * fundamental limit of static localization, not a parser shortcoming.\n */\nfunction localizeUrlsInExpressionSource(\n source: string,\n transformUrl: (url: string, linkType: 'markdown' | 'href') => string | null,\n rootStringIsUrl: boolean = false\n): string {\n if (!source.trim()) return source;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let ast: any;\n try {\n ast = parseBabel(source, {\n sourceType: 'module',\n errorRecovery: true,\n plugins: ['jsx', 'typescript'],\n });\n } catch {\n // Dynamic / unparseable expression — leave it untouched.\n return source;\n }\n\n const replacements: Array<{ start: number; end: number; text: string }> = [];\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pushLiteralReplacement = (literal: any) => {\n if (\n !literal ||\n // A bare string at statement position is parsed by Babel as a\n // DirectiveLiteral (cf. \"use strict\") rather than a StringLiteral.\n (literal.type !== 'StringLiteral' &&\n literal.type !== 'DirectiveLiteral') ||\n typeof literal.value !== 'string' ||\n typeof literal.start !== 'number' ||\n typeof literal.end !== 'number'\n ) {\n return;\n }\n const newUrl = transformUrl(literal.value, 'href');\n if (!newUrl) return;\n // Preserve the original quote style; URLs never contain quote chars.\n const quote = source[literal.start] === \"'\" ? \"'\" : '\"';\n replacements.push({\n start: literal.start,\n end: literal.end,\n text: `${quote}${newUrl}${quote}`,\n });\n };\n\n // `href={\"/docs/x\"}`: the entire expression is the URL string.\n if (rootStringIsUrl) {\n const program = ast.program;\n const body = (program?.body ?? []) as Array<{\n type: string;\n expression?: unknown;\n }>;\n if (body.length === 1 && body[0]?.type === 'ExpressionStatement') {\n pushLiteralReplacement(body[0].expression);\n } else if (body.length === 0 && program?.directives?.length === 1) {\n // Babel parses a lone string literal as a directive, not a statement.\n pushLiteralReplacement(program.directives[0]?.value);\n }\n }\n\n // Walk the AST for JSX url-attributes anywhere in the expression.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const seen = new Set<any>();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const walk = (node: any) => {\n if (!node || typeof node !== 'object') return;\n if (Array.isArray(node)) {\n for (const item of node) walk(item);\n return;\n }\n if (seen.has(node)) return;\n seen.add(node);\n\n if (\n node.type === 'JSXAttribute' &&\n node.name?.type === 'JSXIdentifier' &&\n LOCALIZABLE_URL_ATTRIBUTES.has(node.name.name) &&\n node.value\n ) {\n if (node.value.type === 'StringLiteral') {\n pushLiteralReplacement(node.value);\n } else if (\n node.value.type === 'JSXExpressionContainer' &&\n node.value.expression?.type === 'StringLiteral'\n ) {\n pushLiteralReplacement(node.value.expression);\n }\n }\n\n for (const key in node) {\n if (\n key === 'loc' ||\n key === 'start' ||\n key === 'end' ||\n key === 'range'\n ) {\n continue;\n }\n const child = node[key];\n if (child && typeof child === 'object') walk(child);\n }\n };\n walk(ast.program ?? ast);\n\n if (replacements.length === 0) return source;\n\n // Apply end-to-start so earlier offsets remain valid.\n let result = source;\n replacements\n .sort((a, b) => b.start - a.start)\n .forEach(({ start, end, text }) => {\n result = result.slice(0, start) + text + result.slice(end);\n });\n return result;\n}\n\nexport type StaticUrlSettings = StaticLocalizationSettings;\n\n/**\n * Localizes static urls in content files.\n * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)\n * @param settings - The settings object containing the project configuration.\n * @returns void\n *\n * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before\n * it can be enabled by default.\n *\n * Before this becomes a non-experimental feature, we need to:\n * - Support more file types\n * - Support more complex paths\n */\nexport default async function localizeStaticUrls(\n settings: StaticUrlSettings,\n targetLocales?: string[],\n includeFiles?: Set<string>\n) {\n if (\n !settings.files ||\n (Object.keys(settings.files.placeholderPaths).length === 1 &&\n settings.files.placeholderPaths.gt)\n ) {\n return;\n }\n const { resolvedPaths: sourceFiles } = settings.files;\n\n // Use filtered locales if provided, otherwise use all locales\n const locales = targetLocales || settings.locales;\n\n const fileMapping = createFileMapping(\n sourceFiles,\n settings.files.placeholderPaths,\n settings.files.transformPaths ?? {},\n settings.files.transformFormats ?? {},\n settings.locales, // Always use all locales for mapping, filter later\n settings.defaultLocale\n );\n\n // Process all file types at once with a single call\n const processPromises = [];\n\n // First, process default locale files (from source files)\n // This is needed because they might not be in the fileMapping if they're not being translated\n // Only process default locale if it's in the target locales filter\n if (\n !fileMapping[settings.defaultLocale] &&\n locales.includes(settings.defaultLocale) &&\n !includeFiles // when filtering, skip default-locale pass\n ) {\n const defaultLocaleFiles: string[] = [];\n\n // Collect all .md and .mdx files from sourceFiles\n if (sourceFiles.md) {\n defaultLocaleFiles.push(...sourceFiles.md);\n }\n if (sourceFiles.mdx) {\n defaultLocaleFiles.push(...sourceFiles.mdx);\n }\n\n if (defaultLocaleFiles.length > 0) {\n const defaultPromise = Promise.all(\n defaultLocaleFiles.map(async (filePath: string) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file using default locale\n const result = localizeStaticUrlsForFile(\n fileContent,\n settings.defaultLocale,\n settings.defaultLocale, // Process as default locale\n settings.options?.experimentalHideDefaultLocale || false,\n settings.options?.docsUrlPattern,\n settings.options?.excludeStaticUrls,\n settings.options?.baseDomain\n );\n // Only write the file if there were changes\n if (result.hasChanges) {\n await fs.promises.writeFile(filePath, result.content);\n }\n })\n );\n processPromises.push(defaultPromise);\n }\n }\n\n // Then process all other locales from fileMapping\n const mappingPromises = Object.entries(fileMapping)\n .filter(([locale]) => locales.includes(locale)) // Filter by target locales\n .map(async ([locale, filesMap]) => {\n // Get all files that are md or mdx\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n // Replace the placeholder path with the target path\n await Promise.all(\n targetFiles.map(async (filePath) => {\n // Check if file exists before processing\n if (!fs.existsSync(filePath)) {\n return;\n }\n // Get file content\n const fileContent = await fs.promises.readFile(filePath, 'utf8');\n // Localize the file (handles both URLs and hrefs in single AST pass)\n const result = localizeStaticUrlsForFile(\n fileContent,\n settings.defaultLocale,\n locale,\n settings.options?.experimentalHideDefaultLocale || false,\n settings.options?.docsUrlPattern,\n settings.options?.excludeStaticUrls,\n settings.options?.baseDomain\n );\n // Only write the file if there were changes\n if (result.hasChanges) {\n await fs.promises.writeFile(filePath, result.content);\n }\n })\n );\n });\n processPromises.push(...mappingPromises);\n\n await Promise.all(processPromises);\n}\n\ninterface UrlTransformResult {\n content: string;\n hasChanges: boolean;\n transformedUrls: Array<{\n originalPath: string;\n newPath: string;\n type: 'markdown' | 'href';\n }>;\n}\n\n/**\n * Determines if a URL should be processed based on pattern matching\n */\nfunction shouldProcessUrl(\n originalUrl: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n baseDomain?: string\n): boolean {\n // Check fragment-only URLs like \"#id-name\"\n if (/^\\s*#/.test(originalUrl)) {\n return false;\n }\n\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n\n // Handle absolute URLs with baseDomain\n let urlToCheck = originalUrl;\n if (baseDomain && originalUrl.startsWith(baseDomain)) {\n urlToCheck = originalUrl.substring(baseDomain.length);\n }\n\n if (targetLocale === defaultLocale) {\n // For default locale processing, check if URL contains the pattern\n return urlToCheck.includes(patternWithoutSlash);\n } else {\n // For non-default locales, check if URL starts with pattern\n return urlToCheck.startsWith(patternWithoutSlash);\n }\n}\n\n/**\n * Determines if a URL should be processed based on the base domain\n */\nfunction shouldProcessAbsoluteUrl(\n originalUrl: string,\n baseDomain: string\n): boolean {\n return originalUrl.startsWith(baseDomain);\n}\n\n/**\n * Checks if a URL should be excluded based on exclusion patterns\n */\nfunction isUrlExcluded(\n originalUrl: string,\n exclude: string[],\n defaultLocale: string\n): boolean {\n const excludePatterns = exclude.map((p) =>\n p.replace(/\\[locale\\]/g, defaultLocale)\n );\n return excludePatterns.some((pattern) => isMatch(originalUrl, pattern));\n}\n\n/**\n * Main URL transformation function that delegates to specific scenarios\n */\nexport function transformUrlPath(\n originalUrl: string,\n patternHead: string,\n targetLocale: string,\n defaultLocale: string,\n hideDefaultLocale: boolean\n): string | null {\n const originalPathArray = originalUrl\n .split('/')\n .filter((path) => path !== '');\n const patternHeadArray = patternHead.split('/').filter((path) => path !== '');\n\n // check if the pattern head matches the original path\n if (!checkIfPathMatchesPattern(originalPathArray, patternHeadArray)) {\n return null;\n }\n\n if (patternHeadArray.length > originalPathArray.length) {\n return null; // Pattern is longer than the URL path\n }\n\n let result = null;\n if (targetLocale === defaultLocale) {\n if (hideDefaultLocale) {\n // check if default locale is already present\n if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {\n return null;\n }\n\n // remove default locale\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n ...originalPathArray.slice(patternHeadArray.length + 1),\n ];\n\n result = newPathArray.join('/');\n } else {\n // check if default locale is already present\n if (originalPathArray?.[patternHeadArray.length] === defaultLocale) {\n return null;\n }\n\n // insert default locale\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n defaultLocale,\n ...originalPathArray.slice(patternHeadArray.length),\n ];\n\n result = newPathArray.join('/');\n }\n } else if (hideDefaultLocale) {\n // Avoid duplicating target locale if already present\n if (originalPathArray?.[patternHeadArray.length] === targetLocale) {\n return null;\n }\n const newPathArray = [\n ...originalPathArray.slice(0, patternHeadArray.length),\n targetLocale,\n ...originalPathArray.slice(patternHeadArray.length),\n ];\n\n result = newPathArray.join('/');\n } else {\n // check default locale\n if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {\n return null;\n }\n\n // replace default locale with target locale\n const newPathArray = [...originalPathArray];\n newPathArray[patternHeadArray.length] = targetLocale;\n\n result = newPathArray.join('/');\n }\n\n // check for leading and trailing slashes\n if (originalUrl.startsWith('/')) {\n result = '/' + result;\n }\n if (originalUrl.endsWith('/')) {\n result = result + '/';\n }\n\n return result;\n}\n\n/**\n * AST-based transformation for MDX files using remark-mdx\n */\nfunction transformMdxUrls(\n mdxContent: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]',\n exclude: string[] = [],\n baseDomain?: string\n): UrlTransformResult {\n const transformedUrls: Array<{\n originalPath: string;\n newPath: string;\n type: 'markdown' | 'href';\n }> = [];\n\n if (!pattern.startsWith('/')) {\n pattern = '/' + pattern;\n }\n\n const patternHead = pattern.split('[locale]')[0];\n\n // Quick check: if the file doesn't contain the pattern, skip expensive AST parsing\n // For default locale processing, we also need to check if content might need adjustment\n if (targetLocale === defaultLocale) {\n // For default locale files, we always need to check as we're looking for either:\n // - paths without locale (when hideDefaultLocale=false)\n // - paths with default locale (when hideDefaultLocale=true)\n const patternWithoutSlash = patternHead.replace(/\\/$/, '');\n if (!mdxContent.includes(patternWithoutSlash)) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n } else {\n // For non-default locales, use the original logic\n if (!mdxContent.includes(patternHead.replace(/\\/$/, ''))) {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n }\n\n // Parse the MDX content into an AST\n let processedAst: Root;\n let neutralizedAnchors: boolean;\n try {\n const parsed = parseMdxForRoundTrip(mdxContent);\n processedAst = parsed.ast;\n neutralizedAnchors = parsed.neutralized;\n } catch {\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls,\n };\n }\n\n // Helper function to transform URL based on pattern\n const transformUrl = (\n originalUrl: string,\n linkType: 'markdown' | 'href'\n ): string | null => {\n // For Markdown links [text](path), only process absolute-root paths starting with '/'\n // Relative markdown links should remain relative to the current page and not be localized.\n if (linkType === 'markdown') {\n const isFragment = /^\\s*#/.test(originalUrl);\n const isAbsoluteRoot = originalUrl.startsWith('/');\n const looksAbsoluteWithDomain = baseDomain\n ? shouldProcessAbsoluteUrl(originalUrl, baseDomain)\n : false;\n if (!isAbsoluteRoot && !looksAbsoluteWithDomain && !isFragment) {\n return null;\n }\n }\n // Check if URL should be processed\n if (\n !shouldProcessUrl(\n originalUrl,\n patternHead,\n targetLocale,\n defaultLocale,\n baseDomain\n )\n ) {\n return null;\n }\n\n // Skip absolute URLs (http://, https://, //, etc.)\n if (baseDomain && shouldProcessAbsoluteUrl(originalUrl, baseDomain)) {\n // Get everything after the base domain\n const afterDomain = originalUrl.substring(baseDomain.length);\n\n const transformedPath = transformUrlPath(\n afterDomain,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale\n );\n if (!transformedPath) {\n return null;\n }\n transformedUrls.push({\n originalPath: originalUrl,\n newPath: transformedPath,\n type: linkType,\n });\n return transformedPath ? baseDomain + transformedPath : null;\n }\n\n // Exclude colon-prefixed URLs (http://, https://, //, etc.)\n if (originalUrl.split('?')[0].includes(':')) {\n return null;\n }\n\n // Transform the URL based on locale and configuration\n const newUrl = transformUrlPath(\n originalUrl,\n patternHead,\n targetLocale,\n defaultLocale,\n hideDefaultLocale\n );\n\n if (!newUrl) {\n return null;\n }\n\n // Check exclusions\n if (isUrlExcluded(originalUrl, exclude, defaultLocale)) {\n return null;\n }\n\n transformedUrls.push({\n originalPath: originalUrl,\n newPath: newUrl,\n type: linkType,\n });\n return newUrl;\n };\n\n // Visit markdown link nodes: [text](url)\n visit(processedAst, 'link', (node: Link) => {\n if (node.url) {\n const newUrl = transformUrl(node.url, 'markdown');\n if (newUrl) {\n node.url = newUrl;\n }\n }\n });\n\n // Visit JSX/HTML elements for href attributes: <a href=\"url\"> or <Card href=\"url\">\n visit(processedAst, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {\n const jsxNode = node as MdxJsxFlowElement | MdxJsxTextElement;\n if (!jsxNode.attributes) return;\n for (const attr of jsxNode.attributes) {\n if (attr.type !== 'mdxJsxAttribute' || !attr.value) continue;\n\n // Plain string attribute value, e.g. <a href=\"/docs/x\">\n if (typeof attr.value === 'string') {\n if (LOCALIZABLE_URL_ATTRIBUTES.has(attr.name)) {\n const newUrl = transformUrl(attr.value, 'href');\n if (newUrl) {\n attr.value = newUrl;\n }\n }\n continue;\n }\n\n // Expression attribute value. Two shapes are handled:\n // href={\"/docs/x\"} (url attr, bare string)\n // type={<span><a href=\"/docs/x\">…</a></span>} (url attr nested deep\n // inside a non-url prop)\n // The nested case never surfaces as an mdast node — it lives in the\n // expression's embedded JS — so it's localized via the source-level walk.\n if (\n typeof attr.value === 'object' &&\n attr.value.type === 'mdxJsxAttributeValueExpression' &&\n typeof attr.value.value === 'string'\n ) {\n const newValue = localizeUrlsInExpressionSource(\n attr.value.value,\n transformUrl,\n LOCALIZABLE_URL_ATTRIBUTES.has(attr.name)\n );\n if (newValue !== attr.value.value) {\n attr.value.value = newValue;\n }\n }\n }\n });\n\n // Visit standalone MDX expressions for URLs inside embedded JSX, e.g.\n // {isBeta && <a href=\"/docs/x\">Beta</a>}\n visit(processedAst, ['mdxFlowExpression', 'mdxTextExpression'], (node) => {\n const exprNode = node as unknown as Literal;\n if (typeof exprNode.value === 'string' && exprNode.value) {\n const newValue = localizeUrlsInExpressionSource(\n exprNode.value,\n transformUrl\n );\n if (newValue !== exprNode.value) {\n exprNode.value = newValue;\n }\n }\n });\n\n // Visit raw JSX nodes for href attributes in JSX strings\n visit(processedAst, 'jsx', (node: Literal) => {\n if (node.value && typeof node.value === 'string') {\n const jsxContent = node.value;\n\n // Use regex to find href attributes in the JSX string\n const hrefRegex = /href\\s*=\\s*[\"']([^\"']+)[\"']/g;\n let match;\n const replacements: Array<{\n start: number;\n end: number;\n oldHrefAttr: string;\n newHrefAttr: string;\n }> = [];\n\n // Reset regex lastIndex to avoid issues with global flag\n hrefRegex.lastIndex = 0;\n\n while ((match = hrefRegex.exec(jsxContent)) !== null) {\n const originalHref = match[1];\n const newUrl = transformUrl(originalHref, 'href');\n\n if (newUrl) {\n // Store replacement info\n const oldHrefAttr = match[0]; // The full match like 'href=\"/quickstart\"'\n const quote = oldHrefAttr.includes('\"') ? '\"' : \"'\";\n const newHrefAttr = `href=${quote}${newUrl}${quote}`;\n\n replacements.push({\n start: match.index!,\n end: match.index! + oldHrefAttr.length,\n oldHrefAttr,\n newHrefAttr,\n });\n }\n }\n\n // Apply replacements in reverse order (from end to start) to avoid position shifts\n if (replacements.length > 0) {\n let newJsxContent = jsxContent;\n replacements\n .sort((a, b) => b.start - a.start)\n .forEach(({ start, end, newHrefAttr }) => {\n newJsxContent =\n newJsxContent.slice(0, start) +\n newHrefAttr +\n newJsxContent.slice(end);\n });\n\n node.value = newJsxContent;\n }\n }\n });\n\n // Convert the modified AST back to MDX string\n let content: string;\n try {\n const stringifyProcessor = unified()\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx)\n .use(normalizeCJKCharacters)\n .use(escapeHtmlInTextNodes)\n .use(remarkStringify, {\n handlers: {\n // Handler to prevent escaping (avoids '<' -> '\\<')\n text(node: Literal) {\n return node.value;\n },\n },\n });\n\n const outTree = stringifyProcessor.runSync(processedAst) as Root;\n content = stringifyProcessor.stringify(outTree);\n if (neutralizedAnchors) {\n content = restoreAnchorIds(content);\n }\n } catch (error) {\n console.warn(\n `Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`\n );\n console.warn(\n 'Returning original content unchanged due to stringify error.'\n );\n return {\n content: mdxContent,\n hasChanges: false,\n transformedUrls: [],\n };\n }\n\n // Handle newline formatting to match original input\n if (content.endsWith('\\n') && !mdxContent.endsWith('\\n')) {\n content = content.slice(0, -1);\n }\n\n // Preserve leading newlines from original content\n if (mdxContent.startsWith('\\n') && !content.startsWith('\\n')) {\n content = '\\n' + content;\n }\n\n return {\n content,\n hasChanges: transformedUrls.length > 0,\n transformedUrls,\n };\n}\n\n// AST-based transformation for MDX files using remark\nfunction localizeStaticUrlsForFile(\n file: string,\n defaultLocale: string,\n targetLocale: string,\n hideDefaultLocale: boolean,\n pattern: string = '/[locale]', // eg /docs/[locale] or /[locale]\n exclude: string[] = [],\n baseDomain?: string\n): UrlTransformResult {\n // Use AST-based transformation for MDX files\n return transformMdxUrls(\n file,\n defaultLocale,\n targetLocale,\n hideDefaultLocale,\n pattern,\n exclude,\n baseDomain || ''\n );\n}\n\nfunction checkIfPathMatchesPattern(\n originalUrlArray: string[],\n patternHeadArray: string[]\n): boolean {\n // check if the pattern head matches the original path\n for (let i = 0; i < patternHeadArray.length; i++) {\n if (patternHeadArray[i] !== originalUrlArray?.[i]) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
/** Maps over a document's lines, leaving fenced code blocks untouched. */
|
|
3
|
+
export declare function mapLinesOutsideCodeFences(content: string, mapLine: (line: string) => string): string;
|
|
4
|
+
/** Visits a document's lines with their 0-based index, skipping code fences. */
|
|
5
|
+
export declare function forEachLineOutsideCodeFences(content: string, visitLine: (line: string, index: number) => void): void;
|
|
6
|
+
/**
|
|
7
|
+
* Escapes `## Heading {#id}` to `## Heading \{#id\}` so MDX can parse it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function neutralizeAnchorIds(content: string): {
|
|
10
|
+
content: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Unescapes anchors back to `{#id}`; Mintlify renders `\{` as a literal brace.
|
|
15
|
+
*/
|
|
16
|
+
export declare function restoreAnchorIds(content: string): string;
|
|
17
|
+
/** Builds the parse-only MDX processor shared by every parse site. */
|
|
18
|
+
export declare function createMdxParseProcessor(): import("unified").Processor<Root, undefined, undefined, undefined, undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Parses MDX, retrying with anchors escaped; rethrows any other parse error.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseMdxTolerantly(content: string): Root;
|
|
23
|
+
/**
|
|
24
|
+
* Parses MDX for a pass that stringifies the tree back out. `neutralized` tells
|
|
25
|
+
* the caller whether to run {@link restoreAnchorIds} on its output.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseMdxForRoundTrip(content: string): {
|
|
28
|
+
ast: Root;
|
|
29
|
+
neutralized: boolean;
|
|
30
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkMdx from "remark-mdx";
|
|
4
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
5
|
+
//#region src/utils/mdxAnchorSyntax.ts
|
|
6
|
+
/**
|
|
7
|
+
* Custom heading IDs (`## Heading {#id}`) break MDX parsing: remark-mdx passes
|
|
8
|
+
* `{#id}` to acorn. Escaping the braces makes the document parse without
|
|
9
|
+
* changing its line count, so mdast line positions still map 1:1 (columns do
|
|
10
|
+
* not). https://mintlify.com/docs/create/headers#custom-heading-ids
|
|
11
|
+
*/
|
|
12
|
+
/** A heading line ending in an unescaped `{#id}`. */
|
|
13
|
+
const UNESCAPED_ANCHOR = /^([ \t]*#{1,6}[ \t]+.*?)[ \t]*\{#([A-Za-z0-9_-]+)\}[ \t]*$/;
|
|
14
|
+
/** A heading line ending in an escaped `\{#id\}`. */
|
|
15
|
+
const ESCAPED_ANCHOR = /^([ \t]*#{1,6}[ \t]+.*?)[ \t]*\\\{#([A-Za-z0-9_-]+)\\\}[ \t]*$/;
|
|
16
|
+
/** Matches an opening or closing fenced-code-block marker. */
|
|
17
|
+
const CODE_FENCE = /^\s*(`{3,}|~{3,})/;
|
|
18
|
+
/** Returns a predicate telling whether a line sits outside a code fence. */
|
|
19
|
+
function createFenceTracker() {
|
|
20
|
+
let inFence = false;
|
|
21
|
+
let fence = null;
|
|
22
|
+
return (line) => {
|
|
23
|
+
const match = line.match(CODE_FENCE);
|
|
24
|
+
if (!match) return !inFence;
|
|
25
|
+
const marker = match[1];
|
|
26
|
+
if (!inFence) {
|
|
27
|
+
inFence = true;
|
|
28
|
+
fence = marker;
|
|
29
|
+
} else if (fence && marker[0] === fence[0] && marker.length >= fence.length) {
|
|
30
|
+
inFence = false;
|
|
31
|
+
fence = null;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Maps over a document's lines, leaving fenced code blocks untouched. */
|
|
37
|
+
function mapLinesOutsideCodeFences(content, mapLine) {
|
|
38
|
+
const isContent = createFenceTracker();
|
|
39
|
+
return content.split("\n").map((line) => isContent(line) ? mapLine(line) : line).join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** Visits a document's lines with their 0-based index, skipping code fences. */
|
|
42
|
+
function forEachLineOutsideCodeFences(content, visitLine) {
|
|
43
|
+
const isContent = createFenceTracker();
|
|
44
|
+
content.split("\n").forEach((line, index) => {
|
|
45
|
+
if (isContent(line)) visitLine(line, index);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Escapes `## Heading {#id}` to `## Heading \{#id\}` so MDX can parse it.
|
|
50
|
+
*/
|
|
51
|
+
function neutralizeAnchorIds(content) {
|
|
52
|
+
let changed = false;
|
|
53
|
+
return {
|
|
54
|
+
content: mapLinesOutsideCodeFences(content, (line) => {
|
|
55
|
+
const match = line.match(UNESCAPED_ANCHOR);
|
|
56
|
+
if (!match) return line;
|
|
57
|
+
changed = true;
|
|
58
|
+
return `${match[1]} \\{#${match[2]}\\}`;
|
|
59
|
+
}),
|
|
60
|
+
changed
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Unescapes anchors back to `{#id}`; Mintlify renders `\{` as a literal brace.
|
|
65
|
+
*/
|
|
66
|
+
function restoreAnchorIds(content) {
|
|
67
|
+
return mapLinesOutsideCodeFences(content, (line) => {
|
|
68
|
+
const match = line.match(ESCAPED_ANCHOR);
|
|
69
|
+
if (!match) return line;
|
|
70
|
+
return `${match[1]} {#${match[2]}}`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Builds the parse-only MDX processor shared by every parse site. */
|
|
74
|
+
function createMdxParseProcessor() {
|
|
75
|
+
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"]).use(remarkMdx);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses MDX, retrying with anchors escaped; rethrows any other parse error.
|
|
79
|
+
*/
|
|
80
|
+
function parseMdxTolerantly(content) {
|
|
81
|
+
const processor = createMdxParseProcessor();
|
|
82
|
+
try {
|
|
83
|
+
return processor.runSync(processor.parse(content));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const { content: neutralized, changed } = neutralizeAnchorIds(content);
|
|
86
|
+
if (!changed) throw error;
|
|
87
|
+
return processor.runSync(processor.parse(neutralized));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parses MDX for a pass that stringifies the tree back out. `neutralized` tells
|
|
92
|
+
* the caller whether to run {@link restoreAnchorIds} on its output.
|
|
93
|
+
*/
|
|
94
|
+
function parseMdxForRoundTrip(content) {
|
|
95
|
+
const processor = createMdxParseProcessor();
|
|
96
|
+
try {
|
|
97
|
+
return {
|
|
98
|
+
ast: processor.runSync(processor.parse(content)),
|
|
99
|
+
neutralized: false
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const { content: neutralized, changed } = neutralizeAnchorIds(content);
|
|
103
|
+
if (!changed) throw error;
|
|
104
|
+
return {
|
|
105
|
+
ast: processor.runSync(processor.parse(neutralized)),
|
|
106
|
+
neutralized: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { createMdxParseProcessor, forEachLineOutsideCodeFences, mapLinesOutsideCodeFences, neutralizeAnchorIds, parseMdxForRoundTrip, parseMdxTolerantly, restoreAnchorIds };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=mdxAnchorSyntax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mdxAnchorSyntax.js","names":[],"sources":["../../src/utils/mdxAnchorSyntax.ts"],"sourcesContent":["import { unified } from 'unified';\nimport remarkParse from 'remark-parse';\nimport remarkMdx from 'remark-mdx';\nimport remarkFrontmatter from 'remark-frontmatter';\nimport type { Root } from 'mdast';\n\n/**\n * Custom heading IDs (`## Heading {#id}`) break MDX parsing: remark-mdx passes\n * `{#id}` to acorn. Escaping the braces makes the document parse without\n * changing its line count, so mdast line positions still map 1:1 (columns do\n * not). https://mintlify.com/docs/create/headers#custom-heading-ids\n */\n\n/** A heading line ending in an unescaped `{#id}`. */\nconst UNESCAPED_ANCHOR =\n /^([ \\t]*#{1,6}[ \\t]+.*?)[ \\t]*\\{#([A-Za-z0-9_-]+)\\}[ \\t]*$/;\n\n/** A heading line ending in an escaped `\\{#id\\}`. */\nconst ESCAPED_ANCHOR =\n /^([ \\t]*#{1,6}[ \\t]+.*?)[ \\t]*\\\\\\{#([A-Za-z0-9_-]+)\\\\\\}[ \\t]*$/;\n\n/** Matches an opening or closing fenced-code-block marker. */\nconst CODE_FENCE = /^\\s*(`{3,}|~{3,})/;\n\n/** Returns a predicate telling whether a line sits outside a code fence. */\nfunction createFenceTracker(): (line: string) => boolean {\n let inFence = false;\n let fence: string | null = null;\n\n return (line: string): boolean => {\n const match = line.match(CODE_FENCE);\n if (!match) return !inFence;\n\n const marker = match[1];\n if (!inFence) {\n inFence = true;\n fence = marker;\n } else if (\n fence &&\n marker[0] === fence[0] &&\n marker.length >= fence.length\n ) {\n inFence = false;\n fence = null;\n }\n return false;\n };\n}\n\n/** Maps over a document's lines, leaving fenced code blocks untouched. */\nexport function mapLinesOutsideCodeFences(\n content: string,\n mapLine: (line: string) => string\n): string {\n const isContent = createFenceTracker();\n return content\n .split('\\n')\n .map((line) => (isContent(line) ? mapLine(line) : line))\n .join('\\n');\n}\n\n/** Visits a document's lines with their 0-based index, skipping code fences. */\nexport function forEachLineOutsideCodeFences(\n content: string,\n visitLine: (line: string, index: number) => void\n): void {\n const isContent = createFenceTracker();\n content.split('\\n').forEach((line, index) => {\n if (isContent(line)) visitLine(line, index);\n });\n}\n\n/**\n * Escapes `## Heading {#id}` to `## Heading \\{#id\\}` so MDX can parse it.\n */\nexport function neutralizeAnchorIds(content: string): {\n content: string;\n changed: boolean;\n} {\n let changed = false;\n\n const next = mapLinesOutsideCodeFences(content, (line) => {\n const match = line.match(UNESCAPED_ANCHOR);\n if (!match) return line;\n changed = true;\n return `${match[1]} \\\\{#${match[2]}\\\\}`;\n });\n\n return { content: next, changed };\n}\n\n/**\n * Unescapes anchors back to `{#id}`; Mintlify renders `\\{` as a literal brace.\n */\nexport function restoreAnchorIds(content: string): string {\n return mapLinesOutsideCodeFences(content, (line) => {\n const match = line.match(ESCAPED_ANCHOR);\n if (!match) return line;\n return `${match[1]} {#${match[2]}}`;\n });\n}\n\n/** Builds the parse-only MDX processor shared by every parse site. */\nexport function createMdxParseProcessor() {\n return unified()\n .use(remarkParse)\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx);\n}\n\n/**\n * Parses MDX, retrying with anchors escaped; rethrows any other parse error.\n */\nexport function parseMdxTolerantly(content: string): Root {\n const processor = createMdxParseProcessor();\n try {\n return processor.runSync(processor.parse(content)) as Root;\n } catch (error) {\n const { content: neutralized, changed } = neutralizeAnchorIds(content);\n if (!changed) throw error;\n return processor.runSync(processor.parse(neutralized)) as Root;\n }\n}\n\n/**\n * Parses MDX for a pass that stringifies the tree back out. `neutralized` tells\n * the caller whether to run {@link restoreAnchorIds} on its output.\n */\nexport function parseMdxForRoundTrip(content: string): {\n ast: Root;\n neutralized: boolean;\n} {\n const processor = createMdxParseProcessor();\n try {\n return {\n ast: processor.runSync(processor.parse(content)) as Root,\n neutralized: false,\n };\n } catch (error) {\n const { content: neutralized, changed } = neutralizeAnchorIds(content);\n if (!changed) throw error;\n return {\n ast: processor.runSync(processor.parse(neutralized)) as Root,\n neutralized: true,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,mBACJ;;AAGF,MAAM,iBACJ;;AAGF,MAAM,aAAa;;AAGnB,SAAS,qBAAgD;CACvD,IAAI,UAAU;CACd,IAAI,QAAuB;AAE3B,SAAQ,SAA0B;EAChC,MAAM,QAAQ,KAAK,MAAM,WAAW;AACpC,MAAI,CAAC,MAAO,QAAO,CAAC;EAEpB,MAAM,SAAS,MAAM;AACrB,MAAI,CAAC,SAAS;AACZ,aAAU;AACV,WAAQ;aAER,SACA,OAAO,OAAO,MAAM,MACpB,OAAO,UAAU,MAAM,QACvB;AACA,aAAU;AACV,WAAQ;;AAEV,SAAO;;;;AAKX,SAAgB,0BACd,SACA,SACQ;CACR,MAAM,YAAY,oBAAoB;AACtC,QAAO,QACJ,MAAM,KAAK,CACX,KAAK,SAAU,UAAU,KAAK,GAAG,QAAQ,KAAK,GAAG,KAAM,CACvD,KAAK,KAAK;;;AAIf,SAAgB,6BACd,SACA,WACM;CACN,MAAM,YAAY,oBAAoB;AACtC,SAAQ,MAAM,KAAK,CAAC,SAAS,MAAM,UAAU;AAC3C,MAAI,UAAU,KAAK,CAAE,WAAU,MAAM,MAAM;GAC3C;;;;;AAMJ,SAAgB,oBAAoB,SAGlC;CACA,IAAI,UAAU;AASd,QAAO;EAAE,SAPI,0BAA0B,UAAU,SAAS;GACxD,MAAM,QAAQ,KAAK,MAAM,iBAAiB;AAC1C,OAAI,CAAC,MAAO,QAAO;AACnB,aAAU;AACV,UAAO,GAAG,MAAM,GAAG,OAAO,MAAM,GAAG;IAGf;EAAE;EAAS;;;;;AAMnC,SAAgB,iBAAiB,SAAyB;AACxD,QAAO,0BAA0B,UAAU,SAAS;EAClD,MAAM,QAAQ,KAAK,MAAM,eAAe;AACxC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,GAAG,MAAM,GAAG,KAAK,MAAM,GAAG;GACjC;;;AAIJ,SAAgB,0BAA0B;AACxC,QAAO,SAAS,CACb,IAAI,YAAY,CAChB,IAAI,mBAAmB,CAAC,QAAQ,OAAO,CAAC,CACxC,IAAI,UAAU;;;;;AAMnB,SAAgB,mBAAmB,SAAuB;CACxD,MAAM,YAAY,yBAAyB;AAC3C,KAAI;AACF,SAAO,UAAU,QAAQ,UAAU,MAAM,QAAQ,CAAC;UAC3C,OAAO;EACd,MAAM,EAAE,SAAS,aAAa,YAAY,oBAAoB,QAAQ;AACtE,MAAI,CAAC,QAAS,OAAM;AACpB,SAAO,UAAU,QAAQ,UAAU,MAAM,YAAY,CAAC;;;;;;;AAQ1D,SAAgB,qBAAqB,SAGnC;CACA,MAAM,YAAY,yBAAyB;AAC3C,KAAI;AACF,SAAO;GACL,KAAK,UAAU,QAAQ,UAAU,MAAM,QAAQ,CAAC;GAChD,aAAa;GACd;UACM,OAAO;EACd,MAAM,EAAE,SAAS,aAAa,YAAY,oBAAoB,QAAQ;AACtE,MAAI,CAAC,QAAS,OAAM;AACpB,SAAO;GACL,KAAK,UAAU,QAAQ,UAAU,MAAM,YAAY,CAAC;GACpD,aAAa;GACd"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validates if an MDX file content can be parsed as a valid AST
|
|
3
|
+
*
|
|
4
|
+
* Mintlify-style custom heading IDs (`## Heading {#id}`) are tolerated: they are
|
|
5
|
+
* not valid MDX expressions, but the CLI supports them end-to-end, so a document
|
|
6
|
+
* whose only parse error comes from them is considered valid.
|
|
7
|
+
*
|
|
3
8
|
* @param content - The MDX file content to validate
|
|
4
9
|
* @param filePath - The file path for error reporting
|
|
5
10
|
* @returns object with isValid boolean and optional error message
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import remarkParse from "remark-parse";
|
|
3
|
-
import remarkMdx from "remark-mdx";
|
|
4
|
-
import remarkFrontmatter from "remark-frontmatter";
|
|
1
|
+
import { parseMdxTolerantly } from "./mdxAnchorSyntax.js";
|
|
5
2
|
//#region src/utils/validateMdx.ts
|
|
6
3
|
/**
|
|
7
4
|
* Validates if an MDX file content can be parsed as a valid AST
|
|
5
|
+
*
|
|
6
|
+
* Mintlify-style custom heading IDs (`## Heading {#id}`) are tolerated: they are
|
|
7
|
+
* not valid MDX expressions, but the CLI supports them end-to-end, so a document
|
|
8
|
+
* whose only parse error comes from them is considered valid.
|
|
9
|
+
*
|
|
8
10
|
* @param content - The MDX file content to validate
|
|
9
11
|
* @param filePath - The file path for error reporting
|
|
10
12
|
* @returns object with isValid boolean and optional error message
|
|
11
13
|
*/
|
|
12
14
|
function isValidMdx(content, _filePath) {
|
|
13
15
|
try {
|
|
14
|
-
|
|
15
|
-
const ast = parseProcessor.parse(content);
|
|
16
|
-
parseProcessor.runSync(ast);
|
|
16
|
+
parseMdxTolerantly(content);
|
|
17
17
|
return { isValid: true };
|
|
18
18
|
} catch (error) {
|
|
19
19
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateMdx.js","names":[],"sources":["../../src/utils/validateMdx.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"validateMdx.js","names":[],"sources":["../../src/utils/validateMdx.ts"],"sourcesContent":["import { parseMdxTolerantly } from './mdxAnchorSyntax.js';\n\n/**\n * Validates if an MDX file content can be parsed as a valid AST\n *\n * Mintlify-style custom heading IDs (`## Heading {#id}`) are tolerated: they are\n * not valid MDX expressions, but the CLI supports them end-to-end, so a document\n * whose only parse error comes from them is considered valid.\n *\n * @param content - The MDX file content to validate\n * @param filePath - The file path for error reporting\n * @returns object with isValid boolean and optional error message\n */\nexport function isValidMdx(\n content: string,\n _filePath: string\n): {\n isValid: boolean;\n error?: string;\n} {\n try {\n parseMdxTolerantly(content);\n return { isValid: true };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return { isValid: false, error: errorMessage };\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,WACd,SACA,WAIA;AACA,KAAI;AACF,qBAAmB,QAAQ;AAC3B,SAAO,EAAE,SAAS,MAAM;UACjB,OAAO;AAEd,SAAO;GAAE,SAAS;GAAO,OADJ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;GAC7B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gt",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "bin/main.js",
|
|
6
6
|
"files": [
|
|
@@ -117,10 +117,10 @@
|
|
|
117
117
|
"yaml": "^2.8.0",
|
|
118
118
|
"@generaltranslation/icu": "0.1.2",
|
|
119
119
|
"@generaltranslation/format": "0.1.8",
|
|
120
|
-
"@generaltranslation/python-extractor": "0.2.
|
|
121
|
-
"@generaltranslation/supported-locales": "2.1.
|
|
122
|
-
"@generaltranslation/vue-extractor": "0.1.
|
|
123
|
-
"generaltranslation": "9.1.
|
|
120
|
+
"@generaltranslation/python-extractor": "0.2.44",
|
|
121
|
+
"@generaltranslation/supported-locales": "2.1.24",
|
|
122
|
+
"@generaltranslation/vue-extractor": "0.1.4",
|
|
123
|
+
"generaltranslation": "9.1.11",
|
|
124
124
|
"gt-remark": "1.0.12"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|