Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/features/messages/components/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,43 @@ describe("Markdown file-like href behavior", () => {
expect(clickEvent.defaultPrevented).toBe(true);
expect(onOpenFileLink).not.toHaveBeenCalled();
});

it("does not turn natural-language slash phrases into file links", () => {
const { container } = render(
<Markdown
value="Keep the current app/daemon behavior and the existing Git/Plan experience."
className="markdown"
/>,
);

expect(container.querySelector(".message-file-link")).toBeNull();
expect(container.textContent).toContain("app/daemon");
expect(container.textContent).toContain("Git/Plan");
});

it("does not turn longer slash phrases into file links", () => {
const { container } = render(
<Markdown
value="This keeps Spec/Verification/Evidence in the note without turning it into a file link."
className="markdown"
/>,
);

expect(container.querySelector(".message-file-link")).toBeNull();
expect(container.textContent).toContain("Spec/Verification/Evidence");
});

it("still turns clear file paths in plain text into file links", () => {
const { container } = render(
<Markdown
value="See docs/setup.md and /Users/example/project/src/index.ts for details."
className="markdown"
/>,
);

const fileLinks = [...container.querySelectorAll(".message-file-link")];
expect(fileLinks).toHaveLength(2);
expect(fileLinks[0]?.textContent).toContain("setup.md");
expect(fileLinks[1]?.textContent).toContain("index.ts");
});
});
23 changes: 7 additions & 16 deletions src/utils/remarkFileLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@ const FILE_PATH_PATTERN =
const FILE_PATH_MATCH = new RegExp(`^${FILE_PATH_PATTERN.source}$`);

const TRAILING_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", ")", "]", "}"]);
const RELATIVE_ALLOWED_PREFIXES = [
"src/",
"app/",
"lib/",
"tests/",
"test/",
"packages/",
"apps/",
"docs/",
"scripts/",
];
const LETTER_OR_NUMBER_PATTERN = /[\p{L}\p{N}.]/u;

type MarkdownNode = {
type: string;
Expand All @@ -43,7 +33,11 @@ function isPathCandidate(
return false;
}
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
if (value.startsWith("/") && previousChar && /[A-Za-z0-9.]/.test(previousChar)) {
if (
value.startsWith("/") &&
previousChar &&
LETTER_OR_NUMBER_PATTERN.test(previousChar)
) {
return false;
}
return true;
Expand All @@ -52,10 +46,7 @@ function isPathCandidate(
return true;
}
const lastSegment = value.split("/").pop() ?? "";
if (lastSegment.includes(".")) {
return true;
}
return RELATIVE_ALLOWED_PREFIXES.some((prefix) => value.startsWith(prefix));
return lastSegment.includes(".");
}

function splitTrailingPunctuation(value: string) {
Expand Down
Loading