mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
171 lines
6.6 KiB
Swift
171 lines
6.6 KiB
Swift
import Foundation
|
|
import HapiUI
|
|
import Testing
|
|
|
|
/// File-path autolink detection (remark-file-path-links port) + the
|
|
/// `hapi-file://` URL round trip.
|
|
@Suite("File path detection")
|
|
struct FilePathDetectionTests {
|
|
private func paths(in text: String) -> [String] {
|
|
MarkdownTransforms.detectFilePathRanges(in: text).map { $0.link.path }
|
|
}
|
|
|
|
// MARK: - Positive cases
|
|
|
|
@Test func linksRelativeCodePath() {
|
|
let text = "see src/lib/foo.ts here"
|
|
let hits = MarkdownTransforms.detectFilePathRanges(in: text)
|
|
#expect(hits.count == 1)
|
|
#expect(hits.first?.link == FilePathLink(path: "src/lib/foo.ts", line: nil))
|
|
if let range = hits.first?.range {
|
|
#expect(String(text[range]) == "src/lib/foo.ts")
|
|
}
|
|
}
|
|
|
|
@Test func stripsLineSuffixIntoLineNumber() {
|
|
let text = "open src/lib/foo.ts:12 please"
|
|
let hits = MarkdownTransforms.detectFilePathRanges(in: text)
|
|
#expect(hits.first?.link == FilePathLink(path: "src/lib/foo.ts", line: 12))
|
|
if let range = hits.first?.range {
|
|
// Display range keeps the :12 suffix.
|
|
#expect(String(text[range]) == "src/lib/foo.ts:12")
|
|
}
|
|
}
|
|
|
|
@Test func lineColumnSuffixKeepsLineOnly() {
|
|
let hits = MarkdownTransforms.detectFilePathRanges(in: "at foo.ts:12:5")
|
|
#expect(hits.first?.link == FilePathLink(path: "foo.ts", line: 12))
|
|
}
|
|
|
|
@Test func linksDotSlashPaths() {
|
|
// The `./` prefix stays in the target path (web parity).
|
|
#expect(paths(in: "run ./x/y.tsx now") == ["./x/y.tsx"])
|
|
}
|
|
|
|
@Test func linksBareFilenames() {
|
|
#expect(paths(in: "See screenshot.png and README.md") == ["screenshot.png", "README.md"])
|
|
}
|
|
|
|
@Test func excludesTrailingSentencePunctuation() {
|
|
let text = "Edit src/a.ts."
|
|
let hits = MarkdownTransforms.detectFilePathRanges(in: text)
|
|
#expect(hits.first?.link.path == "src/a.ts")
|
|
if let range = hits.first?.range {
|
|
#expect(String(text[range]) == "src/a.ts")
|
|
}
|
|
}
|
|
|
|
@Test func linksPathInsideParens() {
|
|
#expect(paths(in: "(src/a.ts)") == ["src/a.ts"])
|
|
}
|
|
|
|
@Test func linksNodeJsLikeTokens() {
|
|
// `js` is an allowlisted extension, so `Node.js` links (web parity —
|
|
// the allowlist exists to reject TLD lookalikes, not dotted words
|
|
// with real code extensions).
|
|
#expect(paths(in: "built on Node.js today") == ["Node.js"])
|
|
}
|
|
|
|
// MARK: - Negative cases
|
|
|
|
@Test func doesNotLinkInsideURLs() {
|
|
#expect(paths(in: "https://example.org/x.ts").isEmpty)
|
|
#expect(paths(in: "see https://example.org today").isEmpty)
|
|
}
|
|
|
|
@Test func doesNotLinkTLDLookalikes() {
|
|
#expect(paths(in: "visit example.org and example.com and hapi.dev").isEmpty)
|
|
}
|
|
|
|
@Test func doesNotLinkVersionNumbers() {
|
|
#expect(paths(in: "bump to v1.2.3 now").isEmpty)
|
|
}
|
|
|
|
@Test func doesNotLinkAbsoluteOrParentPaths() {
|
|
#expect(paths(in: "/abs/path.ts").isEmpty)
|
|
#expect(paths(in: "~/notes.md").isEmpty)
|
|
#expect(paths(in: "../rel/foo.ts").isEmpty)
|
|
}
|
|
|
|
@Test func doesNotLinkWindowsAbsolutePaths() {
|
|
// iOS delta: the web autolinks these behind workspace containment;
|
|
// HapiUI has no containment context, so it fails closed.
|
|
#expect(paths(in: #"open C:\src\a.ts now"#).isEmpty)
|
|
}
|
|
|
|
@Test func doesNotLinkUnknownExtensions() {
|
|
#expect(paths(in: "cargo build --bin foo.abcxyzqwerty").isEmpty)
|
|
}
|
|
|
|
// MARK: - Inline code linking
|
|
|
|
@Test func linksWholeValueInlineCodePath() {
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "src/a.ts") == FilePathLink(path: "src/a.ts", line: nil))
|
|
}
|
|
|
|
@Test func linksBareFilenameInlineCodeWithLine() {
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "README.md:12") == FilePathLink(path: "README.md", line: 12))
|
|
}
|
|
|
|
@Test func linksMermaidSourceInlineCode() {
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "docs/flow.mmd") == FilePathLink(path: "docs/flow.mmd", line: nil))
|
|
}
|
|
|
|
@Test func rejectsCodeSnippetsInInlineCode() {
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "npm run build") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "str.split()") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "Math.PI") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "a=b.js") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "x.md#y") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "") == nil)
|
|
}
|
|
|
|
@Test func rejectsUnsafeInlineCodePaths() {
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "/etc/passwd.conf") == nil)
|
|
#expect(MarkdownTransforms.filePathLink(forInlineCode: "../secrets.env") == nil)
|
|
}
|
|
|
|
// MARK: - Explicit link rewrite
|
|
|
|
@Test func rewritesRelativeMarkdownLink() {
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "docs/a.md") == FilePathLink(path: "docs/a.md", line: nil))
|
|
}
|
|
|
|
@Test func rewritesRelativeLinkWithLineSuffix() {
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "docs/a.md:7") == FilePathLink(path: "docs/a.md", line: 7))
|
|
}
|
|
|
|
@Test func rewritesRelativeLinkStrippingFragment() {
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "docs/a.md#section") == FilePathLink(path: "docs/a.md", line: nil))
|
|
}
|
|
|
|
@Test func rejectsNonFileHrefs() {
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "/settings") == nil)
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "/abs/file.md") == nil)
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "../up.md") == nil)
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "https://x.com/a.md") == nil)
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: #"C:\x\y.ts"#) == nil)
|
|
#expect(MarkdownTransforms.fileLinkTarget(forExplicitHref: "#section") == nil)
|
|
}
|
|
|
|
// MARK: - hapi-file URL round trip
|
|
|
|
@Test func filePathLinkURLRoundTripsWithLine() throws {
|
|
let link = FilePathLink(path: "src/a b.ts", line: 12)
|
|
let url = try #require(link.url)
|
|
#expect(url.scheme == "hapi-file")
|
|
#expect(FilePathLink(url: url) == link)
|
|
}
|
|
|
|
@Test func filePathLinkURLRoundTripsWithoutLine() throws {
|
|
let link = FilePathLink(path: "README.md")
|
|
let url = try #require(link.url)
|
|
#expect(FilePathLink(url: url) == link)
|
|
}
|
|
|
|
@Test func rejectsForeignURLs() throws {
|
|
let https = try #require(URL(string: "https://example.com/?path=x"))
|
|
#expect(FilePathLink(url: https) == nil)
|
|
}
|
|
}
|