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
16 changes: 15 additions & 1 deletion packages/shared/src/markdownLinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe("inlineCodeFilePathCandidate", () => {
["origin/main", null],
["z-ai/glm-5.3", null],
["share/man/ls.1", "share/man/ls.1"],
["src/main.ts/", null],
["./patches/", "./patches/"],
])("distinguishes file paths from code and hostnames in %s", (source, candidate) => {
expect(inlineCodeFilePathCandidate(source)).toBe(candidate);
});
Expand All @@ -40,13 +42,18 @@ describe("parseMarkdownFileLink", () => {
expect(parseMarkdownFileLink(href)?.path).toBe(path);
});

it.each(["https://example.com/docs", "/chat/settings#L3", "TODO:12"])(
it.each(["https://example.com/docs", "/chat/settings#L3", "TODO:12", "patches"])(
"does not treat %s as a file",
(href) => {
expect(parseMarkdownFileLink(href)).toBeNull();
},
);

it("recognizes a relative folder that ends in a slash", () => {
expect(parseMarkdownFileLink("patches/")).toEqual({ path: "patches/" });
expect(parseMarkdownFileLink("docs/My%20Folder/")?.path).toBe("docs/My Folder/");
});

it("accepts conventional extensionless names with or without a position", () => {
expect(parseMarkdownFileLink("Makefile")).toEqual({ path: "Makefile" });
expect(parseMarkdownFileLink("Dockerfile:8")).toEqual({ path: "Dockerfile", line: 8 });
Expand Down Expand Up @@ -89,6 +96,13 @@ describe("extractMarkdownLinkHrefs", () => {
});

describe("resolveMarkdownFileLinkTarget", () => {
it("resolves a relative folder with a trailing slash against cwd", () => {
expect(resolveMarkdownFileLinkTarget("patches/", "/repo/project")).toBe(
"/repo/project/patches/",
);
expect(resolveMarkdownFileLinkTarget("patches", "/repo/project")).toBeNull();
});

it("does not treat filename line references as external schemes", () => {
expect(resolveMarkdownFileLinkTarget("script.ts:10", "/Users/julius/project")).toBe(
"/Users/julius/project/script.ts:10",
Expand Down
7 changes: 6 additions & 1 deletion packages/shared/src/markdownLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {

const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/;

// The final segment is optional so an explicit folder link may end in `/`
// (`patches/`, `docs/My Folder/`). A bare name with no slash still fails.
const RELATIVE_FILE_PATH_PATTERN =
/^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2}$/;
/^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2})?$/;

const RELATIVE_FILE_NAME_PATTERN =
/^[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\.[A-Za-z0-9_-]+(?::\d+){0,2}$/;
Expand Down Expand Up @@ -176,6 +178,9 @@ export function inlineCodeFilePathCandidate(codeText: string): string | null {
isWindowsAbsolutePath(candidate);
if (!hasExplicitPathShape) {
const withoutPosition = candidate.replace(POSITION_SUFFIX_PATTERN, "");
// Markdown destinations accept `patches/`. Inline code still needs a
// file-shaped final segment, so a trailing slash stays plain code.
if (withoutPosition.endsWith("/")) return null;
const firstSegment = withoutPosition.split("/")[0] ?? withoutPosition;
if (looksLikeHostname(firstSegment, hasPosition)) return null;
const basename =
Expand Down
Loading