1 package d2exporter
2
3 import (
4 "context"
5 "net/url"
6 "strconv"
7 "strings"
8
9 "oss.terrastruct.com/util-go/go2"
10
11 "oss.terrastruct.com/d2/d2graph"
12 "oss.terrastruct.com/d2/d2parser"
13 "oss.terrastruct.com/d2/d2renderers/d2fonts"
14 "oss.terrastruct.com/d2/d2target"
15 "oss.terrastruct.com/d2/d2themes"
16 "oss.terrastruct.com/d2/lib/color"
17 "oss.terrastruct.com/d2/lib/geo"
18 )
19
20 func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
21 diagram := d2target.NewDiagram()
22 applyStyles(&diagram.Root, g.Root)
23 if g.Root.Label.MapKey == nil {
24 diagram.Root.Label = g.Name
25 } else {
26 diagram.Root.Label = g.Root.Label.Value
27 }
28 diagram.Name = g.Name
29 diagram.IsFolderOnly = g.IsFolderOnly
30 if fontFamily == nil {
31 fontFamily = go2.Pointer(d2fonts.SourceSansPro)
32 }
33 if g.Theme != nil && g.Theme.SpecialRules.Mono {
34 fontFamily = go2.Pointer(d2fonts.SourceCodePro)
35 }
36 diagram.FontFamily = fontFamily
37
38 diagram.Shapes = make([]d2target.Shape, len(g.Objects))
39 for i := range g.Objects {
40 diagram.Shapes[i] = toShape(g.Objects[i], g)
41 }
42
43 diagram.Connections = make([]d2target.Connection, len(g.Edges))
44 for i := range g.Edges {
45 diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
46 }
47
48 return diagram, nil
49 }
50
51 func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
52 shape.Stroke = obj.GetStroke(shape.StrokeDash)
53 shape.Fill = obj.GetFill()
54 if obj.Shape.Value == d2target.ShapeText {
55 shape.Color = color.N1
56 }
57 if obj.Shape.Value == d2target.ShapeSQLTable || obj.Shape.Value == d2target.ShapeClass {
58 shape.PrimaryAccentColor = color.B2
59 shape.SecondaryAccentColor = color.AA2
60 shape.NeutralAccentColor = color.N2
61 }
62
63
64 if theme != nil {
65 if theme.SpecialRules.OuterContainerDoubleBorder {
66 if obj.Level() == 1 && len(obj.ChildrenArray) > 0 {
67 shape.DoubleBorder = true
68 }
69 }
70 if theme.SpecialRules.ContainerDots {
71 if len(obj.ChildrenArray) > 0 {
72 shape.FillPattern = "dots"
73 }
74 } else if theme.SpecialRules.AllPaper {
75 shape.FillPattern = "paper"
76 }
77 if theme.SpecialRules.Mono {
78 shape.FontFamily = "mono"
79 }
80 }
81 }
82
83 func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
84 if obj.Style.Opacity != nil {
85 shape.Opacity, _ = strconv.ParseFloat(obj.Style.Opacity.Value, 64)
86 }
87 if obj.Style.StrokeDash != nil {
88 shape.StrokeDash, _ = strconv.ParseFloat(obj.Style.StrokeDash.Value, 64)
89 }
90 if obj.Style.Fill != nil {
91 shape.Fill = obj.Style.Fill.Value
92 } else if obj.Shape.Value == d2target.ShapeText {
93 shape.Fill = "transparent"
94 }
95 if obj.Style.FillPattern != nil {
96 shape.FillPattern = obj.Style.FillPattern.Value
97 }
98 if obj.Style.Stroke != nil {
99 shape.Stroke = obj.Style.Stroke.Value
100 }
101 if obj.Style.StrokeWidth != nil {
102 shape.StrokeWidth, _ = strconv.Atoi(obj.Style.StrokeWidth.Value)
103 }
104 if obj.Style.Shadow != nil {
105 shape.Shadow, _ = strconv.ParseBool(obj.Style.Shadow.Value)
106 }
107 if obj.Style.ThreeDee != nil {
108 shape.ThreeDee, _ = strconv.ParseBool(obj.Style.ThreeDee.Value)
109 }
110 if obj.Style.Multiple != nil {
111 shape.Multiple, _ = strconv.ParseBool(obj.Style.Multiple.Value)
112 }
113 if obj.Style.BorderRadius != nil {
114 shape.BorderRadius, _ = strconv.Atoi(obj.Style.BorderRadius.Value)
115 }
116
117 if obj.Style.FontColor != nil {
118 shape.Color = obj.Style.FontColor.Value
119 }
120 if obj.Style.Italic != nil {
121 shape.Italic, _ = strconv.ParseBool(obj.Style.Italic.Value)
122 }
123 if obj.Style.Bold != nil {
124 shape.Bold, _ = strconv.ParseBool(obj.Style.Bold.Value)
125 }
126 if obj.Style.Underline != nil {
127 shape.Underline, _ = strconv.ParseBool(obj.Style.Underline.Value)
128 }
129 if obj.Style.Font != nil {
130 shape.FontFamily = obj.Style.Font.Value
131 }
132 if obj.Style.DoubleBorder != nil {
133 shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value)
134 }
135 }
136
137 func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
138 shape := d2target.BaseShape()
139 shape.SetType(obj.Shape.Value)
140 shape.ID = obj.AbsID()
141 shape.Classes = obj.Classes
142 shape.ZIndex = obj.ZIndex
143 shape.Level = int(obj.Level())
144 shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
145 shape.Width = int(obj.Width)
146 shape.Height = int(obj.Height)
147
148 text := obj.Text()
149 shape.Bold = text.IsBold
150 shape.Italic = text.IsItalic
151 shape.FontSize = text.FontSize
152
153 if obj.IsSequenceDiagram() {
154 shape.StrokeWidth = 0
155 }
156
157 if obj.IsSequenceDiagramGroup() {
158 shape.StrokeWidth = 0
159 shape.Blend = true
160 }
161
162 applyStyles(shape, obj)
163 applyTheme(shape, obj, g.Theme)
164 shape.Color = text.GetColor(shape.Italic)
165 applyStyles(shape, obj)
166
167 switch obj.Shape.Value {
168 case d2target.ShapeCode, d2target.ShapeText:
169 shape.Language = obj.Language
170 shape.Label = obj.Label.Value
171 case d2target.ShapeClass:
172 shape.Class = *obj.Class
173
174 shape.FontSize -= d2target.HeaderFontAdd
175 case d2target.ShapeSQLTable:
176 shape.SQLTable = *obj.SQLTable
177 shape.FontSize -= d2target.HeaderFontAdd
178 case d2target.ShapeCloud:
179 if obj.ContentAspectRatio != nil {
180 shape.ContentAspectRatio = go2.Pointer(*obj.ContentAspectRatio)
181 }
182 }
183 shape.Label = text.Text
184 shape.LabelWidth = text.Dimensions.Width
185
186 shape.LabelHeight = text.Dimensions.Height
187 if obj.LabelPosition != nil {
188 shape.LabelPosition = *obj.LabelPosition
189 if obj.IsSequenceDiagramGroup() {
190 shape.LabelFill = shape.Fill
191 }
192 }
193
194 if obj.Tooltip != nil {
195 shape.Tooltip = obj.Tooltip.Value
196 }
197 if obj.Link != nil {
198 shape.Link = obj.Link.Value
199 shape.PrettyLink = toPrettyLink(g, obj.Link.Value)
200 }
201 shape.Icon = obj.Icon
202 if obj.IconPosition != nil {
203 shape.IconPosition = *obj.IconPosition
204 }
205
206 return *shape
207 }
208
209 func toPrettyLink(g *d2graph.Graph, link string) string {
210 u, err := url.ParseRequestURI(link)
211 if err == nil && u.Host != "" && len(u.RawPath) > 30 {
212 return u.Scheme + "://" + u.Host + u.RawPath[:10] + "..." + u.RawPath[len(u.RawPath)-10:]
213 } else if err != nil {
214 linkKey, err := d2parser.ParseKey(link)
215 if err != nil {
216 return link
217 }
218 rootG := g
219 for rootG.Parent != nil {
220 rootG = rootG.Parent
221 }
222 var prettyLink []string
223 FOR:
224 for i := 0; i < len(linkKey.Path); i++ {
225 p := linkKey.Path[i].Unbox().ScalarString()
226 if i > 0 {
227 switch p {
228 case "layers", "scenarios", "steps":
229 continue FOR
230 }
231 rootG = rootG.GetBoard(p)
232 if rootG == nil {
233 return link
234 }
235 }
236 if rootG.Root.Label.MapKey != nil {
237 prettyLink = append(prettyLink, rootG.Root.Label.Value)
238 } else {
239 prettyLink = append(prettyLink, rootG.Name)
240 }
241 }
242 for _, l := range prettyLink {
243
244 if l == "" {
245 return prettyLink[len(prettyLink)-1]
246 }
247 }
248 return strings.Join(prettyLink, " > ")
249 }
250 return link
251 }
252
253 func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
254 connection := d2target.BaseConnection()
255 connection.ID = edge.AbsID()
256 connection.Classes = edge.Classes
257 connection.ZIndex = edge.ZIndex
258 text := edge.Text()
259
260 if edge.SrcArrow {
261 connection.SrcArrow = d2target.DefaultArrowhead
262 if edge.SrcArrowhead != nil {
263 connection.SrcArrow = edge.SrcArrowhead.ToArrowhead()
264 }
265 }
266 if edge.SrcArrowhead != nil {
267 if edge.SrcArrowhead.Label.Value != "" {
268 connection.SrcLabel = &d2target.Text{
269 Label: edge.SrcArrowhead.Label.Value,
270 LabelWidth: edge.SrcArrowhead.LabelDimensions.Width,
271 LabelHeight: edge.SrcArrowhead.LabelDimensions.Height,
272 }
273 if edge.SrcArrowhead.Style.FontColor != nil {
274 connection.SrcLabel.Color = edge.SrcArrowhead.Style.FontColor.Value
275 }
276 }
277 }
278 if edge.DstArrow {
279 connection.DstArrow = d2target.DefaultArrowhead
280 if edge.DstArrowhead != nil {
281 connection.DstArrow = edge.DstArrowhead.ToArrowhead()
282 }
283 }
284 if edge.DstArrowhead != nil {
285 if edge.DstArrowhead.Label.Value != "" {
286 connection.DstLabel = &d2target.Text{
287 Label: edge.DstArrowhead.Label.Value,
288 LabelWidth: edge.DstArrowhead.LabelDimensions.Width,
289 LabelHeight: edge.DstArrowhead.LabelDimensions.Height,
290 }
291 if edge.DstArrowhead.Style.FontColor != nil {
292 connection.DstLabel.Color = edge.DstArrowhead.Style.FontColor.Value
293 }
294 }
295 }
296 if theme != nil && theme.SpecialRules.NoCornerRadius {
297 connection.BorderRadius = 0
298 }
299 if edge.Style.BorderRadius != nil {
300 connection.BorderRadius, _ = strconv.ParseFloat(edge.Style.BorderRadius.Value, 64)
301 }
302
303 if edge.Style.Opacity != nil {
304 connection.Opacity, _ = strconv.ParseFloat(edge.Style.Opacity.Value, 64)
305 }
306
307 if edge.Style.StrokeDash != nil {
308 connection.StrokeDash, _ = strconv.ParseFloat(edge.Style.StrokeDash.Value, 64)
309 }
310 connection.Stroke = edge.GetStroke(connection.StrokeDash)
311 if edge.Style.Stroke != nil {
312 connection.Stroke = edge.Style.Stroke.Value
313 }
314
315 if edge.Style.StrokeWidth != nil {
316 connection.StrokeWidth, _ = strconv.Atoi(edge.Style.StrokeWidth.Value)
317 }
318
319 if edge.Style.Fill != nil {
320 connection.Fill = edge.Style.Fill.Value
321 }
322
323 connection.FontSize = text.FontSize
324 if edge.Style.FontSize != nil {
325 connection.FontSize, _ = strconv.Atoi(edge.Style.FontSize.Value)
326 }
327
328 if edge.Style.Animated != nil {
329 connection.Animated, _ = strconv.ParseBool(edge.Style.Animated.Value)
330 }
331
332 if edge.Tooltip != nil {
333 connection.Tooltip = edge.Tooltip.Value
334 }
335 connection.Icon = edge.Icon
336
337 if edge.Style.Italic != nil {
338 connection.Italic, _ = strconv.ParseBool(edge.Style.Italic.Value)
339 }
340
341 connection.Color = text.GetColor(connection.Italic)
342 if edge.Style.FontColor != nil {
343 connection.Color = edge.Style.FontColor.Value
344 }
345 if edge.Style.Bold != nil {
346 connection.Bold, _ = strconv.ParseBool(edge.Style.Bold.Value)
347 }
348 if theme != nil && theme.SpecialRules.Mono {
349 connection.FontFamily = "mono"
350 }
351 if edge.Style.Font != nil {
352 connection.FontFamily = edge.Style.Font.Value
353 }
354 connection.Label = text.Text
355 connection.LabelWidth = text.Dimensions.Width
356 connection.LabelHeight = text.Dimensions.Height
357
358 if edge.LabelPosition != nil {
359 connection.LabelPosition = *edge.LabelPosition
360 }
361 if edge.LabelPercentage != nil {
362 connection.LabelPercentage = float64(float32(*edge.LabelPercentage))
363 }
364 connection.Route = make([]*geo.Point, 0, len(edge.Route))
365 for i := range edge.Route {
366 p := edge.Route[i].Copy()
367 p.TruncateDecimals()
368 p.TruncateFloat32()
369 connection.Route = append(connection.Route, p)
370 }
371
372 connection.IsCurve = edge.IsCurve
373
374 connection.Src = edge.Src.AbsID()
375 connection.Dst = edge.Dst.AbsID()
376
377 return *connection
378 }
379
View as plain text