diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 56f152973..74d61119b 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,6 +1,7 @@
#### Features ๐
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
+- Markdown: Github-flavored tables work in `md` blocks [#2221](https://github.com/terrastruct/d2/pull/2221)
#### Improvements ๐งน
diff --git a/e2etests/testdata/txtar/md-tables/dagre/board.exp.json b/e2etests/testdata/txtar/md-tables/dagre/board.exp.json
new file mode 100644
index 000000000..c93eefc67
--- /dev/null
+++ b/e2etests/testdata/txtar/md-tables/dagre/board.exp.json
@@ -0,0 +1,954 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "savings",
+ "type": "text",
+ "pos": {
+ "x": 169,
+ "y": 50
+ },
+ "width": 343,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f4f8",
+ "stroke": "#4a90e2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Month | Savings | Expenses | Balance |\n| -------- | ------- | -------- | ------- |\n| January | $250 | $150 | $100 |\n| February | $80 | $200 | -$120 |\n| March | $420 | $180 | $240 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 343,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "status",
+ "type": "text",
+ "pos": {
+ "x": 270,
+ "y": 321
+ },
+ "width": 142,
+ "height": 113,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8e8e8",
+ "stroke": "#e24a4a",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Status | Count |\n| ------ | ----- |\n| Done | 42 |\n| Todo | 17 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 142,
+ "labelHeight": 113,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "metrics",
+ "type": "text",
+ "pos": {
+ "x": 254,
+ "y": 655
+ },
+ "width": 173,
+ "height": 335,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f0f8e8",
+ "stroke": "#82e24a",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Metric | Value |\n| --------- | ------- |\n| Uptime | 99.9% |\n| Latency | 150ms |\n| Errors | 0.01% |\n| Requests | 15k/min |\n| CPU | 45% |\n| Memory | 68% |\n| Disk | 72% |\n| Network | 33% |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 173,
+ "labelHeight": 335,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "financial",
+ "type": "rectangle",
+ "pos": {
+ "x": 562,
+ "y": 20
+ },
+ "width": 389,
+ "height": 444,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f4f8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Financial Overview",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 220,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "financial.monthly",
+ "type": "text",
+ "pos": {
+ "x": 592,
+ "y": 50
+ },
+ "width": 329,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Month | Revenue | Costs | Margin |\n| --------- | -------- | -------- | ------ |\n| January | $25,000 | $18,000 | 28% |\n| February | $28,500 | $19,200 | 33% |\n| March | $31,200 | $21,500 | 31% |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 329,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "financial.quarterly",
+ "type": "text",
+ "pos": {
+ "x": 599,
+ "y": 321
+ },
+ "width": 315,
+ "height": 113,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Quarter | Target | Actual | Variance |\n|:--------|-------:|:------:|:---------|\n| Q1 2024 | $75K | $84.7K | +12.9% |\n| Q2 2024 | $82K | - | - |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 315,
+ "labelHeight": 113,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "monitoring",
+ "type": "rectangle",
+ "pos": {
+ "x": 548,
+ "y": 718
+ },
+ "width": 417,
+ "height": 573,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8e8e8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "System Health",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 168,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "monitoring.availability",
+ "type": "text",
+ "pos": {
+ "x": 578,
+ "y": 748
+ },
+ "width": 357,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Service | Status | Uptime | Last Incident |\n|:-----------|:------:|:------:|:--------------|\n| API | โ
| 99.9% | 15 days ago |\n| Database | โ
| 99.8% | 3 days ago |\n| Cache | โ ๏ธ | 98.5% | 1 hour ago |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 357,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "monitoring.performance",
+ "type": "text",
+ "pos": {
+ "x": 585,
+ "y": 1111
+ },
+ "width": 344,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Metric | P50 | P90 | P99 |\n|:----------------|:-----:|:-----:|:-----:|\n| Response Time | 120ms | 350ms | 750ms |\n| DB Query Time | 45ms | 180ms | 450ms |\n| Cache Latency | 5ms | 12ms | 30ms |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 344,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "projects",
+ "type": "rectangle",
+ "pos": {
+ "x": 10,
+ "y": 1452
+ },
+ "width": 1008,
+ "height": 210,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f8e8",
+ "stroke": "#4a90e2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Project Tracking",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 190,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "projects.status",
+ "type": "text",
+ "pos": {
+ "x": 40,
+ "y": 1482
+ },
+ "width": 425,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Project | Priority | Progress | Due Date | Owner |\n|---------|:--------:|:--------:|:---------|:------|\n| Alpha | HIGH |  80% | 2024-04-01 | Alice |\n| Beta | MEDIUM |  45% | 2024-05-15 | Bob |\n| Gamma | LOW |  20% | 2024-06-30 | Carol |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 425,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "projects.risks",
+ "type": "text",
+ "pos": {
+ "x": 525,
+ "y": 1482
+ },
+ "width": 463,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Risk ID | Description | Impact | Mitigation |\n|:-------:|:------------|:------:|:-----------|\n| R1 | Resource shortage | ๐ด High | Hire contractors |\n| R2 | Technical debt | ๐ก Med | Code review |\n| R3 | Scope creep | ๐ข Low | Clear requirements |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 463,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "team",
+ "type": "rectangle",
+ "pos": {
+ "x": 551,
+ "y": 1823
+ },
+ "width": 411,
+ "height": 247,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8f0e8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Team Analytics",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 177,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "team.velocity",
+ "type": "text",
+ "pos": {
+ "x": 581,
+ "y": 1853
+ },
+ "width": 351,
+ "height": 187,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Sprint | Points | Completed | Carryover |\n|:-------|:------:|:---------:|:---------:|\n| SP-1 | 34 | 30 | 4 |\n| SP-2 | 38 | 35 | 3 |\n| SP-3 | 42 | 40 | 2 |\n| Average| 38 | 35 | 3 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 351,
+ "labelHeight": 187,
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "(savings -> status)[0]",
+ "src": "savings",
+ "srcArrow": "none",
+ "dst": "status",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 5,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "Triggers",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 54,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 340.5,
+ "y": 199.5
+ },
+ {
+ "x": 340.5,
+ "y": 248.3000030517578
+ },
+ {
+ "x": 340.5,
+ "y": 272.70001220703125
+ },
+ {
+ "x": 340.5,
+ "y": 321.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(status -> metrics)[0]",
+ "src": "status",
+ "srcArrow": "none",
+ "dst": "metrics",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Monitors",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 58,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 340.5,
+ "y": 434
+ },
+ {
+ "x": 340.5,
+ "y": 474
+ },
+ {
+ "x": 340.5,
+ "y": 496.1000061035156
+ },
+ {
+ "x": 340.5,
+ "y": 514.25
+ },
+ {
+ "x": 340.5,
+ "y": 532.4000244140625
+ },
+ {
+ "x": 340.5,
+ "y": 615
+ },
+ {
+ "x": 340.5,
+ "y": 655
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "financial.(monthly -> quarterly)[0]",
+ "src": "financial.monthly",
+ "srcArrow": "none",
+ "dst": "financial.quarterly",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Aggregates",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 77,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 756.5,
+ "y": 199.5
+ },
+ {
+ "x": 756.5,
+ "y": 248.3000030517578
+ },
+ {
+ "x": 756.5,
+ "y": 272.70001220703125
+ },
+ {
+ "x": 756.5,
+ "y": 321.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "monitoring.(availability -> performance)[0]",
+ "src": "monitoring.availability",
+ "srcArrow": "none",
+ "dst": "monitoring.performance",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Affects",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 46,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 756.5,
+ "y": 898.5
+ },
+ {
+ "x": 756.5,
+ "y": 1020.0999755859375
+ },
+ {
+ "x": 756.5,
+ "y": 1062.699951171875
+ },
+ {
+ "x": 756.5,
+ "y": 1111.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(financial -> monitoring)[0]",
+ "src": "financial",
+ "srcArrow": "none",
+ "dst": "monitoring",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Impacts",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 54,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 756.5,
+ "y": 463.5
+ },
+ {
+ "x": 756.5,
+ "y": 528.2999877929688
+ },
+ {
+ "x": 756.5,
+ "y": 568.0999755859375
+ },
+ {
+ "x": 756.5,
+ "y": 662.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(monitoring -> projects)[0]",
+ "src": "monitoring",
+ "srcArrow": "none",
+ "dst": "projects",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 5,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "Informs",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 756.5,
+ "y": 1290.5
+ },
+ {
+ "x": 756.5,
+ "y": 1355.300048828125
+ },
+ {
+ "x": 756.5,
+ "y": 1387.699951171875
+ },
+ {
+ "x": 756.5,
+ "y": 1452.5
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(projects -> team)[0]",
+ "src": "projects",
+ "srcArrow": "none",
+ "dst": "team",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Assigns",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 756.5,
+ "y": 1661.5
+ },
+ {
+ "x": 756.5,
+ "y": 1726.300048828125
+ },
+ {
+ "x": 756.5,
+ "y": 1750.5
+ },
+ {
+ "x": 756.5,
+ "y": 1782.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg b/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg
new file mode 100644
index 000000000..72c7bd136
--- /dev/null
+++ b/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg
@@ -0,0 +1,1174 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/md-tables/elk/board.exp.json b/e2etests/testdata/txtar/md-tables/elk/board.exp.json
new file mode 100644
index 000000000..150eee838
--- /dev/null
+++ b/e2etests/testdata/txtar/md-tables/elk/board.exp.json
@@ -0,0 +1,879 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "savings",
+ "type": "text",
+ "pos": {
+ "x": 12,
+ "y": 386
+ },
+ "width": 343,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f4f8",
+ "stroke": "#4a90e2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Month | Savings | Expenses | Balance |\n| -------- | ------- | -------- | ------- |\n| January | $250 | $150 | $100 |\n| February | $80 | $200 | -$120 |\n| March | $420 | $180 | $240 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 343,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "status",
+ "type": "text",
+ "pos": {
+ "x": 112,
+ "y": 921
+ },
+ "width": 142,
+ "height": 113,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8e8e8",
+ "stroke": "#e24a4a",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Status | Count |\n| ------ | ----- |\n| Done | 42 |\n| Todo | 17 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 142,
+ "labelHeight": 113,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "metrics",
+ "type": "text",
+ "pos": {
+ "x": 97,
+ "y": 1419
+ },
+ "width": 173,
+ "height": 335,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f0f8e8",
+ "stroke": "#82e24a",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Metric | Value |\n| --------- | ------- |\n| Uptime | 99.9% |\n| Latency | 150ms |\n| Errors | 0.01% |\n| Requests | 15k/min |\n| CPU | 45% |\n| Memory | 68% |\n| Disk | 72% |\n| Network | 33% |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 173,
+ "labelHeight": 335,
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "financial",
+ "type": "rectangle",
+ "pos": {
+ "x": 579,
+ "y": 12
+ },
+ "width": 429,
+ "height": 524,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f4f8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Financial Overview",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 220,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "financial.monthly",
+ "type": "text",
+ "pos": {
+ "x": 629,
+ "y": 62
+ },
+ "width": 329,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Month | Revenue | Costs | Margin |\n| --------- | -------- | -------- | ------ |\n| January | $25,000 | $18,000 | 28% |\n| February | $28,500 | $19,200 | 33% |\n| March | $31,200 | $21,500 | 31% |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 329,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "financial.quarterly",
+ "type": "text",
+ "pos": {
+ "x": 636,
+ "y": 373
+ },
+ "width": 315,
+ "height": 113,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Quarter | Target | Actual | Variance |\n|:--------|-------:|:------:|:---------|\n| Q1 2024 | $75K | $84.7K | +12.9% |\n| Q2 2024 | $82K | - | - |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 315,
+ "labelHeight": 113,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "monitoring",
+ "type": "rectangle",
+ "pos": {
+ "x": 565,
+ "y": 712
+ },
+ "width": 442,
+ "height": 546,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8e8e8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": true,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "System Health",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 168,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "monitoring.availability",
+ "type": "text",
+ "pos": {
+ "x": 608,
+ "y": 754
+ },
+ "width": 357,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Service | Status | Uptime | Last Incident |\n|:-----------|:------:|:------:|:--------------|\n| API | โ
| 99.9% | 15 days ago |\n| Database | โ
| 99.8% | 3 days ago |\n| Cache | โ ๏ธ | 98.5% | 1 hour ago |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 357,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "monitoring.performance",
+ "type": "text",
+ "pos": {
+ "x": 614,
+ "y": 1065
+ },
+ "width": 344,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Metric | P50 | P90 | P99 |\n|:----------------|:-----:|:-----:|:-----:|\n| Response Time | 120ms | 350ms | 750ms |\n| DB Query Time | 45ms | 180ms | 450ms |\n| Cache Latency | 5ms | 12ms | 30ms |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 344,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "projects",
+ "type": "rectangle",
+ "pos": {
+ "x": 290,
+ "y": 1461
+ },
+ "width": 1008,
+ "height": 250,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e8f8e8",
+ "stroke": "#4a90e2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": true,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Project Tracking",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 190,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "projects.status",
+ "type": "text",
+ "pos": {
+ "x": 340,
+ "y": 1511
+ },
+ "width": 425,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Project | Priority | Progress | Due Date | Owner |\n|---------|:--------:|:--------:|:---------|:------|\n| Alpha | HIGH |  80% | 2024-04-01 | Alice |\n| Beta | MEDIUM |  45% | 2024-05-15 | Bob |\n| Gamma | LOW |  20% | 2024-06-30 | Carol |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 425,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "projects.risks",
+ "type": "text",
+ "pos": {
+ "x": 785,
+ "y": 1511
+ },
+ "width": 463,
+ "height": 150,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Risk ID | Description | Impact | Mitigation |\n|:-------:|:------------|:------:|:-----------|\n| R1 | Resource shortage | ๐ด High | Hire contractors |\n| R2 | Technical debt | ๐ก Med | Code review |\n| R3 | Scope creep | ๐ข Low | Clear requirements |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 463,
+ "labelHeight": 150,
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "team",
+ "type": "rectangle",
+ "pos": {
+ "x": 568,
+ "y": 1915
+ },
+ "width": 451,
+ "height": 287,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#f8f0e8",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Team Analytics",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 177,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "team.velocity",
+ "type": "text",
+ "pos": {
+ "x": 618,
+ "y": 1965
+ },
+ "width": 351,
+ "height": 187,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "transparent",
+ "stroke": "N1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "| Sprint | Points | Completed | Carryover |\n|:-------|:------:|:---------:|:---------:|\n| SP-1 | 34 | 30 | 4 |\n| SP-2 | 38 | 35 | 3 |\n| SP-3 | 42 | 40 | 2 |\n| Average| 38 | 35 | 3 |",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "markdown",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 351,
+ "labelHeight": 187,
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [
+ {
+ "id": "(savings -> status)[0]",
+ "src": "savings",
+ "srcArrow": "none",
+ "dst": "status",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 5,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "Triggers",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 54,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 183.5,
+ "y": 536
+ },
+ {
+ "x": 183.5,
+ "y": 921
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(status -> metrics)[0]",
+ "src": "status",
+ "srcArrow": "none",
+ "dst": "metrics",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Monitors",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 58,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 183.5,
+ "y": 1034
+ },
+ {
+ "x": 183.5,
+ "y": 1419
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "financial.(monthly -> quarterly)[0]",
+ "src": "financial.monthly",
+ "srcArrow": "none",
+ "dst": "financial.quarterly",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Aggregates",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 77,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 794,
+ "y": 212
+ },
+ {
+ "x": 794,
+ "y": 373
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "monitoring.(availability -> performance)[0]",
+ "src": "monitoring.availability",
+ "srcArrow": "none",
+ "dst": "monitoring.performance",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Affects",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 46,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 786.5,
+ "y": 904.5
+ },
+ {
+ "x": 786.5,
+ "y": 1065.5
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(financial -> monitoring)[0]",
+ "src": "financial",
+ "srcArrow": "none",
+ "dst": "monitoring",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Impacts",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 54,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 794,
+ "y": 536
+ },
+ {
+ "x": 794,
+ "y": 697
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(monitoring -> projects)[0]",
+ "src": "monitoring",
+ "srcArrow": "none",
+ "dst": "projects",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 5,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "Informs",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 51,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 794,
+ "y": 1257.5
+ },
+ {
+ "x": 794,
+ "y": 1461.5
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(projects -> team)[0]",
+ "src": "projects",
+ "srcArrow": "none",
+ "dst": "team",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "Assigns",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 50,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 794,
+ "y": 1711
+ },
+ {
+ "x": 794,
+ "y": 1915
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg b/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg
new file mode 100644
index 000000000..d63ebdc30
--- /dev/null
+++ b/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg
@@ -0,0 +1,1174 @@
+
+
+
+| Month |
+Savings |
+Expenses |
+Balance |
+
+
+
+
+| January |
+$250 |
+$150 |
+$100 |
+
+
+| February |
+$80 |
+$200 |
+-$120 |
+
+
+| March |
+$420 |
+$180 |
+$240 |
+
+
+
+
+
+
+| Status |
+Count |
+
+
+
+
+| Done |
+42 |
+
+
+| Todo |
+17 |
+
+
+
+
+
+
+| Metric |
+Value |
+
+
+
+
+| Uptime |
+99.9% |
+
+
+| Latency |
+150ms |
+
+
+| Errors |
+0.01% |
+
+
+| Requests |
+15k/min |
+
+
+| CPU |
+45% |
+
+
+| Memory |
+68% |
+
+
+| Disk |
+72% |
+
+
+| Network |
+33% |
+
+
+
+
Financial Overview
+
+System HealthProject TrackingTeam Analytics
+
+
+| Month |
+Revenue |
+Costs |
+Margin |
+
+
+
+
+| January |
+$25,000 |
+$18,000 |
+28% |
+
+
+| February |
+$28,500 |
+$19,200 |
+33% |
+
+
+| March |
+$31,200 |
+$21,500 |
+31% |
+
+
+
+
+
+
+| Quarter |
+Target |
+Actual |
+Variance |
+
+
+
+
+| Q1 2024 |
+$75K |
+$84.7K |
++12.9% |
+
+
+| Q2 2024 |
+$82K |
+- |
+- |
+
+
+
+
+
+
+| Service |
+Status |
+Uptime |
+Last Incident |
+
+
+
+
+| API |
+โ
|
+99.9% |
+15 days ago |
+
+
+| Database |
+โ
|
+99.8% |
+3 days ago |
+
+
+| Cache |
+โ ๏ธ |
+98.5% |
+1 hour ago |
+
+
+
+
+
+
+| Metric |
+P50 |
+P90 |
+P99 |
+
+
+
+
+| Response Time |
+120ms |
+350ms |
+750ms |
+
+
+| DB Query Time |
+45ms |
+180ms |
+450ms |
+
+
+| Cache Latency |
+5ms |
+12ms |
+30ms |
+
+
+
+
+
+
+| Risk ID |
+Description |
+Impact |
+Mitigation |
+
+
+
+
+| R1 |
+Resource shortage |
+๐ด High |
+Hire contractors |
+
+
+| R2 |
+Technical debt |
+๐ก Med |
+Code review |
+
+
+| R3 |
+Scope creep |
+๐ข Low |
+Clear requirements |
+
+
+
+
+
+
+| Sprint |
+Points |
+Completed |
+Carryover |
+
+
+
+
+| SP-1 |
+34 |
+30 |
+4 |
+
+
+| SP-2 |
+38 |
+35 |
+3 |
+
+
+| SP-3 |
+42 |
+40 |
+2 |
+
+
+| Average |
+38 |
+35 |
+3 |
+
+
+
+
Triggers MonitorsAggregatesAffectsImpactsInformsAssigns
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index 193acb48e..a9d4bffa8 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -483,3 +483,163 @@ description: |md
```
|
b -> description -> a
+
+-- md-tables --
+# Financial table
+savings: ||md
+ | Month | Savings | Expenses | Balance |
+ | -------- | ------- | -------- | ------- |
+ | January | $250 | $150 | $100 |
+ | February | $80 | $200 | -$120 |
+ | March | $420 | $180 | $240 |
+||
+
+# Simple 2x2 table
+status: ||md
+ | Status | Count |
+ | ------ | ----- |
+ | Done | 42 |
+ | Todo | 17 |
+||
+
+# Long table with many rows
+metrics: ||md
+ | Metric | Value |
+ | --------- | ------- |
+ | Uptime | 99.9% |
+ | Latency | 150ms |
+ | Errors | 0.01% |
+ | Requests | 15k/min |
+ | CPU | 45% |
+ | Memory | 68% |
+ | Disk | 72% |
+ | Network | 33% |
+||
+
+# Connect tables with labeled edges
+savings -> status: Triggers {
+ style: {
+ stroke-dash: 5
+ }
+}
+status -> metrics: Monitors {
+ style: {
+ stroke-width: 2
+ }
+}
+
+# Add some styling
+savings.style: {
+ fill: "#e8f4f8"
+ stroke: "#4a90e2"
+}
+
+status.style: {
+ fill: "#f8e8e8"
+ stroke: "#e24a4a"
+}
+
+metrics.style: {
+ fill: "#f0f8e8"
+ stroke: "#82e24a"
+}
+
+# Container for financial data
+financial: {
+ label: "Financial Overview"
+ style.stroke-width: 2
+
+ monthly: ||md
+ | Month | Revenue | Costs | Margin |
+ | --------- | -------- | -------- | ------ |
+ | January | $25,000 | $18,000 | 28% |
+ | February | $28,500 | $19,200 | 33% |
+ | March | $31,200 | $21,500 | 31% |
+ ||
+
+ quarterly: ||md
+ | Quarter | Target | Actual | Variance |
+ |:--------|-------:|:------:|:---------|
+ | Q1 2024 | $75K | $84.7K | +12.9% |
+ | Q2 2024 | $82K | - | - |
+ ||
+
+ monthly -> quarterly: "Aggregates"
+}
+
+# Container for system metrics
+monitoring: {
+ label: "System Health"
+ style.3d: true
+
+ availability: ||md
+ | Service | Status | Uptime | Last Incident |
+ |:-----------|:------:|:------:|:--------------|
+ | API | โ
| 99.9% | 15 days ago |
+ | Database | โ
| 99.8% | 3 days ago |
+ | Cache | โ ๏ธ | 98.5% | 1 hour ago |
+ ||
+
+ performance: ||md
+ | Metric | P50 | P90 | P99 |
+ |:----------------|:-----:|:-----:|:-----:|
+ | Response Time | 120ms | 350ms | 750ms |
+ | DB Query Time | 45ms | 180ms | 450ms |
+ | Cache Latency | 5ms | 12ms | 30ms |
+ ||
+
+ availability -> performance: "Affects"
+}
+
+# Container for project status
+projects: {
+ label: "Project Tracking"
+ style.stroke: "#4a90e2"
+ style.double-border: true
+
+ status: ||md
+ | Project | Priority | Progress | Due Date | Owner |
+ |---------|:--------:|:--------:|:---------|:------|
+ | Alpha | HIGH |  80% | 2024-04-01 | Alice |
+ | Beta | MEDIUM |  45% | 2024-05-15 | Bob |
+ | Gamma | LOW |  20% | 2024-06-30 | Carol |
+ ||
+
+ risks: ||md
+ | Risk ID | Description | Impact | Mitigation |
+ |:-------:|:------------|:------:|:-----------|
+ | R1 | Resource shortage | ๐ด High | Hire contractors |
+ | R2 | Technical debt | ๐ก Med | Code review |
+ | R3 | Scope creep | ๐ข Low | Clear requirements |
+ ||
+}
+
+# Container for team stats
+team: {
+ label: "Team Analytics"
+ style.fill: "#f5f5f5"
+
+ velocity: ||md
+ | Sprint | Points | Completed | Carryover |
+ |:-------|:------:|:---------:|:---------:|
+ | SP-1 | 34 | 30 | 4 |
+ | SP-2 | 38 | 35 | 3 |
+ | SP-3 | 42 | 40 | 2 |
+ | Average| 38 | 35 | 3 |
+ ||
+}
+
+# Connect containers
+financial -> monitoring: "Impacts"
+monitoring -> projects: "Informs" {
+ style.stroke-dash: 5
+}
+projects -> team: "Assigns" {
+ style.animated: true
+}
+
+# Styling
+financial.style.fill: "#e8f4f8"
+monitoring.style.fill: "#f8e8e8"
+projects.style.fill: "#e8f8e8"
+team.style.fill: "#f8f0e8"
diff --git a/lib/textmeasure/markdown.go b/lib/textmeasure/markdown.go
index 41e2ea135..f85423988 100644
--- a/lib/textmeasure/markdown.go
+++ b/lib/textmeasure/markdown.go
@@ -98,6 +98,7 @@ func init() {
),
goldmark.WithExtensions(
extension.Strikethrough,
+ extension.Table,
),
)
}
@@ -185,7 +186,8 @@ func isBlockElement(elType string) bool {
"ol",
"p",
"pre",
- "ul":
+ "ul",
+ "table", "thead", "tbody", "tr", "td", "th": // Added table elements here
return true
default:
return false
@@ -204,6 +206,7 @@ func hasAncestorElement(n *html.Node, elType string) bool {
type blockAttrs struct {
width, height, marginTop, marginBottom float64
+ extraData interface{}
}
func (b *blockAttrs) isNotEmpty() bool {
@@ -280,7 +283,7 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, fontFamily *d2fonts.Fon
if debugMeasure {
fmt.Printf("%stext(%v,%v)\n", depthStr, w, h)
}
- return blockAttrs{w + spaceWidths, h, 0, 0}
+ return blockAttrs{w + spaceWidths, h, 0, 0, 0}
case html.ElementNode:
isCode := false
switch n.Data {
@@ -429,6 +432,142 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, fontFamily *d2fonts.Fon
block.height += Height_hr_em * float64(fontSize)
block.marginTop = go2.Max(block.marginTop, MarginTopBottom_hr)
block.marginBottom = go2.Max(block.marginBottom, MarginTopBottom_hr)
+ case "table":
+ var columnWidths []float64
+ var tableHeight float64
+
+ // Border width for table (outer border)
+ tableBorder := 1.0
+
+ // Iterate over child nodes (tbody, thead, tr)
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ if child.Type == html.ElementNode && (child.Data == "tbody" || child.Data == "thead" || child.Data == "tfoot") {
+ childAttrs := ruler.measureNode(depth+1, child, fontFamily, fontSize, fontStyle)
+ tableHeight += childAttrs.height
+
+ if childColumnWidths, ok := childAttrs.extraData.([][]float64); ok {
+ columnWidths = mergeColumnWidths(columnWidths, childColumnWidths)
+ }
+ } else if child.Type == html.ElementNode && child.Data == "tr" {
+ rowAttrs := ruler.measureNode(depth+1, child, fontFamily, fontSize, fontStyle)
+ tableHeight += rowAttrs.height
+
+ if rowCellWidths, ok := rowAttrs.extraData.([]float64); ok {
+ columnWidths = mergeColumnWidths(columnWidths, [][]float64{rowCellWidths})
+ }
+ }
+ }
+
+ // Calculate total table width including ALL borders
+ tableWidth := 0.0
+ if len(columnWidths) > 0 {
+ // Add widths of all columns
+ for _, colWidth := range columnWidths {
+ tableWidth += colWidth
+ }
+
+ // Add border for every column division (including outer borders)
+ tableWidth += float64(len(columnWidths)+1) * tableBorder
+ }
+
+ // Add outer borders to height
+ tableHeight += 2 * tableBorder
+
+ block.width = tableWidth
+ block.height = tableHeight
+
+ case "thead", "tbody", "tfoot":
+ var sectionWidth, sectionHeight float64
+ var sectionColumnWidths [][]float64
+
+ // Iterate over tr elements
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ if child.Type == html.ElementNode && child.Data == "tr" {
+ childAttrs := ruler.measureNode(depth+1, child, fontFamily, fontSize, fontStyle)
+ sectionHeight += childAttrs.height
+ sectionWidth = go2.Max(sectionWidth, childAttrs.width)
+
+ if rowCellWidths, ok := childAttrs.extraData.([]float64); ok {
+ sectionColumnWidths = append(sectionColumnWidths, rowCellWidths)
+ }
+ }
+ }
+
+ block.width = sectionWidth
+ block.height = sectionHeight
+ block.extraData = sectionColumnWidths // Pass column widths back to table
+
+ case "td", "th":
+ // Apply semibold style to header cells
+ cellFontStyle := fontStyle
+ if n.Data == "th" {
+ cellFontStyle = d2fonts.FONT_STYLE_SEMIBOLD
+ }
+
+ // Measure cell content with appropriate font style
+ var cellContentWidth, cellContentHeight float64
+
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ // Pass the header-specific font style to child measurements
+ childAttrs := ruler.measureNode(depth+1, child, fontFamily, fontSize, cellFontStyle)
+ cellContentWidth = go2.Max(cellContentWidth, childAttrs.width)
+ cellContentHeight += childAttrs.height
+ }
+
+ block.width = cellContentWidth
+ block.height = cellContentHeight
+
+ case "tr":
+ var rowWidth, rowHeight float64
+ var cellWidths []float64
+
+ cellBorder := 1.0
+ rowBorder := 1.0
+
+ maxCellHeight := 0.0
+ cellCount := 0
+
+ // Check if this row is in a thead to determine default font style for cells
+ inHeader := hasAncestorElement(n, "thead")
+ rowFontStyle := fontStyle
+ if inHeader {
+ rowFontStyle = d2fonts.FONT_STYLE_SEMIBOLD
+ }
+
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ if child.Type == html.ElementNode && (child.Data == "td" || child.Data == "th") {
+ cellCount++
+
+ // Use semibold for th elements regardless of location
+ childFontStyle := rowFontStyle
+ if child.Data == "th" {
+ childFontStyle = d2fonts.FONT_STYLE_SEMIBOLD
+ }
+
+ childAttrs := ruler.measureNode(depth+1, child, fontFamily, fontSize, childFontStyle)
+ cellPaddingH := 13.0 * 2
+ cellPaddingV := 6.0 * 2
+
+ cellWidth := childAttrs.width + cellPaddingH
+ cellHeight := childAttrs.height + cellPaddingV
+
+ cellWidths = append(cellWidths, cellWidth)
+ maxCellHeight = go2.Max(maxCellHeight, cellHeight)
+ }
+ }
+
+ if cellCount > 0 {
+ for _, w := range cellWidths {
+ rowWidth += w
+ }
+ rowWidth += float64(cellCount+1) * cellBorder
+ }
+
+ rowHeight = maxCellHeight + rowBorder
+
+ block.width = rowWidth
+ block.height = rowHeight
+ block.extraData = cellWidths
}
if block.height > 0 && block.height < lineHeightPx {
block.height = lineHeightPx
@@ -440,3 +579,16 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, fontFamily *d2fonts.Fon
}
return blockAttrs{}
}
+
+func mergeColumnWidths(existing []float64, new [][]float64) []float64 {
+ for _, rowWidths := range new {
+ for i, width := range rowWidths {
+ if i >= len(existing) {
+ existing = append(existing, width)
+ } else {
+ existing[i] = go2.Max(existing[i], width)
+ }
+ }
+ }
+ return existing
+}