fix(markdown): bare prose paths need an extension, line suffix or depth

Slash-separated English pairs were recognized as file links in prose. Require stronger path intent there while preserving the existing inline-code recognition rule.
This commit is contained in:
Mohamed Boudra
2026-10-16 09:08:29 +02:00
parent d9178eac75
commit a502cfd0d0
2 changed files with 41 additions and 10 deletions
@@ -444,14 +444,20 @@ it.each([
it.each([
"src/index.ts",
"README.md",
"packages/app",
"./scripts/run",
"./x/y",
"../x/y",
"/x/y",
"a/b/c",
String.raw`C:\Users\x`,
"src/index.ts:12:3",
"src/run:12",
"src/run:12:3",
"src/run:12-20",
"~/dev/paseo",
String.raw`C:\Users\x\file.txt`,
"file.ts:12",
"file.ts:12-20",
"123/file",
"src/123",
"123.ts",
"FILE.TS",
"https://example.com/path",
@@ -463,6 +469,20 @@ it.each([
expect(linkRecognition.inlineCode.test(token)).toBe(true);
});
it.each([
"before/after",
"and/or",
"input/output",
"read/write",
"packages/app",
"123/file",
"src/123",
])("requires stronger path intent in prose while preserving inline code: %s", (token) => {
const { linkRecognition } = prepareMarkdownSource(`Compare ${token}, then continue.`);
expect([...`Compare ${token}, then continue.`.matchAll(linkRecognition.text)]).toEqual([]);
expect(linkRecognition.inlineCode.test(token)).toBe(true);
});
it("keeps prose and inline-code recognition enabled through the native regex adapter", () => {
const { linkRecognition } = prepareMarkdownSource("3/4 src/index.ts");
const native = toNativeLinkRecognition(linkRecognition);
+18 -7
View File
@@ -16,20 +16,31 @@ const LINE_SUFFIX = String.raw`(?::\d+(?::\d+)?(?:-\d+(?::\d+)?)?)?`;
const FILE_EXTENSIONS = [...ASSISTANT_FILE_EXTENSIONS]
.map((extension) => extension.replace(/[a-z]/g, (letter) => `[${letter}${letter.toUpperCase()}]`))
.join("|");
const SEPARATED_PATH = String.raw`(?:[A-Za-z]:[\\/]|\.{0,2}\/|~\/)?(?=${PATH_CHARACTER}*(?:[\\/]${PATH_CHARACTER}*)*${PATH_LETTER})${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT})+`;
const PATH_PREFIX = String.raw`(?:[A-Za-z]:[\\/]|\.{0,2}\/|~\/)`;
const HAS_PATH_LETTER = String.raw`(?=${PATH_CHARACTER}*(?:[\\/]${PATH_CHARACTER}*)*${PATH_LETTER})`;
const SEPARATED_PATH = String.raw`${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT})+`;
const FILE_NAME = String.raw`${PATH_SEGMENT}\.(?:${FILE_EXTENSIONS})`;
// The native adapter mistakes a literal + in lookbehind for a quantifier; use its hex escape.
const FILE_PATTERN = new RegExp(
String.raw`(?:https?://[^\s<>]+|(?<![a-zA-Z0-9\u0080-\uFFFF_@.%\x2b~:/\\-])(?:${SEPARATED_PATH}|${FILE_NAME})${LINE_SUFFIX}(?![a-zA-Z0-9\u0080-\uFFFF_@%+:/\\-]|\.${PATH_CHARACTER}))`,
"g",
);
// Bare prose paths need a known extension, a line suffix, or at least two separators.
const PROSE_PATH = String.raw`(?:${PATH_PREFIX}${HAS_PATH_LETTER}${SEPARATED_PATH}|${HAS_PATH_LETTER}(?:${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT})*[\\/]${FILE_NAME}|${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT}){2,}|${SEPARATED_PATH}(?=:)))`;
const INLINE_CODE_PATH = String.raw`${PATH_PREFIX}?${HAS_PATH_LETTER}${SEPARATED_PATH}`;
function createFilePattern(path: string): RegExp {
// The native adapter mistakes a literal + in lookbehind for a quantifier; use its hex escape.
return new RegExp(
String.raw`(?:https?://[^\s<>]+|(?<![a-zA-Z0-9\u0080-\uFFFF_@.%\x2b~:/\\-])(?:${path}|${FILE_NAME})${LINE_SUFFIX}(?![a-zA-Z0-9\u0080-\uFFFF_@%+:/\\-]|\.${PATH_CHARACTER}))`,
"g",
);
}
const FILE_PATTERN = createFilePattern(PROSE_PATH);
const INLINE_CODE_FILE_PATTERN = createFilePattern(INLINE_CODE_PATH);
function linkMetadata() {
return {
linkRecognition: {
text: FILE_PATTERN,
inlineCode: new RegExp(
String.raw`^(?:${FILE_PATTERN.source})(?:\(\d+(?:,\d+)?(?:-\d+(?:,\d+)?)?\)| lines? \d+(?:-\d+)?)?$`,
String.raw`^(?:${INLINE_CODE_FILE_PATTERN.source})(?:\(\d+(?:,\d+)?(?:-\d+(?:,\d+)?)?\)| lines? \d+(?:-\d+)?)?$`,
),
},
linkSource: markdownLinkSource,