d2ir: replace substitutions in markdown

This commit is contained in:
Alexander Wang 2024-11-18 07:01:47 -08:00
parent c3fbf873bf
commit 7e6a3d8132
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
10 changed files with 2452 additions and 40 deletions

View file

@ -1,5 +1,7 @@
#### Features 🚀
- Vars: vars in markdown blocks are substituted [#2218](https://github.com/terrastruct/d2/pull/2218)
#### Improvements 🧹
- Composition: links pointing to own board are purged [#2203](https://github.com/terrastruct/d2/pull/2203)

View file

@ -2868,17 +2868,28 @@ x*: {
{
name: "var_in_markdown",
text: `vars: {
v: {
ok
}
v: ok
}
x: |md
m${v}y
` + "`hey ${v}`" + `
regular markdown
` + "```" + `
bye ${v}
` + "```" + `
|
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "moky", g.Objects[0].Attributes.Label.Value)
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "moky"))
tassert.False(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "m${v}y"))
// Code spans untouched
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "hey ${v}"))
// Code blocks untouched
tassert.True(t, strings.Contains(g.Objects[0].Attributes.Label.Value, "bye ${v}"))
},
},
{

View file

@ -15,6 +15,7 @@ import (
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/textmeasure"
)
type globContext struct {
@ -342,10 +343,32 @@ func (c *compiler) resolveSubstitutions(varsStack []*Map, node Node) (removedFie
if subbed {
s.Coalesce()
}
case *d2ast.BlockString:
variables := make(map[string]string)
for _, vars := range varsStack {
c.collectVariables(vars, variables)
}
preprocessedValue := textmeasure.ReplaceSubstitutionsMarkdown(s.Value, variables)
// Update the block string value
s.Value = preprocessedValue
}
return removedField
}
func (c *compiler) collectVariables(vars *Map, variables map[string]string) {
if vars == nil {
return
}
for _, f := range vars.Fields {
if f.Primary() != nil {
variables[f.Name] = f.Primary().Value.ScalarString()
} else if f.Map() != nil {
c.collectVariables(f.Map(), variables)
}
}
}
func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast.Substitution, isCurrentScopeVars bool) *Field {
if vars == nil {
return nil

View file

@ -0,0 +1,306 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "x",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "Kube",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "description",
"type": "text",
"pos": {
"x": 86,
"y": 166
},
"width": 163,
"height": 96,
"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": "Kube is a service\n\n```\nLet ${y} be ${x}\n```",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "N1",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 163,
"labelHeight": 96,
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "rectangle",
"pos": {
"x": 141,
"y": 0
},
"width": 53,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "b",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 8,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 141,
"y": 362
},
"width": 53,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "a",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 8,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(b -> description)[0]",
"src": "b",
"srcArrow": "none",
"dst": "description",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 167.5,
"y": 66
},
{
"x": 167.5,
"y": 106
},
{
"x": 167.5,
"y": 126
},
{
"x": 167.5,
"y": 166
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(description -> a)[0]",
"src": "description",
"srcArrow": "none",
"dst": "a",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 167.5,
"y": 262
},
{
"x": 167.5,
"y": 302
},
{
"x": 167.5,
"y": 322
},
{
"x": 167.5,
"y": 362
}
],
"isCurve": true,
"animated": false,
"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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,288 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "x",
"type": "rectangle",
"pos": {
"x": 12,
"y": 12
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "Kube",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "description",
"type": "text",
"pos": {
"x": 58,
"y": 148
},
"width": 163,
"height": 96,
"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": "Kube is a service\n\n```\nLet ${y} be ${x}\n```",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "N1",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 163,
"labelHeight": 96,
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "rectangle",
"pos": {
"x": 113,
"y": 12
},
"width": 53,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "b",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 8,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 113,
"y": 314
},
"width": 53,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"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": "a",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 8,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(b -> description)[0]",
"src": "b",
"srcArrow": "none",
"dst": "description",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 139.5,
"y": 78
},
{
"x": 139.5,
"y": 148
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(description -> a)[0]",
"src": "description",
"srcArrow": "none",
"dst": "a",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 139.5,
"y": 244
},
{
"x": 139.5,
"y": 314
}
],
"animated": false,
"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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -469,3 +469,17 @@ colors: {
style.font-color: "linear-gradient(to bottom right, red 0%, yellow 25%, green 50%, cyan 75%, blue 100%)"
}
gradient -> colors
-- var_in_markdown --
vars: {
service-x: Kube
}
x: ${service-x}
description: |md
${service-x} is a service
```
Let ${y} be ${x}
```
|
b -> description -> a

View file

@ -0,0 +1,77 @@
package textmeasure
import (
"sort"
"strings"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
func ReplaceSubstitutionsMarkdown(mdText string, variables map[string]string) string {
source := []byte(mdText)
reader := text.NewReader(source)
doc := markdownRenderer.Parser().Parse(reader)
type substitution struct {
start int
stop int
newVal string
}
var substitutions []substitution
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if isCodeNode(n) {
return ast.WalkSkipChildren, nil
}
if textNode, ok := n.(*ast.Text); ok {
segment := textNode.Segment
originalText := string(segment.Value(source))
newText := replaceVariables(originalText, variables)
if originalText != newText {
substitutions = append(substitutions, substitution{
start: segment.Start,
stop: segment.Stop,
newVal: newText,
})
}
}
return ast.WalkContinue, nil
})
if len(substitutions) == 0 {
return mdText
}
sort.Slice(substitutions, func(i, j int) bool {
return substitutions[i].start > substitutions[j].start
})
result := string(source)
for _, sub := range substitutions {
result = result[:sub.start] + sub.newVal + result[sub.stop:]
}
return result
}
func isCodeNode(n ast.Node) bool {
switch n.Kind() {
case ast.KindCodeBlock, ast.KindFencedCodeBlock, ast.KindCodeSpan:
return true
}
return false
}
func replaceVariables(s string, vars map[string]string) string {
for k, v := range vars {
s = strings.ReplaceAll(s, "${"+k+"}", v)
}
return s
}

View file

@ -3,11 +3,11 @@
"name": "",
"isFolderOnly": false,
"ast": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:0:0-9:0:47",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:0:0-15:0:90",
"nodes": [
{
"map_key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:0:0-4:1:27",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:0:0-2:1:17",
"key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:0:0-0:4:4",
"path": [
@ -27,11 +27,11 @@
"primary": {},
"value": {
"map": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:6:6-4:1:27",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,0:6:6-2:1:17",
"nodes": [
{
"map_key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,1:2:10-3:3:25",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,1:2:10-1:7:15",
"key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,1:2:10-1:3:11",
"path": [
@ -50,31 +50,12 @@
},
"primary": {},
"value": {
"map": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,1:5:13-3:3:25",
"nodes": [
"unquoted_string": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,1:5:13-1:7:15",
"value": [
{
"map_key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,2:4:19-2:6:21",
"key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,2:4:19-2:6:21",
"path": [
{
"unquoted_string": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,2:4:19-2:6:21",
"value": [
{
"string": "ok",
"raw_string": "ok"
}
]
}
}
]
},
"primary": {},
"value": {}
}
"string": "ok",
"raw_string": "ok"
}
]
}
@ -88,13 +69,13 @@
},
{
"map_key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:0:29-8:1:46",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:0:19-14:1:89",
"key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:0:29-6:1:30",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:0:19-4:1:20",
"path": [
{
"unquoted_string": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:0:29-6:1:30",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:0:19-4:1:20",
"value": [
{
"string": "x",
@ -108,10 +89,10 @@
"primary": {},
"value": {
"block_string": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:3:32-8:1:46",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:3:22-14:1:89",
"quote": "",
"tag": "md",
"value": "m${v}y"
"value": "moky\n\n`hey ${v}`\n\nregular markdown\n\n```\nbye ${v}\n```"
}
}
}
@ -149,11 +130,11 @@
"references": [
{
"key": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:0:29-6:1:30",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:0:19-4:1:20",
"path": [
{
"unquoted_string": {
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,6:0:29-6:1:30",
"range": "d2/testdata/d2compiler/TestCompile/var_in_markdown.d2,4:0:19-4:1:20",
"value": [
{
"string": "x",
@ -170,7 +151,7 @@
],
"attributes": {
"label": {
"value": "m${v}y"
"value": "moky\n\n`hey ${v}`\n\nregular markdown\n\n```\nbye ${v}\n```"
},
"labelDimensions": {
"width": 0,