@intlayer/lsp 8.11.3 → 8.11.4
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/findFieldInFile.mjs +156 -0
- package/dist/esm/findFieldInFile.mjs.map +1 -0
- package/dist/esm/findKeyAtOffset.mjs +26 -12
- package/dist/esm/findKeyAtOffset.mjs.map +1 -1
- package/dist/esm/findKeyInContentFile.mjs +131 -0
- package/dist/esm/findKeyInContentFile.mjs.map +1 -0
- package/dist/esm/findUsageFieldAtOffset.mjs +72 -0
- package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -0
- package/dist/esm/index.mjs +361 -34
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs +60 -0
- package/dist/esm/oxcUtils.mjs.map +1 -0
- package/dist/esm/utils.mjs +6 -0
- package/dist/types/findFieldInFile.d.ts +30 -0
- package/dist/types/findFieldInFile.d.ts.map +1 -0
- package/dist/types/findKeyAtOffset.d.ts +11 -3
- package/dist/types/findKeyAtOffset.d.ts.map +1 -1
- package/dist/types/findKeyInContentFile.d.ts +31 -0
- package/dist/types/findKeyInContentFile.d.ts.map +1 -0
- package/dist/types/findUsageFieldAtOffset.d.ts +22 -0
- package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -0
- package/dist/types/oxcUtils.d.ts +24 -0
- package/dist/types/oxcUtils.d.ts.map +1 -0
- package/dist/types/utils.d.ts +5 -0
- package/package.json +11 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtils.mjs";
|
|
2
|
+
import { Position, Range } from "vscode-languageserver/node.js";
|
|
3
|
+
|
|
4
|
+
//#region src/findFieldInFile.ts
|
|
5
|
+
const escapeRegularExpression = (content) => content.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6
|
+
const offsetToRange = (text, start, end) => {
|
|
7
|
+
const beforeStart = text.slice(0, start);
|
|
8
|
+
const startLine = (beforeStart.match(/\n/g) ?? []).length;
|
|
9
|
+
const startCharacter = start - (beforeStart.lastIndexOf("\n") + 1);
|
|
10
|
+
const beforeEnd = text.slice(0, end);
|
|
11
|
+
const endLine = (beforeEnd.match(/\n/g) ?? []).length;
|
|
12
|
+
const endCharacter = end - (beforeEnd.lastIndexOf("\n") + 1);
|
|
13
|
+
return Range.create(Position.create(startLine, startCharacter), Position.create(endLine, endCharacter));
|
|
14
|
+
};
|
|
15
|
+
const isComputedAccess = (node) => !!node["computed"];
|
|
16
|
+
/**
|
|
17
|
+
* Walk up a chain of MemberExpressions to find the root Identifier name.
|
|
18
|
+
* Returns null for computed access (`a[b]`) or non-Identifier roots.
|
|
19
|
+
*/
|
|
20
|
+
const getChainRoot = (node) => {
|
|
21
|
+
let current = node;
|
|
22
|
+
while (current["type"] === "MemberExpression") {
|
|
23
|
+
if (isComputedAccess(current)) return null;
|
|
24
|
+
current = current["object"];
|
|
25
|
+
}
|
|
26
|
+
return current["type"] === "Identifier" ? current["name"] : null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Collect the property names in a chain of MemberExpressions, excluding the
|
|
30
|
+
* root Identifier. Returns null for any computed access.
|
|
31
|
+
* e.g. `content.searchInput.text` → `['searchInput', 'text']`
|
|
32
|
+
*/
|
|
33
|
+
const getChainProperties = (node) => {
|
|
34
|
+
const chain = [];
|
|
35
|
+
let current = node;
|
|
36
|
+
while (current["type"] === "MemberExpression") {
|
|
37
|
+
if (isComputedAccess(current)) return null;
|
|
38
|
+
const name = current["property"]["name"];
|
|
39
|
+
if (!name) return null;
|
|
40
|
+
chain.unshift(name);
|
|
41
|
+
current = current["object"];
|
|
42
|
+
}
|
|
43
|
+
return chain;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Recursively search for a field at `path[depth]` inside an ObjectPattern
|
|
47
|
+
* (destructure). Returns the Range of the matched key node, or null.
|
|
48
|
+
*
|
|
49
|
+
* e.g. `{ searchInput: { text } }` with path `['searchInput', 'text']` depth 0
|
|
50
|
+
* → returns the range of the `text` identifier inside the nested pattern.
|
|
51
|
+
*/
|
|
52
|
+
const findInObjectPattern = (pattern, path, depth, text) => {
|
|
53
|
+
if (pattern["type"] !== "ObjectPattern") return null;
|
|
54
|
+
if (depth >= path.length) return null;
|
|
55
|
+
const targetName = path[depth];
|
|
56
|
+
for (const prop of pattern["properties"] ?? []) {
|
|
57
|
+
if (prop["type"] !== "Property" && prop["type"] !== "ObjectProperty") continue;
|
|
58
|
+
const keyNode = prop["key"];
|
|
59
|
+
if (!keyNode) continue;
|
|
60
|
+
if ((keyNode["name"] ?? keyNode["value"]) !== targetName) continue;
|
|
61
|
+
if (depth === path.length - 1) return offsetToRange(text, keyNode["start"], keyNode["end"]);
|
|
62
|
+
const valueNode = prop["value"];
|
|
63
|
+
if (!valueNode) return null;
|
|
64
|
+
return findInObjectPattern(valueNode, path, depth + 1, text);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Recursively collect variables that were destructured out of an ObjectPattern,
|
|
70
|
+
* recording the "remaining intlayer path" each variable represents.
|
|
71
|
+
*
|
|
72
|
+
* e.g. given `{ searchInput }` with basePath `[]`, the variable `searchInput`
|
|
73
|
+
* maps to remaining path `['searchInput']` (meaning it holds the searchInput
|
|
74
|
+
* branch of the dictionary).
|
|
75
|
+
*
|
|
76
|
+
* e.g. given `{ searchInput: { text: alias } }` with basePath `[]`, the
|
|
77
|
+
* variable `alias` maps to remaining path `['searchInput', 'text']`.
|
|
78
|
+
*/
|
|
79
|
+
const collectDestructuredVariables = (pattern, basePath, map) => {
|
|
80
|
+
if (pattern["type"] !== "ObjectPattern") return;
|
|
81
|
+
for (const prop of pattern["properties"] ?? []) {
|
|
82
|
+
if (prop["type"] !== "Property" && prop["type"] !== "ObjectProperty") continue;
|
|
83
|
+
const keyNode = prop["key"];
|
|
84
|
+
const valueNode = prop["value"];
|
|
85
|
+
if (!keyNode || !valueNode) continue;
|
|
86
|
+
const keyName = keyNode["name"] ?? keyNode["value"];
|
|
87
|
+
if (!keyName) continue;
|
|
88
|
+
const thisPath = [...basePath, keyName];
|
|
89
|
+
if (valueNode["type"] === "Identifier") map.set(valueNode["name"], thisPath);
|
|
90
|
+
else if (valueNode["type"] === "ObjectPattern") collectDestructuredVariables(valueNode, thisPath, map);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Within a single file's text, find all positions where `fieldPath` (or legacy
|
|
95
|
+
* `fieldName`) is used as a destructured property or member access on a
|
|
96
|
+
* `useIntlayer(key)` / `getIntlayer(key)` result.
|
|
97
|
+
*
|
|
98
|
+
* Covers four patterns:
|
|
99
|
+
* 1. Direct destructure:
|
|
100
|
+
* const { field } = useIntlayer("key")
|
|
101
|
+
* 2. Nested destructure:
|
|
102
|
+
* const { parent: { child } } = useIntlayer("key")
|
|
103
|
+
* 3. Indirect variable + member access:
|
|
104
|
+
* const { parent } = useIntlayer("key")
|
|
105
|
+
* parent.child
|
|
106
|
+
* 4. Full chain member access:
|
|
107
|
+
* const content = useIntlayer("key")
|
|
108
|
+
* content.parent.child
|
|
109
|
+
*
|
|
110
|
+
* `fieldPath` (optional 5th parameter) is the preferred input; when omitted
|
|
111
|
+
* the function derives the single-element path from `escapedField`, preserving
|
|
112
|
+
* backward compatibility.
|
|
113
|
+
*/
|
|
114
|
+
const findFieldRangesInFile = (text, escapedKey, _fieldName, escapedField, fieldPath) => {
|
|
115
|
+
const dictionaryKey = escapedKey.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
|
|
116
|
+
const unescapedLeaf = escapedField.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
|
|
117
|
+
const path = fieldPath && fieldPath.length > 0 ? fieldPath : [unescapedLeaf];
|
|
118
|
+
const program = parseText(text);
|
|
119
|
+
if (!program) return [];
|
|
120
|
+
const ranges = [];
|
|
121
|
+
const variableToRemainingPath = /* @__PURE__ */ new Map();
|
|
122
|
+
walkAst(program, (node) => {
|
|
123
|
+
if (node["type"] !== "VariableDeclarator") return;
|
|
124
|
+
const initializer = node["init"];
|
|
125
|
+
if (!initializer || !isIntlayerCall(initializer)) return;
|
|
126
|
+
if (getFirstStringArg(initializer) !== dictionaryKey) return;
|
|
127
|
+
const idNode = node["id"];
|
|
128
|
+
if (!idNode) return;
|
|
129
|
+
if (idNode["type"] === "Identifier") variableToRemainingPath.set(idNode["name"], []);
|
|
130
|
+
if (idNode["type"] === "ObjectPattern") {
|
|
131
|
+
const range = findInObjectPattern(idNode, path, 0, text);
|
|
132
|
+
if (range) ranges.push(range);
|
|
133
|
+
collectDestructuredVariables(idNode, [], variableToRemainingPath);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
walkAst(program, (node) => {
|
|
137
|
+
if (node["type"] !== "MemberExpression") return;
|
|
138
|
+
if (isComputedAccess(node)) return;
|
|
139
|
+
const root = getChainRoot(node);
|
|
140
|
+
if (!root) return;
|
|
141
|
+
const remainingPath = variableToRemainingPath.get(root);
|
|
142
|
+
if (remainingPath === void 0) return;
|
|
143
|
+
const chain = getChainProperties(node);
|
|
144
|
+
if (!chain) return;
|
|
145
|
+
const fullPath = [...remainingPath, ...chain];
|
|
146
|
+
if (fullPath.length !== path.length) return;
|
|
147
|
+
if (!fullPath.every((segment, i) => segment === path[i])) return;
|
|
148
|
+
const leafPropertyNode = node["property"];
|
|
149
|
+
ranges.push(offsetToRange(text, leafPropertyNode["start"], leafPropertyNode["end"]));
|
|
150
|
+
});
|
|
151
|
+
return ranges;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { escapeRegularExpression, findFieldRangesInFile, offsetToRange };
|
|
156
|
+
//# sourceMappingURL=findFieldInFile.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findFieldInFile.mjs","names":[],"sources":["../../src/findFieldInFile.ts"],"sourcesContent":["import { Position, Range } from 'vscode-languageserver/node.js';\nimport {\n getFirstStringArg,\n isIntlayerCall,\n type OxcNode,\n parseText,\n walkAst,\n} from './oxcUtils';\n\nexport const escapeRegularExpression = (content: string): string =>\n content.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const offsetToRange = (\n text: string,\n start: number,\n end: number\n): Range => {\n const beforeStart = text.slice(0, start);\n const startLine = (beforeStart.match(/\\n/g) ?? []).length;\n const startCharacter = start - (beforeStart.lastIndexOf('\\n') + 1);\n const beforeEnd = text.slice(0, end);\n const endLine = (beforeEnd.match(/\\n/g) ?? []).length;\n const endCharacter = end - (beforeEnd.lastIndexOf('\\n') + 1);\n\n return Range.create(\n Position.create(startLine, startCharacter),\n Position.create(endLine, endCharacter)\n );\n};\n\n// ---------------------------------------------------------------------------\n// Helpers for nested path matching\n// ---------------------------------------------------------------------------\n\nconst isComputedAccess = (node: OxcNode): boolean =>\n !!(node['computed'] as boolean);\n\n/**\n * Walk up a chain of MemberExpressions to find the root Identifier name.\n * Returns null for computed access (`a[b]`) or non-Identifier roots.\n */\nconst getChainRoot = (node: OxcNode): string | null => {\n let current = node;\n while (current['type'] === 'MemberExpression') {\n if (isComputedAccess(current)) return null;\n current = current['object'] as OxcNode;\n }\n return current['type'] === 'Identifier' ? (current['name'] as string) : null;\n};\n\n/**\n * Collect the property names in a chain of MemberExpressions, excluding the\n * root Identifier. Returns null for any computed access.\n * e.g. `content.searchInput.text` → `['searchInput', 'text']`\n */\nconst getChainProperties = (node: OxcNode): string[] | null => {\n const chain: string[] = [];\n let current = node;\n while (current['type'] === 'MemberExpression') {\n if (isComputedAccess(current)) return null;\n const prop = current['property'] as OxcNode;\n const name = prop['name'] as string | undefined;\n if (!name) return null;\n chain.unshift(name);\n current = current['object'] as OxcNode;\n }\n return chain;\n};\n\n/**\n * Recursively search for a field at `path[depth]` inside an ObjectPattern\n * (destructure). Returns the Range of the matched key node, or null.\n *\n * e.g. `{ searchInput: { text } }` with path `['searchInput', 'text']` depth 0\n * → returns the range of the `text` identifier inside the nested pattern.\n */\nconst findInObjectPattern = (\n pattern: OxcNode,\n path: string[],\n depth: number,\n text: string\n): Range | null => {\n if (pattern['type'] !== 'ObjectPattern') return null;\n if (depth >= path.length) return null;\n\n const targetName = path[depth]!;\n\n for (const prop of (pattern['properties'] as OxcNode[]) ?? []) {\n if (prop['type'] !== 'Property' && prop['type'] !== 'ObjectProperty')\n continue;\n const keyNode = prop['key'] as OxcNode | undefined;\n if (!keyNode) continue;\n\n const keyName =\n (keyNode['name'] as string | undefined) ??\n (keyNode['value'] as string | undefined);\n if (keyName !== targetName) continue;\n\n if (depth === path.length - 1) {\n return offsetToRange(\n text,\n keyNode['start'] as number,\n keyNode['end'] as number\n );\n }\n\n const valueNode = prop['value'] as OxcNode | undefined;\n if (!valueNode) return null;\n return findInObjectPattern(valueNode, path, depth + 1, text);\n }\n\n return null;\n};\n\n/**\n * Recursively collect variables that were destructured out of an ObjectPattern,\n * recording the \"remaining intlayer path\" each variable represents.\n *\n * e.g. given `{ searchInput }` with basePath `[]`, the variable `searchInput`\n * maps to remaining path `['searchInput']` (meaning it holds the searchInput\n * branch of the dictionary).\n *\n * e.g. given `{ searchInput: { text: alias } }` with basePath `[]`, the\n * variable `alias` maps to remaining path `['searchInput', 'text']`.\n */\nconst collectDestructuredVariables = (\n pattern: OxcNode,\n basePath: string[],\n map: Map<string, string[]>\n): void => {\n if (pattern['type'] !== 'ObjectPattern') return;\n\n for (const prop of (pattern['properties'] as OxcNode[]) ?? []) {\n if (prop['type'] !== 'Property' && prop['type'] !== 'ObjectProperty')\n continue;\n const keyNode = prop['key'] as OxcNode | undefined;\n const valueNode = prop['value'] as OxcNode | undefined;\n if (!keyNode || !valueNode) continue;\n\n const keyName =\n (keyNode['name'] as string | undefined) ??\n (keyNode['value'] as string | undefined);\n if (!keyName) continue;\n\n const thisPath = [...basePath, keyName];\n\n if (valueNode['type'] === 'Identifier') {\n // `{ a }` or `{ a: alias }` → variable name is valueNode.name\n map.set(valueNode['name'] as string, thisPath);\n } else if (valueNode['type'] === 'ObjectPattern') {\n // `{ a: { b } }` → 'a' is consumed as a pattern level; recurse\n collectDestructuredVariables(valueNode, thisPath, map);\n }\n // AssignmentPattern defaults (`{ a = 'x' }`) are ignored for simplicity\n }\n};\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Within a single file's text, find all positions where `fieldPath` (or legacy\n * `fieldName`) is used as a destructured property or member access on a\n * `useIntlayer(key)` / `getIntlayer(key)` result.\n *\n * Covers four patterns:\n * 1. Direct destructure:\n * const { field } = useIntlayer(\"key\")\n * 2. Nested destructure:\n * const { parent: { child } } = useIntlayer(\"key\")\n * 3. Indirect variable + member access:\n * const { parent } = useIntlayer(\"key\")\n * parent.child\n * 4. Full chain member access:\n * const content = useIntlayer(\"key\")\n * content.parent.child\n *\n * `fieldPath` (optional 5th parameter) is the preferred input; when omitted\n * the function derives the single-element path from `escapedField`, preserving\n * backward compatibility.\n */\nexport const findFieldRangesInFile = (\n text: string,\n escapedKey: string,\n _fieldName: string,\n escapedField: string,\n fieldPath?: string[]\n): Range[] => {\n const dictionaryKey = escapedKey.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n const unescapedLeaf = escapedField.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n\n // Effective path: use the provided fieldPath when available, else single-element\n const path = fieldPath && fieldPath.length > 0 ? fieldPath : [unescapedLeaf];\n\n const program = parseText(text);\n if (!program) return [];\n\n const ranges: Range[] = [];\n\n // variable name → \"remaining intlayer path\" represented by this variable\n // '' (root) entry would map to [] for `const v = useIntlayer('key')`\n const variableToRemainingPath = new Map<string, string[]>();\n\n // First pass: collect useIntlayer / getIntlayer variable assignments and\n // destructures, and emit ranges for direct/nested ObjectPattern matches.\n walkAst(program, (node) => {\n if (node['type'] !== 'VariableDeclarator') return;\n\n const initializer = node['init'] as OxcNode | undefined;\n if (!initializer || !isIntlayerCall(initializer)) return;\n\n const dk = getFirstStringArg(initializer);\n if (dk !== dictionaryKey) return;\n\n const idNode = node['id'] as OxcNode | undefined;\n if (!idNode) return;\n\n if (idNode['type'] === 'Identifier') {\n // `const content = useIntlayer('key')` → root variable, remaining path []\n variableToRemainingPath.set(idNode['name'] as string, []);\n }\n\n if (idNode['type'] === 'ObjectPattern') {\n // Check if the target path is matched directly in the destructure pattern\n const range = findInObjectPattern(idNode, path, 0, text);\n if (range) ranges.push(range);\n\n // Collect destructured variables for the member-access pass\n collectDestructuredVariables(idNode, [], variableToRemainingPath);\n }\n });\n\n // Second pass: find member-access chains that, combined with the variable's\n // remaining path, equal the target path.\n walkAst(program, (node) => {\n if (node['type'] !== 'MemberExpression') return;\n if (isComputedAccess(node)) return;\n\n const root = getChainRoot(node);\n if (!root) return;\n\n const remainingPath = variableToRemainingPath.get(root);\n if (remainingPath === undefined) return;\n\n const chain = getChainProperties(node);\n if (!chain) return;\n\n const fullPath = [...remainingPath, ...chain];\n if (fullPath.length !== path.length) return;\n if (!fullPath.every((segment, i) => segment === path[i])) return;\n\n // Highlight the last (leaf) property in the chain\n const leafPropertyNode = node['property'] as OxcNode;\n ranges.push(\n offsetToRange(\n text,\n leafPropertyNode['start'] as number,\n leafPropertyNode['end'] as number\n )\n );\n });\n\n return ranges;\n};\n"],"mappings":"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"}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
|
+
import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtils.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/findKeyAtOffset.ts
|
|
2
|
-
const INTLAYER_GETTERS = ["useIntlayer", "getIntlayer"];
|
|
3
|
-
const buildGetterRegex = () => new RegExp(`\\b(${INTLAYER_GETTERS.join("|")})\\b\\s*(?:<[^<>()]*>)?\\s*\\(\\s*(['"\`])([^'"\`]+)\\2`, "g");
|
|
4
4
|
/**
|
|
5
|
-
* Scan `text` for
|
|
6
|
-
* Returns the dictionary key string, or `null`
|
|
7
|
-
* recognised call.
|
|
5
|
+
* Scan `text` for a useIntlayer / getIntlayer call whose AST span contains
|
|
6
|
+
* `offset`. Returns the dictionary key string, or `null` when the cursor is
|
|
7
|
+
* not inside any recognised call.
|
|
8
|
+
*
|
|
9
|
+
* Handles:
|
|
10
|
+
* useIntlayer("my-key")
|
|
11
|
+
* getIntlayer<Type>("my-key", locale)
|
|
12
|
+
* useIntlayer(
|
|
13
|
+
* "my-key" ← multi-line
|
|
14
|
+
* )
|
|
15
|
+
* useIntlayer(`my-key`)
|
|
8
16
|
*/
|
|
9
17
|
const findKeyAtOffset = (text, offset) => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const program = parseText(text);
|
|
19
|
+
if (!program) return null;
|
|
20
|
+
let result = null;
|
|
21
|
+
walkAst(program, (node) => {
|
|
22
|
+
if (result) return true;
|
|
23
|
+
if (!isIntlayerCall(node)) return;
|
|
24
|
+
const start = node["start"];
|
|
25
|
+
const end = node["end"];
|
|
26
|
+
if (offset < start || offset > end) return true;
|
|
27
|
+
result = getFirstStringArg(node);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
17
31
|
};
|
|
18
32
|
|
|
19
33
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findKeyAtOffset.mjs","names":[],"sources":["../../src/findKeyAtOffset.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"findKeyAtOffset.mjs","names":[],"sources":["../../src/findKeyAtOffset.ts"],"sourcesContent":["import {\n getFirstStringArg,\n isIntlayerCall,\n parseText,\n walkAst,\n} from './oxcUtils';\n\n/**\n * Scan `text` for a useIntlayer / getIntlayer call whose AST span contains\n * `offset`. Returns the dictionary key string, or `null` when the cursor is\n * not inside any recognised call.\n *\n * Handles:\n * useIntlayer(\"my-key\")\n * getIntlayer<Type>(\"my-key\", locale)\n * useIntlayer(\n * \"my-key\" ← multi-line\n * )\n * useIntlayer(`my-key`)\n */\nexport const findKeyAtOffset = (\n text: string,\n offset: number\n): string | null => {\n const program = parseText(text);\n if (!program) return null;\n\n let result: string | null = null;\n\n walkAst(program, (node) => {\n if (result) return true; // already found — prune remaining\n if (!isIntlayerCall(node)) return;\n\n const start = node['start'] as number;\n const end = node['end'] as number;\n if (offset < start || offset > end) return true; // outside span — prune\n\n result = getFirstStringArg(node);\n return true;\n });\n\n return result;\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,MAAa,mBACX,MACA,WACkB;CAClB,MAAM,UAAU,UAAU,IAAI;CAC9B,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,SAAwB;CAE5B,QAAQ,UAAU,SAAS;EACzB,IAAI,QAAQ,OAAO;EACnB,IAAI,CAAC,eAAe,IAAI,GAAG;EAE3B,MAAM,QAAQ,KAAK;EACnB,MAAM,MAAM,KAAK;EACjB,IAAI,SAAS,SAAS,SAAS,KAAK,OAAO;EAE3C,SAAS,kBAAkB,IAAI;EAC/B,OAAO;CACT,CAAC;CAED,OAAO;AACT"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseText, walkAst } from "./oxcUtils.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/findKeyInContentFile.ts
|
|
4
|
+
const KEY_DECLARATION_REGULAR_EXPRESSION = /\bkey\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/g;
|
|
5
|
+
/**
|
|
6
|
+
* If `offset` falls within a `key:` declaration in a content file,
|
|
7
|
+
* return the declared key string; otherwise return null.
|
|
8
|
+
* Handles quoted values (TS/JS/JSON) and bare values (YAML/Markdown frontmatter).
|
|
9
|
+
*/
|
|
10
|
+
const findKeyInContentFile = (text, offset) => {
|
|
11
|
+
for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {
|
|
12
|
+
const start = match.index;
|
|
13
|
+
const end = start + match[0].length;
|
|
14
|
+
if (offset >= start && offset <= end) return match[2] ?? match[3] ?? null;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
/** Returns true when `node` is a call to the intlayer `t()` translation function. */
|
|
19
|
+
const isTCall = (node) => {
|
|
20
|
+
if (node["type"] !== "CallExpression") return false;
|
|
21
|
+
const callee = node["callee"];
|
|
22
|
+
return callee?.["type"] === "Identifier" && callee["name"] === "t";
|
|
23
|
+
};
|
|
24
|
+
/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */
|
|
25
|
+
const getStringValue = (node) => {
|
|
26
|
+
if ((node["type"] === "Literal" || node["type"] === "StringLiteral") && typeof node["value"] === "string") return node["value"];
|
|
27
|
+
if (node["type"] === "TemplateLiteral") {
|
|
28
|
+
const valueNode = node["quasis"]?.[0]?.["value"];
|
|
29
|
+
return valueNode?.["cooked"] ?? valueNode?.["raw"] ?? null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Fallback for non-JS/TS files (YAML, Markdown, JSON) or when Oxc cannot parse
|
|
35
|
+
* the file. Uses the original regex approach, returning a single-element path.
|
|
36
|
+
*/
|
|
37
|
+
const findContentFieldAtOffsetRegex = (text, offset) => {
|
|
38
|
+
const keyMatch = /\bkey\s*:\s*(?:(['"`])([^'"`\r\n]+)\1|([^\s'"`{},\r\n]+))/.exec(text);
|
|
39
|
+
if (!keyMatch) return null;
|
|
40
|
+
const dictionaryKey = keyMatch[2] ?? keyMatch[3];
|
|
41
|
+
if (!dictionaryKey) return null;
|
|
42
|
+
let wordStart = offset;
|
|
43
|
+
while (wordStart > 0 && /\w/.test(text[wordStart - 1])) wordStart--;
|
|
44
|
+
let wordEnd = offset;
|
|
45
|
+
while (wordEnd < text.length && /\w/.test(text[wordEnd])) wordEnd++;
|
|
46
|
+
if (wordStart === wordEnd) return null;
|
|
47
|
+
const word = text.slice(wordStart, wordEnd);
|
|
48
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(word)) return null;
|
|
49
|
+
const after = text.slice(wordEnd);
|
|
50
|
+
if (!/^\s*:(?!:)/.test(after)) return null;
|
|
51
|
+
return {
|
|
52
|
+
dictionaryKey,
|
|
53
|
+
fieldName: word,
|
|
54
|
+
fieldPath: [word]
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* If `offset` falls on a content field property key inside an intlayer content
|
|
59
|
+
* file, return the dictionary key, the leaf field name, and the full field path
|
|
60
|
+
* (array of names from the content root to the clicked field).
|
|
61
|
+
*
|
|
62
|
+
* For example, in:
|
|
63
|
+
* content: { searchInput: { text: t({...}) } }
|
|
64
|
+
*
|
|
65
|
+
* Clicking on `text` returns `{ dictionaryKey, fieldName: 'text', fieldPath: ['searchInput', 'text'] }`.
|
|
66
|
+
* Clicking on `searchInput` returns `{ ..., fieldName: 'searchInput', fieldPath: ['searchInput'] }`.
|
|
67
|
+
*
|
|
68
|
+
* Locale keys inside `t({en:…, fr:…})` calls are correctly skipped.
|
|
69
|
+
*
|
|
70
|
+
* Uses Oxc AST for JS/TS/TSX/JSX files; falls back to a regex heuristic for
|
|
71
|
+
* YAML and Markdown where Oxc cannot parse.
|
|
72
|
+
*/
|
|
73
|
+
const findContentFieldAtOffset = (text, offset) => {
|
|
74
|
+
const program = parseText(text);
|
|
75
|
+
if (!program) return findContentFieldAtOffsetRegex(text, offset);
|
|
76
|
+
let foundResult = null;
|
|
77
|
+
walkAst(program, (node) => {
|
|
78
|
+
if (foundResult) return true;
|
|
79
|
+
if (node["type"] !== "ObjectExpression") return;
|
|
80
|
+
const properties = node["properties"];
|
|
81
|
+
if (!properties?.length) return;
|
|
82
|
+
let dictionaryKey = null;
|
|
83
|
+
let contentNode = null;
|
|
84
|
+
for (const prop of properties) {
|
|
85
|
+
if (prop["type"] !== "Property") continue;
|
|
86
|
+
const propKey = prop["key"];
|
|
87
|
+
const propValue = prop["value"];
|
|
88
|
+
if (!propKey || !propValue) continue;
|
|
89
|
+
const keyName = propKey["name"] ?? propKey["value"];
|
|
90
|
+
if (keyName === "key") dictionaryKey = getStringValue(propValue);
|
|
91
|
+
if (keyName === "content") contentNode = propValue;
|
|
92
|
+
}
|
|
93
|
+
if (!dictionaryKey || !contentNode) return;
|
|
94
|
+
const walkContent = (obj, path) => {
|
|
95
|
+
if (obj["type"] !== "ObjectExpression") return false;
|
|
96
|
+
const props = obj["properties"];
|
|
97
|
+
if (!props) return false;
|
|
98
|
+
for (const prop of props) {
|
|
99
|
+
if (prop["type"] !== "Property") continue;
|
|
100
|
+
const propKey = prop["key"];
|
|
101
|
+
const propValue = prop["value"];
|
|
102
|
+
if (!propKey) continue;
|
|
103
|
+
const fieldName = propKey["name"] ?? propKey["value"];
|
|
104
|
+
if (!fieldName) continue;
|
|
105
|
+
const keyStart = propKey["start"];
|
|
106
|
+
const keyEnd = propKey["end"];
|
|
107
|
+
if (offset >= keyStart && offset <= keyEnd) {
|
|
108
|
+
const fieldPath = [...path, fieldName];
|
|
109
|
+
foundResult = {
|
|
110
|
+
dictionaryKey,
|
|
111
|
+
fieldName,
|
|
112
|
+
fieldPath
|
|
113
|
+
};
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (!propValue) continue;
|
|
117
|
+
if (isTCall(propValue)) continue;
|
|
118
|
+
if (propValue["type"] === "ObjectExpression") {
|
|
119
|
+
if (walkContent(propValue, [...path, fieldName])) return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
if (walkContent(contentNode, [])) return true;
|
|
125
|
+
});
|
|
126
|
+
return foundResult;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
export { findContentFieldAtOffset, findKeyInContentFile };
|
|
131
|
+
//# sourceMappingURL=findKeyInContentFile.mjs.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getFirstStringArg, isIntlayerCall, parseText, walkAst } from "./oxcUtils.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/findUsageFieldAtOffset.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detect when the cursor is on a field name that was obtained from a
|
|
6
|
+
* useIntlayer / getIntlayer call in the same file. Two patterns:
|
|
7
|
+
*
|
|
8
|
+
* Pattern 1 — destructuring:
|
|
9
|
+
* const { localeSwitcherLabel } = useIntlayer("locale-switcher")
|
|
10
|
+
* ^^^^^^^^^^^^^^^^^^ ← cursor here
|
|
11
|
+
*
|
|
12
|
+
* Pattern 2 — member access:
|
|
13
|
+
* const t = useIntlayer("locale-switcher")
|
|
14
|
+
* t.localeSwitcherLabel ← cursor on the property
|
|
15
|
+
*
|
|
16
|
+
* Returns { dictionaryKey, fieldName } when matched, null otherwise.
|
|
17
|
+
*/
|
|
18
|
+
const findUsageFieldAtOffset = (text, offset) => {
|
|
19
|
+
const program = parseText(text);
|
|
20
|
+
if (!program) return null;
|
|
21
|
+
const variableToDictionaryKeyMap = /* @__PURE__ */ new Map();
|
|
22
|
+
let result = null;
|
|
23
|
+
walkAst(program, (node) => {
|
|
24
|
+
if (result) return true;
|
|
25
|
+
if (node["type"] !== "VariableDeclarator") return;
|
|
26
|
+
const initializer = node["init"];
|
|
27
|
+
if (!initializer || !isIntlayerCall(initializer)) return;
|
|
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;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { findUsageFieldAtOffset };
|
|
72
|
+
//# sourceMappingURL=findUsageFieldAtOffset.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import {\n getFirstStringArg,\n isIntlayerCall,\n type OxcNode,\n parseText,\n walkAst,\n} from './oxcUtils';\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 * 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 program = parseText(text);\n if (!program) return null;\n\n // Map: variable name → dictionary key (for member-access look-up)\n const variableToDictionaryKeyMap = new Map<string, string>();\n let result: { dictionaryKey: string; fieldName: string } | null = null;\n\n // Single pass: collect variable→key assignments AND check destructure spans\n walkAst(program, (node) => {\n if (result) return true;\n if (node['type'] !== 'VariableDeclarator') return;\n\n const initializer = node['init'] as OxcNode | undefined;\n if (!initializer || !isIntlayerCall(initializer)) return;\n\n const dictionaryKey = getFirstStringArg(initializer);\n if (!dictionaryKey) return;\n\n const identifierNode = node['id'] as OxcNode | undefined;\n if (!identifierNode) return;\n\n // Pattern 1: ObjectPattern destructure → { field } = useIntlayer(\"key\")\n if (identifierNode['type'] === 'ObjectPattern') {\n for (const propertyNode of (identifierNode['properties'] as OxcNode[]) ??\n []) {\n const keyNode = propertyNode['key'] as OxcNode | undefined;\n if (!keyNode) continue;\n const keyStart = keyNode['start'] as number;\n const keyEnd = keyNode['end'] as number;\n if (offset >= keyStart && offset <= keyEnd) {\n result = {\n dictionaryKey: dictionaryKey,\n fieldName: keyNode['name'] as string,\n };\n return true;\n }\n }\n }\n\n // Record simple Identifier assignments for Pattern 2\n if (identifierNode['type'] === 'Identifier') {\n variableToDictionaryKeyMap.set(\n identifierNode['name'] as string,\n dictionaryKey\n );\n }\n });\n\n if (result) return result;\n\n // Pattern 2: MemberExpression → variableName.field where variableName = useIntlayer(\"key\")\n walkAst(program, (node) => {\n if (result) return true;\n if (node['type'] !== 'MemberExpression') return;\n\n const propertyNode = node['property'] as OxcNode | undefined;\n if (!propertyNode) return;\n const propertyStart = propertyNode['start'] as number;\n const propertyEnd = propertyNode['end'] as number;\n if (offset < propertyStart || offset > propertyEnd) return;\n\n const objectNode = node['object'] as OxcNode | undefined;\n if (objectNode?.['type'] !== 'Identifier') return;\n const variableName = objectNode['name'] as string;\n const dictionaryKey = variableToDictionaryKeyMap.get(variableName);\n if (!dictionaryKey) return;\n\n result = {\n dictionaryKey: dictionaryKey,\n fieldName: propertyNode['name'] as string,\n };\n return true;\n });\n\n return result;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,UAAU,UAAU,IAAI;CAC9B,IAAI,CAAC,SAAS,OAAO;CAGrB,MAAM,6CAA6B,IAAI,IAAoB;CAC3D,IAAI,SAA8D;CAGlE,QAAQ,UAAU,SAAS;EACzB,IAAI,QAAQ,OAAO;EACnB,IAAI,KAAK,YAAY,sBAAsB;EAE3C,MAAM,cAAc,KAAK;EACzB,IAAI,CAAC,eAAe,CAAC,eAAe,WAAW,GAAG;EAElD,MAAM,gBAAgB,kBAAkB,WAAW;EACnD,IAAI,CAAC,eAAe;EAEpB,MAAM,iBAAiB,KAAK;EAC5B,IAAI,CAAC,gBAAgB;EAGrB,IAAI,eAAe,YAAY,iBAC7B,KAAK,MAAM,gBAAiB,eAAe,iBACzC,CAAC,GAAG;GACJ,MAAM,UAAU,aAAa;GAC7B,IAAI,CAAC,SAAS;GACd,MAAM,WAAW,QAAQ;GACzB,MAAM,SAAS,QAAQ;GACvB,IAAI,UAAU,YAAY,UAAU,QAAQ;IAC1C,SAAS;KACQ;KACf,WAAW,QAAQ;IACrB;IACA,OAAO;GACT;EACF;EAIF,IAAI,eAAe,YAAY,cAC7B,2BAA2B,IACzB,eAAe,SACf,aACF;CAEJ,CAAC;CAED,IAAI,QAAQ,OAAO;CAGnB,QAAQ,UAAU,SAAS;EACzB,IAAI,QAAQ,OAAO;EACnB,IAAI,KAAK,YAAY,oBAAoB;EAEzC,MAAM,eAAe,KAAK;EAC1B,IAAI,CAAC,cAAc;EACnB,MAAM,gBAAgB,aAAa;EACnC,MAAM,cAAc,aAAa;EACjC,IAAI,SAAS,iBAAiB,SAAS,aAAa;EAEpD,MAAM,aAAa,KAAK;EACxB,IAAI,aAAa,YAAY,cAAc;EAC3C,MAAM,eAAe,WAAW;EAChC,MAAM,gBAAgB,2BAA2B,IAAI,YAAY;EACjE,IAAI,CAAC,eAAe;EAEpB,SAAS;GACQ;GACf,WAAW,aAAa;EAC1B;EACA,OAAO;CACT,CAAC;CAED,OAAO;AACT"}
|