gt 2.17.2 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/api/collectUserEditDiffs.js +28 -5
- package/dist/api/collectUserEditDiffs.js.map +1 -1
- package/dist/cli/commands/translate.js +1 -1
- package/dist/cli/commands/translate.js.map +1 -1
- package/dist/formats/files/aggregateFiles.js +27 -1
- package/dist/formats/files/aggregateFiles.js.map +1 -1
- package/dist/formats/files/supportedFiles.d.ts +3 -1
- package/dist/formats/files/supportedFiles.js +6 -2
- package/dist/formats/files/supportedFiles.js.map +1 -1
- package/dist/formats/files/transformFormat.d.ts +4 -0
- package/dist/formats/files/transformFormat.js +9 -3
- package/dist/formats/files/transformFormat.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/utils/addExplicitAnchorIds.d.ts +21 -1
- package/dist/utils/addExplicitAnchorIds.js +130 -198
- package/dist/utils/addExplicitAnchorIds.js.map +1 -1
- package/dist/utils/localizeRelativeAssets.js +6 -3
- package/dist/utils/localizeRelativeAssets.js.map +1 -1
- package/dist/utils/localizeStaticImports.js +2 -7
- package/dist/utils/localizeStaticImports.js.map +1 -1
- package/dist/utils/localizeStaticUrls.js +6 -4
- package/dist/utils/localizeStaticUrls.js.map +1 -1
- package/dist/utils/mdxAnchorSyntax.d.ts +30 -0
- package/dist/utils/mdxAnchorSyntax.js +113 -0
- package/dist/utils/mdxAnchorSyntax.js.map +1 -0
- package/dist/utils/validateMdx.d.ts +5 -0
- package/dist/utils/validateMdx.js +7 -7
- package/dist/utils/validateMdx.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { logger } from "../console/logger.js";
|
|
2
|
-
import {
|
|
3
|
-
import remarkParse from "remark-parse";
|
|
4
|
-
import remarkMdx from "remark-mdx";
|
|
5
|
-
import remarkFrontmatter from "remark-frontmatter";
|
|
2
|
+
import { forEachLineOutsideCodeFences, mapLinesOutsideCodeFences, parseMdxTolerantly } from "./mdxAnchorSyntax.js";
|
|
6
3
|
import { visit } from "unist-util-visit";
|
|
7
|
-
import remarkStringify from "remark-stringify";
|
|
8
|
-
import { escapeHtmlInTextNodes, normalizeCJKCharacters } from "gt-remark";
|
|
9
|
-
import { decode } from "html-entities";
|
|
10
4
|
//#region src/utils/addExplicitAnchorIds.ts
|
|
5
|
+
/** An ATX heading line, split into indentation, marker and text. */
|
|
6
|
+
const ATX_HEADING = /^([ \t]*)(#{1,6}[ \t]+)(.*)$/;
|
|
7
|
+
/** A trailing custom anchor ID, in either the plain or MDX-escaped form. */
|
|
8
|
+
const TRAILING_ANCHOR = /\s*(?:\\\{#[^}]+\\\}|\{#[^}]+\})\s*$/;
|
|
11
9
|
/**
|
|
12
10
|
* Generates a slug from heading text
|
|
13
11
|
*/
|
|
@@ -25,41 +23,31 @@ function extractHeadingText(heading) {
|
|
|
25
23
|
return text;
|
|
26
24
|
}
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Used as a fallback when MDX parsing fails.
|
|
26
|
+
* Line-by-line heading extractor used when MDX parsing fails outright.
|
|
30
27
|
*/
|
|
31
28
|
function extractHeadingsWithFallback(mdxContent) {
|
|
32
29
|
const headings = [];
|
|
33
|
-
const lines = mdxContent.split("\n");
|
|
34
30
|
let position = 0;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
if (fenceMatch) {
|
|
40
|
-
const fenceString = fenceMatch[2];
|
|
41
|
-
if (!inFence) {
|
|
42
|
-
inFence = true;
|
|
43
|
-
fenceChar = fenceString;
|
|
44
|
-
} else if (fenceChar && fenceString[0] === fenceChar[0] && fenceString.length >= fenceChar.length) {
|
|
45
|
-
inFence = false;
|
|
46
|
-
fenceChar = null;
|
|
47
|
-
}
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (inFence) continue;
|
|
51
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
52
|
-
if (!headingMatch) continue;
|
|
53
|
-
const hashes = headingMatch[1];
|
|
54
|
-
const rawText = headingMatch[2];
|
|
31
|
+
forEachLineOutsideCodeFences(mdxContent, (line, index) => {
|
|
32
|
+
const headingMatch = line.match(ATX_HEADING);
|
|
33
|
+
if (!headingMatch) return;
|
|
34
|
+
const [, indent, marker, rawText] = headingMatch;
|
|
55
35
|
const { cleanedText, explicitId } = parseHeadingContent(rawText);
|
|
56
|
-
if (cleanedText
|
|
36
|
+
if (!cleanedText && !explicitId) return;
|
|
37
|
+
headings.push({
|
|
57
38
|
text: cleanedText,
|
|
58
|
-
level:
|
|
39
|
+
level: marker.trim().length,
|
|
59
40
|
slug: explicitId ?? generateSlug(cleanedText),
|
|
60
|
-
position: position
|
|
41
|
+
position: position++,
|
|
42
|
+
startLine: index + 1,
|
|
43
|
+
endLine: index + 1,
|
|
44
|
+
startColumn: indent.length + 1,
|
|
45
|
+
textEndColumn: line.length + 1,
|
|
46
|
+
wrapperId: null,
|
|
47
|
+
explicit: explicitId !== void 0
|
|
61
48
|
});
|
|
62
|
-
}
|
|
49
|
+
});
|
|
50
|
+
assignUniqueSlugs(headings);
|
|
63
51
|
return headings;
|
|
64
52
|
}
|
|
65
53
|
function parseHeadingContent(text) {
|
|
@@ -72,39 +60,72 @@ function parseHeadingContent(text) {
|
|
|
72
60
|
};
|
|
73
61
|
}
|
|
74
62
|
/**
|
|
75
|
-
*
|
|
63
|
+
* Suffixes repeated slugs `-2`, `-3`, ... as Mintlify does. Author-written IDs
|
|
64
|
+
* are never renumbered, only reserved.
|
|
65
|
+
*/
|
|
66
|
+
function assignUniqueSlugs(headings) {
|
|
67
|
+
const used = new Set(headings.filter((heading) => heading.explicit).map((heading) => heading.slug));
|
|
68
|
+
for (const heading of headings) {
|
|
69
|
+
if (heading.explicit) continue;
|
|
70
|
+
const base = heading.slug || "section";
|
|
71
|
+
let slug = base;
|
|
72
|
+
let suffix = 1;
|
|
73
|
+
while (used.has(slug)) {
|
|
74
|
+
suffix += 1;
|
|
75
|
+
slug = `${base}-${suffix}`;
|
|
76
|
+
}
|
|
77
|
+
heading.slug = slug;
|
|
78
|
+
used.add(slug);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finds the `id` of a wrapper element already anchoring this heading. Requiring
|
|
83
|
+
* the heading to be its only child rules out containers like `<Tab>`.
|
|
76
84
|
*/
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
85
|
+
function findWrapperId(heading, parent) {
|
|
86
|
+
if (!parent || parent.type !== "mdxJsxFlowElement") return null;
|
|
87
|
+
const element = parent;
|
|
88
|
+
if (element.children.length !== 1 || element.children[0] !== heading) return null;
|
|
89
|
+
const position = element.attributes.find((attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "id")?.position;
|
|
90
|
+
if (!position || position.start.line !== position.end.line) return null;
|
|
91
|
+
return {
|
|
92
|
+
line: position.start.line,
|
|
93
|
+
startColumn: position.start.column,
|
|
94
|
+
endColumn: position.end.column
|
|
95
|
+
};
|
|
81
96
|
}
|
|
82
97
|
/**
|
|
83
|
-
* Extracts heading information from content (read-only, no modifications)
|
|
98
|
+
* Extracts heading information from content (read-only, no modifications).
|
|
99
|
+
* Source and translation are matched by position, so both must parse the same
|
|
100
|
+
* way — the fallback extractor misses headings nested in JSX.
|
|
84
101
|
*/
|
|
85
102
|
function extractHeadingInfo(mdxContent) {
|
|
86
|
-
|
|
87
|
-
let processedAst;
|
|
103
|
+
let ast;
|
|
88
104
|
try {
|
|
89
|
-
|
|
90
|
-
const ast = parseProcessor.parse(mdxContent);
|
|
91
|
-
processedAst = parseProcessor.runSync(ast);
|
|
105
|
+
ast = parseMdxTolerantly(mdxContent);
|
|
92
106
|
} catch {
|
|
93
107
|
return extractHeadingsWithFallback(mdxContent);
|
|
94
108
|
}
|
|
109
|
+
const headings = [];
|
|
95
110
|
let position = 0;
|
|
96
|
-
visit(
|
|
111
|
+
visit(ast, "heading", (heading, _index, parent) => {
|
|
97
112
|
const { cleanedText, explicitId } = parseHeadingContent(extractHeadingText(heading));
|
|
98
|
-
if (cleanedText
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
if (!cleanedText && !explicitId) return;
|
|
114
|
+
const lastChild = heading.children[heading.children.length - 1];
|
|
115
|
+
headings.push({
|
|
116
|
+
text: cleanedText,
|
|
117
|
+
level: heading.depth,
|
|
118
|
+
slug: explicitId ?? generateSlug(cleanedText),
|
|
119
|
+
position: position++,
|
|
120
|
+
startLine: heading.position?.start.line ?? -1,
|
|
121
|
+
endLine: heading.position?.end.line ?? -1,
|
|
122
|
+
startColumn: heading.position?.start.column ?? 1,
|
|
123
|
+
textEndColumn: lastChild?.position?.end.column ?? heading.position?.end.column ?? -1,
|
|
124
|
+
wrapperId: findWrapperId(heading, parent),
|
|
125
|
+
explicit: explicitId !== void 0
|
|
126
|
+
});
|
|
107
127
|
});
|
|
128
|
+
assignUniqueSlugs(headings);
|
|
108
129
|
return headings;
|
|
109
130
|
}
|
|
110
131
|
/**
|
|
@@ -123,23 +144,28 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
|
|
|
123
144
|
sourceHeadingMap.forEach((sourceHeading, index) => {
|
|
124
145
|
const translatedHeading = translatedHeadings[index];
|
|
125
146
|
if (translatedHeading && translatedHeading.level === sourceHeading.level) {
|
|
126
|
-
idMappings.set(index,
|
|
147
|
+
idMappings.set(index, {
|
|
148
|
+
id: sourceHeading.slug,
|
|
149
|
+
explicit: sourceHeading.explicit
|
|
150
|
+
});
|
|
127
151
|
addedIds.push({
|
|
128
152
|
heading: translatedHeading.text,
|
|
129
153
|
id: sourceHeading.slug
|
|
130
154
|
});
|
|
131
155
|
}
|
|
132
156
|
});
|
|
133
|
-
if (idMappings.size === 0) return {
|
|
134
|
-
content: translatedContent,
|
|
135
|
-
hasChanges: false,
|
|
136
|
-
addedIds: []
|
|
137
|
-
};
|
|
138
157
|
const translatedIsMdx = translatedPath ? translatedPath.toLowerCase().endsWith(".mdx") : true;
|
|
139
158
|
const shouldEscapeAnchors = fileTypeHint === "mdx" ? true : fileTypeHint === "md" ? false : translatedIsMdx;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
if (idMappings.size === 0) {
|
|
160
|
+
const content = useDivWrapping ? translatedContent : normalizeInlineAnchors(translatedContent, shouldEscapeAnchors);
|
|
161
|
+
return {
|
|
162
|
+
content,
|
|
163
|
+
hasChanges: content !== translatedContent,
|
|
164
|
+
addedIds: []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
let content = applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, shouldEscapeAnchors);
|
|
168
|
+
if (!useDivWrapping) content = normalizeInlineAnchors(content, shouldEscapeAnchors);
|
|
143
169
|
return {
|
|
144
170
|
content,
|
|
145
171
|
hasChanges: content !== translatedContent,
|
|
@@ -147,149 +173,55 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
|
|
|
147
173
|
};
|
|
148
174
|
}
|
|
149
175
|
/**
|
|
150
|
-
*
|
|
176
|
+
* Writes anchor IDs onto the translated document, locating headings by parser
|
|
177
|
+
* line position rather than by text. Edits run bottom-up to keep lines valid.
|
|
151
178
|
*/
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors);
|
|
167
|
-
}
|
|
168
|
-
let headingIndex = 0;
|
|
169
|
-
let actuallyModifiedContent = false;
|
|
170
|
-
visit(processedAst, "heading", (heading) => {
|
|
171
|
-
const id = idMappings.get(headingIndex);
|
|
172
|
-
if (id) {
|
|
173
|
-
if (hasExplicitId(heading, processedAst)) {
|
|
174
|
-
if (escapeAnchors) {
|
|
175
|
-
const lastChild = heading.children[heading.children.length - 1];
|
|
176
|
-
if (lastChild?.type === "text") {
|
|
177
|
-
const match = lastChild.value.match(/\{#([^}]+)\}\s*$/);
|
|
178
|
-
const alreadyEscaped = lastChild.value.match(/\\\{#[^}]+\\\}\s*$/);
|
|
179
|
-
if (match && !alreadyEscaped) {
|
|
180
|
-
const anchorId = match[1];
|
|
181
|
-
lastChild.value = `${lastChild.value.replace(/\s*\{#[^}]+\}\s*$/, "")} \\{#${anchorId}\\}`;
|
|
182
|
-
actuallyModifiedContent = true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
headingIndex++;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const lastChild = heading.children[heading.children.length - 1];
|
|
190
|
-
if (lastChild?.type === "text") lastChild.value += escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`;
|
|
191
|
-
else heading.children.push({
|
|
192
|
-
type: "text",
|
|
193
|
-
value: escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`
|
|
194
|
-
});
|
|
195
|
-
actuallyModifiedContent = true;
|
|
196
|
-
}
|
|
197
|
-
headingIndex++;
|
|
198
|
-
});
|
|
199
|
-
if (!actuallyModifiedContent) return escapeInlineAnchors(translatedContent);
|
|
200
|
-
try {
|
|
201
|
-
const stringifyProcessor = unified().use(remarkFrontmatter, ["yaml", "toml"]).use(remarkMdx).use(normalizeCJKCharacters).use(escapeHtmlInTextNodes).use(remarkStringify, { handlers: { text(node) {
|
|
202
|
-
return node.value;
|
|
203
|
-
} } });
|
|
204
|
-
const outTree = stringifyProcessor.runSync(processedAst);
|
|
205
|
-
let content = stringifyProcessor.stringify(outTree);
|
|
206
|
-
if (content.endsWith("\n") && !translatedContent.endsWith("\n")) content = content.slice(0, -1);
|
|
207
|
-
if (translatedContent.startsWith("\n") && !content.startsWith("\n")) content = "\n" + content;
|
|
208
|
-
return content;
|
|
209
|
-
} catch {
|
|
210
|
-
return translatedContent;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Fallback string-based inline ID application when AST parsing fails
|
|
215
|
-
*/
|
|
216
|
-
function applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors) {
|
|
217
|
-
let headingIndex = 0;
|
|
218
|
-
let inFence = false;
|
|
219
|
-
let fenceChar = null;
|
|
220
|
-
return translatedContent.split("\n").map((line) => {
|
|
221
|
-
const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})/);
|
|
222
|
-
if (fenceMatch) {
|
|
223
|
-
const fenceString = fenceMatch[2];
|
|
224
|
-
if (!inFence) {
|
|
225
|
-
inFence = true;
|
|
226
|
-
fenceChar = fenceString;
|
|
227
|
-
} else if (fenceChar && fenceString[0] === fenceChar[0] && fenceString.length >= fenceChar.length) {
|
|
228
|
-
inFence = false;
|
|
229
|
-
fenceChar = null;
|
|
230
|
-
}
|
|
231
|
-
return line;
|
|
179
|
+
function applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, escapeAnchors) {
|
|
180
|
+
const lines = translatedContent.split("\n");
|
|
181
|
+
const ordered = [...translatedHeadings].sort((a, b) => b.startLine - a.startLine);
|
|
182
|
+
for (const heading of ordered) {
|
|
183
|
+
const mapping = idMappings.get(heading.position);
|
|
184
|
+
if (!mapping) continue;
|
|
185
|
+
if (heading.startLine < 1 || heading.endLine > lines.length) continue;
|
|
186
|
+
const index = heading.startLine - 1;
|
|
187
|
+
if (!useDivWrapping || mapping.explicit) {
|
|
188
|
+
if (heading.textEndColumn < 1) continue;
|
|
189
|
+
const anchor = escapeAnchors && !mapping.explicit ? `\\{#${mapping.id}\\}` : `{#${mapping.id}}`;
|
|
190
|
+
const line = lines[index];
|
|
191
|
+
lines[index] = `${line.slice(0, heading.textEndColumn - 1).replace(TRAILING_ANCHOR, "")} ${anchor}${line.slice(heading.textEndColumn - 1)}`;
|
|
192
|
+
continue;
|
|
232
193
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const id = idMappings.get(headingIndex++);
|
|
239
|
-
if (!id) return line;
|
|
240
|
-
const hasEscaped = /\\\{#[^}]+\\\}\s*$/.test(text);
|
|
241
|
-
const hasUnescaped = /\{#[^}]+\}\s*$/.test(text);
|
|
242
|
-
if (hasEscaped) return line;
|
|
243
|
-
if (hasUnescaped) {
|
|
244
|
-
if (!escapeAnchors) return line;
|
|
245
|
-
return `${prefix}${text.replace(/\{#([^}]+)\}\s*$/, "\\{#$1\\}")}`;
|
|
194
|
+
if (heading.wrapperId) {
|
|
195
|
+
const { line, startColumn, endColumn } = heading.wrapperId;
|
|
196
|
+
const wrapper = lines[line - 1];
|
|
197
|
+
lines[line - 1] = wrapper.slice(0, startColumn - 1) + `id="${mapping.id}"` + wrapper.slice(endColumn - 1);
|
|
198
|
+
continue;
|
|
246
199
|
}
|
|
247
|
-
|
|
248
|
-
|
|
200
|
+
const indent = lines[index].slice(0, Math.max(0, heading.startColumn - 1));
|
|
201
|
+
const body = lines.slice(index, heading.endLine).map((line) => ` ${line}`);
|
|
202
|
+
lines.splice(index, heading.endLine - heading.startLine + 1, `${indent}<div id="${mapping.id}">`, ...body, `${indent}</div>`);
|
|
203
|
+
}
|
|
204
|
+
return lines.join("\n");
|
|
249
205
|
}
|
|
250
206
|
/**
|
|
251
|
-
*
|
|
207
|
+
* Normalizes every inline anchor: escaped for MDX, bare for Markdown.
|
|
252
208
|
*/
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
level,
|
|
263
|
-
index
|
|
264
|
-
});
|
|
209
|
+
function normalizeInlineAnchors(content, escapeAnchors) {
|
|
210
|
+
return mapLinesOutsideCodeFences(content, (line) => {
|
|
211
|
+
const atx = line.match(ATX_HEADING);
|
|
212
|
+
if (!atx) return line;
|
|
213
|
+
const escaped = atx[3].match(/\\\{#([A-Za-z0-9_-]+)\\\}\s*$/);
|
|
214
|
+
const bare = atx[3].match(/(?<!\\)\{#([A-Za-z0-9_-]+)\}\s*$/);
|
|
215
|
+
if (escapeAnchors && bare) {
|
|
216
|
+
const text = atx[3].replace(TRAILING_ANCHOR, "");
|
|
217
|
+
return `${atx[1]}${atx[2]}${text} \\{#${bare[1]}\\}`;
|
|
265
218
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
translatedHeadings.forEach((heading, position) => {
|
|
270
|
-
const id = idMappings.get(position);
|
|
271
|
-
if (id) {
|
|
272
|
-
const matchingLine = headingLines.find((hl) => {
|
|
273
|
-
return decode(hl.line.replace(/^#{1,6}\s+/, "").trim().replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/`(.*?)`/g, "$1").replace(/\[(.*?)\]\(.*?\)/g, "$1").trim()).trim() === decode(heading.text).trim() && hl.level === heading.level;
|
|
274
|
-
});
|
|
275
|
-
if (matchingLine) headingsToWrap.push({
|
|
276
|
-
originalLine: matchingLine.line,
|
|
277
|
-
id
|
|
278
|
-
});
|
|
219
|
+
if (!escapeAnchors && escaped) {
|
|
220
|
+
const text = atx[3].replace(TRAILING_ANCHOR, "");
|
|
221
|
+
return `${atx[1]}${atx[2]}${text} {#${escaped[1]}}`;
|
|
279
222
|
}
|
|
223
|
+
return line;
|
|
280
224
|
});
|
|
281
|
-
if (headingsToWrap.length > 0) {
|
|
282
|
-
const sortedHeadings = headingsToWrap.sort((a, b) => b.originalLine.length - a.originalLine.length);
|
|
283
|
-
for (const heading of sortedHeadings) {
|
|
284
|
-
if (content.includes(`<div id="${heading.id}">`)) continue;
|
|
285
|
-
const escapedLine = heading.originalLine.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
286
|
-
const headingPattern = new RegExp(`^${escapedLine}\\s*$`, "gm");
|
|
287
|
-
content = content.replace(headingPattern, (match) => {
|
|
288
|
-
return `<div id="${heading.id}">\n ${match.trim()}\n</div>\n`;
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return content;
|
|
293
225
|
}
|
|
294
226
|
//#endregion
|
|
295
227
|
export { addExplicitAnchorIds, extractHeadingInfo };
|
|
@@ -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":";;;;;;;;;;;;;AAmBA,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;;;;;;AAOT,SAAS,4BAA4B,YAAmC;CACtE,MAAM,WAA0B,EAAE;CAClC,MAAM,QAAQ,WAAW,MAAM,KAAK;CAEpC,IAAI,WAAW;CACf,IAAI,UAAU;CACd,IAAI,YAA2B;AAE/B,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAa,KAAK,MAAM,sBAAsB;AACpD,MAAI,YAAY;GACd,MAAM,cAAc,WAAW;AAC/B,OAAI,CAAC,SAAS;AACZ,cAAU;AACV,gBAAY;cAEZ,aACA,YAAY,OAAO,UAAU,MAC7B,YAAY,UAAU,UAAU,QAChC;AACA,cAAU;AACV,gBAAY;;AAEd;;AAGF,MAAI,QACF;EAGF,MAAM,eAAe,KAAK,MAAM,oBAAoB;AACpD,MAAI,CAAC,aACH;EAGF,MAAM,SAAS,aAAa;EAC5B,MAAM,UAAU,aAAa;EAC7B,MAAM,EAAE,aAAa,eAAe,oBAAoB,QAAQ;AAEhE,MAAI,eAAe,WACjB,UAAS,KAAK;GACZ,MAAM;GACN,OAAO,OAAO;GACd,MAAM,cAAc,aAAa,YAAY;GAC7C,UAAU;GACX,CAAC;;AAIN,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;;;;;AAMpC,SAAS,cAAc,SAAkB,MAAqB;CAC5D,MAAM,YAAY,QAAQ,SAAS,QAAQ,SAAS,SAAS;AAC7D,KAAI,WAAW,SAAS,OACtB,QAAO,6CAA6C,KAAK,UAAU,MAAM;AAE3E,QAAO;;;;;AAgBT,SAAgB,mBAAmB,YAAmC;CACpE,MAAM,WAA0B,EAAE;CAGlC,IAAI;AACJ,KAAI;EACF,MAAM,iBAAiB,SAAS,CAC7B,IAAI,YAAY,CAChB,IAAI,mBAAmB,CAAC,QAAQ,OAAO,CAAC,CACxC,IAAI,UAAU;EAEjB,MAAM,MAAM,eAAe,MAAM,WAAW;AAC5C,iBAAe,eAAe,QAAQ,IAAI;SACpC;AAEN,SAAO,4BAA4B,WAAW;;CAGhD,IAAI,WAAW;AACf,OAAM,cAAc,YAAY,YAAqB;EAEnD,MAAM,EAAE,aAAa,eAAe,oBADhB,mBAAmB,QAC4B,CAAC;AACpE,MAAI,eAAe,YAAY;GAC7B,MAAM,OAAO,cAAc,aAAa,YAAY;AACpD,YAAS,KAAK;IACZ,MAAM;IACN,OAAO,QAAQ;IACf;IACA,UAAU;IACX,CAAC;;GAEJ;AAEF,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,KAAqB;AAC5C,kBAAiB,SAAS,eAAe,UAAU;EACjD,MAAM,oBAAoB,mBAAmB;AAE7C,MAAI,qBAAqB,kBAAkB,UAAU,cAAc,OAAO;AACxE,cAAW,IAAI,OAAO,cAAc,KAAK;AACzC,YAAS,KAAK;IACZ,SAAS,kBAAkB;IAC3B,IAAI,cAAc;IACnB,CAAC;;GAEJ;AAEF,KAAI,WAAW,SAAS,EACtB,QAAO;EACL,SAAS;EACT,YAAY;EACZ,UAAU,EAAE;EACb;CAGH,MAAM,kBAAkB,iBACpB,eAAe,aAAa,CAAC,SAAS,OAAO,GAC7C;CACJ,MAAM,sBACJ,iBAAiB,QACb,OACA,iBAAiB,OACf,QACA;CAGR,IAAI;AACJ,KAAI,eACF,WAAU,mBACR,mBACA,oBACA,WACD;KAED,WAAU,eACR,mBACA,YACA,oBACD;AAGH,QAAO;EACL;EACA,YAAY,YAAY;EACxB;EACD;;;;;AAMH,SAAS,eACP,mBACA,YACA,eACQ;CACR,MAAM,uBAAuB,YAA4B;AACvD,MAAI,CAAC,cAAe,QAAO;AAC3B,SAAO,QAAQ,QACb,2BACC,OAAO,IAAI,QAAQ,QAAQ;AAC1B,OAAI,SAAS,KAAK,IAAI,SAAS,OAAO,KACpC,QAAO;AAET,UAAO,OAAO,GAAG;IAEpB;;CAIH,IAAI;AACJ,KAAI;EACF,MAAM,iBAAiB,SAAS,CAC7B,IAAI,YAAY,CAChB,IAAI,mBAAmB,CAAC,QAAQ,OAAO,CAAC,CACxC,IAAI,UAAU;EAEjB,MAAM,MAAM,eAAe,MAAM,kBAAkB;AACnD,iBAAe,eAAe,QAAQ,IAAI;SACpC;AACN,SAAO,6BACL,mBACA,YACA,cACD;;CAIH,IAAI,eAAe;CACnB,IAAI,0BAA0B;AAE9B,OAAM,cAAc,YAAY,YAAqB;EACnD,MAAM,KAAK,WAAW,IAAI,aAAa;AACvC,MAAI,IAAI;AAEN,OAAI,cAAc,SAAS,aAAa,EAAE;AACxC,QAAI,eAAe;KAEjB,MAAM,YAAY,QAAQ,SAAS,QAAQ,SAAS,SAAS;AAC7D,SAAI,WAAW,SAAS,QAAQ;MAC9B,MAAM,QAAQ,UAAU,MAAM,MAAM,mBAAmB;MACvD,MAAM,iBAAiB,UAAU,MAAM,MAAM,qBAAqB;AAClE,UAAI,SAAS,CAAC,gBAAgB;OAC5B,MAAM,WAAW,MAAM;AAEvB,iBAAU,QAAQ,GADL,UAAU,MAAM,QAAQ,qBAAqB,GACjC,CAAC,OAAO,SAAS;AAC1C,iCAA0B;;;;AAIhC;AACA;;GAIF,MAAM,YAAY,QAAQ,SAAS,QAAQ,SAAS,SAAS;AAC7D,OAAI,WAAW,SAAS,OACtB,WAAU,SAAS,gBAAgB,QAAQ,GAAG,OAAO,MAAM,GAAG;OAG9D,SAAQ,SAAS,KAAK;IACpB,MAAM;IACN,OAAO,gBAAgB,QAAQ,GAAG,OAAO,MAAM,GAAG;IACnD,CAAC;AAEJ,6BAA0B;;AAE5B;GACA;AAGF,KAAI,CAAC,wBAEH,QADgB,oBAAoB,kBACtB;AAIhB,KAAI;EACF,MAAM,qBAAqB,SAAS,CACjC,IAAI,mBAAmB,CAAC,QAAQ,OAAO,CAAC,CACxC,IAAI,UAAU,CACd,IAAI,uBAAuB,CAC3B,IAAI,sBAAsB,CAC1B,IAAI,iBAAiB,EACpB,UAAU,EAER,KAAK,MAAe;AAClB,UAAO,KAAK;KAEf,EACF,CAAC;EAEJ,MAAM,UAAU,mBAAmB,QAAQ,aAAa;EACxD,IAAI,UAAU,mBAAmB,UAAU,QAAQ;AAGnD,MAAI,QAAQ,SAAS,KAAK,IAAI,CAAC,kBAAkB,SAAS,KAAK,CAC7D,WAAU,QAAQ,MAAM,GAAG,GAAG;AAIhC,MAAI,kBAAkB,WAAW,KAAK,IAAI,CAAC,QAAQ,WAAW,KAAK,CACjE,WAAU,OAAO;AAGnB,SAAO;SACD;AACN,SAAO;;;;;;AAOX,SAAS,6BACP,mBACA,YACA,eACQ;CACR,IAAI,eAAe;CACnB,IAAI,UAAU;CACd,IAAI,YAA2B;AAuD/B,QArDuB,kBAAkB,MAAM,KAAK,CAAC,KAAK,SAAS;EACjE,MAAM,aAAa,KAAK,MAAM,sBAAsB;AACpD,MAAI,YAAY;GACd,MAAM,cAAc,WAAW;AAC/B,OAAI,CAAC,SAAS;AACZ,cAAU;AACV,gBAAY;cAEZ,aACA,YAAY,OAAO,UAAU,MAC7B,YAAY,UAAU,UAAU,QAChC;AACA,cAAU;AACV,gBAAY;;AAEd,UAAO;;AAGT,MAAI,QACF,QAAO;EAGT,MAAM,eAAe,KAAK,MAAM,oBAAoB;AACpD,MAAI,CAAC,aACH,QAAO;EAGT,MAAM,SAAS,aAAa;EAC5B,MAAM,OAAO,aAAa;EAC1B,MAAM,KAAK,WAAW,IAAI,eAAe;AAEzC,MAAI,CAAC,GACH,QAAO;EAGT,MAAM,aAAa,qBAAqB,KAAK,KAAK;EAClD,MAAM,eAAe,iBAAiB,KAAK,KAAK;AAEhD,MAAI,WACF,QAAO;AAGT,MAAI,cAAc;AAChB,OAAI,CAAC,cACH,QAAO;AAET,UAAO,GAAG,SAAS,KAAK,QAAQ,oBAAoB,YAAY;;AAIlE,SAAO,GAAG,SAAS,OADJ,gBAAgB,QAAQ,GAAG,OAAO,MAAM,GAAG;GAIvC,CAAC,KAAK,KAAK;;;;;AAMlC,SAAS,mBACP,mBACA,oBACA,YACQ;CAER,MAAM,QAAQ,kBAAkB,MAAM,KAAK;CAC3C,MAAM,eACJ,EAAE;AAEJ,OAAM,SAAS,MAAM,UAAU;EAC7B,MAAM,eAAe,KAAK,MAAM,oBAAoB;AACpD,MAAI,cAAc;GAChB,MAAM,QAAQ,aAAa,GAAG;AAC9B,gBAAa,KAAK;IAAE;IAAM;IAAO;IAAO,CAAC;;GAE3C;CAGF,IAAI,UAAU;CACd,MAAM,iBAGD,EAAE;AAGP,oBAAmB,SAAS,SAAS,aAAa;EAChD,MAAM,KAAK,WAAW,IAAI,SAAS;AACnC,MAAI,IAAI;GAEN,MAAM,eAAe,aAAa,MAAM,OAAO;AAc7C,WAH2B,OATL,GAAG,KAAK,QAAQ,cAAc,GAAG,CAAC,MAErB,CAChC,QAAQ,kBAAkB,KAAK,CAC/B,QAAQ,cAAc,KAAK,CAC3B,QAAQ,YAAY,KAAK,CACzB,QAAQ,qBAAqB,KAAK,CAClC,MAE4C,CAAC,CAAC,MAI7B,KAHU,OAAO,QAAQ,KAAK,CAAC,MAGL,IAC5C,GAAG,UAAU,QAAQ;KAEvB;AAEF,OAAI,aACF,gBAAe,KAAK;IAClB,cAAc,aAAa;IAC3B;IACD,CAAC;;GAGN;AAEF,KAAI,eAAe,SAAS,GAAG;EAE7B,MAAM,iBAAiB,eAAe,MACnC,GAAG,MAAM,EAAE,aAAa,SAAS,EAAE,aAAa,OAClD;AAED,OAAK,MAAM,WAAW,gBAAgB;AAEpC,OAAI,QAAQ,SAAS,YAAY,QAAQ,GAAG,IAAI,CAC9C;GAGF,MAAM,cAAc,QAAQ,aAAa,QACvC,uBACA,OACD;GACD,MAAM,iBAAiB,IAAI,OAAO,IAAI,YAAY,QAAQ,KAAK;AAE/D,aAAU,QAAQ,QAAQ,iBAAiB,UAAU;AACnD,WAAO,YAAY,QAAQ,GAAG,QAAQ,MAAM,MAAM,CAAC;KACnD;;;AAIN,QAAO"}
|
|
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":"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"}
|
|
@@ -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
|
}
|