Skip to content

Commit b50262c

Browse files
committed
Fix root-path mentions minted from relative filenames in prose quotes
1 parent 36d5369 commit b50262c

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

‎src/tui/image-attachments.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,18 @@ describe("findImagePathMentions", () => {
191191
findImagePathMentions(`He said "look at /tmp/shot.png" today`, "/repo"),
192192
).toEqual([{ raw: "/tmp/shot.png", path: "/tmp/shot.png" }]);
193193
});
194+
195+
test("does not mint a root path from a relative filename inside prose quotes", () => {
196+
expect(
197+
findImagePathMentions('She wrote "notes/plan.png" in the doc', "/repo"),
198+
).toEqual([]);
199+
expect(
200+
findImagePathMentions('"see https://example.com/x.png"', "/repo"),
201+
).toEqual([]);
202+
expect(findImagePathMentions(`'a'/b.png`, "/repo")).toEqual([
203+
{ raw: "/b.png", path: "/b.png" },
204+
]);
205+
});
194206
});
195207

196208
describe("image attachment helpers", () => {

‎src/tui/image-attachments.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function findImagePathMentions(
8181
// Keep a lone trailing \r on CRLF out of the scan window.
8282
const contentEnd =
8383
lineEnd > lineStart && text[lineEnd - 1] === "\r" ? lineEnd - 1 : lineEnd;
84-
scanImagePathLine(text, lineStart, contentEnd, cwd, push);
84+
scanImagePathLine(text, lineStart, contentEnd, cwd, push, false);
8585
if (lineEnd === text.length) break;
8686
lineStart = lineEnd + 1;
8787
}
@@ -98,6 +98,7 @@ function scanImagePathLine(
9898
end: number,
9999
cwd: string,
100100
push: (raw: string, path: string | undefined) => void,
101+
inQuotedProse: boolean,
101102
): void {
102103
let i = start;
103104
while (i < end) {
@@ -120,7 +121,7 @@ function scanImagePathLine(
120121
}
121122
// Balanced quotes around prose are not path wrappers. Search inside
122123
// so an absolute path can still be found, then resume after the closer.
123-
scanImagePathLine(text, i + 1, close, cwd, push);
124+
scanImagePathLine(text, i + 1, close, cwd, push, true);
124125
i = close + 1;
125126
continue;
126127
}
@@ -129,7 +130,12 @@ function scanImagePathLine(
129130
continue;
130131
}
131132

132-
if (canStartUnquotedPath(text, i, end)) {
133+
// Inside prose a candidate must open a new token, or the separator in
134+
// `notes/plan.png` mints a root-level `/plan.png` mention.
135+
if (
136+
canStartUnquotedPath(text, i, end) &&
137+
(!inQuotedProse || i === start || !isTokenContinuation(text[i - 1]))
138+
) {
133139
const match = UNQUOTED_AT.exec(text.slice(i, end));
134140
if (match?.[0] !== undefined) {
135141
const raw = trimTrailingPunctuation(match[0]);
@@ -151,6 +157,16 @@ function isWordChar(ch: string | undefined): boolean {
151157
);
152158
}
153159

160+
// Characters that can glue a `/` (or another path start) onto the token
161+
// before it: relative segments (`notes/plan.png`), URLs (`https://x.png`),
162+
// and shell-style quote concatenation (`'a'/b.png`).
163+
const TOKEN_CONTINUATION = "'\"`/\\:.~_-+@%";
164+
165+
function isTokenContinuation(ch: string | undefined): boolean {
166+
if (ch === undefined) return false;
167+
return isWordChar(ch) || TOKEN_CONTINUATION.includes(ch);
168+
}
169+
154170
function looksLikeQuotedImagePath(inner: string): boolean {
155171
if (inner.startsWith("file://")) return true;
156172
if (inner === "~" || inner.startsWith("~/") || inner.startsWith("~\\"))

0 commit comments

Comments
 (0)