Files
GSP/toolkit/internal/gsp/graph.go

297 lines
6.8 KiB
Go

package gsp
import (
"encoding/json"
"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, Title: unit.Title, 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.DisplayLabel()
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 (g Graph) Markdown() string {
var builder strings.Builder
builder.WriteString("# GSP Graph\n\n")
builder.WriteString("```mermaid\n")
builder.WriteString(g.Mermaid())
builder.WriteString("```\n")
return builder.String()
}
func (g Graph) Canvas() ([]byte, error) {
canvas := canvasDocument{
Nodes: make([]canvasNode, 0, len(g.Nodes)),
Edges: make([]canvasEdge, 0, len(g.Edges)),
}
positions := g.canvasPositions()
for _, node := range g.Nodes {
pos := positions[node.ID]
canvas.Nodes = append(canvas.Nodes, canvasNode{
ID: canvasNodeID(node.ID),
Type: "text",
Text: node.CanvasText(),
X: pos.X,
Y: pos.Y,
Width: 280,
Height: 140,
Color: node.CanvasColor(),
})
}
for _, edge := range g.Edges {
canvas.Edges = append(canvas.Edges, canvasEdge{
ID: canvasEdgeID(edge),
FromNode: canvasNodeID(edge.From),
FromSide: "right",
ToNode: canvasNodeID(edge.To),
ToSide: "left",
Label: edge.Kind,
})
}
data, err := json.MarshalIndent(canvas, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
func (n GraphNode) DisplayLabel() string {
title := strings.TrimSpace(n.Title)
if title != "" {
return title
}
return n.ID
}
func (n GraphNode) CanvasText() string {
var lines []string
lines = append(lines, "## "+n.DisplayLabel())
if n.Title != "" && n.ID != "" {
lines = append(lines, "`"+n.ID+"`")
}
if n.Type != "" {
lines = append(lines, "type: "+n.Type)
}
if n.Resolution != "" {
lines = append(lines, "resolution: "+n.Resolution)
}
if n.Missing {
lines = append(lines, "missing: true")
}
return strings.Join(lines, "\n")
}
func (n GraphNode) CanvasColor() string {
if n.Missing {
return "1"
}
switch n.Resolution {
case "L0", "L1":
return "2"
case "L2":
return "3"
case "L3":
return "4"
case "L4", "L5":
return "5"
default:
return ""
}
}
type canvasDocument struct {
Nodes []canvasNode `json:"nodes"`
Edges []canvasEdge `json:"edges"`
}
type canvasNode struct {
ID string `json:"id"`
Type string `json:"type"`
Text string `json:"text"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Color string `json:"color,omitempty"`
}
type canvasEdge struct {
ID string `json:"id"`
FromNode string `json:"fromNode"`
FromSide string `json:"fromSide,omitempty"`
ToNode string `json:"toNode"`
ToSide string `json:"toSide,omitempty"`
Label string `json:"label,omitempty"`
}
type canvasPosition struct {
X int
Y int
}
func (g Graph) canvasPositions() map[string]canvasPosition {
incoming := map[string]int{}
outgoing := map[string][]string{}
for _, node := range g.Nodes {
incoming[node.ID] = 0
}
for _, edge := range g.Edges {
if _, ok := incoming[edge.From]; !ok {
incoming[edge.From] = 0
}
if _, ok := incoming[edge.To]; !ok {
incoming[edge.To] = 0
}
incoming[edge.To]++
outgoing[edge.From] = append(outgoing[edge.From], edge.To)
}
for id := range outgoing {
sort.Strings(outgoing[id])
}
var queue []string
for _, node := range g.Nodes {
if incoming[node.ID] == 0 {
queue = append(queue, node.ID)
}
}
if len(queue) == 0 {
for _, node := range g.Nodes {
queue = append(queue, node.ID)
}
}
sort.Strings(queue)
depth := map[string]int{}
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
for _, to := range outgoing[id] {
if depth[to] < depth[id]+1 {
depth[to] = depth[id] + 1
}
incoming[to]--
if incoming[to] == 0 {
queue = append(queue, to)
sort.Strings(queue)
}
}
}
columns := map[int][]string{}
for _, node := range g.Nodes {
columns[depth[node.ID]] = append(columns[depth[node.ID]], node.ID)
}
positions := map[string]canvasPosition{}
var keys []int
for key := range columns {
keys = append(keys, key)
}
sort.Ints(keys)
for _, key := range keys {
sort.Strings(columns[key])
for row, id := range columns[key] {
positions[id] = canvasPosition{X: key * 360, Y: row * 190}
}
}
return positions
}
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
}
func canvasNodeID(id string) string {
return "gsp_" + mermaidID(id)
}
func canvasEdgeID(edge GraphEdge) string {
return "edge_" + mermaidID(edge.From) + "_" + mermaidID(edge.Kind) + "_" + mermaidID(edge.To)
}