diff --git a/apps/mobile/src/lib/enrichedLinkPresentation.test.ts b/apps/mobile/src/lib/enrichedLinkPresentation.test.ts index 4dd5ecbd08317..771674cc4eff6 100644 --- a/apps/mobile/src/lib/enrichedLinkPresentation.test.ts +++ b/apps/mobile/src/lib/enrichedLinkPresentation.test.ts @@ -34,6 +34,8 @@ describe("Enriched link presentation", () => { "https://example.com/app.tsx", "x + y", "README.md", + "Dockerfile", + "docker/Dockerfiles", "/app/settings", ]) { expect(ENRICHED_INLINE_FILE_LINK_REGEX.test(text), text).toBe(false); diff --git a/apps/mobile/src/lib/enrichedLinkPresentation.ts b/apps/mobile/src/lib/enrichedLinkPresentation.ts index 5d2e9431339d1..f31d8659b6614 100644 --- a/apps/mobile/src/lib/enrichedLinkPresentation.ts +++ b/apps/mobile/src/lib/enrichedLinkPresentation.ts @@ -17,6 +17,7 @@ import { parseComposerContextHref, } from "@t3tools/shared/composerContextReferences"; import { hasThreadLinks, parseThreadLinkHref } from "@t3tools/shared/threadLinks"; +import { EXTENSIONLESS_FILE_NAMES } from "@t3tools/shared/markdownLinks"; export function enrichedContextLinkPresentation( href: string, @@ -91,8 +92,14 @@ export function enrichedLinkVariantPattern(url: string) { return `^${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`; } -export const ENRICHED_INLINE_FILE_LINK_REGEX = - /^(?!\S*:\/\/)(?:(?:\.{1,2}[\\/]|~[\\/]|\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes|private|root|usr|bin|sbin|lib|lib64|srv|dev|proc|sys|run|boot|media|workspace|workspaces)\/|[A-Za-z]:[\\/])[^\s`]+|(?:[\p{L}\p{M}\p{N}._-]+[\\/])+[\p{L}\p{M}\p{N}._-]+\.[A-Za-z0-9_-]+(?::\d+(?::\d+|-\d+)?)?|[\p{L}\p{M}\p{N}._-]+\.[A-Za-z0-9_-]+:\d+(?::\d+|-\d+)?)$/u; +const EXTENSIONLESS_FILE_NAME_PATTERN = [...EXTENSIONLESS_FILE_NAMES] + .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("|"); + +export const ENRICHED_INLINE_FILE_LINK_REGEX = new RegExp( + String.raw`^(?!\S*:\/\/)(?:(?:\.{1,2}[\\/]|~[\\/]|\/(?:Users|home|tmp|var|etc|opt|mnt|Volumes|private|root|usr|bin|sbin|lib|lib64|srv|dev|proc|sys|run|boot|media|workspace|workspaces)\/|[A-Za-z]:[\\/])[^\s\x60]+|(?:[\p{L}\p{M}\p{N}._-]+[\\/])+(?:[\p{L}\p{M}\p{N}._-]+\.[A-Za-z0-9_-]+|(?:${EXTENSIONLESS_FILE_NAME_PATTERN}))(?::\d+(?::\d+|-\d+)?)?|[\p{L}\p{M}\p{N}._-]+\.[A-Za-z0-9_-]+:\d+(?::\d+|-\d+)?)$`, + "u", +); export function enrichedSkillLinkRegex(skills?: ReadonlyArray) { const names = [...new Set(skills?.map((skill) => skill.name) ?? [])].filter(Boolean); diff --git a/apps/mobile/src/lib/markdownInlineFileLinks.test.ts b/apps/mobile/src/lib/markdownInlineFileLinks.test.ts index e2d81cf869561..3de6590dc7a4b 100644 --- a/apps/mobile/src/lib/markdownInlineFileLinks.test.ts +++ b/apps/mobile/src/lib/markdownInlineFileLinks.test.ts @@ -93,7 +93,13 @@ describe("inline code that names a file", () => { expect(rewrite("See `src/math.ts:2` and `./README.md`.")).toBe( "See [src/math.ts:2]() and [./README.md](<./README.md>).", ); - for (const path of ["src/數學.ts:2", "數學/math.ts:2", "數學.ts:2", "src/數學.ts:2-4"]) { + for (const path of [ + "src/數學.ts:2", + "數學/math.ts:2", + "數學.ts:2", + "src/數學.ts:2-4", + "docker/Dockerfile", + ]) { expect(rewrite(`See \`${path}\`.`), path).toBe(`See [${path}](<${path}>).`); } }); diff --git a/packages/shared/src/markdownLinks.test.ts b/packages/shared/src/markdownLinks.test.ts index 74c4236c53c8f..76699b40d7a95 100644 --- a/packages/shared/src/markdownLinks.test.ts +++ b/packages/shared/src/markdownLinks.test.ts @@ -32,6 +32,7 @@ describe("inlineCodeFilePathCandidate", () => { ["origin/main", null], ["z-ai/glm-5.3", null], ["share/man/ls.1", "share/man/ls.1"], + ["docker/Dockerfile", "docker/Dockerfile"], ["src/main.ts/", null], ["./patches/", "./patches/"], // regression: #30757 an agent's shortened path is not a file diff --git a/packages/shared/src/markdownLinks.ts b/packages/shared/src/markdownLinks.ts index 8cc537194e194..0ae35231179e1 100644 --- a/packages/shared/src/markdownLinks.ts +++ b/packages/shared/src/markdownLinks.ts @@ -68,7 +68,7 @@ const POSIX_FILE_ROOT_PREFIXES = [ "/workspaces/", ] as const; -const EXTENSIONLESS_FILE_NAMES = new Set([ +export const EXTENSIONLESS_FILE_NAMES: ReadonlySet = new Set([ "Makefile", "makefile", "GNUmakefile", @@ -202,7 +202,7 @@ export function inlineCodeFilePathCandidate(codeText: string): string | null { .at(-1) ?? ""; if (VERSION_SUFFIX_PATTERN.test(basename)) return null; if (!FILE_EXTENSION_PATTERN.test(basename)) { - if (!hasPosition) return null; + if (!hasPosition) return EXTENSIONLESS_FILE_NAMES.has(basename) ? candidate : null; // A `:line` suffix alone does not make `I/W217:220` a file reference. if ( withoutPosition.includes("/") &&