server.garden privileged automation agent (mirror of https://git.sequentialread.com/forest/rootsystem)
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
764 lines
23 KiB
764 lines
23 KiB
package automation |
|
|
|
import ( |
|
"encoding/xml" |
|
"fmt" |
|
"html" |
|
"regexp" |
|
"strconv" |
|
"strings" |
|
|
|
errors "git.sequentialread.com/forest/pkg-errors" |
|
) |
|
|
|
type XMLAttr struct { |
|
Name string |
|
Value string |
|
Namespace string |
|
Local string |
|
} |
|
|
|
type XMLNode struct { |
|
XMLName xml.Name |
|
Attrs []*XMLAttr `xml:"-"` |
|
Content []byte `xml:",innerxml"` |
|
Children []*XMLNode `xml:",any"` |
|
Parent *XMLNode `xml:"-"` |
|
} |
|
|
|
type XMLQuery struct { |
|
DirectChild bool |
|
NodeType string |
|
Class string |
|
Attr string |
|
AttrValue string |
|
} |
|
|
|
type Rect2D struct { |
|
NonEmpty bool |
|
Top float64 |
|
Left float64 |
|
Bottom float64 |
|
Right float64 |
|
} |
|
|
|
const terraformSvgFontFamily = "-apple-system,system-ui,BlinkMacSystemFont,Ubuntu,Roboto,Segoe UI,sans-serif" |
|
const terraformActionRedClass = "red" |
|
const terraformActionYellowClass = "yellow" |
|
const terraformActionGreenClass = "green" |
|
const terraformActionCircleRadius = 14 |
|
const terraformActionFontSize = 18 |
|
const terraformActionTextOffsetY = 5 |
|
const terraformActionOffsetY = 4 |
|
const terraformActionOffsetX = -4 |
|
const terraformActionTextSpacingX = 7 |
|
const dotStringCharactersMinPadding = 12 |
|
const dotStringLinearPadding = float32(0) |
|
|
|
func (node *XMLNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { |
|
attrPointers := []*XMLAttr{} |
|
for _, attr := range start.Attr { |
|
// I dont really give a shit about XML namespaces but I do have to search for xlink:title which is separate from normal title |
|
// hardcoded that this is happening inside the svg namespace so all non svg namespaces attrs will be specified like ns:attr |
|
name := attr.Name.Local |
|
if attr.Name.Space != "" { |
|
// attr.Name.Space is a url like http://www.w3.org/1999/xlink or ttp://www.w3.org/2000/svg so we are just grabbing the last part |
|
split := strings.Split(attr.Name.Space, "/") |
|
ns := split[len(split)-1] |
|
if ns != "svg" { |
|
name = fmt.Sprintf("%s:%s", ns, name) |
|
} |
|
} |
|
myAttr := XMLAttr{ |
|
Name: name, |
|
Value: attr.Value, |
|
Namespace: attr.Name.Space, |
|
Local: attr.Name.Local, |
|
} |
|
attrPointers = append(attrPointers, &myAttr) |
|
} |
|
node.Attrs = attrPointers |
|
|
|
// this little derived type wont directly have this UnmarshalXML method, |
|
// thus prevent the decoder from thinking it has to invoke our custom UnmarshalXML again... |
|
type dontCauseInfiniteLoop XMLNode |
|
|
|
return d.DecodeElement((*dontCauseInfiniteLoop)(node), &start) |
|
} |
|
|
|
func (node XMLNode) MarshalXML(e *xml.Encoder, start xml.StartElement) error { |
|
|
|
attrsToEncode := []xml.Attr{} |
|
for _, attr := range node.Attrs { |
|
// this encoder will print some stupid shit if you give it an xmlns attr: |
|
// for example: <svg ... xmlns:_ _xmlns:xlink="http://www.w3.org/1999/xlink"> |
|
if attr.Namespace != "xmlns" { |
|
attrsToEncode = append(attrsToEncode, xml.Attr{ |
|
Name: xml.Name{ |
|
Local: attr.Local, |
|
Space: attr.Namespace, |
|
}, |
|
Value: attr.Value, |
|
}) |
|
} |
|
|
|
} |
|
start.Attr = attrsToEncode |
|
start.Name = node.XMLName |
|
|
|
// this little derived type wont directly have this UnmarshalXML method, |
|
// thus prevent the decoder from thinking it has to invoke our custom UnmarshalXML again... |
|
type dontCauseInfiniteLoop XMLNode |
|
|
|
return e.EncodeElement((dontCauseInfiniteLoop)(node), start) |
|
} |
|
|
|
func makeSVGFromSimpleStatus(simpleStatus *SimplifiedTerraformStatus) ([]byte, error) { |
|
|
|
moduleDots := []string{} |
|
for moduleName, module := range simpleStatus.Modules { |
|
resourceDots := []string{} |
|
for _, resource := range module.Resources { |
|
id := fmt.Sprintf(`%s_%s`, moduleName, resource.DisplayName) |
|
id = strings.ReplaceAll(strings.ReplaceAll(id, ".", "_"), "-", "_") |
|
resourceStyle := `, margin = 0.1, shape = "box3d"` |
|
if resource.DisplayName == "none" { |
|
resourceStyle = "" |
|
} |
|
tooltip := "" |
|
if !strings.HasPrefix(moduleName, "module.ansible-") && resource.DisplayName != "none" { |
|
tooltip = fmt.Sprintf(`, tooltip = "%s.%s"`, moduleName, resource.DisplayName) |
|
} |
|
resourceDot := fmt.Sprintf(` |
|
"%s" [label = "%s"%s%s]`, |
|
id, padStringForDot(resource.DisplayName), tooltip, resourceStyle, |
|
) |
|
resourceDots = append(resourceDots, resourceDot) |
|
} |
|
i := 0 |
|
for _, resource := range module.Resources { |
|
if i != 0 { |
|
id0 := fmt.Sprintf(`%s_%s`, moduleName, module.Resources[i-1].DisplayName) |
|
id0 = strings.ReplaceAll(strings.ReplaceAll(id0, ".", "_"), "-", "_") |
|
id1 := fmt.Sprintf(`%s_%s`, moduleName, resource.DisplayName) |
|
id1 = strings.ReplaceAll(strings.ReplaceAll(id1, ".", "_"), "-", "_") |
|
resourceDot := fmt.Sprintf(` |
|
"%s" -> "%s" [style=invis]`, id0, id1, |
|
) |
|
resourceDots = append(resourceDots, resourceDot) |
|
} |
|
i++ |
|
} |
|
moduleDot := fmt.Sprintf(` |
|
subgraph cluster_%s { |
|
bgcolor = lightgrey; |
|
tooltip = "%s"; |
|
label = "%s"; |
|
%s |
|
}`, |
|
strings.ReplaceAll(strings.ReplaceAll(moduleName, ".", "_"), "-", "_"), |
|
moduleName, |
|
padStringForDot(moduleName), |
|
strings.Join(resourceDots, ""), |
|
) |
|
//fmt.Println(moduleName) |
|
moduleDots = append(moduleDots, moduleDot) |
|
} |
|
|
|
variableDots := []string{} |
|
connectedVariables := map[string]bool{} |
|
for _, connection := range simpleStatus.Connections { |
|
if connection.From == "data.terraform_remote_state" { |
|
variableDot := fmt.Sprintf(` |
|
"%s" [label = "%s", tooltip = "%s", margin = 0.1, shape = "note"];`, |
|
strings.ReplaceAll(strings.ReplaceAll(connection.From, ".", "_"), "-", "_"), |
|
padStringForDot(remote_state_placeholder_text), |
|
connection.From, |
|
) |
|
variableDots = append(variableDots, variableDot) |
|
} else { |
|
split := strings.Split(connection.From, ".") |
|
connectedVariables[split[len(split)-1]] = true |
|
} |
|
} |
|
for variableName, value := range simpleStatus.Variables { |
|
if connectedVariables[variableName] { |
|
variableDot := fmt.Sprintf(` |
|
"var_%s" [label = "%s", tooltip = "%s", margin = 0.1, shape = "note"];`, |
|
strings.ReplaceAll(variableName, "-", "_"), padStringForDot(value), fmt.Sprintf("var.%s", variableName), |
|
) |
|
variableDots = append(variableDots, variableDot) |
|
} |
|
} |
|
|
|
edgeMap := map[string]bool{} |
|
connectionDots := []string{} |
|
for _, connection := range simpleStatus.Connections { |
|
|
|
tailLocation := fmt.Sprintf( |
|
"ltail=\"cluster_%s\", ", |
|
strings.ReplaceAll(strings.ReplaceAll(connection.From, ".", "_"), "-", "_"), |
|
) |
|
headLocation := fmt.Sprintf( |
|
"lhead=\"cluster_%s\", ", |
|
strings.ReplaceAll(strings.ReplaceAll(connection.To, ".", "_"), "-", "_"), |
|
) |
|
from := connection.From |
|
if strings.HasPrefix(connection.From, "var") || connection.From == "data.terraform_remote_state" { |
|
tailLocation = "" |
|
} else { |
|
fromResources := simpleStatus.Modules[connection.From].Resources |
|
from = fmt.Sprintf("%s_%s", connection.From, fromResources[len(fromResources)-1].DisplayName) |
|
} |
|
|
|
toResources := simpleStatus.Modules[connection.To].Resources |
|
to := fmt.Sprintf("%s_%s", connection.To, toResources[0].DisplayName) |
|
|
|
from = strings.ReplaceAll(strings.ReplaceAll(from, ".", "_"), "-", "_") |
|
to = strings.ReplaceAll(strings.ReplaceAll(to, ".", "_"), "-", "_") |
|
paramName := strings.ReplaceAll(strings.ReplaceAll(connection.DisplayName, ".", "_"), "-", "_") |
|
|
|
sourceEdge := fmt.Sprintf(` |
|
"%s" -> "param_%s" [%sarrowhead=none];`, |
|
from, paramName, tailLocation, |
|
) |
|
if !edgeMap[sourceEdge] { |
|
edgeMap[sourceEdge] = true |
|
} else { |
|
sourceEdge = "" |
|
} |
|
destEdge := fmt.Sprintf(` |
|
"param_%s" -> "%s" [%s];`, |
|
paramName, to, headLocation, |
|
) |
|
if !edgeMap[destEdge] { |
|
edgeMap[destEdge] = true |
|
} else { |
|
destEdge = "" |
|
} |
|
|
|
connectionDot := fmt.Sprintf(` |
|
"param_%s" [label = "%s", shape = "box", style=filled, color=lightgrey]; %s %s`, |
|
paramName, connection.DisplayName, sourceEdge, destEdge, |
|
) |
|
connectionDots = append(connectionDots, connectionDot) |
|
} |
|
|
|
dot := fmt.Sprintf(` |
|
digraph { |
|
compound = "true" |
|
newrank = "true" |
|
ranksep = 0.1; |
|
|
|
%s |
|
%s |
|
%s |
|
}`, |
|
strings.Join(moduleDots, ""), |
|
strings.Join(variableDots, ""), |
|
strings.Join(connectionDots, ""), |
|
) |
|
|
|
exitCode, dotStdout, dotStderr, err := shellExecInputPipe(".", &dot, "dot", "-Tsvg") |
|
err = errorFromShellExecResult("dot -Tsvg", exitCode, dotStdout, dotStderr, err) |
|
if err != nil { |
|
return []byte{}, err |
|
} |
|
|
|
svgString := string(dotStdout) |
|
|
|
// the svg output from dot has html comments that contain metadata preceeding each entity. |
|
// for example: |
|
// <!-- param_ssh_private_key_filepath --> |
|
// <g id="node16" class="node"> |
|
// We will attach that metadata to the svg elements as a data property |
|
// Note that this will not work perfectly for the "cluster"s (aka modules) |
|
// they are handled separately after the SVG has been inserted. |
|
// Yes parsing XML with regex is "evil" but keep in mind, this XML was generated by a program, not written by hand. |
|
// So it is a lot more predictable. Also, I couldn't figure out how to parse the comments with the go std lib XML parser. |
|
commentRegex := regexp.MustCompile(`<!--\s*(.+)\s*-->\s*\n\s*(<[a-z]+\s)`) |
|
svgString = commentRegex.ReplaceAllStringFunc(svgString, func(matched string) string { |
|
matches := commentRegex.FindStringSubmatch(matched) |
|
return fmt.Sprintf("%s dot=\"%s\" ", matches[2], strings.Trim(html.UnescapeString(matches[1]), " ")) |
|
}) |
|
|
|
//ioutil.WriteFile("./test-gen.svg", []byte(svgString), 0777) |
|
|
|
var svgDoc XMLNode |
|
err = xml.Unmarshal(dotStdout, &svgDoc) |
|
if err != nil { |
|
panic(err) |
|
} |
|
|
|
var setParentRecurse func(*XMLNode) |
|
setParentRecurse = func(node *XMLNode) { |
|
for _, child := range node.Children { |
|
child.Parent = node |
|
setParentRecurse(child) |
|
} |
|
} |
|
setParentRecurse(&svgDoc) |
|
|
|
// remove the white background from the SVG |
|
svgDoc.WithQuerySelector( |
|
// 'g.graph > polygon' |
|
[]XMLQuery{XMLQuery{NodeType: "g", Class: "graph"}, XMLQuery{NodeType: "polygon", DirectChild: true}}, |
|
func(node *XMLNode) { |
|
node.SetAttr("fill", "none") |
|
}, |
|
) |
|
|
|
// correctly set the dot property for resources, variables, and parameters |
|
svgDoc.WithQuerySelector( |
|
// 'g.node > title' |
|
[]XMLQuery{XMLQuery{NodeType: "g", Class: "node"}, XMLQuery{NodeType: "title", DirectChild: true}}, |
|
func(node *XMLNode) { |
|
if node.Parent != nil { |
|
title := html.UnescapeString(string(node.Content)) |
|
node.Parent.SetAttr("data-dot", title) |
|
if strings.HasPrefix(title, "var_") || strings.HasPrefix(title, "data_") { |
|
node.Parent.SetAttr("class", "variable") |
|
} |
|
if strings.HasPrefix(title, "param_") { |
|
node.Parent.SetAttr("class", "parameter") |
|
|
|
// the parameters start out as boxes |
|
//but lets squeeze them into keystone shape to differentiate them a bit |
|
node.Parent.WithQuerySelector([]XMLQuery{XMLQuery{NodeType: "polygon"}}, func(polygonNode *XMLNode) { |
|
vertices, err := polygonNode.GetSVGPolygonVertices() |
|
if err == nil { |
|
center := []float64{0, 0} |
|
for _, vertex := range vertices { |
|
center[0] += vertex[0] |
|
center[1] += vertex[1] |
|
} |
|
center[0] = center[0] / float64(len(vertices)) |
|
center[1] = center[1] / float64(len(vertices)) |
|
|
|
newVertices := [][]float64{} |
|
for _, vertex := range vertices { |
|
x := vertex[0] |
|
y := vertex[1] |
|
if y > center[1] { |
|
if x > center[0] { |
|
x -= 10 |
|
} else { |
|
x += 10 |
|
} |
|
} |
|
newVertices = append(newVertices, []float64{x, y}) |
|
} |
|
polygonNode.SetSVGPolygonVertices(newVertices) |
|
} |
|
|
|
}) |
|
} |
|
if strings.HasPrefix(title, "module_") { |
|
node.Parent.SetAttr("class", "resource") |
|
} |
|
} |
|
}, |
|
) |
|
|
|
// correctly set the dot property for the "cluster"s (aka modules) |
|
moduleNames := []string{} |
|
svgDoc.WithQuerySelector( |
|
// 'a[xlink:title]' |
|
[]XMLQuery{XMLQuery{NodeType: "a", Attr: "xlink:title"}}, |
|
func(node *XMLNode) { |
|
if node.Parent != nil && node.Parent.Parent != nil { |
|
title := html.UnescapeString(node.GetAttr("xlink:title")) |
|
title = regexp.MustCompile(`[.-]`).ReplaceAllString(title, "_") |
|
if node.Parent.Parent.GetAttr("data-dot") == "" { |
|
moduleNames = append(moduleNames, title) |
|
node.Parent.Parent.SetAttr("data-dot", title) |
|
node.Parent.Parent.SetAttr("class", "module") |
|
} |
|
|
|
} |
|
}, |
|
) |
|
|
|
// correctly set the dot property for edges |
|
// in order to get dot to render it correctly, the connections are actually between resources, |
|
// not between modules. So we have to trim the resource name off of the dot attribute |
|
// on the connections to make them match what we have in the status JSON object. |
|
svgDoc.WithQuerySelector( |
|
// 'g.edge title' |
|
[]XMLQuery{XMLQuery{NodeType: "g", Class: "edge"}, XMLQuery{NodeType: "title"}}, |
|
func(node *XMLNode) { |
|
if node.Parent != nil { |
|
title := html.UnescapeString(string(node.Content)) |
|
fromTo := strings.Split(title, "->") |
|
if len(fromTo) != 2 { |
|
fmt.Printf("malformed dot string on svg edge: fromTo.length != 2: %s", title) |
|
} |
|
|
|
// if this edge starts or ends at a resource inside a module, override the dot string |
|
// to make it start or end at that module instead. |
|
for _, moduleName := range moduleNames { |
|
if strings.HasPrefix(fromTo[0], moduleName) { |
|
node.Parent.SetAttr("data-dot", html.EscapeString(fmt.Sprintf("%s->%s", moduleName, fromTo[1]))) |
|
} |
|
if strings.HasPrefix(fromTo[1], moduleName) { |
|
node.Parent.SetAttr("data-dot", html.EscapeString(fmt.Sprintf("%s->%s", fromTo[0], moduleName))) |
|
} |
|
} |
|
} |
|
}, |
|
) |
|
|
|
svgDoc.WithQuerySelector( |
|
// '[font-family]' |
|
[]XMLQuery{XMLQuery{Attr: "font-family"}}, |
|
func(node *XMLNode) { |
|
node.SetAttr("font-family", terraformSvgFontFamily) |
|
}, |
|
) |
|
|
|
// two last things to do: |
|
// 1. center each resource horizontally within the bounding box of its parent module |
|
// because dot is dumb and it just kinda throws them wherever |
|
// 2. create little create/delete/modify icons next to each changed resource |
|
// similar to how terraform displays -, +, +/-, or ~ on each line-item in the plan |
|
|
|
for moduleName, module := range simpleStatus.Modules { |
|
|
|
moduleId := regexp.MustCompile(`[.-]`).ReplaceAllString(moduleName, "_") |
|
svgDoc.WithQuerySelector( |
|
[]XMLQuery{XMLQuery{Attr: "data-dot", AttrValue: moduleId}}, |
|
func(node *XMLNode) { |
|
moduleRect := node.GetBoundingBox() |
|
|
|
for _, resource := range module.Resources { |
|
resourceId := fmt.Sprintf("%s_%s", moduleId, regexp.MustCompile(`[.-]`).ReplaceAllString(resource.DisplayName, "_")) |
|
svgDoc.WithQuerySelector( |
|
// '[data-dot=module_dns_gandi_gandi_livedns_record_dns_entries]' for example |
|
[]XMLQuery{XMLQuery{Attr: "data-dot", AttrValue: resourceId}}, |
|
func(node *XMLNode) { |
|
resourceRect := node.GetBoundingBox() |
|
|
|
// Step 1 center the resources |
|
nudgeX := ((moduleRect.Left + moduleRect.Right) - (resourceRect.Left + resourceRect.Right)) * float64(0.5) |
|
node.SetAttr("transform", fmt.Sprintf("scale(1 1) rotate(0) translate(%d %d)", int(nudgeX), 0)) |
|
|
|
// Step 2 add the terraform action display if there is a planned action |
|
if resource.Plan != "none" && resource.Plan != "" { |
|
|
|
actionX := resourceRect.Left + nudgeX + float64(terraformActionOffsetX) |
|
actionY := resourceRect.Top + float64(terraformActionOffsetY) |
|
|
|
// start with a circle element |
|
actionContents := []*XMLNode{ |
|
&XMLNode{ |
|
XMLName: xml.Name{Local: "circle", Space: "http://www.w3.org/2000/svg"}, |
|
Attrs: []*XMLAttr{ |
|
&XMLAttr{ |
|
Local: "cx", |
|
Value: fmt.Sprintf("%.4f", actionX), |
|
}, |
|
&XMLAttr{ |
|
Local: "fill", |
|
Value: "#ffffff", |
|
}, |
|
&XMLAttr{ |
|
Local: "cy", |
|
Value: fmt.Sprintf("%.4f", actionY), |
|
}, |
|
&XMLAttr{ |
|
Local: "r", |
|
Value: strconv.Itoa(terraformActionCircleRadius), |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
// macro to add text to the inside of the circle |
|
createText := func(str, class string, offset int) { |
|
actionContents = append(actionContents, &XMLNode{ |
|
XMLName: xml.Name{Local: "text", Space: "http://www.w3.org/2000/svg"}, |
|
Attrs: []*XMLAttr{ |
|
&XMLAttr{Local: "text-anchor", Value: "middle"}, |
|
&XMLAttr{Local: "font-family", Value: terraformSvgFontFamily}, |
|
&XMLAttr{Local: "text-size", Value: strconv.Itoa(terraformActionFontSize)}, |
|
&XMLAttr{Local: "class", Value: class}, |
|
&XMLAttr{Local: "x", Value: fmt.Sprintf("%.4f", actionX+float64(offset*terraformActionTextSpacingX))}, |
|
&XMLAttr{Local: "y", Value: fmt.Sprintf("%.4f", actionY+float64(terraformActionTextOffsetY))}, |
|
}, |
|
Content: []byte(str), |
|
}) |
|
} |
|
|
|
// add text to the circle depending on the planned action |
|
if resource.Plan == "recreate" { |
|
createText("-", terraformActionRedClass, -1) |
|
createText("/", "", 0) |
|
createText("+", terraformActionGreenClass, 1) |
|
} |
|
if resource.Plan == "update" { |
|
createText("~", terraformActionYellowClass, 0) |
|
} |
|
if resource.Plan == "create" { |
|
createText("+", terraformActionGreenClass, 0) |
|
} |
|
if resource.Plan == "delete" { |
|
createText("-", terraformActionRedClass, 0) |
|
} |
|
|
|
// Finally, add all the action content inside a single <g> tag & append to the svg |
|
node.Parent.Children = append(node.Parent.Children, &XMLNode{ |
|
XMLName: xml.Name{Local: "g", Space: "http://www.w3.org/2000/svg"}, |
|
Attrs: []*XMLAttr{ |
|
&XMLAttr{ |
|
Local: "class", |
|
Value: "action-circle", |
|
}, |
|
&XMLAttr{ |
|
Local: "data-dot", |
|
Value: fmt.Sprintf("%s_action_circle", resourceId), |
|
}, |
|
}, |
|
Children: actionContents, |
|
}) |
|
|
|
} |
|
}, |
|
) |
|
|
|
} |
|
|
|
}, |
|
) |
|
|
|
} |
|
|
|
var cleanUpSvgRecurse func(*XMLNode) |
|
cleanUpSvgRecurse = func(node *XMLNode) { |
|
// We cant serialize cyclical relationships like parent -> child -> parent |
|
node.Parent = nil |
|
|
|
// if we don't zero out the content for non-leaf nodes, we will get duplicates of everything |
|
if len(node.Children) != 0 { |
|
node.Content = []byte{} |
|
} |
|
|
|
for _, child := range node.Children { |
|
cleanUpSvgRecurse(child) |
|
} |
|
} |
|
cleanUpSvgRecurse(&svgDoc) |
|
|
|
// testJson2, err := json.MarshalIndent(svgDoc, "", " ") |
|
// if err != nil { |
|
// panic(err) |
|
// } |
|
// ioutil.WriteFile("./test-processed.json", testJson2, 0777) |
|
|
|
svgBytes, err := xml.MarshalIndent(svgDoc, "", " ") |
|
|
|
// it goes crazy spamming xmlns everywhere, lets just remove all of them. |
|
svgBytes = regexp.MustCompile(`xmlns[^=]*="[^"]+"`).ReplaceAll(svgBytes, []byte{}) |
|
|
|
// now we add the xmlns back to just the root element |
|
startOfSvgTagWithXMLNS := []byte(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" `) |
|
svgBytes = regexp.MustCompile(`^\s*<svg\s*`).ReplaceAll(svgBytes, startOfSvgTagWithXMLNS) |
|
|
|
svgHeader := []byte(`<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" |
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> |
|
<!-- Generated by graphviz & server.garden rootsystem --> |
|
`) |
|
|
|
svgBytes = append(svgHeader, svgBytes...) |
|
|
|
return svgBytes, nil |
|
} |
|
|
|
func padStringForDot(str string) string { |
|
pad := strings.Repeat(" ", int(float32(len(str)+dotStringCharactersMinPadding)*dotStringLinearPadding)) |
|
return fmt.Sprintf("%s%s%s", pad, str, pad) |
|
} |
|
|
|
func (doc *XMLNode) QuerySelectorAll(querySteps []XMLQuery) []*XMLNode { |
|
|
|
iterate := func(search []*XMLNode, q XMLQuery) []*XMLNode { |
|
toReturn := []*XMLNode{} |
|
var recurse func(*XMLNode, bool) |
|
recurse = func(node *XMLNode, isDirectChild bool) { |
|
match := true |
|
if q.NodeType != "" && node.XMLName.Local != q.NodeType { |
|
match = false |
|
} |
|
if q.Class != "" { |
|
found := false |
|
for _, attr := range node.Attrs { |
|
classes := strings.Split(attr.Value, " ") |
|
if attr.Name == "class" { |
|
for _, class := range classes { |
|
if class == q.Class { |
|
found = true |
|
} |
|
} |
|
} |
|
} |
|
if !found { |
|
match = false |
|
} |
|
} |
|
if q.Attr != "" { |
|
found := false |
|
for _, attr := range node.Attrs { |
|
if attr.Name == q.Attr { |
|
if q.AttrValue == "" || attr.Value == q.AttrValue { |
|
found = true |
|
} |
|
} |
|
} |
|
if !found { |
|
match = false |
|
} |
|
} |
|
if match && (isDirectChild || !q.DirectChild) { |
|
toReturn = append(toReturn, node) |
|
} |
|
if isDirectChild || !q.DirectChild { |
|
for _, child := range node.Children { |
|
recurse(child, false) |
|
} |
|
} |
|
} |
|
for _, node := range search { |
|
for _, child := range node.Children { |
|
recurse(child, true) |
|
} |
|
} |
|
|
|
return toReturn |
|
} |
|
|
|
matched := []*XMLNode{doc} |
|
for _, step := range querySteps { |
|
matched = iterate(matched, step) |
|
} |
|
|
|
return matched |
|
} |
|
|
|
func (doc *XMLNode) WithQuerySelector(querySteps []XMLQuery, withResult func(*XMLNode)) { |
|
allResults := doc.QuerySelectorAll(querySteps) |
|
for _, res := range allResults { |
|
withResult(res) |
|
} |
|
} |
|
|
|
func (node *XMLNode) SetAttr(attr string, value string) { |
|
has := false |
|
for _, a := range node.Attrs { |
|
if a.Name == attr { |
|
a.Value = value |
|
has = true |
|
} |
|
} |
|
if !has { |
|
newAttr := XMLAttr{ |
|
Name: attr, |
|
Value: value, |
|
Local: attr, |
|
} |
|
node.Attrs = append(node.Attrs, &newAttr) |
|
} |
|
} |
|
|
|
func (node *XMLNode) GetAttr(attr string) string { |
|
for _, a := range node.Attrs { |
|
if a.Name == attr { |
|
return a.Value |
|
} |
|
} |
|
return "" |
|
} |
|
|
|
// Get Rect Kid |
|
func (rect *Rect2D) Encapsulate(x, y float64) { |
|
if !rect.NonEmpty { |
|
rect.Bottom = y |
|
rect.Top = y |
|
rect.Left = x |
|
rect.Right = x |
|
rect.NonEmpty = true |
|
} else { |
|
if x < rect.Left { |
|
rect.Left = x |
|
} |
|
if x > rect.Right { |
|
rect.Right = x |
|
} |
|
if y < rect.Top { |
|
rect.Top = y |
|
} |
|
if y > rect.Bottom { |
|
rect.Bottom = y |
|
} |
|
} |
|
} |
|
|
|
func (node *XMLNode) GetBoundingBox() *Rect2D { |
|
|
|
toReturn := Rect2D{} |
|
polygons := node.QuerySelectorAll([]XMLQuery{XMLQuery{NodeType: "polygon"}}) |
|
ellipses := node.QuerySelectorAll([]XMLQuery{XMLQuery{NodeType: "ellipse"}}) |
|
|
|
for _, polygon := range polygons { |
|
vertices, err := polygon.GetSVGPolygonVertices() |
|
if err == nil { |
|
for _, vertex := range vertices { |
|
toReturn.Encapsulate(vertex[0], vertex[1]) |
|
} |
|
} |
|
} |
|
|
|
for _, ellipse := range ellipses { |
|
cx, errCx := strconv.ParseFloat(ellipse.GetAttr("cx"), 64) |
|
cy, errCy := strconv.ParseFloat(ellipse.GetAttr("cy"), 64) |
|
rx, errRx := strconv.ParseFloat(ellipse.GetAttr("rx"), 64) |
|
ry, errRy := strconv.ParseFloat(ellipse.GetAttr("ry"), 64) |
|
if errCx == nil && errCy == nil && errRx == nil && errRy == nil { |
|
toReturn.Encapsulate(cx-rx, cy) |
|
toReturn.Encapsulate(cx+rx, cy) |
|
toReturn.Encapsulate(cx, cy-ry) |
|
toReturn.Encapsulate(cx, cy+ry) |
|
} |
|
} |
|
|
|
return &toReturn |
|
} |
|
|
|
func (node *XMLNode) GetSVGPolygonVertices() ([][]float64, error) { |
|
toReturn := [][]float64{} |
|
var err error |
|
|
|
points := strings.Split(node.GetAttr("points"), " ") |
|
for _, point := range points { |
|
coords := strings.Split(point, ",") |
|
if len(coords) == 2 { |
|
x, errX := strconv.ParseFloat(coords[0], 64) |
|
y, errY := strconv.ParseFloat(coords[1], 64) |
|
if errX == nil && errY == nil { |
|
toReturn = append(toReturn, []float64{x, y}) |
|
} else { |
|
err = errors.New("parsing svg polygon points failed cuz strconv.ParseFloat failed") |
|
} |
|
} else { |
|
err = errors.New("parsing svg polygon points failed cuz len(coords) != 2") |
|
} |
|
} |
|
return toReturn, err |
|
} |
|
|
|
func (node *XMLNode) SetSVGPolygonVertices(points [][]float64) { |
|
coordStrings := []string{} |
|
for _, point := range points { |
|
coordStrings = append(coordStrings, fmt.Sprintf("%.4f,%.4f", point[0], point[1])) |
|
} |
|
node.SetAttr("points", strings.Join(coordStrings, " ")) |
|
}
|
|
|