gt 2.17.2 → 2.17.3
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 +43 -0
- package/dist/cli/commands/translate.js +1 -1
- package/dist/cli/commands/translate.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":"addExplicitAnchorIds.js","names":[],"sources":["../../src/utils/addExplicitAnchorIds.ts"],"sourcesContent":["import { 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, Heading, Literal, Node } from 'mdast';\nimport { logger } from '../console/logger.js';\nimport { escapeHtmlInTextNodes, normalizeCJKCharacters } from 'gt-remark';\nimport { decode } from 'html-entities';\nimport type { AdditionalOptions } from '../types/index.js';\n\ntype AnchorIdSettings = {\n options?: Pick<AdditionalOptions, 'experimentalAddHeaderAnchorIds'>;\n};\n\n/**\n * Generates a slug from heading text\n */\nfunction generateSlug(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '') // Remove special chars except spaces and hyphens\n .trim()\n .replace(/\\s+/g, '-') // Replace spaces with hyphens\n .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen\n .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens\n}\n\n/**\n * Extracts text content from heading nodes\n */\nfunction extractHeadingText(heading: Heading): string {\n let text = '';\n\n visit(heading, ['text', 'inlineCode'], (node: Node) => {\n if ('value' in node && typeof node.value === 'string') {\n text += node.value;\n }\n });\n\n return text;\n}\n\n/**\n * Simple line-by-line heading extractor that skips fenced code blocks.\n * Used as a fallback when MDX parsing fails.\n */\nfunction extractHeadingsWithFallback(mdxContent: string): HeadingInfo[] {\n const headings: HeadingInfo[] = [];\n const lines = mdxContent.split('\\n');\n\n let position = 0;\n let inFence = false;\n let fenceChar: string | null = null;\n\n for (const line of lines) {\n const fenceMatch = line.match(/^(\\s*)(`{3,}|~{3,})/);\n if (fenceMatch) {\n const fenceString = fenceMatch[2];\n if (!inFence) {\n inFence = true;\n fenceChar = fenceString;\n } else if (\n fenceChar &&\n fenceString[0] === fenceChar[0] &&\n fenceString.length >= fenceChar.length\n ) {\n inFence = false;\n fenceChar = null;\n }\n continue;\n }\n\n if (inFence) {\n continue;\n }\n\n const headingMatch = line.match(/^(#{1,6})\\s+(.*)$/);\n if (!headingMatch) {\n continue;\n }\n\n const hashes = headingMatch[1];\n const rawText = headingMatch[2];\n const { cleanedText, explicitId } = parseHeadingContent(rawText);\n\n if (cleanedText || explicitId) {\n headings.push({\n text: cleanedText,\n level: hashes.length,\n slug: explicitId ?? generateSlug(cleanedText),\n position: position++,\n });\n }\n }\n\n return headings;\n}\n\nfunction parseHeadingContent(text: string): {\n cleanedText: string;\n explicitId?: string;\n} {\n // Support both {#id} and escaped \\{#id\\} forms\n const anchorMatch = text.match(/(\\\\\\{#([^}]+)\\\\\\}|\\{#([^}]+)\\})\\s*$/);\n\n if (!anchorMatch) {\n return { cleanedText: text };\n }\n\n const explicitId = anchorMatch[2] || anchorMatch[3];\n const cleanedText = text.replace(anchorMatch[0], '').trimEnd();\n\n return { cleanedText, explicitId };\n}\n\n/**\n * Checks if a heading is already wrapped in a div with id\n */\nfunction hasExplicitId(heading: Heading, _ast: Root): boolean {\n const lastChild = heading.children[heading.children.length - 1];\n if (lastChild?.type === 'text') {\n return /(\\{#[^}]+\\}|\\\\\\{#[^}]+\\\\\\}|\\[[^\\]]+\\])\\s*$/.test(lastChild.value);\n }\n return false;\n}\n\n/**\n * Represents a heading with its position and metadata\n */\nexport interface HeadingInfo {\n text: string;\n level: number;\n slug: string;\n position: number;\n}\n\n/**\n * Extracts heading information from content (read-only, no modifications)\n */\nexport function extractHeadingInfo(mdxContent: string): HeadingInfo[] {\n const headings: HeadingInfo[] = [];\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 // Fallback: line-by-line extraction skipping fenced code blocks\n return extractHeadingsWithFallback(mdxContent);\n }\n\n let position = 0;\n visit(processedAst, 'heading', (heading: Heading) => {\n const headingText = extractHeadingText(heading);\n const { cleanedText, explicitId } = parseHeadingContent(headingText);\n if (cleanedText || explicitId) {\n const slug = explicitId ?? generateSlug(cleanedText);\n headings.push({\n text: cleanedText,\n level: heading.depth,\n slug,\n position: position++,\n });\n }\n });\n\n return headings;\n}\n\n/**\n * Applies anchor IDs to translated content based on source heading mapping\n */\nexport function addExplicitAnchorIds(\n translatedContent: string,\n sourceHeadingMap: HeadingInfo[],\n settings?: AnchorIdSettings,\n sourcePath?: string,\n translatedPath?: string,\n fileTypeHint?: 'md' | 'mdx'\n): {\n content: string;\n hasChanges: boolean;\n addedIds: Array<{ heading: string; id: string }>;\n} {\n const addedIds: Array<{ heading: string; id: string }> = [];\n const useDivWrapping =\n settings?.options?.experimentalAddHeaderAnchorIds === 'mintlify';\n\n // Extract headings from translated content\n const translatedHeadings = extractHeadingInfo(translatedContent);\n\n // Pre-processing validation: check if header counts match\n if (sourceHeadingMap.length !== translatedHeadings.length) {\n const sourceFile = sourcePath\n ? `Source file: ${sourcePath}`\n : 'Source file';\n const translatedFile = translatedPath\n ? `translated file: ${translatedPath}`\n : 'translated file';\n\n logger.warn(\n `Header count mismatch detected! ${sourceFile} has ${sourceHeadingMap.length} headers but ${translatedFile} has ${translatedHeadings.length} headers. ` +\n `This likely means your source file was edited after translation was requested, causing a mismatch between ` +\n `the number of headers in your source file vs the translated file. Re-translate this file to resolve the issue.`\n );\n }\n\n // Create ID mapping based on positional matching\n const idMappings = new Map<number, string>();\n sourceHeadingMap.forEach((sourceHeading, index) => {\n const translatedHeading = translatedHeadings[index];\n // Match by position and level for safety\n if (translatedHeading && translatedHeading.level === sourceHeading.level) {\n idMappings.set(index, sourceHeading.slug);\n addedIds.push({\n heading: translatedHeading.text,\n id: sourceHeading.slug,\n });\n }\n });\n\n if (idMappings.size === 0) {\n return {\n content: translatedContent,\n hasChanges: false,\n addedIds: [],\n };\n }\n\n const translatedIsMdx = translatedPath\n ? translatedPath.toLowerCase().endsWith('.mdx')\n : true; // default to mdx-style escaping when unknown\n const shouldEscapeAnchors =\n fileTypeHint === 'mdx'\n ? true\n : fileTypeHint === 'md'\n ? false\n : translatedIsMdx;\n\n // Apply IDs to translated content\n let content: string;\n if (useDivWrapping) {\n content = applyDivWrappedIds(\n translatedContent,\n translatedHeadings,\n idMappings\n );\n } else {\n content = applyInlineIds(\n translatedContent,\n idMappings,\n shouldEscapeAnchors\n );\n }\n\n return {\n content,\n hasChanges: content !== translatedContent,\n addedIds,\n };\n}\n\n/**\n * Adds inline {#id} syntax to headings (standard markdown approach)\n */\nfunction applyInlineIds(\n translatedContent: string,\n idMappings: Map<number, string>,\n escapeAnchors: boolean\n): string {\n const escapeInlineAnchors = (content: string): string => {\n if (!escapeAnchors) return content;\n return content.replace(\n /\\{#([A-Za-z0-9-_]+)\\}/g,\n (match, id, offset, str) => {\n if (offset > 0 && str[offset - 1] === '\\\\') {\n return match;\n }\n return `\\\\{#${id}\\\\}`;\n }\n );\n };\n\n // Parse the translated content\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(translatedContent);\n processedAst = parseProcessor.runSync(ast) as Root;\n } catch {\n return applyInlineIdsStringFallback(\n translatedContent,\n idMappings,\n escapeAnchors\n );\n }\n\n // Apply IDs to headings based on position\n let headingIndex = 0;\n let actuallyModifiedContent = false;\n\n visit(processedAst, 'heading', (heading: Heading) => {\n const id = idMappings.get(headingIndex);\n if (id) {\n // Skip if heading already has explicit ID\n if (hasExplicitId(heading, processedAst)) {\n if (escapeAnchors) {\n // Normalize existing inline IDs to escaped form\n const lastChild = heading.children[heading.children.length - 1];\n if (lastChild?.type === 'text') {\n const match = lastChild.value.match(/\\{#([^}]+)\\}\\s*$/);\n const alreadyEscaped = lastChild.value.match(/\\\\\\{#[^}]+\\\\\\}\\s*$/);\n if (match && !alreadyEscaped) {\n const anchorId = match[1];\n const base = lastChild.value.replace(/\\s*\\{#[^}]+\\}\\s*$/, '');\n lastChild.value = `${base} \\\\{#${anchorId}\\\\}`;\n actuallyModifiedContent = true;\n }\n }\n }\n headingIndex++;\n return;\n }\n\n // Add the ID to the heading\n const lastChild = heading.children[heading.children.length - 1];\n if (lastChild?.type === 'text') {\n lastChild.value += escapeAnchors ? ` \\\\{#${id}\\\\}` : ` {#${id}}`;\n } else {\n // If last child is not text, add a new text node\n heading.children.push({\n type: 'text',\n value: escapeAnchors ? ` \\\\{#${id}\\\\}` : ` {#${id}}`,\n });\n }\n actuallyModifiedContent = true;\n }\n headingIndex++;\n });\n\n // If we didn't modify any headings, return original content\n if (!actuallyModifiedContent) {\n const escaped = escapeInlineAnchors(translatedContent);\n return escaped;\n }\n\n // Convert the modified AST back to MDX 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 // Custom handler to prevent escaping of {#id} syntax\n text(node: Literal) {\n return node.value;\n },\n },\n });\n\n const outTree = stringifyProcessor.runSync(processedAst) as Root;\n let content = stringifyProcessor.stringify(outTree);\n\n // Handle newline formatting to match original input\n if (content.endsWith('\\n') && !translatedContent.endsWith('\\n')) {\n content = content.slice(0, -1);\n }\n\n // Preserve leading newlines from original content\n if (translatedContent.startsWith('\\n') && !content.startsWith('\\n')) {\n content = '\\n' + content;\n }\n\n return content;\n } catch {\n return translatedContent;\n }\n}\n\n/**\n * Fallback string-based inline ID application when AST parsing fails\n */\nfunction applyInlineIdsStringFallback(\n translatedContent: string,\n idMappings: Map<number, string>,\n escapeAnchors: boolean\n): string {\n let headingIndex = 0;\n let inFence = false;\n let fenceChar: string | null = null;\n\n const processedLines = translatedContent.split('\\n').map((line) => {\n const fenceMatch = line.match(/^(\\s*)(`{3,}|~{3,})/);\n if (fenceMatch) {\n const fenceString = fenceMatch[2];\n if (!inFence) {\n inFence = true;\n fenceChar = fenceString;\n } else if (\n fenceChar &&\n fenceString[0] === fenceChar[0] &&\n fenceString.length >= fenceChar.length\n ) {\n inFence = false;\n fenceChar = null;\n }\n return line;\n }\n\n if (inFence) {\n return line;\n }\n\n const headingMatch = line.match(/^(#{1,6}\\s+)(.*)$/);\n if (!headingMatch) {\n return line;\n }\n\n const prefix = headingMatch[1];\n const text = headingMatch[2];\n const id = idMappings.get(headingIndex++);\n\n if (!id) {\n return line;\n }\n\n const hasEscaped = /\\\\\\{#[^}]+\\\\\\}\\s*$/.test(text);\n const hasUnescaped = /\\{#[^}]+\\}\\s*$/.test(text);\n\n if (hasEscaped) {\n return line;\n }\n\n if (hasUnescaped) {\n if (!escapeAnchors) {\n return line;\n }\n return `${prefix}${text.replace(/\\{#([^}]+)\\}\\s*$/, '\\\\{#$1\\\\}')}`;\n }\n\n const suffix = escapeAnchors ? ` \\\\{#${id}\\\\}` : ` {#${id}}`;\n return `${prefix}${text}${suffix}`;\n });\n\n return processedLines.join('\\n');\n}\n\n/**\n * Wraps headings in divs with IDs (Mintlify approach)\n */\nfunction applyDivWrappedIds(\n translatedContent: string,\n translatedHeadings: HeadingInfo[],\n idMappings: Map<number, string>\n): string {\n // Extract all heading lines from the translated markdown\n const lines = translatedContent.split('\\n');\n const headingLines: Array<{ line: string; level: number; index: number }> =\n [];\n\n lines.forEach((line, index) => {\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n headingLines.push({ line, level, index });\n }\n });\n\n // Use string-based approach to wrap headings in divs\n let content = translatedContent;\n const headingsToWrap: Array<{\n originalLine: string;\n id: string;\n }> = [];\n\n // Match translated headings with their corresponding lines by position and level\n translatedHeadings.forEach((heading, position) => {\n const id = idMappings.get(position);\n if (id) {\n // Find the corresponding original line for this heading\n const matchingLine = headingLines.find((hl) => {\n // Extract clean text from the original line for comparison\n const lineCleanText = hl.line.replace(/^#{1,6}\\s+/, '').trim();\n // Create a version without markdown formatting for comparison\n const cleanLineText = lineCleanText\n .replace(/\\*\\*(.*?)\\*\\*/g, '$1') // Remove bold\n .replace(/\\*(.*?)\\*/g, '$1') // Remove italic\n .replace(/`(.*?)`/g, '$1') // Remove inline code\n .replace(/\\[(.*?)\\]\\(.*?\\)/g, '$1') // Remove links, keep text\n .trim();\n\n const normalizedLineText = decode(cleanLineText).trim();\n const normalizedHeadingText = decode(heading.text).trim();\n\n return (\n normalizedLineText === normalizedHeadingText &&\n hl.level === heading.level\n );\n });\n\n if (matchingLine) {\n headingsToWrap.push({\n originalLine: matchingLine.line,\n id,\n });\n }\n }\n });\n\n if (headingsToWrap.length > 0) {\n // Process headings from longest to shortest original line to avoid partial matches\n const sortedHeadings = headingsToWrap.sort(\n (a, b) => b.originalLine.length - a.originalLine.length\n );\n\n for (const heading of sortedHeadings) {\n // If already wrapped with this id, skip (idempotent)\n if (content.includes(`<div id=\"${heading.id}\">`)) {\n continue;\n }\n // Escape the original line for use in regex\n const escapedLine = heading.originalLine.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n const headingPattern = new RegExp(`^${escapedLine}\\\\s*$`, 'gm');\n\n content = content.replace(headingPattern, (match) => {\n return `<div id=\"${heading.id}\">\\n ${match.trim()}\\n</div>\\n`;\n });\n }\n }\n\n return content;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"addExplicitAnchorIds.js","names":[],"sources":["../../src/utils/addExplicitAnchorIds.ts"],"sourcesContent":["import { visit } from 'unist-util-visit';\nimport type { Heading, Node } from 'mdast';\nimport type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';\nimport { logger } from '../console/logger.js';\nimport type { AdditionalOptions } from '../types/index.js';\nimport {\n forEachLineOutsideCodeFences,\n mapLinesOutsideCodeFences,\n parseMdxTolerantly,\n} from './mdxAnchorSyntax.js';\n\ntype AnchorIdSettings = {\n options?: Pick<AdditionalOptions, 'experimentalAddHeaderAnchorIds'>;\n};\n\n/** An ATX heading line, split into indentation, marker and text. */\nconst ATX_HEADING = /^([ \\t]*)(#{1,6}[ \\t]+)(.*)$/;\n\n/** A trailing custom anchor ID, in either the plain or MDX-escaped form. */\nconst TRAILING_ANCHOR = /\\s*(?:\\\\\\{#[^}]+\\\\\\}|\\{#[^}]+\\})\\s*$/;\n\n/**\n * Generates a slug from heading text\n */\nfunction generateSlug(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '') // Remove special chars except spaces and hyphens\n .trim()\n .replace(/\\s+/g, '-') // Replace spaces with hyphens\n .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen\n .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens\n}\n\n/**\n * Extracts text content from heading nodes\n */\nfunction extractHeadingText(heading: Heading): string {\n let text = '';\n\n visit(heading, ['text', 'inlineCode'], (node: Node) => {\n if ('value' in node && typeof node.value === 'string') {\n text += node.value;\n }\n });\n\n return text;\n}\n\n/**\n * Line-by-line heading extractor used when MDX parsing fails outright.\n */\nfunction extractHeadingsWithFallback(mdxContent: string): HeadingInfo[] {\n const headings: HeadingInfo[] = [];\n let position = 0;\n\n forEachLineOutsideCodeFences(mdxContent, (line, index) => {\n const headingMatch = line.match(ATX_HEADING);\n if (!headingMatch) return;\n\n const [, indent, marker, rawText] = headingMatch;\n const { cleanedText, explicitId } = parseHeadingContent(rawText);\n if (!cleanedText && !explicitId) return;\n\n headings.push({\n text: cleanedText,\n level: marker.trim().length,\n slug: explicitId ?? generateSlug(cleanedText),\n position: position++,\n startLine: index + 1,\n endLine: index + 1,\n startColumn: indent.length + 1,\n // Without a parser there is nothing finer to go on than end of line.\n textEndColumn: line.length + 1,\n wrapperId: null,\n explicit: explicitId !== undefined,\n });\n });\n\n assignUniqueSlugs(headings);\n return headings;\n}\n\nfunction parseHeadingContent(text: string): {\n cleanedText: string;\n explicitId?: string;\n} {\n // Support both {#id} and escaped \\{#id\\} forms\n const anchorMatch = text.match(/(\\\\\\{#([^}]+)\\\\\\}|\\{#([^}]+)\\})\\s*$/);\n\n if (!anchorMatch) {\n return { cleanedText: text };\n }\n\n const explicitId = anchorMatch[2] || anchorMatch[3];\n const cleanedText = text.replace(anchorMatch[0], '').trimEnd();\n\n return { cleanedText, explicitId };\n}\n\n/**\n * Suffixes repeated slugs `-2`, `-3`, ... as Mintlify does. Author-written IDs\n * are never renumbered, only reserved.\n */\nfunction assignUniqueSlugs(headings: HeadingInfo[]): void {\n // Reserve every explicit ID up front, including ones later in the document,\n // so a generated slug never claims an ID an author asked for.\n const used = new Set(\n headings\n .filter((heading) => heading.explicit)\n .map((heading) => heading.slug)\n );\n\n for (const heading of headings) {\n if (heading.explicit) continue;\n\n // Headings with no slug-able characters would produce id=\"\".\n const base = heading.slug || 'section';\n let slug = base;\n let suffix = 1;\n while (used.has(slug)) {\n suffix += 1;\n slug = `${base}-${suffix}`;\n }\n\n heading.slug = slug;\n used.add(slug);\n }\n}\n\n/** A source range on a single line, as 1-based inclusive/exclusive columns. */\ninterface ColumnRange {\n line: number;\n startColumn: number;\n endColumn: number;\n}\n\n/**\n * Represents a heading with its position and metadata\n */\nexport interface HeadingInfo {\n text: string;\n level: number;\n slug: string;\n position: number;\n /** 1-based line the heading starts on. */\n startLine: number;\n /** 1-based line the heading ends on (differs from startLine for setext). */\n endLine: number;\n /** 1-based column of the heading marker; anything left of it is indentation. */\n startColumn: number;\n /** 1-based column just past the text, before any closing `##`; -1 if unknown. */\n textEndColumn: number;\n /** `id` attribute of a wrapper element already anchoring this heading. */\n wrapperId: ColumnRange | null;\n /** Whether the author wrote an explicit `{#id}`. */\n explicit: boolean;\n}\n\n/**\n * Finds the `id` of a wrapper element already anchoring this heading. Requiring\n * the heading to be its only child rules out containers like `<Tab>`.\n */\nfunction findWrapperId(\n heading: Heading,\n parent: Node | undefined\n): ColumnRange | null {\n if (!parent || parent.type !== 'mdxJsxFlowElement') return null;\n\n const element = parent as MdxJsxFlowElement;\n if (element.children.length !== 1 || element.children[0] !== heading) {\n return null;\n }\n\n const id = element.attributes.find(\n (attribute) =>\n attribute.type === 'mdxJsxAttribute' && attribute.name === 'id'\n );\n const position = id?.position;\n if (!position || position.start.line !== position.end.line) return null;\n\n return {\n line: position.start.line,\n startColumn: position.start.column,\n endColumn: position.end.column,\n };\n}\n\n/**\n * Extracts heading information from content (read-only, no modifications).\n * Source and translation are matched by position, so both must parse the same\n * way — the fallback extractor misses headings nested in JSX.\n */\nexport function extractHeadingInfo(mdxContent: string): HeadingInfo[] {\n let ast;\n try {\n ast = parseMdxTolerantly(mdxContent);\n } catch {\n // Fallback: line-by-line extraction skipping fenced code blocks\n return extractHeadingsWithFallback(mdxContent);\n }\n\n const headings: HeadingInfo[] = [];\n let position = 0;\n\n visit(ast, 'heading', (heading: Heading, _index, parent) => {\n const headingText = extractHeadingText(heading);\n const { cleanedText, explicitId } = parseHeadingContent(headingText);\n if (!cleanedText && !explicitId) return;\n\n const lastChild = heading.children[heading.children.length - 1];\n\n headings.push({\n text: cleanedText,\n level: heading.depth,\n slug: explicitId ?? generateSlug(cleanedText),\n position: position++,\n startLine: heading.position?.start.line ?? -1,\n endLine: heading.position?.end.line ?? -1,\n startColumn: heading.position?.start.column ?? 1,\n textEndColumn:\n lastChild?.position?.end.column ?? heading.position?.end.column ?? -1,\n wrapperId: findWrapperId(heading, parent),\n explicit: explicitId !== undefined,\n });\n });\n\n assignUniqueSlugs(headings);\n return headings;\n}\n\n/**\n * Applies anchor IDs to translated content based on source heading mapping\n */\nexport function addExplicitAnchorIds(\n translatedContent: string,\n sourceHeadingMap: HeadingInfo[],\n settings?: AnchorIdSettings,\n sourcePath?: string,\n translatedPath?: string,\n fileTypeHint?: 'md' | 'mdx'\n): {\n content: string;\n hasChanges: boolean;\n addedIds: Array<{ heading: string; id: string }>;\n} {\n const addedIds: Array<{ heading: string; id: string }> = [];\n const useDivWrapping =\n settings?.options?.experimentalAddHeaderAnchorIds === 'mintlify';\n\n // Extract headings from translated content\n const translatedHeadings = extractHeadingInfo(translatedContent);\n\n // Pre-processing validation: check if header counts match\n if (sourceHeadingMap.length !== translatedHeadings.length) {\n const sourceFile = sourcePath\n ? `Source file: ${sourcePath}`\n : 'Source file';\n const translatedFile = translatedPath\n ? `translated file: ${translatedPath}`\n : 'translated file';\n\n logger.warn(\n `Header count mismatch detected! ${sourceFile} has ${sourceHeadingMap.length} headers but ${translatedFile} has ${translatedHeadings.length} headers. ` +\n `This likely means your source file was edited after translation was requested, causing a mismatch between ` +\n `the number of headers in your source file vs the translated file. Re-translate this file to resolve the issue.`\n );\n }\n\n // Create ID mapping based on positional matching\n const idMappings = new Map<number, { id: string; explicit: boolean }>();\n sourceHeadingMap.forEach((sourceHeading, index) => {\n const translatedHeading = translatedHeadings[index];\n // Match by position and level for safety\n if (translatedHeading && translatedHeading.level === sourceHeading.level) {\n idMappings.set(index, {\n id: sourceHeading.slug,\n explicit: sourceHeading.explicit,\n });\n addedIds.push({\n heading: translatedHeading.text,\n id: sourceHeading.slug,\n });\n }\n });\n\n const translatedIsMdx = translatedPath\n ? translatedPath.toLowerCase().endsWith('.mdx')\n : true; // default to mdx-style escaping when unknown\n const shouldEscapeAnchors =\n fileTypeHint === 'mdx'\n ? true\n : fileTypeHint === 'md'\n ? false\n : translatedIsMdx;\n\n if (idMappings.size === 0) {\n // Normalize anchors the translation carried over.\n const content = useDivWrapping\n ? translatedContent\n : normalizeInlineAnchors(translatedContent, shouldEscapeAnchors);\n return {\n content,\n hasChanges: content !== translatedContent,\n addedIds: [],\n };\n }\n\n let content = applyAnchorIds(\n translatedContent,\n translatedHeadings,\n idMappings,\n useDivWrapping,\n shouldEscapeAnchors\n );\n\n if (!useDivWrapping) {\n content = normalizeInlineAnchors(content, shouldEscapeAnchors);\n }\n\n return {\n content,\n hasChanges: content !== translatedContent,\n addedIds,\n };\n}\n\n/**\n * Writes anchor IDs onto the translated document, locating headings by parser\n * line position rather than by text. Edits run bottom-up to keep lines valid.\n */\nfunction applyAnchorIds(\n translatedContent: string,\n translatedHeadings: HeadingInfo[],\n idMappings: Map<number, { id: string; explicit: boolean }>,\n useDivWrapping: boolean,\n escapeAnchors: boolean\n): string {\n const lines = translatedContent.split('\\n');\n\n const ordered = [...translatedHeadings].sort(\n (a, b) => b.startLine - a.startLine\n );\n\n for (const heading of ordered) {\n const mapping = idMappings.get(heading.position);\n if (!mapping) continue;\n if (heading.startLine < 1 || heading.endLine > lines.length) continue;\n\n const index = heading.startLine - 1;\n\n // Author-written IDs stay inline; derived ones go in a wrapper.\n const inline = !useDivWrapping || mapping.explicit;\n\n if (inline) {\n // Setext headings have no column to append to.\n if (heading.textEndColumn < 1) continue;\n\n const escape = escapeAnchors && !mapping.explicit;\n const anchor = escape ? `\\\\{#${mapping.id}\\\\}` : `{#${mapping.id}}`;\n const line = lines[index];\n const text = line\n .slice(0, heading.textEndColumn - 1)\n .replace(TRAILING_ANCHOR, '');\n const trailer = line.slice(heading.textEndColumn - 1);\n\n lines[index] = `${text} ${anchor}${trailer}`;\n continue;\n }\n\n if (heading.wrapperId) {\n // Already wrapped: fix the ID in place rather than nesting another.\n const { line, startColumn, endColumn } = heading.wrapperId;\n const wrapper = lines[line - 1];\n lines[line - 1] =\n wrapper.slice(0, startColumn - 1) +\n `id=\"${mapping.id}\"` +\n wrapper.slice(endColumn - 1);\n continue;\n }\n\n const indent = lines[index].slice(0, Math.max(0, heading.startColumn - 1));\n const body = lines.slice(index, heading.endLine).map((line) => ` ${line}`);\n\n lines.splice(\n index,\n heading.endLine - heading.startLine + 1,\n `${indent}<div id=\"${mapping.id}\">`,\n ...body,\n `${indent}</div>`\n );\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Normalizes every inline anchor: escaped for MDX, bare for Markdown.\n */\nfunction normalizeInlineAnchors(\n content: string,\n escapeAnchors: boolean\n): string {\n return mapLinesOutsideCodeFences(content, (line) => {\n const atx = line.match(ATX_HEADING);\n if (!atx) return line;\n\n const escaped = atx[3].match(/\\\\\\{#([A-Za-z0-9_-]+)\\\\\\}\\s*$/);\n const bare = atx[3].match(/(?<!\\\\)\\{#([A-Za-z0-9_-]+)\\}\\s*$/);\n\n if (escapeAnchors && bare) {\n const text = atx[3].replace(TRAILING_ANCHOR, '');\n return `${atx[1]}${atx[2]}${text} \\\\{#${bare[1]}\\\\}`;\n }\n if (!escapeAnchors && escaped) {\n const text = atx[3].replace(TRAILING_ANCHOR, '');\n return `${atx[1]}${atx[2]}${text} {#${escaped[1]}}`;\n }\n return line;\n });\n}\n"],"mappings":";;;;;AAgBA,MAAM,cAAc;;AAGpB,MAAM,kBAAkB;;;;AAKxB,SAAS,aAAa,MAAsB;AAC1C,QAAO,KACJ,aAAa,CACb,QAAQ,aAAa,GAAG,CACxB,MAAM,CACN,QAAQ,QAAQ,IAAI,CACpB,QAAQ,OAAO,IAAI,CACnB,QAAQ,UAAU,GAAG;;;;;AAM1B,SAAS,mBAAmB,SAA0B;CACpD,IAAI,OAAO;AAEX,OAAM,SAAS,CAAC,QAAQ,aAAa,GAAG,SAAe;AACrD,MAAI,WAAW,QAAQ,OAAO,KAAK,UAAU,SAC3C,SAAQ,KAAK;GAEf;AAEF,QAAO;;;;;AAMT,SAAS,4BAA4B,YAAmC;CACtE,MAAM,WAA0B,EAAE;CAClC,IAAI,WAAW;AAEf,8BAA6B,aAAa,MAAM,UAAU;EACxD,MAAM,eAAe,KAAK,MAAM,YAAY;AAC5C,MAAI,CAAC,aAAc;EAEnB,MAAM,GAAG,QAAQ,QAAQ,WAAW;EACpC,MAAM,EAAE,aAAa,eAAe,oBAAoB,QAAQ;AAChE,MAAI,CAAC,eAAe,CAAC,WAAY;AAEjC,WAAS,KAAK;GACZ,MAAM;GACN,OAAO,OAAO,MAAM,CAAC;GACrB,MAAM,cAAc,aAAa,YAAY;GAC7C,UAAU;GACV,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,aAAa,OAAO,SAAS;GAE7B,eAAe,KAAK,SAAS;GAC7B,WAAW;GACX,UAAU,eAAe,KAAA;GAC1B,CAAC;GACF;AAEF,mBAAkB,SAAS;AAC3B,QAAO;;AAGT,SAAS,oBAAoB,MAG3B;CAEA,MAAM,cAAc,KAAK,MAAM,sCAAsC;AAErE,KAAI,CAAC,YACH,QAAO,EAAE,aAAa,MAAM;CAG9B,MAAM,aAAa,YAAY,MAAM,YAAY;AAGjD,QAAO;EAAE,aAFW,KAAK,QAAQ,YAAY,IAAI,GAAG,CAAC,SAEjC;EAAE;EAAY;;;;;;AAOpC,SAAS,kBAAkB,UAA+B;CAGxD,MAAM,OAAO,IAAI,IACf,SACG,QAAQ,YAAY,QAAQ,SAAS,CACrC,KAAK,YAAY,QAAQ,KAAK,CAClC;AAED,MAAK,MAAM,WAAW,UAAU;AAC9B,MAAI,QAAQ,SAAU;EAGtB,MAAM,OAAO,QAAQ,QAAQ;EAC7B,IAAI,OAAO;EACX,IAAI,SAAS;AACb,SAAO,KAAK,IAAI,KAAK,EAAE;AACrB,aAAU;AACV,UAAO,GAAG,KAAK,GAAG;;AAGpB,UAAQ,OAAO;AACf,OAAK,IAAI,KAAK;;;;;;;AAqClB,SAAS,cACP,SACA,QACoB;AACpB,KAAI,CAAC,UAAU,OAAO,SAAS,oBAAqB,QAAO;CAE3D,MAAM,UAAU;AAChB,KAAI,QAAQ,SAAS,WAAW,KAAK,QAAQ,SAAS,OAAO,QAC3D,QAAO;CAOT,MAAM,WAJK,QAAQ,WAAW,MAC3B,cACC,UAAU,SAAS,qBAAqB,UAAU,SAAS,KAE5C,EAAE;AACrB,KAAI,CAAC,YAAY,SAAS,MAAM,SAAS,SAAS,IAAI,KAAM,QAAO;AAEnE,QAAO;EACL,MAAM,SAAS,MAAM;EACrB,aAAa,SAAS,MAAM;EAC5B,WAAW,SAAS,IAAI;EACzB;;;;;;;AAQH,SAAgB,mBAAmB,YAAmC;CACpE,IAAI;AACJ,KAAI;AACF,QAAM,mBAAmB,WAAW;SAC9B;AAEN,SAAO,4BAA4B,WAAW;;CAGhD,MAAM,WAA0B,EAAE;CAClC,IAAI,WAAW;AAEf,OAAM,KAAK,YAAY,SAAkB,QAAQ,WAAW;EAE1D,MAAM,EAAE,aAAa,eAAe,oBADhB,mBAAmB,QAC4B,CAAC;AACpE,MAAI,CAAC,eAAe,CAAC,WAAY;EAEjC,MAAM,YAAY,QAAQ,SAAS,QAAQ,SAAS,SAAS;AAE7D,WAAS,KAAK;GACZ,MAAM;GACN,OAAO,QAAQ;GACf,MAAM,cAAc,aAAa,YAAY;GAC7C,UAAU;GACV,WAAW,QAAQ,UAAU,MAAM,QAAQ;GAC3C,SAAS,QAAQ,UAAU,IAAI,QAAQ;GACvC,aAAa,QAAQ,UAAU,MAAM,UAAU;GAC/C,eACE,WAAW,UAAU,IAAI,UAAU,QAAQ,UAAU,IAAI,UAAU;GACrE,WAAW,cAAc,SAAS,OAAO;GACzC,UAAU,eAAe,KAAA;GAC1B,CAAC;GACF;AAEF,mBAAkB,SAAS;AAC3B,QAAO;;;;;AAMT,SAAgB,qBACd,mBACA,kBACA,UACA,YACA,gBACA,cAKA;CACA,MAAM,WAAmD,EAAE;CAC3D,MAAM,iBACJ,UAAU,SAAS,mCAAmC;CAGxD,MAAM,qBAAqB,mBAAmB,kBAAkB;AAGhE,KAAI,iBAAiB,WAAW,mBAAmB,QAAQ;EACzD,MAAM,aAAa,aACf,gBAAgB,eAChB;EACJ,MAAM,iBAAiB,iBACnB,oBAAoB,mBACpB;AAEJ,SAAO,KACL,mCAAmC,WAAW,OAAO,iBAAiB,OAAO,eAAe,eAAe,OAAO,mBAAmB,OAAO,oOAG7I;;CAIH,MAAM,6BAAa,IAAI,KAAgD;AACvE,kBAAiB,SAAS,eAAe,UAAU;EACjD,MAAM,oBAAoB,mBAAmB;AAE7C,MAAI,qBAAqB,kBAAkB,UAAU,cAAc,OAAO;AACxE,cAAW,IAAI,OAAO;IACpB,IAAI,cAAc;IAClB,UAAU,cAAc;IACzB,CAAC;AACF,YAAS,KAAK;IACZ,SAAS,kBAAkB;IAC3B,IAAI,cAAc;IACnB,CAAC;;GAEJ;CAEF,MAAM,kBAAkB,iBACpB,eAAe,aAAa,CAAC,SAAS,OAAO,GAC7C;CACJ,MAAM,sBACJ,iBAAiB,QACb,OACA,iBAAiB,OACf,QACA;AAER,KAAI,WAAW,SAAS,GAAG;EAEzB,MAAM,UAAU,iBACZ,oBACA,uBAAuB,mBAAmB,oBAAoB;AAClE,SAAO;GACL;GACA,YAAY,YAAY;GACxB,UAAU,EAAE;GACb;;CAGH,IAAI,UAAU,eACZ,mBACA,oBACA,YACA,gBACA,oBACD;AAED,KAAI,CAAC,eACH,WAAU,uBAAuB,SAAS,oBAAoB;AAGhE,QAAO;EACL;EACA,YAAY,YAAY;EACxB;EACD;;;;;;AAOH,SAAS,eACP,mBACA,oBACA,YACA,gBACA,eACQ;CACR,MAAM,QAAQ,kBAAkB,MAAM,KAAK;CAE3C,MAAM,UAAU,CAAC,GAAG,mBAAmB,CAAC,MACrC,GAAG,MAAM,EAAE,YAAY,EAAE,UAC3B;AAED,MAAK,MAAM,WAAW,SAAS;EAC7B,MAAM,UAAU,WAAW,IAAI,QAAQ,SAAS;AAChD,MAAI,CAAC,QAAS;AACd,MAAI,QAAQ,YAAY,KAAK,QAAQ,UAAU,MAAM,OAAQ;EAE7D,MAAM,QAAQ,QAAQ,YAAY;AAKlC,MAFe,CAAC,kBAAkB,QAAQ,UAE9B;AAEV,OAAI,QAAQ,gBAAgB,EAAG;GAG/B,MAAM,SADS,iBAAiB,CAAC,QAAQ,WACjB,OAAO,QAAQ,GAAG,OAAO,KAAK,QAAQ,GAAG;GACjE,MAAM,OAAO,MAAM;AAMnB,SAAM,SAAS,GALF,KACV,MAAM,GAAG,QAAQ,gBAAgB,EAAE,CACnC,QAAQ,iBAAiB,GAGN,CAAC,GAAG,SAFV,KAAK,MAAM,QAAQ,gBAAgB,EAET;AAC1C;;AAGF,MAAI,QAAQ,WAAW;GAErB,MAAM,EAAE,MAAM,aAAa,cAAc,QAAQ;GACjD,MAAM,UAAU,MAAM,OAAO;AAC7B,SAAM,OAAO,KACX,QAAQ,MAAM,GAAG,cAAc,EAAE,GACjC,OAAO,QAAQ,GAAG,KAClB,QAAQ,MAAM,YAAY,EAAE;AAC9B;;EAGF,MAAM,SAAS,MAAM,OAAO,MAAM,GAAG,KAAK,IAAI,GAAG,QAAQ,cAAc,EAAE,CAAC;EAC1E,MAAM,OAAO,MAAM,MAAM,OAAO,QAAQ,QAAQ,CAAC,KAAK,SAAS,KAAK,OAAO;AAE3E,QAAM,OACJ,OACA,QAAQ,UAAU,QAAQ,YAAY,GACtC,GAAG,OAAO,WAAW,QAAQ,GAAG,KAChC,GAAG,MACH,GAAG,OAAO,QACX;;AAGH,QAAO,MAAM,KAAK,KAAK;;;;;AAMzB,SAAS,uBACP,SACA,eACQ;AACR,QAAO,0BAA0B,UAAU,SAAS;EAClD,MAAM,MAAM,KAAK,MAAM,YAAY;AACnC,MAAI,CAAC,IAAK,QAAO;EAEjB,MAAM,UAAU,IAAI,GAAG,MAAM,gCAAgC;EAC7D,MAAM,OAAO,IAAI,GAAG,MAAM,mCAAmC;AAE7D,MAAI,iBAAiB,MAAM;GACzB,MAAM,OAAO,IAAI,GAAG,QAAQ,iBAAiB,GAAG;AAChD,UAAO,GAAG,IAAI,KAAK,IAAI,KAAK,KAAK,OAAO,KAAK,GAAG;;AAElD,MAAI,CAAC,iBAAiB,SAAS;GAC7B,MAAM,OAAO,IAAI,GAAG,QAAQ,iBAAiB,GAAG;AAChD,UAAO,GAAG,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,QAAQ,GAAG;;AAEnD,SAAO;GACP"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createFileMapping } from "../formats/files/fileMapping.js";
|
|
2
|
+
import { parseMdxForRoundTrip, restoreAnchorIds } from "./mdxAnchorSyntax.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import * as fs$1 from "fs";
|
|
4
5
|
import { unified } from "unified";
|
|
5
|
-
import remarkParse from "remark-parse";
|
|
6
6
|
import remarkMdx from "remark-mdx";
|
|
7
7
|
import remarkFrontmatter from "remark-frontmatter";
|
|
8
8
|
import { visit } from "unist-util-visit";
|
|
@@ -37,9 +37,11 @@ function isSubPath(child, parent) {
|
|
|
37
37
|
function localizeRelativeAssetsForContent(content, sourcePath, targetPath, cwd) {
|
|
38
38
|
let changed = false;
|
|
39
39
|
let ast;
|
|
40
|
+
let neutralizedAnchors;
|
|
40
41
|
try {
|
|
41
|
-
const
|
|
42
|
-
ast =
|
|
42
|
+
const parsed = parseMdxForRoundTrip(content);
|
|
43
|
+
ast = parsed.ast;
|
|
44
|
+
neutralizedAnchors = parsed.neutralized;
|
|
43
45
|
} catch {
|
|
44
46
|
return {
|
|
45
47
|
content,
|
|
@@ -84,6 +86,7 @@ function localizeRelativeAssetsForContent(content, sourcePath, targetPath, cwd)
|
|
|
84
86
|
} } });
|
|
85
87
|
const outTree = s.runSync(ast);
|
|
86
88
|
let out = s.stringify(outTree);
|
|
89
|
+
if (neutralizedAnchors) out = restoreAnchorIds(out);
|
|
87
90
|
if (out.endsWith("\n") && !content.endsWith("\n")) out = out.slice(0, -1);
|
|
88
91
|
if (content.startsWith("\n") && !out.startsWith("\n")) out = "\n" + out;
|
|
89
92
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localizeRelativeAssets.js","names":["fs"],"sources":["../../src/utils/localizeRelativeAssets.ts"],"sourcesContent":["import * as fs from 'fs';\nimport path from 'node:path';\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 { Literal, Root } from 'mdast';\nimport { escapeHtmlInTextNodes, normalizeCJKCharacters } from 'gt-remark';\nimport type { StaticLocalizationSettings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\n\ntype RewriteResult = { content: string; hasChanges: boolean };\nexport type RelativeAssetSettings = StaticLocalizationSettings;\n\ntype MdxAssetNode = {\n type?: string;\n name?: unknown;\n attributes?: unknown;\n url?: unknown;\n};\n\ntype MdxAttribute = {\n type?: string;\n name?: string;\n value?: unknown;\n};\n\nfunction stripQueryAndHash(url: string): { base: string; suffix: string } {\n const match = url.match(/^[^?#]+/);\n const base = match ? match[0] : url;\n const suffix = url.slice(base.length);\n return { base, suffix };\n}\n\nfunction isSkippableUrl(url: string): boolean {\n if (!url) return true;\n if (url.startsWith('/')) return true;\n if (/^(https?:)?\\/\\//i.test(url)) return true;\n if (url.startsWith('data:')) return true;\n if (url.startsWith('#')) return true;\n if (url.startsWith('mailto:')) return true;\n if (url.startsWith('tel:')) return true;\n return false;\n}\n\nfunction toPosix(p: string): string {\n return p.replace(/\\\\/g, '/');\n}\n\nfunction isSubPath(child: string, parent: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nexport function localizeRelativeAssetsForContent(\n content: string,\n sourcePath: string,\n targetPath: string,\n cwd: string\n): RewriteResult {\n let changed = false;\n\n let ast: Root;\n try {\n const processor = unified()\n .use(remarkParse)\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx);\n ast = processor.runSync(processor.parse(content)) as Root;\n } catch {\n return { content, hasChanges: false };\n }\n\n const sourceDir = path.dirname(sourcePath);\n const targetDir = path.dirname(targetPath);\n\n const maybeRewrite = (url: string): string | null => {\n if (isSkippableUrl(url)) return null;\n const { base, suffix } = stripQueryAndHash(url);\n if (isSkippableUrl(base)) return null;\n\n const targetResolved = path.resolve(targetDir, base);\n if (fs.existsSync(targetResolved)) {\n return null;\n }\n\n const sourceResolved = path.resolve(sourceDir, base);\n if (!fs.existsSync(sourceResolved)) {\n return null;\n }\n\n let newPath: string;\n if (isSubPath(sourceResolved, cwd)) {\n newPath = '/' + toPosix(path.relative(cwd, sourceResolved));\n } else {\n const rel = toPosix(path.relative(targetDir, sourceResolved));\n newPath = rel || toPosix(path.basename(sourceResolved));\n }\n\n if (newPath === base) return null;\n changed = true;\n return newPath + suffix;\n };\n\n visit(ast, (node) => {\n const assetNode = node as MdxAssetNode;\n if (assetNode.type === 'image' && typeof assetNode.url === 'string') {\n const newUrl = maybeRewrite(assetNode.url);\n if (newUrl) assetNode.url = newUrl;\n return;\n }\n if (\n (assetNode.type === 'mdxJsxFlowElement' ||\n assetNode.type === 'mdxJsxTextElement') &&\n assetNode.name === 'img' &&\n Array.isArray(assetNode.attributes)\n ) {\n for (const attr of assetNode.attributes) {\n const attribute = attr as MdxAttribute;\n if (\n attribute.type === 'mdxJsxAttribute' &&\n attribute.name === 'src' &&\n typeof attribute.value === 'string'\n ) {\n const newUrl = maybeRewrite(attribute.value);\n if (newUrl) attribute.value = newUrl;\n }\n }\n }\n });\n\n try {\n const s = unified()\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx)\n .use(normalizeCJKCharacters)\n .use(escapeHtmlInTextNodes)\n .use(remarkStringify, {\n handlers: {\n text(node: Literal) {\n return node.value;\n },\n },\n });\n const outTree = s.runSync(ast) as Root;\n let out = s.stringify(outTree);\n if (out.endsWith('\\n') && !content.endsWith('\\n')) out = out.slice(0, -1);\n if (content.startsWith('\\n') && !out.startsWith('\\n')) out = '\\n' + out;\n return { content: out, hasChanges: changed };\n } catch {\n return { content, hasChanges: false };\n }\n}\n\nexport default async function localizeRelativeAssets(\n settings: RelativeAssetSettings,\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\n const { resolvedPaths: sourceFiles } = settings.files;\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,\n settings.defaultLocale\n );\n\n const cwd = process.cwd();\n const processPromises = Object.entries(fileMapping)\n .filter(([locale]) => locales.includes(locale))\n .map(async ([, filesMap]) => {\n const reverseMap = new Map<string, string>();\n for (const [sourcePath, targetPath] of Object.entries(filesMap)) {\n reverseMap.set(targetPath, sourcePath);\n }\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n await Promise.all(\n targetFiles.map(async (targetPath) => {\n if (!fs.existsSync(targetPath)) return;\n const sourcePath = reverseMap.get(targetPath);\n if (!sourcePath) return;\n if (!fs.existsSync(sourcePath)) return;\n\n const content = await fs.promises.readFile(targetPath, 'utf8');\n const result = localizeRelativeAssetsForContent(\n content,\n sourcePath,\n targetPath,\n cwd\n );\n if (result.hasChanges) {\n await fs.promises.writeFile(targetPath, result.content);\n }\n })\n );\n });\n\n await Promise.all(processPromises);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"localizeRelativeAssets.js","names":["fs"],"sources":["../../src/utils/localizeRelativeAssets.ts"],"sourcesContent":["import * as fs from 'fs';\nimport path from 'node:path';\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 { Literal, Root } from 'mdast';\nimport { escapeHtmlInTextNodes, normalizeCJKCharacters } from 'gt-remark';\nimport { parseMdxForRoundTrip, restoreAnchorIds } from './mdxAnchorSyntax.js';\nimport type { StaticLocalizationSettings } from '../types/index.js';\nimport { createFileMapping } from '../formats/files/fileMapping.js';\n\ntype RewriteResult = { content: string; hasChanges: boolean };\nexport type RelativeAssetSettings = StaticLocalizationSettings;\n\ntype MdxAssetNode = {\n type?: string;\n name?: unknown;\n attributes?: unknown;\n url?: unknown;\n};\n\ntype MdxAttribute = {\n type?: string;\n name?: string;\n value?: unknown;\n};\n\nfunction stripQueryAndHash(url: string): { base: string; suffix: string } {\n const match = url.match(/^[^?#]+/);\n const base = match ? match[0] : url;\n const suffix = url.slice(base.length);\n return { base, suffix };\n}\n\nfunction isSkippableUrl(url: string): boolean {\n if (!url) return true;\n if (url.startsWith('/')) return true;\n if (/^(https?:)?\\/\\//i.test(url)) return true;\n if (url.startsWith('data:')) return true;\n if (url.startsWith('#')) return true;\n if (url.startsWith('mailto:')) return true;\n if (url.startsWith('tel:')) return true;\n return false;\n}\n\nfunction toPosix(p: string): string {\n return p.replace(/\\\\/g, '/');\n}\n\nfunction isSubPath(child: string, parent: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nexport function localizeRelativeAssetsForContent(\n content: string,\n sourcePath: string,\n targetPath: string,\n cwd: string\n): RewriteResult {\n let changed = false;\n\n let ast: Root;\n let neutralizedAnchors: boolean;\n try {\n const parsed = parseMdxForRoundTrip(content);\n ast = parsed.ast;\n neutralizedAnchors = parsed.neutralized;\n } catch {\n return { content, hasChanges: false };\n }\n\n const sourceDir = path.dirname(sourcePath);\n const targetDir = path.dirname(targetPath);\n\n const maybeRewrite = (url: string): string | null => {\n if (isSkippableUrl(url)) return null;\n const { base, suffix } = stripQueryAndHash(url);\n if (isSkippableUrl(base)) return null;\n\n const targetResolved = path.resolve(targetDir, base);\n if (fs.existsSync(targetResolved)) {\n return null;\n }\n\n const sourceResolved = path.resolve(sourceDir, base);\n if (!fs.existsSync(sourceResolved)) {\n return null;\n }\n\n let newPath: string;\n if (isSubPath(sourceResolved, cwd)) {\n newPath = '/' + toPosix(path.relative(cwd, sourceResolved));\n } else {\n const rel = toPosix(path.relative(targetDir, sourceResolved));\n newPath = rel || toPosix(path.basename(sourceResolved));\n }\n\n if (newPath === base) return null;\n changed = true;\n return newPath + suffix;\n };\n\n visit(ast, (node) => {\n const assetNode = node as MdxAssetNode;\n if (assetNode.type === 'image' && typeof assetNode.url === 'string') {\n const newUrl = maybeRewrite(assetNode.url);\n if (newUrl) assetNode.url = newUrl;\n return;\n }\n if (\n (assetNode.type === 'mdxJsxFlowElement' ||\n assetNode.type === 'mdxJsxTextElement') &&\n assetNode.name === 'img' &&\n Array.isArray(assetNode.attributes)\n ) {\n for (const attr of assetNode.attributes) {\n const attribute = attr as MdxAttribute;\n if (\n attribute.type === 'mdxJsxAttribute' &&\n attribute.name === 'src' &&\n typeof attribute.value === 'string'\n ) {\n const newUrl = maybeRewrite(attribute.value);\n if (newUrl) attribute.value = newUrl;\n }\n }\n }\n });\n\n try {\n const s = unified()\n .use(remarkFrontmatter, ['yaml', 'toml'])\n .use(remarkMdx)\n .use(normalizeCJKCharacters)\n .use(escapeHtmlInTextNodes)\n .use(remarkStringify, {\n handlers: {\n text(node: Literal) {\n return node.value;\n },\n },\n });\n const outTree = s.runSync(ast) as Root;\n let out = s.stringify(outTree);\n if (neutralizedAnchors) out = restoreAnchorIds(out);\n if (out.endsWith('\\n') && !content.endsWith('\\n')) out = out.slice(0, -1);\n if (content.startsWith('\\n') && !out.startsWith('\\n')) out = '\\n' + out;\n return { content: out, hasChanges: changed };\n } catch {\n return { content, hasChanges: false };\n }\n}\n\nexport default async function localizeRelativeAssets(\n settings: RelativeAssetSettings,\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\n const { resolvedPaths: sourceFiles } = settings.files;\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,\n settings.defaultLocale\n );\n\n const cwd = process.cwd();\n const processPromises = Object.entries(fileMapping)\n .filter(([locale]) => locales.includes(locale))\n .map(async ([, filesMap]) => {\n const reverseMap = new Map<string, string>();\n for (const [sourcePath, targetPath] of Object.entries(filesMap)) {\n reverseMap.set(targetPath, sourcePath);\n }\n const targetFiles = Object.values(filesMap).filter(\n (p) =>\n (p.endsWith('.md') || p.endsWith('.mdx')) &&\n (!includeFiles || includeFiles.has(p))\n );\n\n await Promise.all(\n targetFiles.map(async (targetPath) => {\n if (!fs.existsSync(targetPath)) return;\n const sourcePath = reverseMap.get(targetPath);\n if (!sourcePath) return;\n if (!fs.existsSync(sourcePath)) return;\n\n const content = await fs.promises.readFile(targetPath, 'utf8');\n const result = localizeRelativeAssetsForContent(\n content,\n sourcePath,\n targetPath,\n cwd\n );\n if (result.hasChanges) {\n await fs.promises.writeFile(targetPath, result.content);\n }\n })\n );\n });\n\n await Promise.all(processPromises);\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { createFileMapping } from "../formats/files/fileMapping.js";
|
|
2
|
+
import { parseMdxTolerantly } from "./mdxAnchorSyntax.js";
|
|
2
3
|
import * as fs$1 from "fs";
|
|
3
4
|
import micromatch from "micromatch";
|
|
4
5
|
import * as path$1 from "path";
|
|
5
|
-
import { unified } from "unified";
|
|
6
|
-
import remarkParse from "remark-parse";
|
|
7
|
-
import remarkMdx from "remark-mdx";
|
|
8
|
-
import remarkFrontmatter from "remark-frontmatter";
|
|
9
6
|
import { visit } from "unist-util-visit";
|
|
10
7
|
//#region src/utils/localizeStaticImports.ts
|
|
11
8
|
const { isMatch } = micromatch;
|
|
@@ -168,9 +165,7 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
|
|
|
168
165
|
};
|
|
169
166
|
let processedAst;
|
|
170
167
|
try {
|
|
171
|
-
|
|
172
|
-
const ast = parseProcessor.parse(mdxContent);
|
|
173
|
-
processedAst = parseProcessor.runSync(ast);
|
|
168
|
+
processedAst = parseMdxTolerantly(mdxContent);
|
|
174
169
|
} catch {
|
|
175
170
|
return transformImportsStringFallback(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, currentFilePath, options);
|
|
176
171
|
}
|
|
@@ -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
|
+
};
|