1 package d2lib
2
3 import (
4 "context"
5 "errors"
6 "io/fs"
7 "os"
8 "strings"
9
10 "oss.terrastruct.com/d2/d2ast"
11 "oss.terrastruct.com/d2/d2compiler"
12 "oss.terrastruct.com/d2/d2exporter"
13 "oss.terrastruct.com/d2/d2graph"
14 "oss.terrastruct.com/d2/d2layouts"
15 "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
16 "oss.terrastruct.com/d2/d2parser"
17 "oss.terrastruct.com/d2/d2renderers/d2fonts"
18 "oss.terrastruct.com/d2/d2renderers/d2svg"
19 "oss.terrastruct.com/d2/d2target"
20 "oss.terrastruct.com/d2/d2themes/d2themescatalog"
21 "oss.terrastruct.com/d2/lib/textmeasure"
22 "oss.terrastruct.com/util-go/go2"
23 )
24
25 type CompileOptions struct {
26 UTF16Pos bool
27 FS fs.FS
28 MeasuredTexts []*d2target.MText
29 Ruler *textmeasure.Ruler
30 RouterResolver func(engine string) (d2graph.RouteEdges, error)
31 LayoutResolver func(engine string) (d2graph.LayoutGraph, error)
32
33 Layout *string
34
35
36
37
38
39
40 FontFamily *d2fonts.FontFamily
41
42 InputPath string
43 }
44
45 func Parse(ctx context.Context, input string, compileOpts *CompileOptions) (*d2ast.Map, error) {
46 if compileOpts == nil {
47 compileOpts = &CompileOptions{}
48 }
49
50 ast, err := d2parser.Parse(compileOpts.InputPath, strings.NewReader(input), &d2parser.ParseOptions{
51 UTF16Pos: compileOpts.UTF16Pos,
52 })
53 return ast, err
54 }
55
56 func Compile(ctx context.Context, input string, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, *d2graph.Graph, error) {
57 if compileOpts == nil {
58 compileOpts = &CompileOptions{}
59 }
60 if renderOpts == nil {
61 renderOpts = &d2svg.RenderOpts{}
62 }
63
64 g, config, err := d2compiler.Compile(compileOpts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{
65 UTF16Pos: compileOpts.UTF16Pos,
66 FS: compileOpts.FS,
67 })
68 if err != nil {
69 return nil, nil, err
70 }
71
72 applyConfigs(config, compileOpts, renderOpts)
73 applyDefaults(compileOpts, renderOpts)
74
75 d, err := compile(ctx, g, compileOpts, renderOpts)
76 if d != nil {
77 d.Config = config
78 }
79 return d, g, err
80 }
81
82 func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, error) {
83 err := g.ApplyTheme(*renderOpts.ThemeID)
84 if err != nil {
85 return nil, err
86 }
87
88 if len(g.Objects) > 0 {
89 err := g.SetDimensions(compileOpts.MeasuredTexts, compileOpts.Ruler, compileOpts.FontFamily)
90 if err != nil {
91 return nil, err
92 }
93
94 coreLayout, err := getLayout(compileOpts)
95 if err != nil {
96 return nil, err
97 }
98 edgeRouter, err := getEdgeRouter(compileOpts)
99 if err != nil {
100 return nil, err
101 }
102
103 graphInfo := d2layouts.NestedGraphInfo(g.Root)
104 err = d2layouts.LayoutNested(ctx, g, graphInfo, coreLayout, edgeRouter)
105 if err != nil {
106 return nil, err
107 }
108 }
109
110 d, err := d2exporter.Export(ctx, g, compileOpts.FontFamily)
111 if err != nil {
112 return nil, err
113 }
114
115 for _, l := range g.Layers {
116 ld, err := compile(ctx, l, compileOpts, renderOpts)
117 if err != nil {
118 return nil, err
119 }
120 d.Layers = append(d.Layers, ld)
121 }
122 for _, l := range g.Scenarios {
123 ld, err := compile(ctx, l, compileOpts, renderOpts)
124 if err != nil {
125 return nil, err
126 }
127 d.Scenarios = append(d.Scenarios, ld)
128 }
129 for _, l := range g.Steps {
130 ld, err := compile(ctx, l, compileOpts, renderOpts)
131 if err != nil {
132 return nil, err
133 }
134 d.Steps = append(d.Steps, ld)
135 }
136 return d, nil
137 }
138
139 func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
140 if opts.Layout != nil {
141 return opts.LayoutResolver(*opts.Layout)
142 } else if os.Getenv("D2_LAYOUT") == "dagre" {
143 defaultLayout := func(ctx context.Context, g *d2graph.Graph) error {
144 return d2dagrelayout.Layout(ctx, g, nil)
145 }
146 return defaultLayout, nil
147 } else {
148 return nil, errors.New("no available layout")
149 }
150 }
151
152 func getEdgeRouter(opts *CompileOptions) (d2graph.RouteEdges, error) {
153 if opts.Layout != nil && opts.RouterResolver != nil {
154 router, err := opts.RouterResolver(*opts.Layout)
155 if err != nil {
156 return nil, err
157 }
158 if router != nil {
159 return router, nil
160 }
161 }
162 return d2layouts.DefaultRouter, nil
163 }
164
165
166
167 func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
168 if config == nil {
169 return
170 }
171
172 if compileOpts.Layout == nil {
173 compileOpts.Layout = config.LayoutEngine
174 }
175
176 if renderOpts.ThemeID == nil {
177 renderOpts.ThemeID = config.ThemeID
178 }
179 if renderOpts.DarkThemeID == nil {
180 renderOpts.DarkThemeID = config.DarkThemeID
181 }
182 if renderOpts.Sketch == nil {
183 renderOpts.Sketch = config.Sketch
184 }
185 if renderOpts.Pad == nil {
186 renderOpts.Pad = config.Pad
187 }
188 if renderOpts.Center == nil {
189 renderOpts.Center = config.Center
190 }
191 renderOpts.ThemeOverrides = config.ThemeOverrides
192 renderOpts.DarkThemeOverrides = config.DarkThemeOverrides
193 }
194
195 func applyDefaults(compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
196 if compileOpts.Layout == nil {
197 compileOpts.Layout = go2.Pointer("dagre")
198 }
199
200 if renderOpts.ThemeID == nil {
201 renderOpts.ThemeID = &d2themescatalog.NeutralDefault.ID
202 }
203 if renderOpts.Sketch == nil {
204 renderOpts.Sketch = go2.Pointer(false)
205 }
206 if *renderOpts.Sketch {
207 compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
208 }
209 if renderOpts.Pad == nil {
210 renderOpts.Pad = go2.Pointer(int64(d2svg.DEFAULT_PADDING))
211 }
212 if renderOpts.Center == nil {
213 renderOpts.Center = go2.Pointer(false)
214 }
215 }
216
View as plain text