| author | |
| committer | |
| log | 99960383d094b850f0001d3067fa604aa3adf7d6 |
| tree | 6c6ad044a442b2800dda5666b8a4ec4ad0037d1b |
| parent | a1379db81847cc70b2c9535fd8370b69f9ebf291 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 54 insertions(+), 4 deletions(-)
apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts+4-2| ... | ... | @@ -3,8 +3,10 @@ import type { MARKDOWN_FILE_ICON_SOURCES } from "./markdownFileIcons.generated"; |
| 3 | 3 | const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/; |
| 4 | 4 | const WINDOWS_UNC_PATH_PATTERN = /^\\\\/; |
| 5 | 5 | const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; |
| 6 | const RELATIVE_FILE_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}$/; | |
| 7 | const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; | |
| 6 | const RELATIVE_FILE_PATH_PATTERN = | |
| 7 | /^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2}$/; | |
| 8 | const RELATIVE_FILE_NAME_PATTERN = | |
| 9 | /^[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; | |
| 8 | 10 | const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 9 | 11 | const POSIX_FILE_ROOT_PREFIXES = [ |
| 10 | 12 | "/Users/", |
apps/mobile/src/lib/markdownLinks.test.ts+18| ... | ... | @@ -50,6 +50,24 @@ describe("resolveMarkdownLinkPresentation", () => { |
| 50 | 50 | }); |
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | it.each(["md", "html", "xml"])("recognizes a bare spaced .%s filename", (extension) => { | |
| 54 | expect( | |
| 55 | resolveMarkdownLinkPresentation(`Updated%20cutover%20checklist.${extension}`), | |
| 56 | ).toMatchObject({ | |
| 57 | kind: "file", | |
| 58 | path: `Updated cutover checklist.${extension}`, | |
| 59 | label: `Updated cutover checklist.${extension}`, | |
| 60 | }); | |
| 61 | }); | |
| 62 | ||
| 63 | it("recognizes spaced relative paths", () => { | |
| 64 | expect(resolveMarkdownLinkPresentation("docs/My%20Folder/checklist.xml")).toMatchObject({ | |
| 65 | kind: "file", | |
| 66 | path: "docs/My Folder/checklist.xml", | |
| 67 | label: "checklist.xml", | |
| 68 | }); | |
| 69 | }); | |
| 70 | ||
| 53 | 71 | it("extracts line fragments from relative file links", () => { |
| 54 | 72 | expect(resolveMarkdownLinkPresentation("src/main.ts#L18C2")).toMatchObject({ |
| 55 | 73 | kind: "file", |
apps/web/src/components/ChatMarkdown.tsx+2-1| ... | ... | @@ -80,6 +80,7 @@ import { |
| 80 | 80 | resolveInlineCodeFileLinkMeta, |
| 81 | 81 | resolveMarkdownFileLinkMeta, |
| 82 | 82 | rewriteMarkdownFileUriHref, |
| 83 | shouldOpenMarkdownFileLinkInBrowserByDefault, | |
| 83 | 84 | shouldOpenMarkdownFileLinkInEditor, |
| 84 | 85 | type MarkdownFileLinkMeta, |
| 85 | 86 | } from "../markdown-links"; |
| ... | ... | @@ -1402,7 +1403,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1402 | 1403 | handleOpenInEditor(); |
| 1403 | 1404 | return; |
| 1404 | 1405 | } |
| 1405 | if (onOpenInBrowser) { | |
| 1406 | if (onOpenInBrowser && shouldOpenMarkdownFileLinkInBrowserByDefault(iconPath)) { | |
| 1406 | 1407 | handleOpenInBrowser(); |
| 1407 | 1408 | return; |
| 1408 | 1409 | } |
apps/web/src/markdown-links.test.ts+24| ... | ... | @@ -9,6 +9,7 @@ import { |
| 9 | 9 | resolveMarkdownFileLinkMeta, |
| 10 | 10 | resolveMarkdownFileLinkTarget, |
| 11 | 11 | rewriteMarkdownFileUriHref, |
| 12 | shouldOpenMarkdownFileLinkInBrowserByDefault, | |
| 12 | 13 | shouldOpenMarkdownFileLinkInEditor, |
| 13 | 14 | } from "./markdown-links"; |
| 14 | 15 | |
| ... | ... | @@ -69,6 +70,15 @@ describe("shouldOpenMarkdownFileLinkInEditor", () => { |
| 69 | 70 | }); |
| 70 | 71 | }); |
| 71 | 72 | |
| 73 | describe("shouldOpenMarkdownFileLinkInBrowserByDefault", () => { | |
| 74 | it("keeps PDFs browser-first while source files open in the file viewer", () => { | |
| 75 | expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.pdf")).toBe(true); | |
| 76 | expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.PDF?download=1")).toBe(true); | |
| 77 | expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.html")).toBe(false); | |
| 78 | expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.xml")).toBe(false); | |
| 79 | }); | |
| 80 | }); | |
| 81 | ||
| 72 | 82 | describe("rewriteMarkdownFileUriHref", () => { |
| 73 | 83 | it("rewrites file uri hrefs into direct path hrefs", () => { |
| 74 | 84 | expect(rewriteMarkdownFileUriHref("file:///Users/julius/project/src/main.ts#L42")).toBe( |
| ... | ... | @@ -198,6 +208,20 @@ describe("resolveMarkdownFileLinkTarget", () => { |
| 198 | 208 | }); |
| 199 | 209 | }); |
| 200 | 210 | |
| 211 | it.each(["md", "html", "xml"])( | |
| 212 | "resolves a bare spaced .%s filename from the markdown renderer", | |
| 213 | (extension) => { | |
| 214 | const href = renderMarkdownLinkHref(`[checklist](<Updated cutover checklist.${extension}>)`); | |
| 215 | ||
| 216 | expect(href).toBe(`Updated%20cutover%20checklist.${extension}`); | |
| 217 | expect(resolveMarkdownFileLinkMeta(href, "/repo/project")).toMatchObject({ | |
| 218 | targetPath: `/repo/project/Updated cutover checklist.${extension}`, | |
| 219 | workspaceRelativePath: `Updated cutover checklist.${extension}`, | |
| 220 | basename: `Updated cutover checklist.${extension}`, | |
| 221 | }); | |
| 222 | }, | |
| 223 | ); | |
| 224 | ||
| 201 | 225 | it("formats tooltip display paths relative to the cwd for slash-prefixed windows paths", () => { |
| 202 | 226 | expect( |
| 203 | 227 | resolveMarkdownFileLinkMeta( |
apps/web/src/markdown-links.ts+6-1| ... | ... | @@ -11,7 +11,8 @@ const EXTERNAL_SCHEME_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):(.*)$/; |
| 11 | 11 | const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; |
| 12 | 12 | const RELATIVE_FILE_PATH_PATTERN = |
| 13 | 13 | /^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2}$/; |
| 14 | const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; | |
| 14 | const RELATIVE_FILE_NAME_PATTERN = | |
| 15 | /^[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; | |
| 15 | 16 | const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 16 | 17 | const POSITION_ONLY_PATTERN = /^\d+(?::\d+)?$/; |
| 17 | 18 | // Standard OS and dev-container roots; deliberately excludes app-route-ish |
| ... | ... | @@ -71,6 +72,10 @@ export function shouldOpenMarkdownFileLinkInEditor( |
| 71 | 72 | return isTerminalLinkActivation(event, platform); |
| 72 | 73 | } |
| 73 | 74 | |
| 75 | export function shouldOpenMarkdownFileLinkInBrowserByDefault(path: string): boolean { | |
| 76 | return /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? ""); | |
| 77 | } | |
| 78 | ||
| 74 | 79 | function safeDecode(value: string): string { |
| 75 | 80 | try { |
| 76 | 81 | return decodeURIComponent(value); |