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 @@ + +ninetynineeighty eightseventy seven \ 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 @@ + +ninetynineeighty eightseventy seven \ 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 +}