package gsp import ( "fmt" "regexp" "sort" "strings" ) func (p *Project) Graph(id string, depth int) Graph { if id != "" { flattened := p.Flatten(id, depth, Filter{}) return p.graphForUnits(flattened.Units) } return p.graphForUnits(p.Units) } func (p *Project) graphForUnits(units []*Unit) Graph { nodeMap := map[string]GraphNode{} edgeMap := map[string]GraphEdge{} include := map[string]bool{} for _, unit := range units { include[unit.ID] = true nodeMap[unit.ID] = GraphNode{ID: unit.ID, Type: unit.Type, Resolution: unit.Resolution, File: unit.File} } for _, unit := range units { if unit.Refines != "" { addEdge(edgeMap, unit.ID, unit.Refines, "refines") if !include[unit.Refines] { nodeMap[unit.Refines] = GraphNode{ID: unit.Refines, Missing: p.ByID[unit.Refines] == nil} } } for _, rel := range unit.With { addEdge(edgeMap, unit.ID, rel.ID, "with") if !include[rel.ID] { nodeMap[rel.ID] = GraphNode{ID: rel.ID, Missing: p.ByID[rel.ID] == nil} } } } nodes := make([]GraphNode, 0, len(nodeMap)) for _, node := range nodeMap { nodes = append(nodes, node) } sort.Slice(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID }) edges := make([]GraphEdge, 0, len(edgeMap)) for _, edge := range edgeMap { edges = append(edges, edge) } sort.Slice(edges, func(i, j int) bool { if edges[i].From == edges[j].From { if edges[i].To == edges[j].To { return edges[i].Kind < edges[j].Kind } return edges[i].To < edges[j].To } return edges[i].From < edges[j].From }) return Graph{Nodes: nodes, Edges: edges} } func addEdge(edges map[string]GraphEdge, from, to, kind string) { key := from + "\x00" + to + "\x00" + kind edges[key] = GraphEdge{From: from, To: to, Kind: kind} } var mermaidIDPattern = regexp.MustCompile(`[^A-Za-z0-9_]`) func (g Graph) Mermaid() string { var builder strings.Builder builder.WriteString("graph TD\n") if len(g.Nodes) == 0 { return builder.String() } for _, node := range g.Nodes { label := node.ID if node.Missing { label += " (missing)" } builder.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", mermaidID(node.ID), escapeMermaid(label))) } for _, edge := range g.Edges { builder.WriteString(fmt.Sprintf(" %s -- %s --> %s\n", mermaidID(edge.From), edge.Kind, mermaidID(edge.To))) } return builder.String() } func mermaidID(id string) string { value := mermaidIDPattern.ReplaceAllString(id, "_") if value == "" { return "node" } if value[0] >= '0' && value[0] <= '9' { value = "n_" + value } return value } func escapeMermaid(value string) string { value = strings.ReplaceAll(value, `"`, `\"`) return value }