From f207ea94d67ba69389ef5be158534e157a786df3 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 30 Sep 2025 18:24:33 -0700 Subject: [PATCH] fix(memory): ignore @ inside code blocks (#10201) --- .../src/utils/memoryImportProcessor.test.ts | 25 ++++++++++++ .../core/src/utils/memoryImportProcessor.ts | 39 ++++++++----------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/core/src/utils/memoryImportProcessor.test.ts b/packages/core/src/utils/memoryImportProcessor.test.ts index 621c2e5f5f..0fcaf080d3 100644 --- a/packages/core/src/utils/memoryImportProcessor.test.ts +++ b/packages/core/src/utils/memoryImportProcessor.test.ts @@ -423,6 +423,31 @@ describe('memoryImportProcessor', () => { ); }); + it('should not process imports in repeated inline code blocks', async () => { + const content = '`@noimport` and `@noimport`'; + const projectRoot = testPath('test', 'project'); + const basePath = testPath(projectRoot, 'src'); + + const result = await processImports( + content, + basePath, + true, + undefined, + projectRoot, + ); + + expect(result.content).toBe(content); + }); + + it('should not import when @ is inside an inline code block', async () => { + const content = + 'We should not ` @import` when the symbol is inside an inline code string.'; + const testRootDir = testPath('test', 'project'); + const result = await processImports(content, testRootDir); + expect(result.content).toBe(content); + expect(result.importTree.imports).toBeUndefined(); + }); + it('should allow imports from parent and subdirectories within project root', async () => { const content = 'Parent import: @../parent.md Subdir import: @./components/sub.md'; diff --git a/packages/core/src/utils/memoryImportProcessor.ts b/packages/core/src/utils/memoryImportProcessor.ts index 1519ae0c8d..4af71cdfd8 100644 --- a/packages/core/src/utils/memoryImportProcessor.ts +++ b/packages/core/src/utils/memoryImportProcessor.ts @@ -7,7 +7,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { isSubpath } from './paths.js'; -import { marked } from 'marked'; +import { marked, type Token } from 'marked'; // Simple console logger for import processing const logger = { @@ -153,39 +153,32 @@ function isLetter(char: string): boolean { function findCodeRegions(content: string): Array<[number, number]> { const regions: Array<[number, number]> = []; const tokens = marked.lexer(content); + let offset = 0; - // Map from raw content to a queue of its start indices in the original content. - const rawContentIndices = new Map(); - - function walk(token: { type: string; raw: string; tokens?: unknown[] }) { + function walk(token: Token, baseOffset: number) { if (token.type === 'code' || token.type === 'codespan') { - if (!rawContentIndices.has(token.raw)) { - const indices: number[] = []; - let lastIndex = -1; - while ((lastIndex = content.indexOf(token.raw, lastIndex + 1)) !== -1) { - indices.push(lastIndex); - } - rawContentIndices.set(token.raw, indices); - } - - const indices = rawContentIndices.get(token.raw); - if (indices && indices.length > 0) { - // Assume tokens are processed in order of appearance. - // Dequeue the next available index for this raw content. - const idx = indices.shift()!; - regions.push([idx, idx + token.raw.length]); - } + regions.push([baseOffset, baseOffset + token.raw.length]); } if ('tokens' in token && token.tokens) { + let childOffset = 0; for (const child of token.tokens) { - walk(child as { type: string; raw: string; tokens?: unknown[] }); + const childIndexInParent = token.raw.indexOf(child.raw, childOffset); + if (childIndexInParent === -1) { + logger.error( + `Could not find child token in parent raw content. Aborting parsing for this branch. Child raw: "${child.raw}"`, + ); + break; + } + walk(child, baseOffset + childIndexInParent); + childOffset = childIndexInParent + child.raw.length; } } } for (const token of tokens) { - walk(token); + walk(token, offset); + offset += token.raw.length; } return regions;