Files
GSP/toolkit/internal/gsp/links.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()
}