Add graph markdown and canvas outputs

This commit is contained in:
2026-05-06 20:00:50 +08:00
parent 1478972e53
commit f7f5e2c67c
22 changed files with 313 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
package gsp
import (
"encoding/json"
"fmt"
"regexp"
"sort"
@@ -21,7 +22,7 @@ func (p *Project) graphForUnits(units []*Unit) Graph {
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}
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 != "" {
@@ -74,7 +75,7 @@ func (g Graph) Mermaid() string {
return builder.String()
}
for _, node := range g.Nodes {
label := node.ID
label := node.DisplayLabel()
if node.Missing {
label += " (missing)"
}
@@ -86,6 +87,190 @@ func (g Graph) Mermaid() string {
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 == "" {
@@ -101,3 +286,11 @@ 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)
}