144 lines
3.5 KiB
Go
144 lines
3.5 KiB
Go
package gsp
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var allowedLinkRoles = map[string]bool{
|
|
"reference": true,
|
|
"source": true,
|
|
"binding": true,
|
|
"output": true,
|
|
"evidence": true,
|
|
}
|
|
|
|
func (p *Project) Links(id string, depth int) LinkResult {
|
|
flattened := p.Flatten(id, depth, Filter{})
|
|
links := p.resolveLinks(flattened.Units)
|
|
summary := summarizeLinks(links)
|
|
return LinkResult{
|
|
Entry: id,
|
|
Depth: depth,
|
|
Links: links,
|
|
Summary: summary,
|
|
Warnings: flattened.Warnings,
|
|
}
|
|
}
|
|
|
|
func (p *Project) resolveLinks(units []*Unit) []ResolvedLink {
|
|
var result []ResolvedLink
|
|
for _, unit := range units {
|
|
for _, link := range unit.Links {
|
|
result = append(result, p.resolveLink(unit, link))
|
|
}
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
if result[i].Owner == result[j].Owner {
|
|
return result[i].Path < result[j].Path
|
|
}
|
|
return result[i].Owner < result[j].Owner
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (p *Project) resolveLink(unit *Unit, link Link) ResolvedLink {
|
|
role := strings.TrimSpace(link.Role)
|
|
if role == "" {
|
|
role = "reference"
|
|
}
|
|
resolved := ResolvedLink{
|
|
Owner: unit.ID,
|
|
Title: unit.Title,
|
|
Path: strings.TrimSpace(link.Path),
|
|
Role: role,
|
|
Context: strings.TrimSpace(link.Context),
|
|
File: unit.File,
|
|
}
|
|
resolved.Kind, resolved.Status, resolved.Exists = p.classifyLink(resolved.Path)
|
|
return resolved
|
|
}
|
|
|
|
func (p *Project) classifyLink(path string) (kind, status string, exists bool) {
|
|
if path == "" {
|
|
return "unknown", "invalid", false
|
|
}
|
|
if isURL(path) {
|
|
parsed, err := url.ParseRequestURI(path)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "url", "invalid", false
|
|
}
|
|
return "url", "unchecked", false
|
|
}
|
|
checkPath := path
|
|
if !filepath.IsAbs(checkPath) {
|
|
checkPath = filepath.Join(p.Root, filepath.FromSlash(checkPath))
|
|
}
|
|
info, err := os.Stat(checkPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "missing", "missing", false
|
|
}
|
|
return "unknown", "invalid", false
|
|
}
|
|
if info.IsDir() {
|
|
return "folder", "ok", true
|
|
}
|
|
return "file", "ok", true
|
|
}
|
|
|
|
func isURL(value string) bool {
|
|
lower := strings.ToLower(value)
|
|
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://")
|
|
}
|
|
|
|
func summarizeLinks(links []ResolvedLink) LinkSummary {
|
|
summary := LinkSummary{
|
|
Total: len(links),
|
|
ByKind: map[string]int{},
|
|
ByRole: map[string]int{},
|
|
ByState: map[string]int{},
|
|
}
|
|
for _, link := range links {
|
|
summary.ByKind[link.Kind]++
|
|
summary.ByRole[link.Role]++
|
|
summary.ByState[link.Status]++
|
|
}
|
|
if len(summary.ByKind) == 0 {
|
|
summary.ByKind = nil
|
|
}
|
|
if len(summary.ByRole) == 0 {
|
|
summary.ByRole = nil
|
|
}
|
|
if len(summary.ByState) == 0 {
|
|
summary.ByState = nil
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func (r LinkResult) Markdown() string {
|
|
var builder strings.Builder
|
|
builder.WriteString("# GSP Links\n\n")
|
|
builder.WriteString(fmt.Sprintf("- Entry: `%s`\n", r.Entry))
|
|
builder.WriteString(fmt.Sprintf("- Links: `%d`\n", r.Summary.Total))
|
|
if len(r.Links) == 0 {
|
|
builder.WriteString("\nNo links.\n")
|
|
return builder.String()
|
|
}
|
|
builder.WriteString("\n## Links\n\n")
|
|
for _, link := range r.Links {
|
|
builder.WriteString(fmt.Sprintf("- `%s` %s\n", link.Owner, link.Path))
|
|
builder.WriteString(fmt.Sprintf(" - role: `%s`\n", link.Role))
|
|
builder.WriteString(fmt.Sprintf(" - kind: `%s`\n", link.Kind))
|
|
builder.WriteString(fmt.Sprintf(" - status: `%s`\n", link.Status))
|
|
if link.Context != "" {
|
|
builder.WriteString(fmt.Sprintf(" - context: %s\n", link.Context))
|
|
}
|
|
}
|
|
return builder.String()
|
|
}
|