Escape double quotes, replace center-chars to left-aligns, and add tests (#557)

diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go
index 09debfb..cde648f 100644
--- a/internal/graph/dotgraph.go
+++ b/internal/graph/dotgraph.go
@@ -127,7 +127,7 @@
 	}
 	title := labels[0]
 	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
-	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
+	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeForDot(labels), `\l`))
 	if b.config.LegendURL != "" {
 		fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL)
 	}
@@ -472,3 +472,14 @@
 	}
 	return b
 }
+
+// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's
+// "center" character (\n) with a left-justified character.
+// See https://graphviz.org/doc/info/attrs.html#k:escString for more info.
+func escapeForDot(in []string) []string {
+	var out = make([]string, len(in))
+	for i := range in {
+		out[i] = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(in[i], `\`, `\\`), `"`, `\"`), "\n", `\l`)
+	}
+	return out
+}
diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go
index b8368b8..4232efa 100644
--- a/internal/graph/dotgraph_test.go
+++ b/internal/graph/dotgraph_test.go
@@ -178,7 +178,7 @@
 	}
 	c := &DotConfig{
 		Title:  "testtitle",
-		Labels: []string{"label1", "label2"},
+		Labels: []string{"label1", "label2", `label3: "foo"`},
 		Total:  100,
 		FormatValue: func(v int64) string {
 			return strconv.FormatInt(v, 10)
@@ -326,6 +326,46 @@
 	}
 }
 
+func TestEscapeForDot(t *testing.T) {
+	for _, tc := range []struct {
+		desc  string
+		input []string
+		want  []string
+	}{
+		{
+			desc:  "with multiple doubles quotes",
+			input: []string{`label: "foo" and "bar"`},
+			want:  []string{`label: \"foo\" and \"bar\"`},
+		},
+		{
+			desc:  "with graphviz center line character",
+			input: []string{"label: foo \n bar"},
+			want:  []string{`label: foo \l bar`},
+		},
+		{
+			desc:  "with two backslashes",
+			input: []string{`label: \\`},
+			want:  []string{`label: \\\\`},
+		},
+		{
+			desc:  "with two double quotes together",
+			input: []string{`label: ""`},
+			want:  []string{`label: \"\"`},
+		},
+		{
+			desc:  "with multiple labels",
+			input: []string{`label1: "foo"`, `label2: "bar"`},
+			want:  []string{`label1: \"foo\"`, `label2: \"bar\"`},
+		},
+	} {
+		t.Run(tc.desc, func(t *testing.T) {
+			if got := escapeForDot(tc.input); !reflect.DeepEqual(got, tc.want) {
+				t.Errorf("escapeForDot(%s) = %s, want %s", tc.input, got, tc.want)
+			}
+		})
+	}
+}
+
 func tagString(t []*Tag) string {
 	var ret []string
 	for _, s := range t {
diff --git a/internal/graph/testdata/compose1.dot b/internal/graph/testdata/compose1.dot
index da349a4..a0842ee 100644
--- a/internal/graph/testdata/compose1.dot
+++ b/internal/graph/testdata/compose1.dot
@@ -1,6 +1,6 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
 N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
 N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
diff --git a/internal/graph/testdata/compose2.dot b/internal/graph/testdata/compose2.dot
index 0c1a6eb..44c2aec 100644
--- a/internal/graph/testdata/compose2.dot
+++ b/internal/graph/testdata/compose2.dot
@@ -1,6 +1,6 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
 N1 [label="SRC10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=24 shape=folder tooltip="src (25)" color="#b23c00" fillcolor="#edddd5" style="bold,filled" peripheries=2 URL="www.google.com" target="_blank"]
 N2 [label="dest\n0 of 25 (25.00%)" id="node2" fontsize=8 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
diff --git a/internal/graph/testdata/compose3.dot b/internal/graph/testdata/compose3.dot
index 1b878b7..f22ad9f 100644
--- a/internal/graph/testdata/compose3.dot
+++ b/internal/graph/testdata/compose3.dot
@@ -1,6 +1,6 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
 N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
 N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
 N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
diff --git a/internal/graph/testdata/compose4.dot b/internal/graph/testdata/compose4.dot
index 302da8c..ed770d1 100644
--- a/internal/graph/testdata/compose4.dot
+++ b/internal/graph/testdata/compose4.dot
@@ -1,4 +1,4 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
 }
diff --git a/internal/graph/testdata/compose5.dot b/internal/graph/testdata/compose5.dot
index 8876e33..3f2285c 100644
--- a/internal/graph/testdata/compose5.dot
+++ b/internal/graph/testdata/compose5.dot
@@ -1,6 +1,6 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
 N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
 N1_0 [label = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
 N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
diff --git a/internal/graph/testdata/compose6.dot b/internal/graph/testdata/compose6.dot
index cf88439..1dfc3fe 100644
--- a/internal/graph/testdata/compose6.dot
+++ b/internal/graph/testdata/compose6.dot
@@ -1,6 +1,6 @@
 digraph "testtitle" {
 node [style=filled fillcolor="#f8f8f8"]
-subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" URL="http://example.com" target="_blank" tooltip="testtitle"] }
+subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" URL="http://example.com" target="_blank" tooltip="testtitle"] }
 N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
 N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
 N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)"]
diff --git a/internal/report/report.go b/internal/report/report.go
index a345208..bc5685d 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -1207,7 +1207,7 @@
 	// Help new users understand the graph.
 	// A new line is intentionally added here to better show this message.
 	if fullHeaders {
-		label = append(label, "\\lSee https://git.io/JfYMW for how to read the graph")
+		label = append(label, "\nSee https://git.io/JfYMW for how to read the graph")
 	}
 
 	return label