diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index fa5f12f07..90d54bc6d 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -1347,10 +1347,23 @@ y -> x.style
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
+ assert.String(t, `"b\nb"`, g.Objects[0].ID)
assert.String(t, `b
b`, g.Objects[0].Attributes.Label.Value)
},
},
+ {
+ name: "unescaped_id_cr",
+
+ text: `b\rb`,
+ assertions: func(t *testing.T, g *d2graph.Graph) {
+ if len(g.Objects) != 1 {
+ t.Fatal(g.Objects)
+ }
+ assert.String(t, "b\rb", g.Objects[0].ID)
+ assert.String(t, "b\rb", g.Objects[0].Attributes.Label.Value)
+ },
+ },
{
name: "class_style",
diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go
index 3b6885bf8..def678ee0 100644
--- a/d2layouts/d2dagrelayout/layout.go
+++ b/d2layouts/d2dagrelayout/layout.go
@@ -256,15 +256,23 @@ func setGraphAttrs(attrs dagreGraphAttrs) string {
)
}
+func escapeID(id string) string {
+ id = strings.ReplaceAll(id, `\n`, `\\n`)
+ // avoid an unescaped \r becoming a \n in the layout result
+ id = strings.ReplaceAll(id, "\r", `\r`)
+ return id
+}
+
func generateAddNodeLine(id string, width, height int) string {
+ id = escapeID(id)
return fmt.Sprintf("g.setNode(`%s`, { id: `%s`, width: %d, height: %d });\n", id, id, width, height)
}
func generateAddParentLine(childID, parentID string) string {
- return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", childID, parentID)
+ return fmt.Sprintf("g.setParent(`%s`, `%s`);\n", escapeID(childID), escapeID(parentID))
}
func generateAddEdgeLine(fromID, toID, edgeID string) string {
// in dagre v is from, w is to, name is to uniquely identify
- return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s` });\n", fromID, toID, edgeID)
+ return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID))
}
diff --git a/e2etests/regression_test.go b/e2etests/regression_test.go
index d2ec37c80..fc0f6c7c7 100644
--- a/e2etests/regression_test.go
+++ b/e2etests/regression_test.go
@@ -5,7 +5,16 @@ import (
)
func testRegression(t *testing.T) {
- tcs := []testCase{}
+ tcs := []testCase{
+ {
+ name: "dagre_id_with_newline",
+ script: `
+ninety\nnine
+eighty\reight
+seventy\r\nseven
+`,
+ },
+ }
runa(t, tcs)
}
diff --git a/e2etests/testdata/regression/dagre_id_with_newline/dagre/board.exp.json b/e2etests/testdata/regression/dagre_id_with_newline/dagre/board.exp.json
new file mode 100644
index 000000000..be2cc9969
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_id_with_newline/dagre/board.exp.json
@@ -0,0 +1,123 @@
+{
+ "name": "",
+ "shapes": [
+ {
+ "id": "\"ninety\\nnine\"",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 151,
+ "height": 142,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ninety\nnine",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 42,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "eighty\reight",
+ "type": "",
+ "pos": {
+ "x": 211,
+ "y": 8
+ },
+ "width": 151,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "eighty\reight",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "\"seventy\r\\nseven\"",
+ "type": "",
+ "pos": {
+ "x": 422,
+ "y": 0
+ },
+ "width": 162,
+ "height": 142,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "seventy\r\nseven",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 62,
+ "labelHeight": 42,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/regression/dagre_id_with_newline/dagre/sketch.exp.svg b/e2etests/testdata/regression/dagre_id_with_newline/dagre/sketch.exp.svg
new file mode 100644
index 000000000..eeb82e050
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_id_with_newline/dagre/sketch.exp.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/regression/dagre_id_with_newline/elk/board.exp.json b/e2etests/testdata/regression/dagre_id_with_newline/elk/board.exp.json
new file mode 100644
index 000000000..a2a7c0e1f
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_id_with_newline/elk/board.exp.json
@@ -0,0 +1,123 @@
+{
+ "name": "",
+ "shapes": [
+ {
+ "id": "\"ninety\\nnine\"",
+ "type": "",
+ "pos": {
+ "x": 194,
+ "y": 12
+ },
+ "width": 151,
+ "height": 142,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ninety\nnine",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 42,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "eighty\reight",
+ "type": "",
+ "pos": {
+ "x": 365,
+ "y": 20
+ },
+ "width": 151,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "eighty\reight",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "\"seventy\r\\nseven\"",
+ "type": "",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 162,
+ "height": 142,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "seventy\r\nseven",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 62,
+ "labelHeight": 42,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/regression/dagre_id_with_newline/elk/sketch.exp.svg b/e2etests/testdata/regression/dagre_id_with_newline/elk/sketch.exp.svg
new file mode 100644
index 000000000..c2b3fa691
--- /dev/null
+++ b/e2etests/testdata/regression/dagre_id_with_newline/elk/sketch.exp.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/testdata/d2compiler/TestCompile/unescaped_id_cr.exp.json b/testdata/d2compiler/TestCompile/unescaped_id_cr.exp.json
new file mode 100644
index 000000000..f9ae8f701
--- /dev/null
+++ b/testdata/d2compiler/TestCompile/unescaped_id_cr.exp.json
@@ -0,0 +1,102 @@
+{
+ "graph": {
+ "ast": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "nodes": [
+ {
+ "map_key": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "value": [
+ {
+ "string": "b\rb",
+ "raw_string": "b\\rb"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "primary": {},
+ "value": {}
+ }
+ }
+ ]
+ },
+ "root": {
+ "id": "",
+ "id_val": "",
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "attributes": {
+ "label": {
+ "value": ""
+ },
+ "style": {},
+ "near_key": null,
+ "shape": {
+ "value": ""
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ },
+ "edges": null,
+ "objects": [
+ {
+ "id": "b\rb",
+ "id_val": "b\rb",
+ "label_dimensions": {
+ "width": 0,
+ "height": 0
+ },
+ "references": [
+ {
+ "key": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "path": [
+ {
+ "unquoted_string": {
+ "range": "d2/testdata/d2compiler/TestCompile/unescaped_id_cr.d2,0:0:0-0:4:4",
+ "value": [
+ {
+ "string": "b\rb",
+ "raw_string": "b\\rb"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "key_path_index": 0,
+ "map_key_edge_index": 0
+ }
+ ],
+ "attributes": {
+ "label": {
+ "value": "b\rb"
+ },
+ "style": {},
+ "near_key": null,
+ "shape": {
+ "value": ""
+ },
+ "direction": {
+ "value": ""
+ }
+ },
+ "zIndex": 0
+ }
+ ]
+ },
+ "err": null
+}