diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go
index 41ad5498d..5a5b7c57d 100644
--- a/e2etests/e2e_test.go
+++ b/e2etests/e2e_test.go
@@ -38,6 +38,7 @@ func TestE2E(t *testing.T) {
t.Run("regression", testRegression)
t.Run("todo", testTodo)
t.Run("measured", testMeasured)
+ t.Run("unicode", testUnicode)
}
func testSanity(t *testing.T) {
diff --git a/e2etests/testdata/regression/dagre_special_ids/dagre/board.exp.json b/e2etests/testdata/regression/dagre_special_ids/dagre/board.exp.json
index d7c736755..2cb1ebfab 100644
--- a/e2etests/testdata/regression/dagre_special_ids/dagre/board.exp.json
+++ b/e2etests/testdata/regression/dagre_special_ids/dagre/board.exp.json
@@ -91,7 +91,7 @@
"x": 302,
"y": 0
},
- "width": 102,
+ "width": 97,
"height": 82,
"opacity": 1,
"strokeDash": 0,
@@ -119,7 +119,7 @@
"italic": false,
"bold": true,
"underline": false,
- "labelWidth": 57,
+ "labelWidth": 52,
"labelHeight": 37,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
@@ -129,7 +129,7 @@
"id": "\"a\\\\yode\"",
"type": "rectangle",
"pos": {
- "x": 464,
+ "x": 459,
"y": 8
},
"width": 94,
@@ -170,7 +170,7 @@
"id": "there",
"type": "rectangle",
"pos": {
- "x": 624,
+ "x": 619,
"y": 182
},
"width": 83,
@@ -211,7 +211,7 @@
"id": "'a\\\"ode'",
"type": "rectangle",
"pos": {
- "x": 618,
+ "x": 613,
"y": 8
},
"width": 94,
@@ -252,7 +252,7 @@
"id": "\"a\\\\node\"",
"type": "rectangle",
"pos": {
- "x": 772,
+ "x": 767,
"y": 8
},
"width": 95,
@@ -317,19 +317,19 @@
"labelPercentage": 0,
"route": [
{
- "x": 511,
+ "x": 506,
"y": 74
},
{
- "x": 511,
+ "x": 506,
"y": 120.4
},
{
- "x": 533.5,
+ "x": 528.5,
"y": 144.12662337662337
},
{
- "x": 623.5,
+ "x": 618.5,
"y": 192.63311688311688
}
],
@@ -365,19 +365,19 @@
"labelPercentage": 0,
"route": [
{
- "x": 665,
+ "x": 660,
"y": 74
},
{
- "x": 665,
+ "x": 660,
"y": 120.4
},
{
- "x": 665,
+ "x": 660,
"y": 142
},
{
- "x": 665,
+ "x": 660,
"y": 182
}
],
@@ -413,19 +413,19 @@
"labelPercentage": 0,
"route": [
{
- "x": 819.5,
+ "x": 814.5,
"y": 74
},
{
- "x": 819.5,
+ "x": 814.5,
"y": 120.4
},
{
- "x": 796.9,
+ "x": 791.9,
"y": 144
},
{
- "x": 706.5,
+ "x": 701.5,
"y": 192
}
],
diff --git a/e2etests/testdata/regression/dagre_special_ids/dagre/sketch.exp.svg b/e2etests/testdata/regression/dagre_special_ids/dagre/sketch.exp.svg
index 35734e017..5b4018056 100644
--- a/e2etests/testdata/regression/dagre_special_ids/dagre/sketch.exp.svg
+++ b/e2etests/testdata/regression/dagre_special_ids/dagre/sketch.exp.svg
@@ -3,7 +3,7 @@
id="d2-svg"
style="background: white;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
-width="1071" height="452" viewBox="-102 -102 1071 452">๐๐๐๐๐๐๐๐โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/emojis/elk/board.exp.json b/e2etests/testdata/unicode/emojis/elk/board.exp.json
new file mode 100644
index 000000000..2e524b35a
--- /dev/null
+++ b/e2etests/testdata/unicode/emojis/elk/board.exp.json
@@ -0,0 +1,170 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 205,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "๐๐๐๐๐๐๐๐",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 160,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ",
+ "type": "rectangle",
+ "pos": {
+ "x": 237,
+ "y": 12
+ },
+ "width": 833,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 788,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "โ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ",
+ "type": "rectangle",
+ "pos": {
+ "x": 201,
+ "y": 148
+ },
+ "width": 905,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "โ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 860,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -> โ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ)[0]",
+ "src": "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "โ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 653.5,
+ "y": 78
+ },
+ {
+ "x": 653.5,
+ "y": 148
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/unicode/emojis/elk/sketch.exp.svg b/e2etests/testdata/unicode/emojis/elk/sketch.exp.svg
new file mode 100644
index 000000000..7ccc8cb4f
--- /dev/null
+++ b/e2etests/testdata/unicode/emojis/elk/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-basic/dagre/board.exp.json b/e2etests/testdata/unicode/japanese-basic/dagre/board.exp.json
new file mode 100644
index 000000000..4bc654165
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-basic/dagre/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 246,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใใใใใใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 201,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/unicode/japanese-basic/dagre/sketch.exp.svg b/e2etests/testdata/unicode/japanese-basic/dagre/sketch.exp.svg
new file mode 100644
index 000000000..b30f6e8c4
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-basic/dagre/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-basic/elk/board.exp.json b/e2etests/testdata/unicode/japanese-basic/elk/board.exp.json
new file mode 100644
index 000000000..7b54eaf15
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-basic/elk/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 246,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใใใใใใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 201,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/unicode/japanese-basic/elk/sketch.exp.svg b/e2etests/testdata/unicode/japanese-basic/elk/sketch.exp.svg
new file mode 100644
index 000000000..29d9caa5c
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-basic/elk/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-full/dagre/board.exp.json b/e2etests/testdata/unicode/japanese-full/dagre/board.exp.json
new file mode 100644
index 000000000..76f3ee1c3
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-full/dagre/board.exp.json
@@ -0,0 +1,138 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 1382,
+ "height": 98,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใๆฅใใใใใ้ใๆญฉใใฆใใใใ้่ทฏใฎๅใใใใใญใฅใฆใชใใใฃใฆๆฅใพใใใ\nใใใใฏ้ฉใใฆๅฐใญใพใใใ\nใใญใฅใฆใชใใใใฉใใใฆใใชใใฏใใใซใใใฎใงใใ๏ผใ ใญใฅใฆใชใฏ็ญใใพใใใใใใชใใจๅใ็็ฑใงใใใซใใพใใใตใฉใใซใชใใใใซใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 1337,
+ "labelHeight": 53,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 8,
+ "y": 219
+ },
+ "width": 1367,
+ "height": 115,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใใฏ็ฎใๅฅใใฆ้ฃในใใใฎใงใใใ",
+ "fontSize": 55,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 1322,
+ "labelHeight": 70,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "ใใใซใฏๆญปใชใชใใๆฒปใใชใใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 289,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 691,
+ "y": 98
+ },
+ {
+ "x": 691,
+ "y": 146.4
+ },
+ {
+ "x": 691,
+ "y": 170.7
+ },
+ {
+ "x": 691,
+ "y": 219.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/unicode/japanese-full/dagre/sketch.exp.svg b/e2etests/testdata/unicode/japanese-full/dagre/sketch.exp.svg
new file mode 100644
index 000000000..e821ce14a
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-full/dagre/sketch.exp.svg
@@ -0,0 +1,59 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-full/elk/board.exp.json b/e2etests/testdata/unicode/japanese-full/elk/board.exp.json
new file mode 100644
index 000000000..08cca5c14
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-full/elk/board.exp.json
@@ -0,0 +1,129 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 1382,
+ "height": 98,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใๆฅใใใใใ้ใๆญฉใใฆใใใใ้่ทฏใฎๅใใใใใญใฅใฆใชใใใฃใฆๆฅใพใใใ\nใใใใฏ้ฉใใฆๅฐใญใพใใใ\nใใญใฅใฆใชใใใใฉใใใฆใใชใใฏใใใซใใใฎใงใใ๏ผใ ใญใฅใฆใชใฏ็ญใใพใใใใใใชใใจๅใ็็ฑใงใใใซใใพใใใตใฉใใซใชใใใใซใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 1337,
+ "labelHeight": 53,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 19,
+ "y": 271
+ },
+ "width": 1367,
+ "height": 115,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใใฏ็ฎใๅฅใใฆ้ฃในใใใฎใงใใใ",
+ "fontSize": 55,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 1322,
+ "labelHeight": 70,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "ใใใซใฏๆญปใชใชใใๆฒปใใชใใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 289,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 703,
+ "y": 110
+ },
+ {
+ "x": 703,
+ "y": 271
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/unicode/japanese-full/elk/sketch.exp.svg b/e2etests/testdata/unicode/japanese-full/elk/sketch.exp.svg
new file mode 100644
index 000000000..16635fff6
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-full/elk/sketch.exp.svg
@@ -0,0 +1,59 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-mixed/dagre/board.exp.json b/e2etests/testdata/unicode/japanese-mixed/dagre/board.exp.json
new file mode 100644
index 000000000..506dfab48
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-mixed/dagre/board.exp.json
@@ -0,0 +1,494 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 1646,
+ "y": 0
+ },
+ "width": 668,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 623,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 166
+ },
+ "width": 3959,
+ "height": 171,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ",
+ "fontSize": 100,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 3914,
+ "labelHeight": 126,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 1817,
+ "y": 437
+ },
+ "width": 326,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ไปๆฅใฏTokyoใงsushiใ้ฃในใพใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 281,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 888,
+ "y": 603
+ },
+ "width": 2184,
+ "height": 100,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ๅ
ๆฅใShibuyaใงๅ้ใจshoppingใๆฅฝ๐ใใใ ๅพใramenๅฑใงdelicious๐ใชใฉใผใกใณใ้ฃในใใ",
+ "fontSize": 43,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 2139,
+ "labelHeight": 55,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 1877,
+ "y": 803
+ },
+ "width": 206,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "English English English",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 161,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 1897,
+ "y": 969
+ },
+ "width": 165,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ๅ
ๆฅๅ
ๆฅๅ
ๆฅ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 120,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1979.5,
+ "y": 66
+ },
+ {
+ "x": 1979.5,
+ "y": 106
+ },
+ {
+ "x": 1979.5,
+ "y": 126
+ },
+ {
+ "x": 1979.5,
+ "y": 166
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(b -> c)[0]",
+ "src": "b",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1979.5,
+ "y": 337
+ },
+ {
+ "x": 1979.5,
+ "y": 377
+ },
+ {
+ "x": 1979.5,
+ "y": 397
+ },
+ {
+ "x": 1979.5,
+ "y": 437
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(c -> d)[0]",
+ "src": "c",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "d",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1979.5,
+ "y": 503
+ },
+ {
+ "x": 1979.5,
+ "y": 543
+ },
+ {
+ "x": 1979.5,
+ "y": 563
+ },
+ {
+ "x": 1979.5,
+ "y": 603
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(d -> e)[0]",
+ "src": "d",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "e",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1979.5,
+ "y": 703
+ },
+ {
+ "x": 1979.5,
+ "y": 743
+ },
+ {
+ "x": 1979.5,
+ "y": 763
+ },
+ {
+ "x": 1979.5,
+ "y": 803
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(e -> f)[0]",
+ "src": "e",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "f",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1979.5,
+ "y": 869
+ },
+ {
+ "x": 1979.5,
+ "y": 909
+ },
+ {
+ "x": 1979.5,
+ "y": 929
+ },
+ {
+ "x": 1979.5,
+ "y": 969
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/unicode/japanese-mixed/dagre/sketch.exp.svg b/e2etests/testdata/unicode/japanese-mixed/dagre/sketch.exp.svg
new file mode 100644
index 000000000..77274b84b
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-mixed/dagre/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/japanese-mixed/elk/board.exp.json b/e2etests/testdata/unicode/japanese-mixed/elk/board.exp.json
new file mode 100644
index 000000000..622db44fb
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-mixed/elk/board.exp.json
@@ -0,0 +1,449 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 1657,
+ "y": 12
+ },
+ "width": 668,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 623,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 148
+ },
+ "width": 3959,
+ "height": 171,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ",
+ "fontSize": 100,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 3914,
+ "labelHeight": 126,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 1828,
+ "y": 389
+ },
+ "width": 326,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ไปๆฅใฏTokyoใงsushiใ้ฃในใพใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 281,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 899,
+ "y": 525
+ },
+ "width": 2184,
+ "height": 100,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ๅ
ๆฅใShibuyaใงๅ้ใจshoppingใๆฅฝ๐ใใใ ๅพใramenๅฑใงdelicious๐ใชใฉใผใกใณใ้ฃในใใ",
+ "fontSize": 43,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 2139,
+ "labelHeight": 55,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 1888,
+ "y": 695
+ },
+ "width": 206,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "English English English",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 161,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 1909,
+ "y": 831
+ },
+ "width": 165,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ๅ
ๆฅๅ
ๆฅๅ
ๆฅ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 120,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1991.5,
+ "y": 78
+ },
+ {
+ "x": 1991.5,
+ "y": 148
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(b -> c)[0]",
+ "src": "b",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1991.5,
+ "y": 319
+ },
+ {
+ "x": 1991.5,
+ "y": 389
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(c -> d)[0]",
+ "src": "c",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "d",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1991.5,
+ "y": 455
+ },
+ {
+ "x": 1991.5,
+ "y": 525
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(d -> e)[0]",
+ "src": "d",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "e",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1991.5,
+ "y": 625
+ },
+ {
+ "x": 1991.5,
+ "y": 695
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(e -> f)[0]",
+ "src": "e",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "f",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 1991.5,
+ "y": 761
+ },
+ {
+ "x": 1991.5,
+ "y": 831
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ]
+}
diff --git a/e2etests/testdata/unicode/japanese-mixed/elk/sketch.exp.svg b/e2etests/testdata/unicode/japanese-mixed/elk/sketch.exp.svg
new file mode 100644
index 000000000..6edeb9aa3
--- /dev/null
+++ b/e2etests/testdata/unicode/japanese-mixed/elk/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/with-style/dagre/board.exp.json b/e2etests/testdata/unicode/with-style/dagre/board.exp.json
new file mode 100644
index 000000000..475a72efb
--- /dev/null
+++ b/e2etests/testdata/unicode/with-style/dagre/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "ใใใใฟใชใใ",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 185,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 15,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใฟใชใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 140,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/unicode/with-style/dagre/sketch.exp.svg b/e2etests/testdata/unicode/with-style/dagre/sketch.exp.svg
new file mode 100644
index 000000000..64a18001c
--- /dev/null
+++ b/e2etests/testdata/unicode/with-style/dagre/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/unicode/with-style/elk/board.exp.json b/e2etests/testdata/unicode/with-style/elk/board.exp.json
new file mode 100644
index 000000000..201320c67
--- /dev/null
+++ b/e2etests/testdata/unicode/with-style/elk/board.exp.json
@@ -0,0 +1,48 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "ใใใใฟใชใใ",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 185,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 15,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "ใใใใฟใชใใ",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 140,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/unicode/with-style/elk/sketch.exp.svg b/e2etests/testdata/unicode/with-style/elk/sketch.exp.svg
new file mode 100644
index 000000000..fa4d7d8a1
--- /dev/null
+++ b/e2etests/testdata/unicode/with-style/elk/sketch.exp.svg
@@ -0,0 +1,52 @@
+
+
\ No newline at end of file
diff --git a/e2etests/unicode_test.go b/e2etests/unicode_test.go
new file mode 100644
index 000000000..0629cdf09
--- /dev/null
+++ b/e2etests/unicode_test.go
@@ -0,0 +1,56 @@
+package e2etests
+
+import (
+ _ "embed"
+ "testing"
+)
+
+func testUnicode(t *testing.T) {
+ tcs := []testCase{
+ {
+ name: "japanese-basic",
+ script: `a: ใใใใใใใใใใ
+`,
+ },
+ {
+ name: "japanese-full",
+ script: `a: "ใใๆฅใใใใใ้ใๆญฉใใฆใใใใ้่ทฏใฎๅใใใใใญใฅใฆใชใใใฃใฆๆฅใพใใใ\nใใใใฏ้ฉใใฆๅฐใญใพใใใ\nใใญใฅใฆใชใใใใฉใใใฆใใชใใฏใใใซใใใฎใงใใ๏ผใ ใญใฅใฆใชใฏ็ญใใพใใใใใใชใใจๅใ็็ฑใงใใใซใใพใใใตใฉใใซใชใใใใซใใ"
+
+b: "ใใใใใฏ็ฎใๅฅใใฆ้ฃในใใใฎใงใใใ" {
+ style.font-size: 55
+}
+
+a -> b: ใใใซใฏๆญปใชใชใใๆฒปใใชใใใ
+`,
+ },
+ {
+ name: "emojis",
+ script: `a: ๐๐๐๐๐๐๐๐
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -> โ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธ
+`,
+ },
+ {
+ name: "with-style",
+ script: `ใใใใฟใชใใ: {style.stroke-width: 15; style.double-border: true}
+`,
+ },
+ {
+ name: "japanese-mixed",
+ script: `a: "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ"
+b: "ใใใใ่ตคใใชใฃใใฎใฏใชใใงใใ๏ผBecause it saw the salad dressing!๐ฉโ๐ฉโ๐งโ๐ถ๐ฉโ๐ฉโ๐งโ๐ถ" {
+ style.font-size: 100
+}
+c: ไปๆฅใฏTokyoใงsushiใ้ฃในใพใใ
+d: ๅ
ๆฅใShibuyaใงๅ้ใจshoppingใๆฅฝ๐ใใใ ๅพใramenๅฑใงdelicious๐ใชใฉใผใกใณใ้ฃในใใ{
+ style.font-size: 43
+}
+e: English English English
+f: ๅ
ๆฅๅ
ๆฅๅ
ๆฅ
+a -> b -> c -> d -> e -> f
+
+`,
+ },
+ }
+
+ runa(t, tcs)
+}
diff --git a/go.mod b/go.mod
index feec9b43c..c8986bd2f 100644
--- a/go.mod
+++ b/go.mod
@@ -10,8 +10,10 @@ require (
github.com/fsnotify/fsnotify v1.6.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lucasb-eyer/go-colorful v1.2.0
+ github.com/mattn/go-runewidth v0.0.14
github.com/mazznoer/csscolorparser v0.1.3
github.com/playwright-community/playwright-go v0.2000.1
+ github.com/rivo/uniseg v0.4.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/yuin/goldmark v1.5.3
diff --git a/go.sum b/go.sum
index 1f31c5679..9b83338c3 100644
--- a/go.sum
+++ b/go.sum
@@ -115,6 +115,8 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mazznoer/csscolorparser v0.1.3 h1:vug4zh6loQxAUxfU1DZEu70gTPufDPspamZlHAkKcxE=
github.com/mazznoer/csscolorparser v0.1.3/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
@@ -128,6 +130,10 @@ github.com/playwright-community/playwright-go v0.2000.1/go.mod h1:1y9cM9b9dVHnuR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
diff --git a/lib/textmeasure/textmeasure.go b/lib/textmeasure/textmeasure.go
index b46cc1711..cdf9c811c 100644
--- a/lib/textmeasure/textmeasure.go
+++ b/lib/textmeasure/textmeasure.go
@@ -5,10 +5,12 @@ package textmeasure
import (
"math"
+ "strings"
"unicode"
"unicode/utf8"
"github.com/golang/freetype/truetype"
+ "github.com/rivo/uniseg"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/lib/geo"
@@ -166,6 +168,44 @@ func (r *Ruler) addFontSize(font d2fonts.Font) {
func (t *Ruler) Measure(font d2fonts.Font, s string) (width, height int) {
w, h := t.MeasurePrecise(font, s)
+ // Weird unicode stuff is going on when this is true
+ // See https://github.com/rivo/uniseg#grapheme-clusters
+ // This method is a good-enough approximation. It overshoots, but not by much.
+ // I suspect we need to import a font with the right glyphs to get the precise measurements
+ // but Hans fonts are heavy.
+ if uniseg.GraphemeClusterCount(s) != len(s) {
+ for _, line := range strings.Split(s, "\n") {
+ lineW, _ := t.MeasurePrecise(font, line)
+ gr := uniseg.NewGraphemes(line)
+
+ mono := d2fonts.SourceCodePro.Font(font.Size, font.Style)
+ for gr.Next() {
+ if gr.Width() == 1 {
+ continue
+ }
+ // For each grapheme which doesn't have width=1, the ruler measured wrongly.
+ // So, replace the measured width with a scaled measurement of a monospace version
+ var prevRune rune
+ dot := t.Orig.Copy()
+ b := newRect()
+ for _, r := range gr.Runes() {
+ var control bool
+ dot, control = t.controlRune(r, dot, font)
+ if control {
+ continue
+ }
+
+ var bounds *rect
+ _, _, bounds, dot = t.atlases[font].DrawRune(prevRune, r, dot)
+ b = b.union(bounds)
+ prevRune = r
+ }
+ lineW -= b.w()
+ lineW += t.spaceWidth(mono) * float64(gr.Width())
+ }
+ w = math.Max(w, lineW)
+ }
+ }
return int(math.Ceil(w)), int(math.Ceil(h))
}