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
24 changes: 23 additions & 1 deletion internal/skill/folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ var (
mdLinkRe = regexp.MustCompile(`\]\(([^)]+)\)`)
)

// looksLikeURL reports whether s looks like a URL rather than a file path.
// It checks for protocol prefixes (http://, https://, ftp://) and domain-like
// patterns (e.g., example.com/path).
func looksLikeURL(s string) bool {
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "ftp://") {
return true
}
// Check for domain-like pattern: word with dot before the first slash
// e.g., "example.com/path", "docs.example.io/guide"
slashIdx := strings.Index(s, "/")
if slashIdx > 0 {
prefix := s[:slashIdx]
if strings.Contains(prefix, ".") && !strings.HasPrefix(prefix, ".") && !strings.HasPrefix(prefix, "..") {
return true
}
}
return false
}

// ExtractFileReferences extracts path-like references from a markdown body.
// It looks for backtick-enclosed paths containing '/' and markdown link targets
// that are not URLs or anchors.
Expand All @@ -53,6 +72,9 @@ func ExtractFileReferences(body string) []string {

for _, m := range backtickPathRe.FindAllStringSubmatch(body, -1) {
p := m[1]
if looksLikeURL(p) {
continue
}
if !seen[p] {
seen[p] = true
refs = append(refs, p)
Expand All @@ -61,7 +83,7 @@ func ExtractFileReferences(body string) []string {

for _, m := range mdLinkRe.FindAllStringSubmatch(body, -1) {
p := m[1]
if strings.HasPrefix(p, "http") || strings.HasPrefix(p, "#") {
if looksLikeURL(p) || strings.HasPrefix(p, "#") {
continue
}
if !seen[p] {
Expand Down
66 changes: 66 additions & 0 deletions internal/skill/folder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,41 @@ func TestExtractFileReferences(t *testing.T) {
body: "Run `scripts/run.py` or see [script](scripts/run.py).",
expected: []string{"scripts/run.py"},
},
{
name: "backtick URL with protocol skipped",
body: "See `https://example.com/docs/guide` for details.",
expected: nil,
},
{
name: "backtick URL without protocol skipped",
body: "Use `example.com/api/v1` as the endpoint.",
expected: nil,
},
{
name: "backtick domain-like paths skipped",
body: "Check `docs.example.io/guide` and `pkg.go.dev/encoding/json` for reference.",
expected: nil,
},
{
name: "markdown link with domain-like URL skipped",
body: "See [docs](docs.example.com/guide) for more info.",
expected: nil,
},
{
name: "backtick URL mixed with real path",
body: "Use `example.com/api` for the API and `scripts/run.py` for processing.",
expected: []string{"scripts/run.py"},
},
{
name: "relative path with dots not skipped",
body: "Use `./scripts/run.py` and `../lib/utils.py` as helpers.",
expected: []string{"./scripts/run.py", "../lib/utils.py"},
},
{
name: "ftp URL in backtick skipped",
body: "Download from `ftp://files.example.com/data`.",
expected: nil,
},
}

for _, tt := range tests {
Expand All @@ -133,6 +168,31 @@ func TestExtractFileReferences(t *testing.T) {
}
}

func TestLooksLikeURL(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"https://example.com/path", true},
{"http://example.com/path", true},
{"ftp://files.example.com/data", true},
{"example.com/path", true},
{"docs.example.io/guide", true},
{"pkg.go.dev/encoding/json", true},
{"scripts/run.py", false},
{"./scripts/run.py", false},
{"../lib/utils.py", false},
{"assets/template.json", false},
{"C://Windows//Users//", false},
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, looksLikeURL(tt.input))
})
}
}

func TestValidateFolder(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -161,6 +221,12 @@ func TestValidateFolder(t *testing.T) {
setup: func(dir string) {},
wantIssues: 0,
},
{
name: "URLs in backticks do not warn",
body: "See `https://example.com/docs` and `example.com/api/v1` for details.",
setup: func(dir string) {},
wantIssues: 0,
},
}

for _, tt := range tests {
Expand Down
Loading