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.
- package/CHANGELOG.md +22 -0
- package/dist/api/collectUserEditDiffs.d.ts +2 -0
- package/dist/api/collectUserEditDiffs.js +31 -8
- package/dist/api/collectUserEditDiffs.js.map +1 -1
- package/dist/api/downloadFileBatch.js +13 -6
- package/dist/api/downloadFileBatch.js.map +1 -1
- package/dist/cli/commands/translate.js.map +1 -1
- package/dist/cli/commands/upload.js +30 -11
- package/dist/cli/commands/upload.js.map +1 -1
- package/dist/console/index.d.ts +4 -0
- package/dist/console/index.js +2 -1
- package/dist/console/index.js.map +1 -1
- package/dist/formats/files/aggregateFiles.js +39 -2
- package/dist/formats/files/aggregateFiles.js.map +1 -1
- package/dist/formats/files/localeContent.d.ts +18 -0
- package/dist/formats/files/localeContent.js +34 -0
- package/dist/formats/files/localeContent.js.map +1 -0
- package/dist/formats/files/supportedFiles.d.ts +2 -1
- package/dist/formats/files/supportedFiles.js +4 -2
- package/dist/formats/files/supportedFiles.js.map +1 -1
- package/dist/formats/files/transformFormat.d.ts +2 -0
- package/dist/formats/files/transformFormat.js +6 -3
- package/dist/formats/files/transformFormat.js.map +1 -1
- package/dist/formats/xcstrings/mergeXcstrings.d.ts +13 -0
- package/dist/formats/xcstrings/mergeXcstrings.js +71 -0
- package/dist/formats/xcstrings/mergeXcstrings.js.map +1 -0
- package/dist/formats/xcstrings/parseXcstrings.d.ts +43 -0
- package/dist/formats/xcstrings/parseXcstrings.js +123 -0
- package/dist/formats/xcstrings/parseXcstrings.js.map +1 -0
- package/dist/fs/config/parseFilesConfig.d.ts +2 -2
- package/dist/fs/config/parseFilesConfig.js +17 -3
- package/dist/fs/config/parseFilesConfig.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/state/recentDownloads.d.ts +1 -1
- package/dist/state/recentDownloads.js +3 -1
- package/dist/state/recentDownloads.js.map +1 -1
- package/dist/utils/addExplicitAnchorIds.d.ts +0 -8
- package/dist/utils/addExplicitAnchorIds.js +19 -45
- package/dist/utils/addExplicitAnchorIds.js.map +1 -1
- package/dist/utils/persistPostprocessHashes.d.ts +1 -1
- package/dist/utils/persistPostprocessHashes.js +18 -14
- package/dist/utils/persistPostprocessHashes.js.map +1 -1
- 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.
|
|
1
|
+
export declare const PACKAGE_VERSION = "2.21.0";
|
|
@@ -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.
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
}
|