gt 2.20.3 → 2.21.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/collectUserEditDiffs.d.ts +2 -0
  3. package/dist/api/collectUserEditDiffs.js +31 -8
  4. package/dist/api/collectUserEditDiffs.js.map +1 -1
  5. package/dist/api/downloadFileBatch.js +13 -6
  6. package/dist/api/downloadFileBatch.js.map +1 -1
  7. package/dist/cli/commands/translate.js.map +1 -1
  8. package/dist/cli/commands/upload.js +30 -11
  9. package/dist/cli/commands/upload.js.map +1 -1
  10. package/dist/console/index.d.ts +4 -0
  11. package/dist/console/index.js +2 -1
  12. package/dist/console/index.js.map +1 -1
  13. package/dist/formats/files/aggregateFiles.js +39 -2
  14. package/dist/formats/files/aggregateFiles.js.map +1 -1
  15. package/dist/formats/files/localeContent.d.ts +18 -0
  16. package/dist/formats/files/localeContent.js +34 -0
  17. package/dist/formats/files/localeContent.js.map +1 -0
  18. package/dist/formats/files/supportedFiles.d.ts +2 -1
  19. package/dist/formats/files/supportedFiles.js +4 -2
  20. package/dist/formats/files/supportedFiles.js.map +1 -1
  21. package/dist/formats/files/transformFormat.d.ts +2 -0
  22. package/dist/formats/files/transformFormat.js +6 -3
  23. package/dist/formats/files/transformFormat.js.map +1 -1
  24. package/dist/formats/xcstrings/mergeXcstrings.d.ts +13 -0
  25. package/dist/formats/xcstrings/mergeXcstrings.js +71 -0
  26. package/dist/formats/xcstrings/mergeXcstrings.js.map +1 -0
  27. package/dist/formats/xcstrings/parseXcstrings.d.ts +43 -0
  28. package/dist/formats/xcstrings/parseXcstrings.js +123 -0
  29. package/dist/formats/xcstrings/parseXcstrings.js.map +1 -0
  30. package/dist/fs/config/parseFilesConfig.d.ts +2 -2
  31. package/dist/fs/config/parseFilesConfig.js +17 -3
  32. package/dist/fs/config/parseFilesConfig.js.map +1 -1
  33. package/dist/generated/version.d.ts +1 -1
  34. package/dist/generated/version.js +1 -1
  35. package/dist/generated/version.js.map +1 -1
  36. package/dist/state/recentDownloads.d.ts +1 -1
  37. package/dist/state/recentDownloads.js +3 -1
  38. package/dist/state/recentDownloads.js.map +1 -1
  39. package/dist/utils/addExplicitAnchorIds.d.ts +0 -8
  40. package/dist/utils/addExplicitAnchorIds.js +19 -45
  41. package/dist/utils/addExplicitAnchorIds.js.map +1 -1
  42. package/dist/utils/persistPostprocessHashes.d.ts +1 -1
  43. package/dist/utils/persistPostprocessHashes.js +18 -14
  44. package/dist/utils/persistPostprocessHashes.js.map +1 -1
  45. package/package.json +6 -6
@@ -0,0 +1,43 @@
1
+ export type XcstringsEntry = {
2
+ localizations?: Record<string, unknown>;
3
+ [key: string]: unknown;
4
+ };
5
+ export type XcstringsCatalog = {
6
+ sourceLanguage: string;
7
+ strings: Record<string, XcstringsEntry>;
8
+ [key: string]: unknown;
9
+ };
10
+ /**
11
+ * Parses a raw .xcstrings document and validates the structure the slicer
12
+ * traverses. The `strings` and `localizations` containers are re-keyed onto
13
+ * prototype-less records so lookups by catalog-chosen names cannot resolve to
14
+ * inherited properties. Throws on invalid content.
15
+ */
16
+ export declare function parseXcstringsCatalog(content: string): XcstringsCatalog;
17
+ /**
18
+ * PINNED SERIALIZATION — DO NOT CHANGE.
19
+ *
20
+ * The source slice is hashed into versionId (see aggregateFiles), so any
21
+ * change to these bytes re-versions every customer .xcstrings file and
22
+ * re-triggers translation fleet-wide. The byte-exact tests on this format are
23
+ * the contract.
24
+ */
25
+ export declare function serializeXcstringsSlice(catalog: XcstringsCatalog): string;
26
+ /**
27
+ * Produces the source-language slice of a validated catalog: a single-locale
28
+ * catalog holding, per entry, only the source-language localization. Entries
29
+ * without a source localization (the key itself is the source) keep their
30
+ * other fields and carry no `localizations`.
31
+ */
32
+ export declare function sliceSourceCatalog(catalog: XcstringsCatalog): XcstringsCatalog;
33
+ /**
34
+ * Produces one locale's translation slice: the entries that carry
35
+ * `localizations[locale]`, each holding only that localization. Returns
36
+ * undefined when the catalog carries no translation for the locale.
37
+ */
38
+ export declare function sliceTranslationCatalog(catalog: XcstringsCatalog, locale: string): XcstringsCatalog | undefined;
39
+ /**
40
+ * Parses, slices to the catalog's source language, and serializes with the
41
+ * pinned byte layout. Throws on invalid content.
42
+ */
43
+ export declare function parseXcstrings(content: string): string;
@@ -0,0 +1,123 @@
1
+ //#region src/formats/xcstrings/parseXcstrings.ts
2
+ function isPlainObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function invalid(path, expected) {
6
+ return /* @__PURE__ */ new Error(`Invalid .xcstrings content: ${path} must be ${expected}`);
7
+ }
8
+ const RESERVED_KEY_NAME = "__proto__";
9
+ function assertSafeKey(name, path) {
10
+ if (name === RESERVED_KEY_NAME) throw new Error(`Invalid .xcstrings content: ${path} uses the reserved name "${name}"`);
11
+ }
12
+ /**
13
+ * Parses a raw .xcstrings document and validates the structure the slicer
14
+ * traverses. The `strings` and `localizations` containers are re-keyed onto
15
+ * prototype-less records so lookups by catalog-chosen names cannot resolve to
16
+ * inherited properties. Throws on invalid content.
17
+ */
18
+ function parseXcstringsCatalog(content) {
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(content);
22
+ } catch {
23
+ throw new Error("Invalid .xcstrings content: not valid JSON");
24
+ }
25
+ if (!isPlainObject(parsed)) throw invalid("document root", "an object");
26
+ if (typeof parsed.sourceLanguage !== "string" || !parsed.sourceLanguage) throw invalid("sourceLanguage", "a non-empty string");
27
+ assertSafeKey(parsed.sourceLanguage, "sourceLanguage");
28
+ if (!isPlainObject(parsed.strings)) throw invalid("strings", "an object");
29
+ const strings = Object.create(null);
30
+ for (const [key, entry] of Object.entries(parsed.strings)) {
31
+ const path = `strings[${JSON.stringify(key)}]`;
32
+ assertSafeKey(key, path);
33
+ if (!isPlainObject(entry)) throw invalid(path, "an object");
34
+ if (entry.localizations === void 0) {
35
+ strings[key] = entry;
36
+ continue;
37
+ }
38
+ if (!isPlainObject(entry.localizations)) throw invalid(`${path}.localizations`, "an object");
39
+ const localizations = Object.create(null);
40
+ for (const [locale, localization] of Object.entries(entry.localizations)) {
41
+ assertSafeKey(locale, `${path}.localizations[${JSON.stringify(locale)}]`);
42
+ localizations[locale] = localization;
43
+ }
44
+ strings[key] = {
45
+ ...entry,
46
+ localizations
47
+ };
48
+ }
49
+ return {
50
+ ...parsed,
51
+ strings
52
+ };
53
+ }
54
+ /**
55
+ * PINNED SERIALIZATION — DO NOT CHANGE.
56
+ *
57
+ * The source slice is hashed into versionId (see aggregateFiles), so any
58
+ * change to these bytes re-versions every customer .xcstrings file and
59
+ * re-triggers translation fleet-wide. The byte-exact tests on this format are
60
+ * the contract.
61
+ */
62
+ function serializeXcstringsSlice(catalog) {
63
+ return JSON.stringify(catalog, null, 2) + "\n";
64
+ }
65
+ /**
66
+ * Clones a validated catalog keeping, per entry, only `localizations[locale]`.
67
+ * An entry without that localization is dropped, or kept without a
68
+ * `localizations` key when `keepEntriesWithoutLocale` is set: an entry whose
69
+ * localizations hold only other locales then slices to the same shape as one
70
+ * with none, so translations added to the catalog leave the source slice,
71
+ * and with it versionId, unchanged. Nodes are cloned, never rebuilt, so
72
+ * unknown fields and key order survive at every level.
73
+ */
74
+ function sliceCatalog(catalog, locale, keepEntriesWithoutLocale) {
75
+ const strings = Object.create(null);
76
+ for (const [key, entry] of Object.entries(catalog.strings)) {
77
+ if (entry.localizations === void 0 || !Object.hasOwn(entry.localizations, locale)) {
78
+ if (!keepEntriesWithoutLocale) continue;
79
+ const { localizations: _otherLocales, ...withoutLocalizations } = entry;
80
+ strings[key] = withoutLocalizations;
81
+ continue;
82
+ }
83
+ const localizations = Object.create(null);
84
+ localizations[locale] = entry.localizations[locale];
85
+ strings[key] = {
86
+ ...entry,
87
+ localizations
88
+ };
89
+ }
90
+ return {
91
+ ...catalog,
92
+ strings
93
+ };
94
+ }
95
+ /**
96
+ * Produces the source-language slice of a validated catalog: a single-locale
97
+ * catalog holding, per entry, only the source-language localization. Entries
98
+ * without a source localization (the key itself is the source) keep their
99
+ * other fields and carry no `localizations`.
100
+ */
101
+ function sliceSourceCatalog(catalog) {
102
+ return sliceCatalog(catalog, catalog.sourceLanguage, true);
103
+ }
104
+ /**
105
+ * Produces one locale's translation slice: the entries that carry
106
+ * `localizations[locale]`, each holding only that localization. Returns
107
+ * undefined when the catalog carries no translation for the locale.
108
+ */
109
+ function sliceTranslationCatalog(catalog, locale) {
110
+ const slice = sliceCatalog(catalog, locale, false);
111
+ return Object.keys(slice.strings).length > 0 ? slice : void 0;
112
+ }
113
+ /**
114
+ * Parses, slices to the catalog's source language, and serializes with the
115
+ * pinned byte layout. Throws on invalid content.
116
+ */
117
+ function parseXcstrings(content) {
118
+ return serializeXcstringsSlice(sliceSourceCatalog(parseXcstringsCatalog(content)));
119
+ }
120
+ //#endregion
121
+ export { parseXcstrings, parseXcstringsCatalog, serializeXcstringsSlice, sliceSourceCatalog, sliceTranslationCatalog };
122
+
123
+ //# sourceMappingURL=parseXcstrings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseXcstrings.js","names":[],"sources":["../../../src/formats/xcstrings/parseXcstrings.ts"],"sourcesContent":["// Slicing for Apple .xcstrings catalogs. One on-disk catalog holds every\n// locale; the upload carries the source-language slice as the source document\n// and one single-locale slice per translated locale. Slices clone nodes and\n// keep only the wanted locale key, so unknown fields survive verbatim at every\n// level and a later download-merge can fold per-locale translations back into\n// the same on-disk catalog.\n\nexport type XcstringsEntry = {\n localizations?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type XcstringsCatalog = {\n sourceLanguage: string;\n strings: Record<string, XcstringsEntry>;\n [key: string]: unknown;\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction invalid(path: string, expected: string): Error {\n return new Error(`Invalid .xcstrings content: ${path} must be ${expected}`);\n}\n\n// Catalogs are untrusted input. Every container keyed by catalog-chosen names\n// (string keys, locales) is built without a prototype, so `constructor` and\n// `prototype` are ordinary properties and stay uploadable, matching the\n// server's validator. `__proto__` is still rejected: assigning it on a plain\n// object reassigns the prototype instead of creating a property.\nconst RESERVED_KEY_NAME = '__proto__';\n\nfunction assertSafeKey(name: string, path: string): void {\n if (name === RESERVED_KEY_NAME) {\n throw new Error(\n `Invalid .xcstrings content: ${path} uses the reserved name \"${name}\"`\n );\n }\n}\n\n/**\n * Parses a raw .xcstrings document and validates the structure the slicer\n * traverses. The `strings` and `localizations` containers are re-keyed onto\n * prototype-less records so lookups by catalog-chosen names cannot resolve to\n * inherited properties. Throws on invalid content.\n */\nexport function parseXcstringsCatalog(content: string): XcstringsCatalog {\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error('Invalid .xcstrings content: not valid JSON');\n }\n if (!isPlainObject(parsed)) throw invalid('document root', 'an object');\n if (typeof parsed.sourceLanguage !== 'string' || !parsed.sourceLanguage) {\n throw invalid('sourceLanguage', 'a non-empty string');\n }\n assertSafeKey(parsed.sourceLanguage, 'sourceLanguage');\n if (!isPlainObject(parsed.strings)) throw invalid('strings', 'an object');\n const strings: Record<string, XcstringsEntry> = Object.create(null);\n for (const [key, entry] of Object.entries(parsed.strings)) {\n const path = `strings[${JSON.stringify(key)}]`;\n assertSafeKey(key, path);\n if (!isPlainObject(entry)) throw invalid(path, 'an object');\n if (entry.localizations === undefined) {\n strings[key] = entry;\n continue;\n }\n if (!isPlainObject(entry.localizations)) {\n throw invalid(`${path}.localizations`, 'an object');\n }\n const localizations: Record<string, unknown> = Object.create(null);\n for (const [locale, localization] of Object.entries(entry.localizations)) {\n assertSafeKey(locale, `${path}.localizations[${JSON.stringify(locale)}]`);\n localizations[locale] = localization;\n }\n strings[key] = { ...entry, localizations };\n }\n return { ...parsed, strings } as XcstringsCatalog;\n}\n\n/**\n * PINNED SERIALIZATION — DO NOT CHANGE.\n *\n * The source slice is hashed into versionId (see aggregateFiles), so any\n * change to these bytes re-versions every customer .xcstrings file and\n * re-triggers translation fleet-wide. The byte-exact tests on this format are\n * the contract.\n */\nexport function serializeXcstringsSlice(catalog: XcstringsCatalog): string {\n return JSON.stringify(catalog, null, 2) + '\\n';\n}\n\n/**\n * Clones a validated catalog keeping, per entry, only `localizations[locale]`.\n * An entry without that localization is dropped, or kept without a\n * `localizations` key when `keepEntriesWithoutLocale` is set: an entry whose\n * localizations hold only other locales then slices to the same shape as one\n * with none, so translations added to the catalog leave the source slice,\n * and with it versionId, unchanged. Nodes are cloned, never rebuilt, so\n * unknown fields and key order survive at every level.\n */\nfunction sliceCatalog(\n catalog: XcstringsCatalog,\n locale: string,\n keepEntriesWithoutLocale: boolean\n): XcstringsCatalog {\n const strings: Record<string, XcstringsEntry> = Object.create(null);\n for (const [key, entry] of Object.entries(catalog.strings)) {\n if (\n entry.localizations === undefined ||\n !Object.hasOwn(entry.localizations, locale)\n ) {\n if (!keepEntriesWithoutLocale) continue;\n const { localizations: _otherLocales, ...withoutLocalizations } = entry;\n strings[key] = withoutLocalizations;\n continue;\n }\n const localizations: Record<string, unknown> = Object.create(null);\n localizations[locale] = entry.localizations[locale];\n strings[key] = { ...entry, localizations };\n }\n return { ...catalog, strings };\n}\n\n/**\n * Produces the source-language slice of a validated catalog: a single-locale\n * catalog holding, per entry, only the source-language localization. Entries\n * without a source localization (the key itself is the source) keep their\n * other fields and carry no `localizations`.\n */\nexport function sliceSourceCatalog(\n catalog: XcstringsCatalog\n): XcstringsCatalog {\n return sliceCatalog(catalog, catalog.sourceLanguage, true);\n}\n\n/**\n * Produces one locale's translation slice: the entries that carry\n * `localizations[locale]`, each holding only that localization. Returns\n * undefined when the catalog carries no translation for the locale.\n */\nexport function sliceTranslationCatalog(\n catalog: XcstringsCatalog,\n locale: string\n): XcstringsCatalog | undefined {\n const slice = sliceCatalog(catalog, locale, false);\n return Object.keys(slice.strings).length > 0 ? slice : undefined;\n}\n\n/**\n * Parses, slices to the catalog's source language, and serializes with the\n * pinned byte layout. Throws on invalid content.\n */\nexport function parseXcstrings(content: string): string {\n const catalog = parseXcstringsCatalog(content);\n return serializeXcstringsSlice(sliceSourceCatalog(catalog));\n}\n"],"mappings":";AAkBA,SAAS,cAAc,OAAkD;AACvE,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM;;AAG7E,SAAS,QAAQ,MAAc,UAAyB;AACtD,wBAAO,IAAI,MAAM,+BAA+B,KAAK,WAAW,WAAW;;AAQ7E,MAAM,oBAAoB;AAE1B,SAAS,cAAc,MAAc,MAAoB;AACvD,KAAI,SAAS,kBACX,OAAM,IAAI,MACR,+BAA+B,KAAK,2BAA2B,KAAK,GACrE;;;;;;;;AAUL,SAAgB,sBAAsB,SAAmC;CACvE,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,QAAQ;SACtB;AACN,QAAM,IAAI,MAAM,6CAA6C;;AAE/D,KAAI,CAAC,cAAc,OAAO,CAAE,OAAM,QAAQ,iBAAiB,YAAY;AACvE,KAAI,OAAO,OAAO,mBAAmB,YAAY,CAAC,OAAO,eACvD,OAAM,QAAQ,kBAAkB,qBAAqB;AAEvD,eAAc,OAAO,gBAAgB,iBAAiB;AACtD,KAAI,CAAC,cAAc,OAAO,QAAQ,CAAE,OAAM,QAAQ,WAAW,YAAY;CACzE,MAAM,UAA0C,OAAO,OAAO,KAAK;AACnE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,QAAQ,EAAE;EACzD,MAAM,OAAO,WAAW,KAAK,UAAU,IAAI,CAAC;AAC5C,gBAAc,KAAK,KAAK;AACxB,MAAI,CAAC,cAAc,MAAM,CAAE,OAAM,QAAQ,MAAM,YAAY;AAC3D,MAAI,MAAM,kBAAkB,KAAA,GAAW;AACrC,WAAQ,OAAO;AACf;;AAEF,MAAI,CAAC,cAAc,MAAM,cAAc,CACrC,OAAM,QAAQ,GAAG,KAAK,iBAAiB,YAAY;EAErD,MAAM,gBAAyC,OAAO,OAAO,KAAK;AAClE,OAAK,MAAM,CAAC,QAAQ,iBAAiB,OAAO,QAAQ,MAAM,cAAc,EAAE;AACxE,iBAAc,QAAQ,GAAG,KAAK,iBAAiB,KAAK,UAAU,OAAO,CAAC,GAAG;AACzE,iBAAc,UAAU;;AAE1B,UAAQ,OAAO;GAAE,GAAG;GAAO;GAAe;;AAE5C,QAAO;EAAE,GAAG;EAAQ;EAAS;;;;;;;;;;AAW/B,SAAgB,wBAAwB,SAAmC;AACzE,QAAO,KAAK,UAAU,SAAS,MAAM,EAAE,GAAG;;;;;;;;;;;AAY5C,SAAS,aACP,SACA,QACA,0BACkB;CAClB,MAAM,UAA0C,OAAO,OAAO,KAAK;AACnE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AAC1D,MACE,MAAM,kBAAkB,KAAA,KACxB,CAAC,OAAO,OAAO,MAAM,eAAe,OAAO,EAC3C;AACA,OAAI,CAAC,yBAA0B;GAC/B,MAAM,EAAE,eAAe,eAAe,GAAG,yBAAyB;AAClE,WAAQ,OAAO;AACf;;EAEF,MAAM,gBAAyC,OAAO,OAAO,KAAK;AAClE,gBAAc,UAAU,MAAM,cAAc;AAC5C,UAAQ,OAAO;GAAE,GAAG;GAAO;GAAe;;AAE5C,QAAO;EAAE,GAAG;EAAS;EAAS;;;;;;;;AAShC,SAAgB,mBACd,SACkB;AAClB,QAAO,aAAa,SAAS,QAAQ,gBAAgB,KAAK;;;;;;;AAQ5D,SAAgB,wBACd,SACA,QAC8B;CAC9B,MAAM,QAAQ,aAAa,SAAS,QAAQ,MAAM;AAClD,QAAO,OAAO,KAAK,MAAM,QAAQ,CAAC,SAAS,IAAI,QAAQ,KAAA;;;;;;AAOzD,SAAgB,eAAe,SAAyB;AAEtD,QAAO,wBAAwB,mBADf,sBAAsB,QACmB,CAAC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { FilesOptions, IncludePattern, ResolvedFiles, Settings, TransformOption } from '../../types/index.js';
1
+ import { FilesOptions, IncludePattern, ResolvedFiles, Settings, SupportedFileExtension, TransformOption } from '../../types/index.js';
2
2
  /**
3
3
  * Resolves the files from the files object
4
4
  * Replaces [locale] with the actual locale in the files
@@ -26,7 +26,7 @@ export declare function normalizeIncludePatterns(patterns: IncludePattern[]): {
26
26
  * @returns The resolved files
27
27
  */
28
28
  export declare function resolveFiles(files: FilesOptions, locale: string, locales: string[], cwd: string, compositePatterns?: string[], requiresReviewDefault?: boolean): Settings['files'];
29
- export declare function expandGlobPatterns(cwd: string, includePatterns: string[], excludePatterns: string[], locale: string, locales: string[], transformPatterns?: TransformOption | string | TransformOption[], compositePatterns?: string[]): {
29
+ export declare function expandGlobPatterns(cwd: string, includePatterns: string[], excludePatterns: string[], locale: string, locales: string[], transformPatterns?: TransformOption | string | TransformOption[], compositePatterns?: string[], fileType?: SupportedFileExtension): {
30
30
  resolvedPaths: string[];
31
31
  placeholderPaths: string[];
32
32
  };
@@ -65,13 +65,14 @@ function resolveFiles(files, locale, locales, cwd, compositePatterns, requiresRe
65
65
  const parsingFlags = {};
66
66
  if (files.gt?.output) placeholderResult.gt = path.resolve(cwd, files.gt.output);
67
67
  for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
68
+ if (fileType === "xcstrings") validateXcstringsInPlace(files.xcstrings);
68
69
  const transform = files[fileType]?.transform;
69
70
  if (transform && (typeof transform === "string" || typeof transform === "object" || Array.isArray(transform))) transformPaths[fileType] = transform;
70
71
  const transformFormat = resolveTransformationFormat(fileType, files[fileType]?.transformationFormat);
71
72
  if (transformFormat) transformFormats[fileType] = transformFormat;
72
73
  if (files[fileType]?.include) {
73
74
  const { paths, publishPatterns, unpublishPatterns } = normalizeIncludePatterns(files[fileType].include);
74
- const filePaths = expandGlobPatterns(cwd, paths, files[fileType]?.exclude || [], locale, locales, transformPaths[fileType] || void 0, compositePatterns);
75
+ const filePaths = expandGlobPatterns(cwd, paths, files[fileType]?.exclude || [], locale, locales, transformPaths[fileType] || void 0, compositePatterns, fileType);
75
76
  resolvedPaths[fileType] = filePaths.resolvedPaths;
76
77
  placeholderResult[fileType] = filePaths.placeholderPaths;
77
78
  classifyPublishPaths(filePaths.resolvedPaths, publishPatterns, unpublishPatterns, cwd, locale, publishPaths, unpublishPaths);
@@ -103,11 +104,11 @@ function resolveFiles(files, locale, locales, cwd, compositePatterns, requiresRe
103
104
  })()
104
105
  };
105
106
  }
106
- function expandGlobPatterns(cwd, includePatterns, excludePatterns, locale, locales, transformPatterns, compositePatterns) {
107
+ function expandGlobPatterns(cwd, includePatterns, excludePatterns, locale, locales, transformPatterns, compositePatterns, fileType) {
107
108
  const resolvedPaths = [];
108
109
  const placeholderPaths = [];
109
110
  for (const pattern of includePatterns) {
110
- if (!pattern.includes("[locale]") && !transformPatterns && !compositePatterns?.includes(pattern)) logger.warn(chalk.yellow(`Pattern "${pattern}" does not include [locale], so the CLI tool may incorrectly save translated files.`));
111
+ if (!pattern.includes("[locale]") && !transformPatterns && !compositePatterns?.includes(pattern) && fileType !== "xcstrings") logger.warn(chalk.yellow(`Pattern "${pattern}" does not include [locale], so the CLI tool may incorrectly save translated files.`));
111
112
  const localePositions = [];
112
113
  let searchIndex = 0;
113
114
  const localeTag = "[locale]";
@@ -179,6 +180,19 @@ function classifyPublishPaths(resolvedPaths, publishPatterns, unpublishPatterns,
179
180
  }
180
181
  }
181
182
  /**
183
+ * An .xcstrings catalog holds every locale in one file and is updated in
184
+ * place: each locale's download is merged into the source catalog. A path
185
+ * transform or a `[locale]` placeholder would map each locale to a separate
186
+ * output, and every write would start from the source catalog and discard the
187
+ * locales written before it.
188
+ */
189
+ function validateXcstringsInPlace(config) {
190
+ if (!config) return;
191
+ if (config.transform) logErrorAndExit("files.xcstrings.transform is not supported. An .xcstrings catalog holds every locale in one file and is updated in place, so remove the transform.");
192
+ const localePatterns = normalizeIncludePatterns(config.include ?? []).paths.filter((pattern) => pattern.includes("[locale]"));
193
+ if (localePatterns.length > 0) logErrorAndExit(`files.xcstrings.include must not contain [locale]: ${localePatterns.map((pattern) => `"${pattern}"`).join(", ")}. An .xcstrings catalog holds every locale in one file and is updated in place, so point the pattern at the catalog itself.`);
194
+ }
195
+ /**
182
196
  * Validates a file-type requiresReview config value. Only a boolean or an
183
197
  * object of include/exclude glob string arrays is accepted — notably not
184
198
  * string "true"/"false", so a misquoted boolean fails loudly instead of
@@ -1 +1 @@
1
- {"version":3,"file":"parseFilesConfig.js","names":[],"sources":["../../../src/fs/config/parseFilesConfig.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n FilesOptions,\n IncludePattern,\n RequiresReviewConfig,\n ResolvedFiles,\n Settings,\n TransformFormats,\n TransformFiles,\n TransformOption,\n} from '../../types/index.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport fg from 'fast-glob';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { logger } from '../../console/logger.js';\nimport chalk from 'chalk';\nimport micromatch from 'micromatch';\nimport { ParseFlagsByFileType } from '../../types/parsing.js';\nimport {\n BASE_PARSING_FLAGS_DEFAULT,\n GT_PARSING_FLAGS_DEFAULT,\n} from '../../config/defaults.js';\nimport { resolveTransformationFormat } from '../../formats/files/transformFormat.js';\nimport { localeForFilePath } from '../../formats/files/localePath.js';\n\n/**\n * Resolves the files from the files object\n * Replaces [locale] with the actual locale in the files\n *\n * @param files - The files object\n * @param locale - The locale to replace [locale] with\n * @returns The resolved files\n */\nexport function resolveLocaleFiles(\n files: ResolvedFiles,\n locale: string\n): ResolvedFiles {\n const result: ResolvedFiles = {};\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n const replacement = localeForFilePath(fileType, locale);\n result[fileType] = files[fileType]?.map((filepath) =>\n filepath.replace(/\\[locale\\]/g, replacement)\n );\n }\n\n // Replace [locale] with locale in all paths\n result.gt = files.gt?.replace(/\\[locale\\]/g, locale);\n\n return result;\n}\n/**\n * Normalizes include patterns into plain path strings and tracks which\n * patterns have explicit publish flags.\n */\nexport function normalizeIncludePatterns(patterns: IncludePattern[]): {\n paths: string[];\n publishPatterns: string[];\n unpublishPatterns: string[];\n} {\n const paths: string[] = [];\n const publishPatterns: string[] = [];\n const unpublishPatterns: string[] = [];\n\n for (const pattern of patterns) {\n if (typeof pattern === 'string') {\n paths.push(pattern);\n } else {\n paths.push(pattern.pattern);\n if (pattern.publish === true) {\n publishPatterns.push(pattern.pattern);\n } else if (pattern.publish === false) {\n unpublishPatterns.push(pattern.pattern);\n }\n }\n }\n\n return { paths, publishPatterns, unpublishPatterns };\n}\n\n/**\n * Resolves the files from the files object.\n * Performs glob pattern expansion on the files.\n * Replaces [locale] with the actual locale in the files.\n *\n * @param files - The files object\n * @returns The resolved files\n */\nexport function resolveFiles(\n files: FilesOptions,\n locale: string,\n locales: string[],\n cwd: string,\n compositePatterns?: string[],\n requiresReviewDefault: boolean = false\n): Settings['files'] {\n // Initialize result object with empty arrays for each file type\n const resolvedPaths: ResolvedFiles = {};\n const placeholderResult: ResolvedFiles = {};\n const transformPaths: TransformFiles = {};\n // Output format transforms are tracked separately from path transforms.\n const transformFormats: TransformFormats = {};\n const publishPaths = new Set<string>();\n const unpublishPaths = new Set<string>();\n const requiresReviewPaths = new Set<string>();\n const parsingFlags: ParseFlagsByFileType = {};\n\n // Process GT files\n if (files.gt?.output) {\n placeholderResult.gt = path.resolve(cwd, files.gt.output);\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n // ==== TRANSFORMS ==== //\n const transform = files[fileType]?.transform;\n if (\n transform &&\n (typeof transform === 'string' ||\n typeof transform === 'object' ||\n Array.isArray(transform))\n ) {\n transformPaths[fileType] = transform;\n }\n // Validate source -> output format transforms during settings generation.\n const transformFormat = resolveTransformationFormat(\n fileType,\n files[fileType]?.transformationFormat\n );\n if (transformFormat) {\n transformFormats[fileType] = transformFormat;\n }\n // ==== PLACEHOLDERS ==== //\n if (files[fileType]?.include) {\n const { paths, publishPatterns, unpublishPatterns } =\n normalizeIncludePatterns(files[fileType].include);\n\n const filePaths = expandGlobPatterns(\n cwd,\n paths,\n files[fileType]?.exclude || [],\n locale,\n locales,\n transformPaths[fileType] || undefined,\n compositePatterns\n );\n resolvedPaths[fileType] = filePaths.resolvedPaths;\n placeholderResult[fileType] = filePaths.placeholderPaths;\n\n // Classify resolved paths into publish/unpublish sets\n classifyPublishPaths(\n filePaths.resolvedPaths,\n publishPatterns,\n unpublishPatterns,\n cwd,\n locale,\n publishPaths,\n unpublishPaths\n );\n\n // Classify resolved paths by effective requiresReview policy\n classifyRequiresReviewPaths(\n filePaths.resolvedPaths,\n validateRequiresReviewConfig(files[fileType]?.requiresReview, fileType),\n requiresReviewDefault,\n cwd,\n locale,\n requiresReviewPaths\n );\n }\n // ==== OTHER ==== //\n if (files[fileType]?.parsingFlags) {\n parsingFlags[fileType] = {\n ...BASE_PARSING_FLAGS_DEFAULT,\n ...files[fileType].parsingFlags,\n };\n }\n }\n\n return {\n resolvedPaths,\n placeholderPaths: placeholderResult,\n transformPaths: transformPaths,\n transformFormats,\n publishPaths,\n unpublishPaths,\n requiresReviewPaths,\n parsingFlags,\n gtJson: (() => {\n const rawGtFlags = (files.gt?.parsingFlags || {}) as Record<\n string,\n unknown\n >;\n return {\n publish: files.gt?.publish,\n parsingFlags: {\n ...GT_PARSING_FLAGS_DEFAULT,\n ...rawGtFlags,\n },\n };\n })(),\n };\n}\n\n// Helper function to expand glob patterns\nexport function expandGlobPatterns(\n cwd: string,\n includePatterns: string[],\n excludePatterns: string[],\n locale: string,\n locales: string[],\n transformPatterns?: TransformOption | string | TransformOption[],\n compositePatterns?: string[]\n): {\n resolvedPaths: string[];\n placeholderPaths: string[];\n} {\n // Expand glob patterns to include all matching files\n const resolvedPaths: string[] = [];\n const placeholderPaths: string[] = [];\n\n // Process include patterns\n for (const pattern of includePatterns) {\n // Track positions where [locale] appears in the original pattern\n // It must be included in the pattern, otherwise the CLI tool will not be able to find the correct output path\n // Warn if it's not included\n // Ignore if is composite pattern\n if (\n !pattern.includes('[locale]') &&\n !transformPatterns &&\n !compositePatterns?.includes(pattern)\n ) {\n logger.warn(\n chalk.yellow(\n `Pattern \"${pattern}\" does not include [locale], so the CLI tool may incorrectly save translated files.`\n )\n );\n }\n const localePositions: number[] = [];\n let searchIndex = 0;\n const localeTag = '[locale]';\n\n while (true) {\n const foundIndex = pattern.indexOf(localeTag, searchIndex);\n if (foundIndex === -1) break;\n localePositions.push(foundIndex);\n searchIndex = foundIndex + localeTag.length;\n }\n\n const expandedPattern = pattern.replace(/\\[locale\\]/g, locale);\n\n // Resolve the absolute pattern path\n const absolutePattern = path.resolve(cwd, expandedPattern);\n\n // Prepare exclude patterns with locale replaced\n const expandedExcludePatterns = Array.from(\n new Set(\n excludePatterns.flatMap((p) =>\n locales.map((targetLocale) =>\n path.resolve(\n cwd,\n p\n .replace(/\\[locale\\]/g, locale)\n .replace(/\\[locales\\]/g, targetLocale)\n )\n )\n )\n )\n );\n\n // Use fast-glob to find all matching files, excluding the patterns\n const matches = fg.sync(absolutePattern, {\n absolute: true,\n ignore: expandedExcludePatterns,\n });\n\n resolvedPaths.push(...matches);\n\n // For each match, create a version with [locale] in the correct positions\n matches.forEach((match) => {\n const absolutePath = path.resolve(cwd, match);\n const patternPath = path.resolve(cwd, pattern);\n let originalAbsolutePath = absolutePath;\n\n if (localePositions.length > 0) {\n const placeholderPath = buildPlaceholderPathFromPattern(\n patternPath,\n absolutePath,\n localeTag\n );\n originalAbsolutePath = placeholderPath;\n }\n\n placeholderPaths.push(originalAbsolutePath);\n });\n }\n\n return { resolvedPaths, placeholderPaths };\n}\n\nfunction buildPlaceholderPathFromPattern(\n patternPath: string,\n absolutePath: string,\n localeTag: string\n): string {\n if (!patternPath.includes(localeTag)) {\n return absolutePath;\n }\n\n const posixPattern = toPosixPath(patternPath);\n const posixPath = toPosixPath(absolutePath);\n\n const baseRegex = micromatch.makeRe(posixPattern, {\n literalBrackets: true,\n });\n const localeRegexSource = baseRegex.source.replace(\n /\\\\\\[locale\\\\\\]/g,\n '([^/]+)'\n );\n const flags = baseRegex.flags.includes('d')\n ? baseRegex.flags\n : `${baseRegex.flags}d`;\n const matcher = new RegExp(localeRegexSource, flags);\n const match = matcher.exec(posixPath);\n\n const matchWithIndices = match as RegExpExecArray & {\n indices?: Array<[number, number]>;\n };\n\n if (!match || !matchWithIndices.indices) {\n return absolutePath;\n }\n\n let placeholderPosixPath = posixPath;\n const indices = matchWithIndices.indices;\n\n for (let i = indices.length - 1; i >= 1; i--) {\n const [start, end] = indices[i];\n if (start === -1 || end === -1) continue;\n placeholderPosixPath =\n placeholderPosixPath.slice(0, start) +\n localeTag +\n placeholderPosixPath.slice(end);\n }\n\n return path.normalize(placeholderPosixPath);\n}\n\nfunction toPosixPath(value: string): string {\n return value.split(path.sep).join(path.posix.sep);\n}\n\n/**\n * Classifies resolved file paths into publish/unpublish sets by matching\n * them against the given glob patterns. Uses POSIX paths for micromatch\n * compatibility but stores platform-native paths in the output sets.\n */\nfunction classifyPublishPaths(\n resolvedPaths: string[],\n publishPatterns: string[],\n unpublishPatterns: string[],\n cwd: string,\n locale: string,\n publishPaths: Set<string>,\n unpublishPaths: Set<string>\n): void {\n if (publishPatterns.length === 0 && unpublishPatterns.length === 0) return;\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n\n for (const pattern of publishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n publishPaths.add(resolvedPaths[i]);\n }\n }\n }\n\n for (const pattern of unpublishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n unpublishPaths.add(resolvedPaths[i]);\n }\n }\n }\n}\n\n/**\n * Validates a file-type requiresReview config value. Only a boolean or an\n * object of include/exclude glob string arrays is accepted — notably not\n * string \"true\"/\"false\", so a misquoted boolean fails loudly instead of\n * silently changing review policy (and with it, version identity).\n */\nfunction validateRequiresReviewConfig(\n config: unknown,\n fileType: string\n): RequiresReviewConfig | undefined {\n if (config === undefined || typeof config === 'boolean') {\n return config;\n }\n const isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((item) => typeof item === 'string');\n if (\n config !== null &&\n typeof config === 'object' &&\n !Array.isArray(config) &&\n Object.keys(config).every((key) => key === 'include' || key === 'exclude')\n ) {\n const { include, exclude } = config as Record<string, unknown>;\n if (\n (include === undefined || isStringArray(include)) &&\n (exclude === undefined || isStringArray(exclude))\n ) {\n return config as RequiresReviewConfig;\n }\n }\n return logErrorAndExit(\n `files.${fileType}.requiresReview must be a boolean or an object of glob string arrays: { include?: string[], exclude?: string[] }`\n );\n}\n\n/**\n * Classifies resolved file paths by effective requiresReview policy and adds\n * paths whose policy is true to requiresReviewPaths. Precedence: file-type\n * include/exclude globs (exclude wins) > file-type boolean > top-level default.\n */\nfunction classifyRequiresReviewPaths(\n resolvedPaths: string[],\n config: RequiresReviewConfig | undefined,\n requiresReviewDefault: boolean,\n cwd: string,\n locale: string,\n requiresReviewPaths: Set<string>\n): void {\n if (typeof config === 'boolean' || config === undefined) {\n if (config ?? requiresReviewDefault) {\n for (const resolvedPath of resolvedPaths) {\n requiresReviewPaths.add(resolvedPath);\n }\n }\n return;\n }\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n const matchAny = (patterns: string[]) => {\n const matched = new Set<string>();\n for (const pattern of patterns) {\n for (const match of micromatch(posixPaths, toAbsoluteGlob(pattern))) {\n matched.add(match);\n }\n }\n return matched;\n };\n\n const included = matchAny(config.include ?? []);\n const excluded = matchAny(config.exclude ?? []);\n\n for (let i = 0; i < posixPaths.length; i++) {\n const requiresReview = excluded.has(posixPaths[i])\n ? false\n : included.has(posixPaths[i]) || requiresReviewDefault;\n if (requiresReview) {\n requiresReviewPaths.add(resolvedPaths[i]);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parseFilesConfig.js","names":[],"sources":["../../../src/fs/config/parseFilesConfig.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n FilesOptions,\n IncludePattern,\n RequiresReviewConfig,\n ResolvedFiles,\n Settings,\n SupportedFileExtension,\n TransformFormats,\n TransformFiles,\n TransformOption,\n} from '../../types/index.js';\nimport { logErrorAndExit } from '../../console/logging.js';\nimport fg from 'fast-glob';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { logger } from '../../console/logger.js';\nimport chalk from 'chalk';\nimport micromatch from 'micromatch';\nimport { ParseFlagsByFileType } from '../../types/parsing.js';\nimport {\n BASE_PARSING_FLAGS_DEFAULT,\n GT_PARSING_FLAGS_DEFAULT,\n} from '../../config/defaults.js';\nimport { resolveTransformationFormat } from '../../formats/files/transformFormat.js';\nimport { localeForFilePath } from '../../formats/files/localePath.js';\n\n/**\n * Resolves the files from the files object\n * Replaces [locale] with the actual locale in the files\n *\n * @param files - The files object\n * @param locale - The locale to replace [locale] with\n * @returns The resolved files\n */\nexport function resolveLocaleFiles(\n files: ResolvedFiles,\n locale: string\n): ResolvedFiles {\n const result: ResolvedFiles = {};\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n const replacement = localeForFilePath(fileType, locale);\n result[fileType] = files[fileType]?.map((filepath) =>\n filepath.replace(/\\[locale\\]/g, replacement)\n );\n }\n\n // Replace [locale] with locale in all paths\n result.gt = files.gt?.replace(/\\[locale\\]/g, locale);\n\n return result;\n}\n/**\n * Normalizes include patterns into plain path strings and tracks which\n * patterns have explicit publish flags.\n */\nexport function normalizeIncludePatterns(patterns: IncludePattern[]): {\n paths: string[];\n publishPatterns: string[];\n unpublishPatterns: string[];\n} {\n const paths: string[] = [];\n const publishPatterns: string[] = [];\n const unpublishPatterns: string[] = [];\n\n for (const pattern of patterns) {\n if (typeof pattern === 'string') {\n paths.push(pattern);\n } else {\n paths.push(pattern.pattern);\n if (pattern.publish === true) {\n publishPatterns.push(pattern.pattern);\n } else if (pattern.publish === false) {\n unpublishPatterns.push(pattern.pattern);\n }\n }\n }\n\n return { paths, publishPatterns, unpublishPatterns };\n}\n\n/**\n * Resolves the files from the files object.\n * Performs glob pattern expansion on the files.\n * Replaces [locale] with the actual locale in the files.\n *\n * @param files - The files object\n * @returns The resolved files\n */\nexport function resolveFiles(\n files: FilesOptions,\n locale: string,\n locales: string[],\n cwd: string,\n compositePatterns?: string[],\n requiresReviewDefault: boolean = false\n): Settings['files'] {\n // Initialize result object with empty arrays for each file type\n const resolvedPaths: ResolvedFiles = {};\n const placeholderResult: ResolvedFiles = {};\n const transformPaths: TransformFiles = {};\n // Output format transforms are tracked separately from path transforms.\n const transformFormats: TransformFormats = {};\n const publishPaths = new Set<string>();\n const unpublishPaths = new Set<string>();\n const requiresReviewPaths = new Set<string>();\n const parsingFlags: ParseFlagsByFileType = {};\n\n // Process GT files\n if (files.gt?.output) {\n placeholderResult.gt = path.resolve(cwd, files.gt.output);\n }\n\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (fileType === 'xcstrings') validateXcstringsInPlace(files.xcstrings);\n // ==== TRANSFORMS ==== //\n const transform = files[fileType]?.transform;\n if (\n transform &&\n (typeof transform === 'string' ||\n typeof transform === 'object' ||\n Array.isArray(transform))\n ) {\n transformPaths[fileType] = transform;\n }\n // Validate source -> output format transforms during settings generation.\n const transformFormat = resolveTransformationFormat(\n fileType,\n files[fileType]?.transformationFormat\n );\n if (transformFormat) {\n transformFormats[fileType] = transformFormat;\n }\n // ==== PLACEHOLDERS ==== //\n if (files[fileType]?.include) {\n const { paths, publishPatterns, unpublishPatterns } =\n normalizeIncludePatterns(files[fileType].include);\n\n const filePaths = expandGlobPatterns(\n cwd,\n paths,\n files[fileType]?.exclude || [],\n locale,\n locales,\n transformPaths[fileType] || undefined,\n compositePatterns,\n fileType\n );\n resolvedPaths[fileType] = filePaths.resolvedPaths;\n placeholderResult[fileType] = filePaths.placeholderPaths;\n\n // Classify resolved paths into publish/unpublish sets\n classifyPublishPaths(\n filePaths.resolvedPaths,\n publishPatterns,\n unpublishPatterns,\n cwd,\n locale,\n publishPaths,\n unpublishPaths\n );\n\n // Classify resolved paths by effective requiresReview policy\n classifyRequiresReviewPaths(\n filePaths.resolvedPaths,\n validateRequiresReviewConfig(files[fileType]?.requiresReview, fileType),\n requiresReviewDefault,\n cwd,\n locale,\n requiresReviewPaths\n );\n }\n // ==== OTHER ==== //\n if (files[fileType]?.parsingFlags) {\n parsingFlags[fileType] = {\n ...BASE_PARSING_FLAGS_DEFAULT,\n ...files[fileType].parsingFlags,\n };\n }\n }\n\n return {\n resolvedPaths,\n placeholderPaths: placeholderResult,\n transformPaths: transformPaths,\n transformFormats,\n publishPaths,\n unpublishPaths,\n requiresReviewPaths,\n parsingFlags,\n gtJson: (() => {\n const rawGtFlags = (files.gt?.parsingFlags || {}) as Record<\n string,\n unknown\n >;\n return {\n publish: files.gt?.publish,\n parsingFlags: {\n ...GT_PARSING_FLAGS_DEFAULT,\n ...rawGtFlags,\n },\n };\n })(),\n };\n}\n\n// Helper function to expand glob patterns\nexport function expandGlobPatterns(\n cwd: string,\n includePatterns: string[],\n excludePatterns: string[],\n locale: string,\n locales: string[],\n transformPatterns?: TransformOption | string | TransformOption[],\n compositePatterns?: string[],\n fileType?: SupportedFileExtension\n): {\n resolvedPaths: string[];\n placeholderPaths: string[];\n} {\n // Expand glob patterns to include all matching files\n const resolvedPaths: string[] = [];\n const placeholderPaths: string[] = [];\n\n // Process include patterns\n for (const pattern of includePatterns) {\n // Track positions where [locale] appears in the original pattern\n // It must be included in the pattern, otherwise the CLI tool will not be able to find the correct output path\n // Warn if it's not included\n // Ignore if is composite pattern\n // xcstrings catalogs hold every locale in one shared file, so a pattern\n // without [locale] is the expected layout there, not a misconfiguration\n if (\n !pattern.includes('[locale]') &&\n !transformPatterns &&\n !compositePatterns?.includes(pattern) &&\n fileType !== 'xcstrings'\n ) {\n logger.warn(\n chalk.yellow(\n `Pattern \"${pattern}\" does not include [locale], so the CLI tool may incorrectly save translated files.`\n )\n );\n }\n const localePositions: number[] = [];\n let searchIndex = 0;\n const localeTag = '[locale]';\n\n while (true) {\n const foundIndex = pattern.indexOf(localeTag, searchIndex);\n if (foundIndex === -1) break;\n localePositions.push(foundIndex);\n searchIndex = foundIndex + localeTag.length;\n }\n\n const expandedPattern = pattern.replace(/\\[locale\\]/g, locale);\n\n // Resolve the absolute pattern path\n const absolutePattern = path.resolve(cwd, expandedPattern);\n\n // Prepare exclude patterns with locale replaced\n const expandedExcludePatterns = Array.from(\n new Set(\n excludePatterns.flatMap((p) =>\n locales.map((targetLocale) =>\n path.resolve(\n cwd,\n p\n .replace(/\\[locale\\]/g, locale)\n .replace(/\\[locales\\]/g, targetLocale)\n )\n )\n )\n )\n );\n\n // Use fast-glob to find all matching files, excluding the patterns\n const matches = fg.sync(absolutePattern, {\n absolute: true,\n ignore: expandedExcludePatterns,\n });\n\n resolvedPaths.push(...matches);\n\n // For each match, create a version with [locale] in the correct positions\n matches.forEach((match) => {\n const absolutePath = path.resolve(cwd, match);\n const patternPath = path.resolve(cwd, pattern);\n let originalAbsolutePath = absolutePath;\n\n if (localePositions.length > 0) {\n const placeholderPath = buildPlaceholderPathFromPattern(\n patternPath,\n absolutePath,\n localeTag\n );\n originalAbsolutePath = placeholderPath;\n }\n\n placeholderPaths.push(originalAbsolutePath);\n });\n }\n\n return { resolvedPaths, placeholderPaths };\n}\n\nfunction buildPlaceholderPathFromPattern(\n patternPath: string,\n absolutePath: string,\n localeTag: string\n): string {\n if (!patternPath.includes(localeTag)) {\n return absolutePath;\n }\n\n const posixPattern = toPosixPath(patternPath);\n const posixPath = toPosixPath(absolutePath);\n\n const baseRegex = micromatch.makeRe(posixPattern, {\n literalBrackets: true,\n });\n const localeRegexSource = baseRegex.source.replace(\n /\\\\\\[locale\\\\\\]/g,\n '([^/]+)'\n );\n const flags = baseRegex.flags.includes('d')\n ? baseRegex.flags\n : `${baseRegex.flags}d`;\n const matcher = new RegExp(localeRegexSource, flags);\n const match = matcher.exec(posixPath);\n\n const matchWithIndices = match as RegExpExecArray & {\n indices?: Array<[number, number]>;\n };\n\n if (!match || !matchWithIndices.indices) {\n return absolutePath;\n }\n\n let placeholderPosixPath = posixPath;\n const indices = matchWithIndices.indices;\n\n for (let i = indices.length - 1; i >= 1; i--) {\n const [start, end] = indices[i];\n if (start === -1 || end === -1) continue;\n placeholderPosixPath =\n placeholderPosixPath.slice(0, start) +\n localeTag +\n placeholderPosixPath.slice(end);\n }\n\n return path.normalize(placeholderPosixPath);\n}\n\nfunction toPosixPath(value: string): string {\n return value.split(path.sep).join(path.posix.sep);\n}\n\n/**\n * Classifies resolved file paths into publish/unpublish sets by matching\n * them against the given glob patterns. Uses POSIX paths for micromatch\n * compatibility but stores platform-native paths in the output sets.\n */\nfunction classifyPublishPaths(\n resolvedPaths: string[],\n publishPatterns: string[],\n unpublishPatterns: string[],\n cwd: string,\n locale: string,\n publishPaths: Set<string>,\n unpublishPaths: Set<string>\n): void {\n if (publishPatterns.length === 0 && unpublishPatterns.length === 0) return;\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n\n for (const pattern of publishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n publishPaths.add(resolvedPaths[i]);\n }\n }\n }\n\n for (const pattern of unpublishPatterns) {\n const matched = new Set(micromatch(posixPaths, toAbsoluteGlob(pattern)));\n for (let i = 0; i < posixPaths.length; i++) {\n if (matched.has(posixPaths[i])) {\n unpublishPaths.add(resolvedPaths[i]);\n }\n }\n }\n}\n\n/**\n * An .xcstrings catalog holds every locale in one file and is updated in\n * place: each locale's download is merged into the source catalog. A path\n * transform or a `[locale]` placeholder would map each locale to a separate\n * output, and every write would start from the source catalog and discard the\n * locales written before it.\n */\nfunction validateXcstringsInPlace(config: FilesOptions['xcstrings']): void {\n if (!config) return;\n if (config.transform) {\n logErrorAndExit(\n 'files.xcstrings.transform is not supported. An .xcstrings catalog holds every locale in one file and is updated in place, so remove the transform.'\n );\n }\n const localePatterns = normalizeIncludePatterns(\n config.include ?? []\n ).paths.filter((pattern) => pattern.includes('[locale]'));\n if (localePatterns.length > 0) {\n logErrorAndExit(\n `files.xcstrings.include must not contain [locale]: ${localePatterns\n .map((pattern) => `\"${pattern}\"`)\n .join(\n ', '\n )}. An .xcstrings catalog holds every locale in one file and is updated in place, so point the pattern at the catalog itself.`\n );\n }\n}\n\n/**\n * Validates a file-type requiresReview config value. Only a boolean or an\n * object of include/exclude glob string arrays is accepted — notably not\n * string \"true\"/\"false\", so a misquoted boolean fails loudly instead of\n * silently changing review policy (and with it, version identity).\n */\nfunction validateRequiresReviewConfig(\n config: unknown,\n fileType: string\n): RequiresReviewConfig | undefined {\n if (config === undefined || typeof config === 'boolean') {\n return config;\n }\n const isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((item) => typeof item === 'string');\n if (\n config !== null &&\n typeof config === 'object' &&\n !Array.isArray(config) &&\n Object.keys(config).every((key) => key === 'include' || key === 'exclude')\n ) {\n const { include, exclude } = config as Record<string, unknown>;\n if (\n (include === undefined || isStringArray(include)) &&\n (exclude === undefined || isStringArray(exclude))\n ) {\n return config as RequiresReviewConfig;\n }\n }\n return logErrorAndExit(\n `files.${fileType}.requiresReview must be a boolean or an object of glob string arrays: { include?: string[], exclude?: string[] }`\n );\n}\n\n/**\n * Classifies resolved file paths by effective requiresReview policy and adds\n * paths whose policy is true to requiresReviewPaths. Precedence: file-type\n * include/exclude globs (exclude wins) > file-type boolean > top-level default.\n */\nfunction classifyRequiresReviewPaths(\n resolvedPaths: string[],\n config: RequiresReviewConfig | undefined,\n requiresReviewDefault: boolean,\n cwd: string,\n locale: string,\n requiresReviewPaths: Set<string>\n): void {\n if (typeof config === 'boolean' || config === undefined) {\n if (config ?? requiresReviewDefault) {\n for (const resolvedPath of resolvedPaths) {\n requiresReviewPaths.add(resolvedPath);\n }\n }\n return;\n }\n\n const posixPaths = resolvedPaths.map(toPosixPath);\n const toAbsoluteGlob = (p: string) =>\n toPosixPath(path.resolve(cwd, p.replace(/\\[locale\\]/g, locale)));\n const matchAny = (patterns: string[]) => {\n const matched = new Set<string>();\n for (const pattern of patterns) {\n for (const match of micromatch(posixPaths, toAbsoluteGlob(pattern))) {\n matched.add(match);\n }\n }\n return matched;\n };\n\n const included = matchAny(config.include ?? []);\n const excluded = matchAny(config.exclude ?? []);\n\n for (let i = 0; i < posixPaths.length; i++) {\n const requiresReview = excluded.has(posixPaths[i])\n ? false\n : included.has(posixPaths[i]) || requiresReviewDefault;\n if (requiresReview) {\n requiresReviewPaths.add(resolvedPaths[i]);\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.20.3";
1
+ export declare const PACKAGE_VERSION = "2.21.0";
@@ -1,5 +1,5 @@
1
1
  //#region src/generated/version.ts
2
- const PACKAGE_VERSION = "2.20.3";
2
+ const PACKAGE_VERSION = "2.21.0";
3
3
  //#endregion
4
4
  export { PACKAGE_VERSION };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.20.3';\n"],"mappings":";AACA,MAAa,kBAAkB"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.21.0';\n"],"mappings":";AACA,MAAa,kBAAkB"}
@@ -16,5 +16,5 @@ export declare function recordRemerged(filePath: string): void;
16
16
  export declare function getDownloaded(): Set<string>;
17
17
  /** Files that need postprocessing: downloaded OR re-merged */
18
18
  export declare function getNeedsPostprocessing(): Set<string>;
19
- export declare function getDownloadedMeta(): Map<string, DownloadMeta>;
19
+ export declare function getDownloadedMeta(): Map<string, DownloadMeta[]>;
20
20
  export declare function clearDownloaded(): void;
@@ -4,7 +4,9 @@ const recentMeta = /* @__PURE__ */ new Map();
4
4
  const remerged = /* @__PURE__ */ new Set();
5
5
  function recordDownloaded(filePath, meta) {
6
6
  recent.add(filePath);
7
- if (meta) recentMeta.set(filePath, meta);
7
+ if (!meta) return;
8
+ const metas = recentMeta.get(filePath) ?? [];
9
+ recentMeta.set(filePath, [...metas.filter((existing) => existing.locale !== meta.locale), meta]);
8
10
  }
9
11
  /**
10
12
  * Track a file that was re-merged with the source
@@ -1 +1 @@
1
- {"version":3,"file":"recentDownloads.js","names":[],"sources":["../../src/state/recentDownloads.ts"],"sourcesContent":["import type { FileFormat } from 'generaltranslation/types';\n\nexport type DownloadMeta = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n // The format the file was written as, so it can be read back the same way.\n fileFormat: FileFormat;\n inputPath?: string;\n};\n\nconst recent = new Set<string>();\nconst recentMeta = new Map<string, DownloadMeta>();\nconst remerged = new Set<string>();\n\nexport function recordDownloaded(filePath: string, meta?: DownloadMeta) {\n recent.add(filePath);\n if (meta) {\n recentMeta.set(filePath, meta);\n }\n}\n\n/**\n * Track a file that was re-merged with the source\n * so that postprocessing still runs on it.\n */\nexport function recordRemerged(filePath: string) {\n remerged.add(filePath);\n}\n\nexport function getDownloaded(): Set<string> {\n return recent;\n}\n\n/** Files that need postprocessing: downloaded OR re-merged */\nexport function getNeedsPostprocessing(): Set<string> {\n return new Set([...recent, ...remerged]);\n}\n\nexport function getDownloadedMeta(): Map<string, DownloadMeta> {\n return recentMeta;\n}\n\nexport function clearDownloaded() {\n recent.clear();\n recentMeta.clear();\n remerged.clear();\n}\n"],"mappings":";AAYA,MAAM,yBAAS,IAAI,KAAa;AAChC,MAAM,6BAAa,IAAI,KAA2B;AAClD,MAAM,2BAAW,IAAI,KAAa;AAElC,SAAgB,iBAAiB,UAAkB,MAAqB;AACtE,QAAO,IAAI,SAAS;AACpB,KAAI,KACF,YAAW,IAAI,UAAU,KAAK;;;;;;AAQlC,SAAgB,eAAe,UAAkB;AAC/C,UAAS,IAAI,SAAS;;AAGxB,SAAgB,gBAA6B;AAC3C,QAAO;;;AAIT,SAAgB,yBAAsC;AACpD,QAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;;AAG1C,SAAgB,oBAA+C;AAC7D,QAAO;;AAGT,SAAgB,kBAAkB;AAChC,QAAO,OAAO;AACd,YAAW,OAAO;AAClB,UAAS,OAAO"}
1
+ {"version":3,"file":"recentDownloads.js","names":[],"sources":["../../src/state/recentDownloads.ts"],"sourcesContent":["import type { FileFormat } from 'generaltranslation/types';\n\nexport type DownloadMeta = {\n branchId: string;\n fileId: string;\n versionId: string;\n locale: string;\n // The format the file was written as, so it can be read back the same way.\n fileFormat: FileFormat;\n inputPath?: string;\n};\n\nconst recent = new Set<string>();\n// A file that holds every locale (a Mintlify composite docs.json) is written once per\n// locale, so a path keeps one entry per locale rather than the last one.\nconst recentMeta = new Map<string, DownloadMeta[]>();\nconst remerged = new Set<string>();\n\nexport function recordDownloaded(filePath: string, meta?: DownloadMeta) {\n recent.add(filePath);\n if (!meta) return;\n const metas = recentMeta.get(filePath) ?? [];\n recentMeta.set(filePath, [\n ...metas.filter((existing) => existing.locale !== meta.locale),\n meta,\n ]);\n}\n\n/**\n * Track a file that was re-merged with the source\n * so that postprocessing still runs on it.\n */\nexport function recordRemerged(filePath: string) {\n remerged.add(filePath);\n}\n\nexport function getDownloaded(): Set<string> {\n return recent;\n}\n\n/** Files that need postprocessing: downloaded OR re-merged */\nexport function getNeedsPostprocessing(): Set<string> {\n return new Set([...recent, ...remerged]);\n}\n\nexport function getDownloadedMeta(): Map<string, DownloadMeta[]> {\n return recentMeta;\n}\n\nexport function clearDownloaded() {\n recent.clear();\n recentMeta.clear();\n remerged.clear();\n}\n"],"mappings":";AAYA,MAAM,yBAAS,IAAI,KAAa;AAGhC,MAAM,6BAAa,IAAI,KAA6B;AACpD,MAAM,2BAAW,IAAI,KAAa;AAElC,SAAgB,iBAAiB,UAAkB,MAAqB;AACtE,QAAO,IAAI,SAAS;AACpB,KAAI,CAAC,KAAM;CACX,MAAM,QAAQ,WAAW,IAAI,SAAS,IAAI,EAAE;AAC5C,YAAW,IAAI,UAAU,CACvB,GAAG,MAAM,QAAQ,aAAa,SAAS,WAAW,KAAK,OAAO,EAC9D,KACD,CAAC;;;;;;AAOJ,SAAgB,eAAe,UAAkB;AAC/C,UAAS,IAAI,SAAS;;AAGxB,SAAgB,gBAA6B;AAC3C,QAAO;;;AAIT,SAAgB,yBAAsC;AACpD,QAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;;AAG1C,SAAgB,oBAAiD;AAC/D,QAAO;;AAGT,SAAgB,kBAAkB;AAChC,QAAO,OAAO;AACd,YAAW,OAAO;AAClB,UAAS,OAAO"}
@@ -2,12 +2,6 @@ import type { AdditionalOptions } from '../types/index.js';
2
2
  type AnchorIdSettings = {
3
3
  options?: Pick<AdditionalOptions, 'experimentalAddHeaderAnchorIds'>;
4
4
  };
5
- /** A source range on a single line, as 1-based inclusive/exclusive columns. */
6
- interface ColumnRange {
7
- line: number;
8
- startColumn: number;
9
- endColumn: number;
10
- }
11
5
  /**
12
6
  * Represents a heading with its position and metadata
13
7
  */
@@ -24,8 +18,6 @@ export interface HeadingInfo {
24
18
  startColumn: number;
25
19
  /** 1-based column just past the text, before any closing `##`; -1 if unknown. */
26
20
  textEndColumn: number;
27
- /** `id` attribute of a wrapper element already anchoring this heading. */
28
- wrapperId: ColumnRange | null;
29
21
  /** Whether the author wrote an explicit `{#id}`. */
30
22
  explicit: boolean;
31
23
  }
@@ -7,6 +7,12 @@ const ATX_HEADING = /^([ \t]*)(#{1,6}[ \t]+)(.*)$/;
7
7
  /** A trailing custom anchor ID, in either the plain or MDX-escaped form. */
8
8
  const TRAILING_ANCHOR = /\s*(?:\\\{#[^}]+\\\}|\{#[^}]+\})\s*$/;
9
9
  /**
10
+ * Deepest indentation at which Mintlify still reads `## Heading {#id}`. It is
11
+ * the CommonMark limit for a heading; MDX itself accepts deeper headings, so
12
+ * past it the `{#id}` reaches the expression parser and fails to compile.
13
+ */
14
+ const MAX_MINTLIFY_HEADING_INDENT = 3;
15
+ /**
10
16
  * Generates a slug from heading text
11
17
  */
12
18
  function generateSlug(text) {
@@ -43,7 +49,6 @@ function extractHeadingsWithFallback(mdxContent) {
43
49
  endLine: index + 1,
44
50
  startColumn: indent.length + 1,
45
51
  textEndColumn: line.length + 1,
46
- wrapperId: null,
47
52
  explicit: explicitId !== void 0
48
53
  });
49
54
  });
@@ -79,22 +84,6 @@ function assignUniqueSlugs(headings) {
79
84
  }
80
85
  }
81
86
  /**
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>`.
84
- */
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
- };
96
- }
97
- /**
98
87
  * Extracts heading information from content (read-only, no modifications).
99
88
  * Source and translation are matched by position, so both must parse the same
100
89
  * way — the fallback extractor misses headings nested in JSX.
@@ -108,7 +97,7 @@ function extractHeadingInfo(mdxContent) {
108
97
  }
109
98
  const headings = [];
110
99
  let position = 0;
111
- visit(ast, "heading", (heading, _index, parent) => {
100
+ visit(ast, "heading", (heading) => {
112
101
  const { cleanedText, explicitId } = parseHeadingContent(extractHeadingText(heading));
113
102
  if (!cleanedText && !explicitId) return;
114
103
  const lastChild = heading.children[heading.children.length - 1];
@@ -121,7 +110,6 @@ function extractHeadingInfo(mdxContent) {
121
110
  endLine: heading.position?.end.line ?? -1,
122
111
  startColumn: heading.position?.start.column ?? 1,
123
112
  textEndColumn: lastChild?.position?.end.column ?? heading.position?.end.column ?? -1,
124
- wrapperId: findWrapperId(heading, parent),
125
113
  explicit: explicitId !== void 0
126
114
  });
127
115
  });
@@ -133,7 +121,7 @@ function extractHeadingInfo(mdxContent) {
133
121
  */
134
122
  function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sourcePath, translatedPath, fileTypeHint) {
135
123
  const addedIds = [];
136
- const useDivWrapping = settings?.options?.experimentalAddHeaderAnchorIds === "mintlify";
124
+ const mintlifyMode = settings?.options?.experimentalAddHeaderAnchorIds === "mintlify";
137
125
  const translatedHeadings = extractHeadingInfo(translatedContent);
138
126
  if (sourceHeadingMap.length !== translatedHeadings.length) {
139
127
  const sourceFile = sourcePath ? `Source file: ${sourcePath}` : "Source file";
@@ -155,17 +143,16 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
155
143
  }
156
144
  });
157
145
  const translatedIsMdx = translatedPath ? translatedPath.toLowerCase().endsWith(".mdx") : true;
158
- const shouldEscapeAnchors = fileTypeHint === "mdx" ? true : fileTypeHint === "md" ? false : translatedIsMdx;
146
+ const shouldEscapeAnchors = mintlifyMode ? false : fileTypeHint === "mdx" ? true : fileTypeHint === "md" ? false : translatedIsMdx;
159
147
  if (idMappings.size === 0) {
160
- const content = useDivWrapping ? translatedContent : normalizeInlineAnchors(translatedContent, shouldEscapeAnchors);
148
+ const content = normalizeInlineAnchors(translatedContent, shouldEscapeAnchors);
161
149
  return {
162
150
  content,
163
151
  hasChanges: content !== translatedContent,
164
152
  addedIds: []
165
153
  };
166
154
  }
167
- let content = applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, shouldEscapeAnchors);
168
- if (!useDivWrapping) content = normalizeInlineAnchors(content, shouldEscapeAnchors);
155
+ const content = normalizeInlineAnchors(applyAnchorIds(translatedContent, translatedHeadings, idMappings, mintlifyMode, shouldEscapeAnchors), shouldEscapeAnchors);
169
156
  return {
170
157
  content,
171
158
  hasChanges: content !== translatedContent,
@@ -176,7 +163,7 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
176
163
  * Writes anchor IDs onto the translated document, locating headings by parser
177
164
  * line position rather than by text. Edits run bottom-up to keep lines valid.
178
165
  */
179
- function applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, escapeAnchors) {
166
+ function applyAnchorIds(translatedContent, translatedHeadings, idMappings, mintlifyMode, escapeAnchors) {
180
167
  const lines = translatedContent.split("\n");
181
168
  const ordered = [...translatedHeadings].sort((a, b) => b.startLine - a.startLine);
182
169
  for (const heading of ordered) {
@@ -184,26 +171,13 @@ function applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDi
184
171
  if (!mapping) continue;
185
172
  if (heading.startLine < 1 || heading.endLine > lines.length) continue;
186
173
  const index = heading.startLine - 1;
187
- if (!useDivWrapping) {
188
- if (heading.textEndColumn < 1) continue;
189
- const anchor = escapeAnchors && !mapping.explicit ? `\\{#${mapping.id}\\}` : `{#${mapping.id}}`;
190
- const { text, trailer } = splitHeadingLine(lines[index], heading);
191
- lines[index] = `${text} ${anchor}${trailer}`;
192
- continue;
193
- }
194
- if (heading.textEndColumn >= 1) {
195
- const { text, trailer } = splitHeadingLine(lines[index], heading);
196
- lines[index] = `${text}${trailer}`;
197
- }
198
- if (heading.wrapperId) {
199
- const { line, startColumn, endColumn } = heading.wrapperId;
200
- const wrapper = lines[line - 1];
201
- lines[line - 1] = wrapper.slice(0, startColumn - 1) + `id="${mapping.id}"` + wrapper.slice(endColumn - 1);
202
- continue;
203
- }
204
- const indent = lines[index].slice(0, Math.max(0, heading.startColumn - 1));
205
- const body = lines.slice(index, heading.endLine).map((line) => ` ${line}`);
206
- lines.splice(index, heading.endLine - heading.startLine + 1, `${indent}<div id="${mapping.id}">`, ...body, `${indent}</div>`);
174
+ if (heading.textEndColumn < 1) continue;
175
+ if (mintlifyMode && heading.endLine > heading.startLine) continue;
176
+ const anchor = escapeAnchors && !mapping.explicit ? `\\{#${mapping.id}\\}` : `{#${mapping.id}}`;
177
+ const { text, trailer } = splitHeadingLine(lines[index], heading);
178
+ const line = `${text} ${anchor}${trailer}`;
179
+ const indent = line.match(/^[ \t]*/)?.[0].length ?? 0;
180
+ lines[index] = mintlifyMode && indent > MAX_MINTLIFY_HEADING_INDENT ? line.trimStart() : line;
207
181
  }
208
182
  return lines.join("\n");
209
183
  }