Skip to content

Commit e906ecc

Browse files
committed
fix: support hints in arrays
1 parent c18d21a commit e906ecc

File tree

2 files changed

+121
-15
lines changed

2 files changed

+121
-15
lines changed

packages/cli/src/cli/loaders/jsonc.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,41 @@ describe("jsonc loader", () => {
219219
},
220220
});
221221
});
222+
223+
it("pullHints should extract comments from arrays", async () => {
224+
const loader = createJsoncLoader();
225+
loader.setDefaultLocale("en");
226+
const jsoncInput = `{
227+
"items": [
228+
{
229+
"value": "First item",
230+
"type": "heading"
231+
},
232+
{
233+
// This is a hint for the second item
234+
"value": "Second item",
235+
"type": "text"
236+
},
237+
{
238+
// This is a hint for the third item
239+
"value": "Third item",
240+
"type": "text"
241+
}
242+
]
243+
}`;
244+
245+
await loader.pull("en", jsoncInput);
246+
const comments = await loader.pullHints(jsoncInput);
247+
248+
expect(comments).toEqual({
249+
items: {
250+
"1": {
251+
value: { hint: "This is a hint for the second item" },
252+
},
253+
"2": {
254+
value: { hint: "This is a hint for the third item" },
255+
},
256+
},
257+
});
258+
});
222259
});

packages/cli/src/cli/loaders/jsonc.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
2323
return {};
2424
}
2525

26-
// Track nesting context
27-
const contextStack: Array<{ key: string; isArray: boolean }> = [];
26+
// Track nesting context with array indices
27+
const contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }> = [];
28+
let arrayObjectCount: Record<number, number> = {}; // Track object count per array depth
2829

2930
for (let i = 0; i < lines.length; i++) {
3031
const line = lines[i];
@@ -42,12 +43,12 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
4243
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
4344
if (keyMatch) {
4445
const key = keyMatch[1];
45-
const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
46+
const path = contextStack.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key).filter(Boolean);
4647
keyInfo = { key, path };
4748
}
4849
} else {
4950
// For standalone comments, find the next key
50-
keyInfo = findAssociatedKey(lines, commentData.lineIndex, contextStack);
51+
keyInfo = findAssociatedKey(lines, commentData.lineIndex, contextStack, arrayObjectCount);
5152
}
5253

5354
if (keyInfo && keyInfo.key) {
@@ -60,7 +61,7 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
6061
}
6162

6263
// Update context for object/array nesting
63-
updateContext(contextStack, line, result);
64+
updateContext(contextStack, line, result, arrayObjectCount);
6465
}
6566

6667
return comments;
@@ -166,7 +167,8 @@ function extractBlockComment(
166167
function findAssociatedKey(
167168
lines: string[],
168169
commentLineIndex: number,
169-
contextStack: Array<{ key: string; isArray: boolean }>,
170+
contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }>,
171+
arrayObjectCount: Record<number, number>,
170172
): { key: string | null; path: string[] } {
171173
// Look for the next key after the comment
172174
for (let i = commentLineIndex + 1; i < lines.length; i++) {
@@ -175,18 +177,50 @@ function findAssociatedKey(
175177
if (
176178
!line ||
177179
line.startsWith("//") ||
178-
line.startsWith("/*") ||
179-
line === "{" ||
180-
line === "}"
180+
line.startsWith("/*")
181181
) {
182182
continue;
183183
}
184184

185+
// Check if we're about to enter an array object
186+
if (line === "{" && contextStack.length > 0) {
187+
const parent = contextStack[contextStack.length - 1];
188+
if (parent.isArray) {
189+
// Get the current array index from arrayObjectCount
190+
const depth = contextStack.length - 1;
191+
const arrayIndex = arrayObjectCount[depth] || 0;
192+
193+
// Continue looking for the key inside this object
194+
for (let j = i + 1; j < lines.length; j++) {
195+
const innerLine = lines[j].trim();
196+
if (!innerLine || innerLine.startsWith("//") || innerLine.startsWith("/*")) continue;
197+
198+
const keyMatch = innerLine.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
199+
if (keyMatch) {
200+
const key = keyMatch[1];
201+
const path = contextStack
202+
.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key)
203+
.filter(Boolean);
204+
path.push(String(arrayIndex));
205+
return { key, path };
206+
}
207+
208+
if (innerLine === "}") break;
209+
}
210+
}
211+
}
212+
213+
if (line === "{" || line === "}") {
214+
continue;
215+
}
216+
185217
// Extract key from line
186218
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
187219
if (keyMatch) {
188220
const key = keyMatch[1];
189-
const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
221+
const path = contextStack
222+
.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key)
223+
.filter(Boolean);
190224
return { key, path };
191225
}
192226
}
@@ -195,12 +229,23 @@ function findAssociatedKey(
195229
}
196230

197231
function updateContext(
198-
contextStack: Array<{ key: string; isArray: boolean }>,
232+
contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }>,
199233
line: string,
200234
parsedJson: any,
235+
arrayObjectCount: Record<number, number>,
201236
): void {
202-
// This is a simplified context tracking - in a full implementation,
203-
// you'd want more sophisticated AST-based tracking
237+
const trimmed = line.trim();
238+
239+
// Track opening of arrays
240+
const arrayMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:\s*\[/);
241+
if (arrayMatch) {
242+
const depth = contextStack.length;
243+
arrayObjectCount[depth] = 0; // Initialize counter for this array
244+
contextStack.push({ key: arrayMatch[1], isArray: true });
245+
return;
246+
}
247+
248+
// Track opening of objects
204249
const openBraces = (line.match(/\{/g) || []).length;
205250
const closeBraces = (line.match(/\}/g) || []).length;
206251

@@ -209,13 +254,37 @@ function updateContext(
209254
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:\s*\{/);
210255
if (keyMatch) {
211256
contextStack.push({ key: keyMatch[1], isArray: false });
257+
} else if (trimmed === '{' && contextStack.length > 0) {
258+
// This is an object within an array
259+
const parent = contextStack[contextStack.length - 1];
260+
if (parent.isArray) {
261+
const depth = contextStack.length - 1;
262+
const arrayIndex = arrayObjectCount[depth] || 0;
263+
contextStack.push({ key: '', isArray: false, arrayIndex });
264+
arrayObjectCount[depth]++;
265+
}
212266
}
213-
} else if (closeBraces > openBraces) {
214-
// Pop context when closing braces
267+
}
268+
269+
// Track closing of objects and arrays
270+
const openBrackets = (line.match(/\[/g) || []).length;
271+
const closeBrackets = (line.match(/\]/g) || []).length;
272+
273+
if (closeBraces > openBraces) {
215274
for (let i = 0; i < closeBraces - openBraces; i++) {
216275
contextStack.pop();
217276
}
218277
}
278+
279+
if (closeBrackets > openBrackets) {
280+
for (let i = 0; i < closeBrackets - openBrackets; i++) {
281+
const popped = contextStack.pop();
282+
if (popped?.isArray) {
283+
const depth = contextStack.length;
284+
delete arrayObjectCount[depth]; // Clean up counter
285+
}
286+
}
287+
}
219288
}
220289

221290
function setCommentAtPath(

0 commit comments

Comments
 (0)