@intlayer/lsp 8.11.4 → 9.0.0-canary.12
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/dist/esm/_virtual/_rolldown/runtime.mjs +36 -0
- package/dist/esm/callers/index.mjs +12 -0
- package/dist/esm/callers/index.mjs.map +1 -0
- package/dist/esm/callers/patterns.mjs +37 -0
- package/dist/esm/callers/patterns.mjs.map +1 -0
- package/dist/esm/dictionaryUtils.mjs +84 -0
- package/dist/esm/dictionaryUtils.mjs.map +1 -0
- package/dist/esm/findFieldInFile.mjs +22 -125
- package/dist/esm/findFieldInFile.mjs.map +1 -1
- package/dist/esm/findKeyAtOffset.mjs +20 -24
- package/dist/esm/findKeyAtOffset.mjs.map +1 -1
- package/dist/esm/findKeyInContentFile.mjs +259 -40
- package/dist/esm/findKeyInContentFile.mjs.map +1 -1
- package/dist/esm/findTCallAtOffset.mjs +33 -0
- package/dist/esm/findTCallAtOffset.mjs.map +1 -0
- package/dist/esm/findUsageFieldAtOffset.mjs +12 -51
- package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -1
- package/dist/esm/index.mjs +338 -170
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs +99 -29
- package/dist/esm/oxcUtils.mjs.map +1 -1
- package/dist/esm/usageAnalyzer.mjs +692 -0
- package/dist/esm/usageAnalyzer.mjs.map +1 -0
- package/dist/esm/utils.mjs +12 -1
- package/dist/types/callers/index.d.ts +8 -0
- package/dist/types/callers/index.d.ts.map +1 -0
- package/dist/types/callers/patterns.d.ts +30 -0
- package/dist/types/callers/patterns.d.ts.map +1 -0
- package/dist/types/dictionaryUtils.d.ts +12 -0
- package/dist/types/dictionaryUtils.d.ts.map +1 -0
- package/dist/types/findFieldInFile.d.ts +12 -14
- package/dist/types/findFieldInFile.d.ts.map +1 -1
- package/dist/types/findKeyAtOffset.d.ts +13 -10
- package/dist/types/findKeyAtOffset.d.ts.map +1 -1
- package/dist/types/findKeyInContentFile.d.ts +15 -13
- package/dist/types/findKeyInContentFile.d.ts.map +1 -1
- package/dist/types/findTCallAtOffset.d.ts +24 -0
- package/dist/types/findTCallAtOffset.d.ts.map +1 -0
- package/dist/types/findUsageFieldAtOffset.d.ts +2 -1
- package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -1
- package/dist/types/oxcUtils.d.ts +42 -13
- package/dist/types/oxcUtils.d.ts.map +1 -1
- package/dist/types/usageAnalyzer.d.ts +129 -0
- package/dist/types/usageAnalyzer.d.ts.map +1 -0
- package/dist/types/utils.d.ts +15 -1
- package/package.json +9 -22
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { parseText, walkAst } from "./oxcUtils.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/findKeyInContentFile.ts
|
|
4
|
-
const KEY_DECLARATION_REGULAR_EXPRESSION =
|
|
4
|
+
const KEY_DECLARATION_REGULAR_EXPRESSION = /(?:^|[{,\s])"?key"?\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^ \t'"`{},\r\n]+))/g;
|
|
5
5
|
/**
|
|
6
6
|
* If `offset` falls within a `key:` declaration in a content file,
|
|
7
7
|
* return the declared key string; otherwise return null.
|
|
8
|
-
* Handles
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Quoted values: key: "foo" | key: 'foo' | key: `foo` (TS/JS)
|
|
10
|
+
* - JSON values: "key": "foo" (JSON/JSON5/JSONC)
|
|
11
|
+
* - Bare values: key: foo-bar (YAML / Markdown frontmatter)
|
|
9
12
|
*/
|
|
10
13
|
const findKeyInContentFile = (text, offset) => {
|
|
11
14
|
for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {
|
|
@@ -15,11 +18,66 @@ const findKeyInContentFile = (text, offset) => {
|
|
|
15
18
|
}
|
|
16
19
|
return null;
|
|
17
20
|
};
|
|
18
|
-
/**
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* If `offset` falls on a content field property key inside an intlayer content
|
|
23
|
+
* file, return the dictionary key, the leaf field name, and the full field path.
|
|
24
|
+
*
|
|
25
|
+
* The `fileExtension` parameter controls which parser is used:
|
|
26
|
+
*
|
|
27
|
+
* - `.json` / `.json5` / `.jsonc` → dedicated JSON parser
|
|
28
|
+
* - `.yaml` / `.yml` → YAML regex heuristic
|
|
29
|
+
* - `.md` / `.mdx` → always null (body is the content value)
|
|
30
|
+
* - everything else → Oxc AST (TS/JS/TSX/JSX)
|
|
31
|
+
*
|
|
32
|
+
* When `fileExtension` is omitted the function falls back to Oxc, then to
|
|
33
|
+
* JSON/YAML heuristics based on text shape.
|
|
34
|
+
*/
|
|
35
|
+
const findContentFieldAtOffset = (text, offset, fileExtension) => {
|
|
36
|
+
const ext = fileExtension?.toLowerCase() ?? "";
|
|
37
|
+
if (ext === ".md" || ext === ".mdx") return null;
|
|
38
|
+
if (ext === ".json" || ext === ".json5" || ext === ".jsonc") return findContentFieldAtOffsetJson(text, offset);
|
|
39
|
+
if (ext === ".yaml" || ext === ".yml") return findContentFieldAtOffsetYaml(text, offset);
|
|
40
|
+
const program = parseText(text);
|
|
41
|
+
if (program) return findContentFieldAtOffsetOxc(text, offset, program);
|
|
42
|
+
if (/^\s*\{/.test(text)) return findContentFieldAtOffsetJson(text, offset);
|
|
43
|
+
if (/^---\s*\r?\n/.test(text)) {
|
|
44
|
+
if (!/^content\s*:/m.test(text)) return null;
|
|
45
|
+
}
|
|
46
|
+
return findContentFieldAtOffsetYaml(text, offset);
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* All Intlayer helper function names whose arguments are locale-map or
|
|
50
|
+
* control-flow data — NOT navigable content field names. The walker must
|
|
51
|
+
* treat these calls as opaque leaf values and never descend into them.
|
|
52
|
+
*
|
|
53
|
+
* `t` – translation ({ en: "…", fr: "…" })
|
|
54
|
+
* `enu` – enumeration ({ "0": "…", ">5": "…" })
|
|
55
|
+
* `cond` – condition ({ true: "…", false: "…" })
|
|
56
|
+
* `plural` – plural rules ({ one: "…", other: "…" })
|
|
57
|
+
* `gender` – gender rules ({ male: "…", female: "…", other: "…" })
|
|
58
|
+
* `insert` – insertion ("Hello {{name}}!")
|
|
59
|
+
* `nest` – nested dict ("navbar")
|
|
60
|
+
* `md` – markdown ("# …")
|
|
61
|
+
* `file` – file reference ("./path/to/file.txt")
|
|
62
|
+
* `html` – html content ("<p>…</p>")
|
|
63
|
+
*/
|
|
64
|
+
const INTLAYER_LEAF_CALL_NAMES = new Set([
|
|
65
|
+
"t",
|
|
66
|
+
"enu",
|
|
67
|
+
"cond",
|
|
68
|
+
"plural",
|
|
69
|
+
"gender",
|
|
70
|
+
"insert",
|
|
71
|
+
"nest",
|
|
72
|
+
"md",
|
|
73
|
+
"file",
|
|
74
|
+
"html"
|
|
75
|
+
]);
|
|
76
|
+
/** Returns true when `node` is a call to any intlayer leaf helper function. */
|
|
77
|
+
const isIntlayerLeafCall = (node) => {
|
|
20
78
|
if (node["type"] !== "CallExpression") return false;
|
|
21
79
|
const callee = node["callee"];
|
|
22
|
-
return callee?.["type"] === "Identifier" && callee["name"]
|
|
80
|
+
return callee?.["type"] === "Identifier" && INTLAYER_LEAF_CALL_NAMES.has(callee["name"]);
|
|
23
81
|
};
|
|
24
82
|
/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */
|
|
25
83
|
const getStringValue = (node) => {
|
|
@@ -31,48 +89,209 @@ const getStringValue = (node) => {
|
|
|
31
89
|
return null;
|
|
32
90
|
};
|
|
33
91
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
92
|
+
* Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.
|
|
93
|
+
* e.g. { "nodeType": "translation", "translation": { "en": "…" } }
|
|
94
|
+
* None of these are navigable content fields.
|
|
36
95
|
*/
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
96
|
+
const JSON_INTLAYER_NODE_KEYS = new Set([
|
|
97
|
+
"nodeType",
|
|
98
|
+
"translation",
|
|
99
|
+
"enumeration",
|
|
100
|
+
"condition",
|
|
101
|
+
"plural",
|
|
102
|
+
"insertion",
|
|
103
|
+
"nested",
|
|
104
|
+
"markdown",
|
|
105
|
+
"html",
|
|
106
|
+
"file",
|
|
107
|
+
"composite",
|
|
108
|
+
"dictionaryKey",
|
|
109
|
+
"$schema",
|
|
110
|
+
"id",
|
|
111
|
+
"projectIds",
|
|
112
|
+
"localId",
|
|
113
|
+
"localIds",
|
|
114
|
+
"filePath",
|
|
115
|
+
"versions",
|
|
116
|
+
"filled",
|
|
117
|
+
"key",
|
|
118
|
+
"title",
|
|
119
|
+
"description",
|
|
120
|
+
"tags",
|
|
121
|
+
"format",
|
|
122
|
+
"locale",
|
|
123
|
+
"schema",
|
|
124
|
+
"location",
|
|
125
|
+
"fill",
|
|
126
|
+
"priority",
|
|
127
|
+
"version",
|
|
128
|
+
"importMode"
|
|
129
|
+
]);
|
|
130
|
+
/**
|
|
131
|
+
* Find the byte offset of `fieldName` (as a JSON property key) inside `text`,
|
|
132
|
+
* searching within the range [searchFrom, searchTo).
|
|
133
|
+
*
|
|
134
|
+
* Matches `"fieldName"` surrounded by typical JSON delimiters.
|
|
135
|
+
*/
|
|
136
|
+
const findJsonKeyOffset = (text, fieldName, searchFrom, searchTo) => {
|
|
137
|
+
const pattern = new RegExp(`"${fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`, "g");
|
|
138
|
+
pattern.lastIndex = searchFrom;
|
|
139
|
+
let match;
|
|
140
|
+
while (true) {
|
|
141
|
+
match = pattern.exec(text);
|
|
142
|
+
if (match === null) break;
|
|
143
|
+
if (match.index >= searchTo) break;
|
|
144
|
+
return match.index;
|
|
145
|
+
}
|
|
146
|
+
return -1;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Strip JSON5/JSONC single-line (//) and multi-line (/* *\/\/) comments,
|
|
150
|
+
* and trailing commas, producing valid JSON that JSON.parse can handle.
|
|
151
|
+
*/
|
|
152
|
+
const stripJsonComments = (text) => text.replace(/(?<!:)\/\/[^\r\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
153
|
+
/**
|
|
154
|
+
* Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content
|
|
155
|
+
* field key, return the dictionary key, leaf field name, and full field path.
|
|
156
|
+
*
|
|
157
|
+
* Returns null when:
|
|
158
|
+
* - The text is not valid JSON (even after comment stripping)
|
|
159
|
+
* - The cursor is not on a navigable content field key
|
|
160
|
+
* - The field is a meta/system key (nodeType, translation, locale codes, …)
|
|
161
|
+
*/
|
|
162
|
+
const findContentFieldAtOffsetJson = (text, offset) => {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(stripJsonComments(text));
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const dictionaryKey = typeof parsed["key"] === "string" ? parsed["key"] : null;
|
|
41
170
|
if (!dictionaryKey) return null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
171
|
+
const contentObj = parsed["content"];
|
|
172
|
+
if (!contentObj || typeof contentObj !== "object" || Array.isArray(contentObj)) return null;
|
|
173
|
+
const contentKeyIndex = text.indexOf("\"content\"");
|
|
174
|
+
if (contentKeyIndex === -1) return null;
|
|
175
|
+
/**
|
|
176
|
+
* Recursively walk the parsed content object.
|
|
177
|
+
* For each property key, check whether the raw text position of that key
|
|
178
|
+
* (within the remaining text slice) overlaps with `offset`.
|
|
179
|
+
*
|
|
180
|
+
* `textFrom` is where we start searching for each key in the raw text —
|
|
181
|
+
* we advance it as we consume properties to avoid false matches on
|
|
182
|
+
* identically-named sibling keys.
|
|
183
|
+
*/
|
|
184
|
+
const walk = (obj, path, searchFrom, searchTo) => {
|
|
185
|
+
for (const [fieldName, fieldValue] of Object.entries(obj)) {
|
|
186
|
+
if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;
|
|
187
|
+
if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;
|
|
188
|
+
const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);
|
|
189
|
+
if (keyStart === -1) continue;
|
|
190
|
+
const keyEnd = keyStart + fieldName.length + 2;
|
|
191
|
+
if (offset >= keyStart && offset <= keyEnd) return {
|
|
192
|
+
dictionaryKey,
|
|
193
|
+
fieldName,
|
|
194
|
+
fieldPath: [...path, fieldName]
|
|
195
|
+
};
|
|
196
|
+
if (fieldValue !== null && typeof fieldValue === "object" && !Array.isArray(fieldValue)) {
|
|
197
|
+
const nested = fieldValue;
|
|
198
|
+
if ("nodeType" in nested) continue;
|
|
199
|
+
const result = walk(nested, [...path, fieldName], keyEnd, searchTo);
|
|
200
|
+
if (result) return result;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
55
204
|
};
|
|
205
|
+
return walk(contentObj, [], contentKeyIndex, text.length);
|
|
56
206
|
};
|
|
57
207
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
208
|
+
* Locale-code pattern — keys matching this inside YAML content blocks are
|
|
209
|
+
* translation locale identifiers, not navigable content fields.
|
|
210
|
+
*/
|
|
211
|
+
const LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
212
|
+
/**
|
|
213
|
+
* YAML / Markdown meta-keys that appear at the top level or inside typed nodes.
|
|
214
|
+
* These should not be treated as navigable content fields.
|
|
215
|
+
*/
|
|
216
|
+
const YAML_META_KEYS = new Set([
|
|
217
|
+
"nodeType",
|
|
218
|
+
"translation",
|
|
219
|
+
"enumeration",
|
|
220
|
+
"condition",
|
|
221
|
+
"plural",
|
|
222
|
+
"insertion",
|
|
223
|
+
"nested",
|
|
224
|
+
"markdown",
|
|
225
|
+
"html",
|
|
226
|
+
"file",
|
|
227
|
+
"composite",
|
|
228
|
+
"dictionaryKey",
|
|
229
|
+
"key",
|
|
230
|
+
"title",
|
|
231
|
+
"description",
|
|
232
|
+
"tags",
|
|
233
|
+
"format",
|
|
234
|
+
"locale",
|
|
235
|
+
"schema",
|
|
236
|
+
"location",
|
|
237
|
+
"fill",
|
|
238
|
+
"priority",
|
|
239
|
+
"version",
|
|
240
|
+
"importMode",
|
|
241
|
+
"$schema"
|
|
242
|
+
]);
|
|
243
|
+
/**
|
|
244
|
+
* Improved regex fallback for YAML and Markdown content files.
|
|
67
245
|
*
|
|
68
|
-
*
|
|
246
|
+
* Strategy:
|
|
247
|
+
* 1. Extract the dictionary key from the frontmatter / top-level `key:` line.
|
|
248
|
+
* 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).
|
|
249
|
+
* 3. Within the content section, find all `fieldName:` patterns at consistent
|
|
250
|
+
* indentation levels, skipping known meta-keys and locale codes.
|
|
251
|
+
* 4. Check whether `offset` falls within one of those key spans.
|
|
252
|
+
* 5. Return a single-element `fieldPath` (deep path tracking is impractical
|
|
253
|
+
* without a full YAML parser, but single-level is enough for most use cases).
|
|
69
254
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
255
|
+
* For Markdown files, `findContentFieldAtOffset` always returns null because
|
|
256
|
+
* the body text is the content value — individual content "fields" are not
|
|
257
|
+
* declared as separate properties.
|
|
72
258
|
*/
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
259
|
+
const findContentFieldAtOffsetYaml = (text, offset) => {
|
|
260
|
+
const keyMatch = /^key\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/m.exec(text);
|
|
261
|
+
if (!keyMatch) return null;
|
|
262
|
+
const dictionaryKey = keyMatch[2] ?? keyMatch[3];
|
|
263
|
+
if (!dictionaryKey) return null;
|
|
264
|
+
const contentSectionMatch = /^content\s*:/m.exec(text);
|
|
265
|
+
if (!contentSectionMatch) return null;
|
|
266
|
+
const contentStart = contentSectionMatch.index + contentSectionMatch[0].length;
|
|
267
|
+
const afterContent = text.slice(contentStart);
|
|
268
|
+
const nextTopLevelMatch = /\r?\n[a-zA-Z_$][a-zA-Z0-9_$]*\s*:/m.exec(afterContent);
|
|
269
|
+
const contentEnd = nextTopLevelMatch ? contentStart + nextTopLevelMatch.index : text.length;
|
|
270
|
+
if (offset < contentStart || offset > contentEnd) return null;
|
|
271
|
+
const contentText = text.slice(contentStart, contentEnd);
|
|
272
|
+
const fieldPattern = /^([ \t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \t]*:/gm;
|
|
273
|
+
let bestMatch = null;
|
|
274
|
+
let fieldMatch;
|
|
275
|
+
while (true) {
|
|
276
|
+
fieldMatch = fieldPattern.exec(contentText);
|
|
277
|
+
if (fieldMatch === null) break;
|
|
278
|
+
const fieldName = fieldMatch[2];
|
|
279
|
+
if (YAML_META_KEYS.has(fieldName)) continue;
|
|
280
|
+
if (LOCALE_CODE_PATTERN.test(fieldName)) continue;
|
|
281
|
+
const fieldKeyStart = contentStart + fieldMatch.index + fieldMatch[1].length;
|
|
282
|
+
const fieldKeyEnd = fieldKeyStart + fieldName.length;
|
|
283
|
+
if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {
|
|
284
|
+
bestMatch = {
|
|
285
|
+
dictionaryKey,
|
|
286
|
+
fieldName,
|
|
287
|
+
fieldPath: [fieldName]
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return bestMatch;
|
|
293
|
+
};
|
|
294
|
+
const findContentFieldAtOffsetOxc = (_text, offset, program) => {
|
|
76
295
|
let foundResult = null;
|
|
77
296
|
walkAst(program, (node) => {
|
|
78
297
|
if (foundResult) return true;
|
|
@@ -114,7 +333,7 @@ const findContentFieldAtOffset = (text, offset) => {
|
|
|
114
333
|
return true;
|
|
115
334
|
}
|
|
116
335
|
if (!propValue) continue;
|
|
117
|
-
if (
|
|
336
|
+
if (isIntlayerLeafCall(propValue)) continue;
|
|
118
337
|
if (propValue["type"] === "ObjectExpression") {
|
|
119
338
|
if (walkContent(propValue, [...path, fieldName])) return true;
|
|
120
339
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo`\n// Group 3: unquoted value — key: foo-bar (YAML / markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /\\bkey\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles quoted values (TS/JS/JSON) and bare values (YAML/Markdown frontmatter).\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/** Returns true when `node` is a call to the intlayer `t()` translation function. */\nconst isTCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' && (callee['name'] as string) === 't'\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n/**\n * Fallback for non-JS/TS files (YAML, Markdown, JSON) or when Oxc cannot parse\n * the file. Uses the original regex approach, returning a single-element path.\n */\nconst findContentFieldAtOffsetRegex = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string; fieldPath: string[] } | null => {\n const keyMatch =\n /\\bkey\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/.exec(text);\n if (!keyMatch) return null;\n\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n let wordStart = offset;\n while (wordStart > 0 && /\\w/.test(text[wordStart - 1]!)) wordStart--;\n let wordEnd = offset;\n while (wordEnd < text.length && /\\w/.test(text[wordEnd]!)) wordEnd++;\n\n if (wordStart === wordEnd) return null;\n\n const word = text.slice(wordStart, wordEnd);\n if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(word)) return null;\n\n const after = text.slice(wordEnd);\n if (!/^\\s*:(?!:)/.test(after)) return null;\n\n return { dictionaryKey, fieldName: word, fieldPath: [word] };\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path\n * (array of names from the content root to the clicked field).\n *\n * For example, in:\n * content: { searchInput: { text: t({...}) } }\n *\n * Clicking on `text` returns `{ dictionaryKey, fieldName: 'text', fieldPath: ['searchInput', 'text'] }`.\n * Clicking on `searchInput` returns `{ ..., fieldName: 'searchInput', fieldPath: ['searchInput'] }`.\n *\n * Locale keys inside `t({en:…, fr:…})` calls are correctly skipped.\n *\n * Uses Oxc AST for JS/TS/TSX/JSX files; falls back to a regex heuristic for\n * YAML and Markdown where Oxc cannot parse.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string; fieldPath: string[] } | null => {\n const program = parseText(text);\n if (!program) return findContentFieldAtOffsetRegex(text, offset);\n\n let foundResult: {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n } | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip t({...}) — those contain locale codes, not navigable content fields\n if (isTCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":";;;AAIA,MAAM,qCACJ;;;;;;AAOF,MAAa,wBACX,MACA,WACkB;CAClB,KAAK,MAAM,SAAS,KAAK,SAAS,kCAAkC,GAAG;EACrE,MAAM,QAAQ,MAAM;EACpB,MAAM,MAAM,QAAQ,MAAM,GAAG;EAE7B,IAAI,UAAU,SAAS,UAAU,KAC/B,OAAO,MAAM,MAAM,MAAM,MAAM;CAEnC;CAEA,OAAO;AACT;;AAGA,MAAM,WAAW,SAA2B;CAC1C,IAAI,KAAK,YAAY,kBAAkB,OAAO;CAC9C,MAAM,SAAS,KAAK;CACpB,OACE,SAAS,YAAY,gBAAiB,OAAO,YAAuB;AAExE;;AAGA,MAAM,kBAAkB,SAAiC;CACvD,KACG,KAAK,YAAY,aAAa,KAAK,YAAY,oBAChD,OAAO,KAAK,aAAa,UAEzB,OAAO,KAAK;CAEd,IAAI,KAAK,YAAY,mBAAmB;EAEtC,MAAM,YADS,KAAK,YACO,KAAK;EAChC,OACG,YAAY,aACZ,YAAY,UACb;CAEJ;CACA,OAAO;AACT;;;;;AAMA,MAAM,iCACJ,MACA,WAC6E;CAC7E,MAAM,WACJ,4DAA4D,KAAK,IAAI;CACvE,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,gBAAgB,SAAS,MAAM,SAAS;CAC9C,IAAI,CAAC,eAAe,OAAO;CAE3B,IAAI,YAAY;CAChB,OAAO,YAAY,KAAK,KAAK,KAAK,KAAK,YAAY,EAAG,GAAG;CACzD,IAAI,UAAU;CACd,OAAO,UAAU,KAAK,UAAU,KAAK,KAAK,KAAK,QAAS,GAAG;CAE3D,IAAI,cAAc,SAAS,OAAO;CAElC,MAAM,OAAO,KAAK,MAAM,WAAW,OAAO;CAC1C,IAAI,CAAC,6BAA6B,KAAK,IAAI,GAAG,OAAO;CAErD,MAAM,QAAQ,KAAK,MAAM,OAAO;CAChC,IAAI,CAAC,aAAa,KAAK,KAAK,GAAG,OAAO;CAEtC,OAAO;EAAE;EAAe,WAAW;EAAM,WAAW,CAAC,IAAI;CAAE;AAC7D;;;;;;;;;;;;;;;;;AAkBA,MAAa,4BACX,MACA,WAC6E;CAC7E,MAAM,UAAU,UAAU,IAAI;CAC9B,IAAI,CAAC,SAAS,OAAO,8BAA8B,MAAM,MAAM;CAE/D,IAAI,cAIO;CAEX,QAAQ,UAAU,SAAS;EACzB,IAAI,aAAa,OAAO;EACxB,IAAI,KAAK,YAAY,oBAAoB;EAEzC,MAAM,aAAa,KAAK;EACxB,IAAI,CAAC,YAAY,QAAQ;EAGzB,IAAI,gBAA+B;EACnC,IAAI,cAA8B;EAElC,KAAK,MAAM,QAAQ,YAAY;GAC7B,IAAI,KAAK,YAAY,YAAY;GACjC,MAAM,UAAU,KAAK;GACrB,MAAM,YAAY,KAAK;GACvB,IAAI,CAAC,WAAW,CAAC,WAAW;GAE5B,MAAM,UACH,QAAQ,WACR,QAAQ;GAEX,IAAI,YAAY,OAAO,gBAAgB,eAAe,SAAS;GAC/D,IAAI,YAAY,WAAW,cAAc;EAC3C;EAEA,IAAI,CAAC,iBAAiB,CAAC,aAAa;EAGpC,MAAM,eAAe,KAAc,SAA4B;GAC7D,IAAI,IAAI,YAAY,oBAAoB,OAAO;GAC/C,MAAM,QAAQ,IAAI;GAClB,IAAI,CAAC,OAAO,OAAO;GAEnB,KAAK,MAAM,QAAQ,OAAO;IACxB,IAAI,KAAK,YAAY,YAAY;IACjC,MAAM,UAAU,KAAK;IACrB,MAAM,YAAY,KAAK;IACvB,IAAI,CAAC,SAAS;IAEd,MAAM,YACH,QAAQ,WACR,QAAQ;IACX,IAAI,CAAC,WAAW;IAEhB,MAAM,WAAW,QAAQ;IACzB,MAAM,SAAS,QAAQ;IAGvB,IAAI,UAAU,YAAY,UAAU,QAAQ;KAC1C,MAAM,YAAY,CAAC,GAAG,MAAM,SAAS;KACrC,cAAc;MAAiB;MAAgB;MAAW;KAAU;KACpE,OAAO;IACT;IAEA,IAAI,CAAC,WAAW;IAEhB,IAAI,QAAQ,SAAS,GAAG;IAExB,IAAI,UAAU,YAAY,oBACxB;SAAI,YAAY,WAAW,CAAC,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO;IAAI;GAEjE;GAEA,OAAO;EACT;EAEA,IAAI,YAAY,aAAa,CAAC,CAAC,GAAG,OAAO;CAC3C,CAAC;CAED,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo` (TS/JS/JSON)\n// Group 3: unquoted value — key: foo-bar (YAML / Markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /(?:^|[{,\\s])\"?key\"?\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^ \\t'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles:\n * - Quoted values: key: \"foo\" | key: 'foo' | key: `foo` (TS/JS)\n * - JSON values: \"key\": \"foo\" (JSON/JSON5/JSONC)\n * - Bare values: key: foo-bar (YAML / Markdown frontmatter)\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path.\n *\n * The `fileExtension` parameter controls which parser is used:\n *\n * - `.json` / `.json5` / `.jsonc` → dedicated JSON parser\n * - `.yaml` / `.yml` → YAML regex heuristic\n * - `.md` / `.mdx` → always null (body is the content value)\n * - everything else → Oxc AST (TS/JS/TSX/JSX)\n *\n * When `fileExtension` is omitted the function falls back to Oxc, then to\n * JSON/YAML heuristics based on text shape.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number,\n fileExtension?: string\n): FieldResult | null => {\n const ext = fileExtension?.toLowerCase() ?? '';\n\n // Markdown has no individual navigable content fields\n if (ext === '.md' || ext === '.mdx') return null;\n\n // JSON / JSON5 / JSONC\n if (ext === '.json' || ext === '.json5' || ext === '.jsonc') {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // YAML / YML\n if (ext === '.yaml' || ext === '.yml') {\n return findContentFieldAtOffsetYaml(text, offset);\n }\n\n // TS / JS / TSX / JSX — use Oxc AST\n const program = parseText(text);\n if (program) {\n return findContentFieldAtOffsetOxc(text, offset, program);\n }\n\n // Oxc failed to parse — fall back on content-shape heuristics\n // JSON: starts with optional whitespace then `{`\n if (/^\\s*\\{/.test(text)) {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // Markdown: YAML frontmatter fence with no `content:` block\n if (/^---\\s*\\r?\\n/.test(text)) {\n if (!/^content\\s*:/m.test(text)) return null;\n }\n\n // YAML (or anything else with a content: block)\n return findContentFieldAtOffsetYaml(text, offset);\n};\n\n// ---------------------------------------------------------------------------\n// Helpers shared by the Oxc and YAML/JSON fallback paths\n// ---------------------------------------------------------------------------\n\n/**\n * All Intlayer helper function names whose arguments are locale-map or\n * control-flow data — NOT navigable content field names. The walker must\n * treat these calls as opaque leaf values and never descend into them.\n *\n * `t` – translation ({ en: \"…\", fr: \"…\" })\n * `enu` – enumeration ({ \"0\": \"…\", \">5\": \"…\" })\n * `cond` – condition ({ true: \"…\", false: \"…\" })\n * `plural` – plural rules ({ one: \"…\", other: \"…\" })\n * `gender` – gender rules ({ male: \"…\", female: \"…\", other: \"…\" })\n * `insert` – insertion (\"Hello {{name}}!\")\n * `nest` – nested dict (\"navbar\")\n * `md` – markdown (\"# …\")\n * `file` – file reference (\"./path/to/file.txt\")\n * `html` – html content (\"<p>…</p>\")\n */\nconst INTLAYER_LEAF_CALL_NAMES: ReadonlySet<string> = new Set([\n 't',\n 'enu',\n 'cond',\n 'plural',\n 'gender',\n 'insert',\n 'nest',\n 'md',\n 'file',\n 'html',\n]);\n\n/** Returns true when `node` is a call to any intlayer leaf helper function. */\nconst isIntlayerLeafCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' &&\n INTLAYER_LEAF_CALL_NAMES.has(callee['name'] as string)\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n// ---------------------------------------------------------------------------\n// JSON content-file support\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.\n * e.g. { \"nodeType\": \"translation\", \"translation\": { \"en\": \"…\" } }\n * None of these are navigable content fields.\n */\nconst JSON_INTLAYER_NODE_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // JSON schema / system properties\n '$schema',\n 'id',\n 'projectIds',\n 'localId',\n 'localIds',\n 'filePath',\n 'versions',\n 'filled',\n // Top-level dictionary meta-properties (not inside content)\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n]);\n\n/**\n * Find the byte offset of `fieldName` (as a JSON property key) inside `text`,\n * searching within the range [searchFrom, searchTo).\n *\n * Matches `\"fieldName\"` surrounded by typical JSON delimiters.\n */\nconst findJsonKeyOffset = (\n text: string,\n fieldName: string,\n searchFrom: number,\n searchTo: number\n): number => {\n const pattern = new RegExp(\n `\"${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\"`,\n 'g'\n );\n pattern.lastIndex = searchFrom;\n let match: RegExpExecArray | null;\n while (true) {\n match = pattern.exec(text);\n if (match === null) break;\n if (match.index >= searchTo) break;\n return match.index;\n }\n return -1;\n};\n\n/**\n * Strip JSON5/JSONC single-line (//) and multi-line (/* *\\/\\/) comments,\n * and trailing commas, producing valid JSON that JSON.parse can handle.\n */\nconst stripJsonComments = (text: string): string =>\n text\n // Remove single-line comments (// …) EXCEPT in URLs (e.g., https://)\n .replace(/(?<!:)\\/\\/[^\\r\\n]*/g, '')\n // Remove block comments (/* … */)\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n // Remove trailing commas before } or ]\n .replace(/,(\\s*[}\\]])/g, '$1');\n\ntype FieldResult = {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n};\n\n/**\n * Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content\n * field key, return the dictionary key, leaf field name, and full field path.\n *\n * Returns null when:\n * - The text is not valid JSON (even after comment stripping)\n * - The cursor is not on a navigable content field key\n * - The field is a meta/system key (nodeType, translation, locale codes, …)\n */\nconst findContentFieldAtOffsetJson = (\n text: string,\n offset: number\n): FieldResult | null => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stripJsonComments(text)) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const dictionaryKey =\n typeof parsed['key'] === 'string' ? parsed['key'] : null;\n if (!dictionaryKey) return null;\n\n const contentObj = parsed['content'];\n if (\n !contentObj ||\n typeof contentObj !== 'object' ||\n Array.isArray(contentObj)\n )\n return null;\n\n // Find the byte range of the \"content\" block in raw text so we can scope\n // offset comparisons to content fields only.\n const contentKeyIndex = text.indexOf('\"content\"');\n if (contentKeyIndex === -1) return null;\n\n /**\n * Recursively walk the parsed content object.\n * For each property key, check whether the raw text position of that key\n * (within the remaining text slice) overlaps with `offset`.\n *\n * `textFrom` is where we start searching for each key in the raw text —\n * we advance it as we consume properties to avoid false matches on\n * identically-named sibling keys.\n */\n const walk = (\n obj: Record<string, unknown>,\n path: string[],\n searchFrom: number,\n searchTo: number\n ): FieldResult | null => {\n for (const [fieldName, fieldValue] of Object.entries(obj)) {\n // Skip Intlayer-internal node keys — they are not navigable content fields\n if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;\n\n // Skip keys that look like locale codes (e.g. \"en\", \"fr\", \"en-GB\")\n if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;\n\n const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);\n if (keyStart === -1) continue;\n const keyEnd = keyStart + fieldName.length + 2; // +2 for surrounding quotes\n\n if (offset >= keyStart && offset <= keyEnd) {\n return {\n dictionaryKey,\n fieldName,\n fieldPath: [...path, fieldName],\n };\n }\n\n // Recurse into plain nested objects (skip typed nodes — they have nodeType)\n if (\n fieldValue !== null &&\n typeof fieldValue === 'object' &&\n !Array.isArray(fieldValue)\n ) {\n const nested = fieldValue as Record<string, unknown>;\n // Skip Intlayer typed nodes — their children are not content fields\n if ('nodeType' in nested) continue;\n\n const result = walk(nested, [...path, fieldName], keyEnd, searchTo);\n if (result) return result;\n }\n }\n return null;\n };\n\n return walk(\n contentObj as Record<string, unknown>,\n [],\n contentKeyIndex,\n text.length\n );\n};\n\n// ---------------------------------------------------------------------------\n// YAML / Markdown frontmatter fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Locale-code pattern — keys matching this inside YAML content blocks are\n * translation locale identifiers, not navigable content fields.\n */\nconst LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\n\n/**\n * YAML / Markdown meta-keys that appear at the top level or inside typed nodes.\n * These should not be treated as navigable content fields.\n */\nconst YAML_META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // Frontmatter / top-level keys\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n '$schema',\n]);\n\n/**\n * Improved regex fallback for YAML and Markdown content files.\n *\n * Strategy:\n * 1. Extract the dictionary key from the frontmatter / top-level `key:` line.\n * 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).\n * 3. Within the content section, find all `fieldName:` patterns at consistent\n * indentation levels, skipping known meta-keys and locale codes.\n * 4. Check whether `offset` falls within one of those key spans.\n * 5. Return a single-element `fieldPath` (deep path tracking is impractical\n * without a full YAML parser, but single-level is enough for most use cases).\n *\n * For Markdown files, `findContentFieldAtOffset` always returns null because\n * the body text is the content value — individual content \"fields\" are not\n * declared as separate properties.\n */\nconst findContentFieldAtOffsetYaml = (\n text: string,\n offset: number\n): FieldResult | null => {\n // Extract dictionary key\n const keyMatch =\n /^key\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/m.exec(text);\n if (!keyMatch) return null;\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n // Find the content: section start\n const contentSectionMatch = /^content\\s*:/m.exec(text);\n if (!contentSectionMatch) return null;\n const contentStart =\n contentSectionMatch.index + contentSectionMatch[0].length;\n\n // Find the end of the content section: next top-level key (no leading whitespace)\n // or end of file.\n const afterContent = text.slice(contentStart);\n const nextTopLevelMatch = /\\r?\\n[a-zA-Z_$][a-zA-Z0-9_$]*\\s*:/m.exec(\n afterContent\n );\n const contentEnd = nextTopLevelMatch\n ? contentStart + nextTopLevelMatch.index\n : text.length;\n\n // The cursor must be within the content block\n if (offset < contentStart || offset > contentEnd) return null;\n\n // Find all ` fieldName:` patterns within the content block.\n // We look for lines that have at least 2 spaces of indentation and a\n // word-character key followed by a colon.\n const contentText = text.slice(contentStart, contentEnd);\n const fieldPattern = /^([ \\t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \\t]*:/gm;\n\n let bestMatch: FieldResult | null = null;\n let fieldMatch: RegExpExecArray | null;\n\n while (true) {\n fieldMatch = fieldPattern.exec(contentText);\n if (fieldMatch === null) break;\n\n const fieldName = fieldMatch[2]!;\n\n // Skip meta-keys and locale codes\n if (YAML_META_KEYS.has(fieldName)) continue;\n if (LOCALE_CODE_PATTERN.test(fieldName)) continue;\n\n const fieldKeyStart =\n contentStart + fieldMatch.index + fieldMatch[1]!.length; // skip indentation\n const fieldKeyEnd = fieldKeyStart + fieldName.length;\n\n if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {\n bestMatch = { dictionaryKey, fieldName, fieldPath: [fieldName] };\n break;\n }\n }\n\n return bestMatch;\n};\n\n// ---------------------------------------------------------------------------\n// Oxc (TS / JS) AST path\n// ---------------------------------------------------------------------------\n\nconst findContentFieldAtOffsetOxc = (\n _text: string,\n offset: number,\n program: OxcNode\n): FieldResult | null => {\n let foundResult: FieldResult | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip all intlayer leaf calls — their arguments are not navigable content fields\n if (isIntlayerLeafCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { findMessageUsageAtOffset } from "./usageAnalyzer.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/findTCallAtOffset.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detect when the cursor is on a message usage carrying an explicit id: a
|
|
6
|
+
* `t('key')` call from any translator caller, a self call
|
|
7
|
+
* (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a
|
|
8
|
+
* lingui tagged template.
|
|
9
|
+
*
|
|
10
|
+
* Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the
|
|
11
|
+
* VS Code extension. `fieldName` is the dotted field path.
|
|
12
|
+
*
|
|
13
|
+
* Handles all assignment forms:
|
|
14
|
+
* const t = useTranslations('ns'); t('field')
|
|
15
|
+
* const { t } = useTranslation('ns'); t('field')
|
|
16
|
+
* const { t: translate } = useTranslation('ns'); translate('field')
|
|
17
|
+
* const t = await getFixedT('en', 'ns'); t('field')
|
|
18
|
+
* const { t } = useI18n({ namespace: 'ns' }); t('field')
|
|
19
|
+
*/
|
|
20
|
+
const findTCallAtOffset = (text, offset) => {
|
|
21
|
+
const usage = findMessageUsageAtOffset(text, offset);
|
|
22
|
+
if (!usage) return null;
|
|
23
|
+
if (usage.kind !== "call" && usage.kind !== "jsx" && usage.kind !== "tagged-template") return null;
|
|
24
|
+
if (usage.fieldPath.length === 0) return null;
|
|
25
|
+
return {
|
|
26
|
+
dictionaryKey: usage.dictionaryKey,
|
|
27
|
+
fieldName: usage.fieldPath.join(".")
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { findTCallAtOffset };
|
|
33
|
+
//# sourceMappingURL=findTCallAtOffset.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findTCallAtOffset.mjs","names":[],"sources":["../../src/findTCallAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a message usage carrying an explicit id: a\n * `t('key')` call from any translator caller, a self call\n * (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a\n * lingui tagged template.\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the\n * VS Code extension. `fieldName` is the dotted field path.\n *\n * Handles all assignment forms:\n * const t = useTranslations('ns'); t('field')\n * const { t } = useTranslation('ns'); t('field')\n * const { t: translate } = useTranslation('ns'); translate('field')\n * const t = await getFixedT('en', 'ns'); t('field')\n * const { t } = useI18n({ namespace: 'ns' }); t('field')\n */\nexport const findTCallAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (\n usage.kind !== 'call' &&\n usage.kind !== 'jsx' &&\n usage.kind !== 'tagged-template'\n ) {\n return null;\n }\n\n if (usage.fieldPath.length === 0) return null;\n\n return {\n dictionaryKey: usage.dictionaryKey,\n fieldName: usage.fieldPath.join('.'),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,qBACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,OAAO;AAEpD,KAAI,CAAC,MAAO,QAAO;AAEnB,KACE,MAAM,SAAS,UACf,MAAM,SAAS,SACf,MAAM,SAAS,kBAEf,QAAO;AAGT,KAAI,MAAM,UAAU,WAAW,EAAG,QAAO;AAEzC,QAAO;EACL,eAAe,MAAM;EACrB,WAAW,MAAM,UAAU,KAAK,IAAI;EACrC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { findMessageUsageAtOffset } from "./usageAnalyzer.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/findUsageFieldAtOffset.ts
|
|
4
4
|
/**
|
|
5
5
|
* Detect when the cursor is on a field name that was obtained from a
|
|
6
|
-
* useIntlayer / getIntlayer call in the same file.
|
|
6
|
+
* useIntlayer / getIntlayer call in the same file. Two patterns:
|
|
7
7
|
*
|
|
8
8
|
* Pattern 1 — destructuring:
|
|
9
9
|
* const { localeSwitcherLabel } = useIntlayer("locale-switcher")
|
|
@@ -13,58 +13,19 @@ import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtil
|
|
|
13
13
|
* const t = useIntlayer("locale-switcher")
|
|
14
14
|
* t.localeSwitcherLabel ← cursor on the property
|
|
15
15
|
*
|
|
16
|
+
* Thin compatibility wrapper over `findMessageUsageAtOffset`.
|
|
16
17
|
* Returns { dictionaryKey, fieldName } when matched, null otherwise.
|
|
17
18
|
*/
|
|
18
19
|
const findUsageFieldAtOffset = (text, offset) => {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const dictionaryKey = getFirstStringArg(initializer);
|
|
29
|
-
if (!dictionaryKey) return;
|
|
30
|
-
const identifierNode = node["id"];
|
|
31
|
-
if (!identifierNode) return;
|
|
32
|
-
if (identifierNode["type"] === "ObjectPattern") for (const propertyNode of identifierNode["properties"] ?? []) {
|
|
33
|
-
const keyNode = propertyNode["key"];
|
|
34
|
-
if (!keyNode) continue;
|
|
35
|
-
const keyStart = keyNode["start"];
|
|
36
|
-
const keyEnd = keyNode["end"];
|
|
37
|
-
if (offset >= keyStart && offset <= keyEnd) {
|
|
38
|
-
result = {
|
|
39
|
-
dictionaryKey,
|
|
40
|
-
fieldName: keyNode["name"]
|
|
41
|
-
};
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (identifierNode["type"] === "Identifier") variableToDictionaryKeyMap.set(identifierNode["name"], dictionaryKey);
|
|
46
|
-
});
|
|
47
|
-
if (result) return result;
|
|
48
|
-
walkAst(program, (node) => {
|
|
49
|
-
if (result) return true;
|
|
50
|
-
if (node["type"] !== "MemberExpression") return;
|
|
51
|
-
const propertyNode = node["property"];
|
|
52
|
-
if (!propertyNode) return;
|
|
53
|
-
const propertyStart = propertyNode["start"];
|
|
54
|
-
const propertyEnd = propertyNode["end"];
|
|
55
|
-
if (offset < propertyStart || offset > propertyEnd) return;
|
|
56
|
-
const objectNode = node["object"];
|
|
57
|
-
if (objectNode?.["type"] !== "Identifier") return;
|
|
58
|
-
const variableName = objectNode["name"];
|
|
59
|
-
const dictionaryKey = variableToDictionaryKeyMap.get(variableName);
|
|
60
|
-
if (!dictionaryKey) return;
|
|
61
|
-
result = {
|
|
62
|
-
dictionaryKey,
|
|
63
|
-
fieldName: propertyNode["name"]
|
|
64
|
-
};
|
|
65
|
-
return true;
|
|
66
|
-
});
|
|
67
|
-
return result;
|
|
20
|
+
const usage = findMessageUsageAtOffset(text, offset);
|
|
21
|
+
if (!usage) return null;
|
|
22
|
+
if (usage.kind !== "member" && usage.kind !== "destructure") return null;
|
|
23
|
+
const fieldName = usage.fieldPath[usage.fieldPath.length - 1];
|
|
24
|
+
if (!fieldName) return null;
|
|
25
|
+
return {
|
|
26
|
+
dictionaryKey: usage.dictionaryKey,
|
|
27
|
+
fieldName
|
|
28
|
+
};
|
|
68
29
|
};
|
|
69
30
|
|
|
70
31
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a field name that was obtained from a\n * useIntlayer / getIntlayer call in the same file. Two patterns:\n *\n * Pattern 1 — destructuring:\n * const { localeSwitcherLabel } = useIntlayer(\"locale-switcher\")\n * ^^^^^^^^^^^^^^^^^^ ← cursor here\n *\n * Pattern 2 — member access:\n * const t = useIntlayer(\"locale-switcher\")\n * t.localeSwitcherLabel ← cursor on the property\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset`.\n * Returns { dictionaryKey, fieldName } when matched, null otherwise.\n */\nexport const findUsageFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (usage.kind !== 'member' && usage.kind !== 'destructure') return null;\n\n const fieldName = usage.fieldPath[usage.fieldPath.length - 1];\n\n if (!fieldName) return null;\n\n return { dictionaryKey: usage.dictionaryKey, fieldName };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,OAAO;AAEpD,KAAI,CAAC,MAAO,QAAO;AAEnB,KAAI,MAAM,SAAS,YAAY,MAAM,SAAS,cAAe,QAAO;CAEpE,MAAM,YAAY,MAAM,UAAU,MAAM,UAAU,SAAS;AAE3D,KAAI,CAAC,UAAW,QAAO;AAEvB,QAAO;EAAE,eAAe,MAAM;EAAe;EAAW"}
|