...
1 package d2format
2
3 import (
4 "path"
5 "strconv"
6 "strings"
7
8 "oss.terrastruct.com/d2/d2ast"
9 )
10
11
12 func Format(n d2ast.Node) string {
13 var p printer
14 p.node(n)
15 return p.sb.String()
16 }
17
18 type printer struct {
19 sb strings.Builder
20 indentStr string
21 inKey bool
22 }
23
24 func (p *printer) indent() {
25 p.indentStr += " " + " "
26 }
27
28 func (p *printer) deindent() {
29 p.indentStr = p.indentStr[:len(p.indentStr)-2]
30 }
31
32 func (p *printer) newline() {
33 p.sb.WriteByte('\n')
34 p.sb.WriteString(p.indentStr)
35 }
36
37 func (p *printer) node(n d2ast.Node) {
38 switch n := n.(type) {
39 case *d2ast.Comment:
40 p.comment(n)
41 case *d2ast.BlockComment:
42 p.blockComment(n)
43 case *d2ast.Null:
44 p.sb.WriteString("null")
45 case *d2ast.Boolean:
46 p.sb.WriteString(strconv.FormatBool(n.Value))
47 case *d2ast.Number:
48 p.sb.WriteString(n.Raw)
49 case *d2ast.UnquotedString:
50 p.interpolationBoxes(n.Value, false)
51 case *d2ast.DoubleQuotedString:
52 p.sb.WriteByte('"')
53 p.interpolationBoxes(n.Value, true)
54 p.sb.WriteByte('"')
55 case *d2ast.SingleQuotedString:
56 p.sb.WriteByte('\'')
57 if n.Raw == "" {
58 n.Raw = escapeSingleQuotedValue(n.Value)
59 }
60 p.sb.WriteString(escapeSingleQuotedValue(n.Value))
61 p.sb.WriteByte('\'')
62 case *d2ast.BlockString:
63 p.blockString(n)
64 case *d2ast.Substitution:
65 p.substitution(n)
66 case *d2ast.Import:
67 p._import(n)
68 case *d2ast.Array:
69 p.array(n)
70 case *d2ast.Map:
71 p._map(n)
72 case *d2ast.Key:
73 p.mapKey(n)
74 case *d2ast.KeyPath:
75 p.key(n)
76 case *d2ast.Edge:
77 p.edge(n)
78 case *d2ast.EdgeIndex:
79 p.edgeIndex(n)
80 }
81 }
82
83 func (p *printer) comment(c *d2ast.Comment) {
84 lines := strings.Split(c.Value, "\n")
85 for i, line := range lines {
86 p.sb.WriteString("#")
87 if line != "" {
88 p.sb.WriteByte(' ')
89 }
90 p.sb.WriteString(line)
91 if i < len(lines)-1 {
92 p.newline()
93 }
94 }
95 }
96
97 func (p *printer) blockComment(bc *d2ast.BlockComment) {
98 p.sb.WriteString(`"""`)
99 if bc.Range.OneLine() {
100 p.sb.WriteByte(' ')
101 }
102
103 lines := strings.Split(bc.Value, "\n")
104 for _, l := range lines {
105 if !bc.Range.OneLine() {
106 if l == "" {
107 p.sb.WriteByte('\n')
108 } else {
109 p.newline()
110 }
111 }
112 p.sb.WriteString(l)
113 }
114
115 if !bc.Range.OneLine() {
116 p.newline()
117 } else {
118 p.sb.WriteByte(' ')
119 }
120 p.sb.WriteString(`"""`)
121 }
122
123 func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleString bool) {
124 for _, b := range boxes {
125 if b.Substitution != nil {
126 p.substitution(b.Substitution)
127 continue
128 }
129 if b.StringRaw == nil {
130 var s string
131 if isDoubleString {
132 s = escapeDoubledQuotedValue(*b.String, p.inKey)
133 } else {
134 s = escapeUnquotedValue(*b.String, p.inKey)
135 }
136 b.StringRaw = &s
137 }
138 p.sb.WriteString(*b.StringRaw)
139 }
140 }
141
142 func (p *printer) blockString(bs *d2ast.BlockString) {
143 quote := bs.Quote
144 for strings.Contains(bs.Value, "|"+quote) {
145 if quote == "" {
146 quote += "|"
147 } else {
148 quote += string(quote[len(quote)-1])
149 }
150 }
151 for strings.Contains(bs.Value, quote+"|") {
152 quote += string(quote[len(quote)-1])
153 }
154
155 if bs.Range == (d2ast.Range{}) {
156 if strings.IndexByte(bs.Value, '\n') > -1 {
157 bs.Range = d2ast.MakeRange(",1:0:0-2:0:0")
158 }
159 bs.Value = strings.TrimSpace(bs.Value)
160 }
161
162 p.sb.WriteString("|" + quote)
163 p.sb.WriteString(bs.Tag)
164 if !bs.Range.OneLine() {
165 p.indent()
166 } else {
167 p.sb.WriteByte(' ')
168 }
169
170 lines := strings.Split(bs.Value, "\n")
171 for _, l := range lines {
172 if !bs.Range.OneLine() {
173 if l == "" {
174 p.sb.WriteByte('\n')
175 } else {
176 p.newline()
177 }
178 }
179 p.sb.WriteString(l)
180 }
181
182 if !bs.Range.OneLine() {
183 p.deindent()
184 p.newline()
185 } else if bs.Value != "" {
186 p.sb.WriteByte(' ')
187 }
188 p.sb.WriteString(quote + "|")
189 }
190
191 func (p *printer) path(els []*d2ast.StringBox) {
192 for i, s := range els {
193 p.node(s.Unbox())
194 if i < len(els)-1 {
195 p.sb.WriteByte('.')
196 }
197 }
198 }
199
200 func (p *printer) substitution(s *d2ast.Substitution) {
201 if s.Spread {
202 p.sb.WriteString("...")
203 }
204 p.sb.WriteString("${")
205 p.path(s.Path)
206 p.sb.WriteByte('}')
207 }
208
209 func (p *printer) _import(i *d2ast.Import) {
210 if i.Spread {
211 p.sb.WriteString("...")
212 }
213 p.sb.WriteString("@")
214 pre := path.Clean(i.Pre)
215 if pre != "." {
216 p.sb.WriteString(pre)
217 p.sb.WriteRune('/')
218 }
219 if len(i.Path) > 0 {
220 i2 := *i
221 i2.Path = append([]*d2ast.StringBox{}, i.Path...)
222 i2.Path[0] = d2ast.RawStringBox(path.Clean(i.Path[0].Unbox().ScalarString()), true)
223 i = &i2
224 }
225 p.path(i.Path)
226 }
227
228 func (p *printer) array(a *d2ast.Array) {
229 p.sb.WriteByte('[')
230 if !a.Range.OneLine() {
231 p.indent()
232 }
233
234 prev := d2ast.Node(a)
235 for i := 0; i < len(a.Nodes); i++ {
236 nb := a.Nodes[i]
237 n := nb.Unbox()
238
239
240 if i > 0 && (nb.Comment != nil || nb.BlockComment != nil) {
241 if n.GetRange().Start.Line == prev.GetRange().End.Line && n.GetRange().OneLine() {
242 p.sb.WriteByte(' ')
243 p.node(n)
244 continue
245 }
246 }
247
248 if !a.Range.OneLine() {
249 if prev != a {
250 if n.GetRange().Start.Line-prev.GetRange().End.Line > 1 {
251 p.sb.WriteByte('\n')
252 }
253 }
254 p.newline()
255 } else if i > 0 {
256 p.sb.WriteString("; ")
257 }
258
259 p.node(n)
260 prev = n
261 }
262
263 if !a.Range.OneLine() {
264 p.deindent()
265 p.newline()
266 }
267 p.sb.WriteByte(']')
268 }
269
270 func (p *printer) _map(m *d2ast.Map) {
271 if !m.IsFileMap() {
272 p.sb.WriteByte('{')
273 if !m.Range.OneLine() {
274 p.indent()
275 }
276 }
277
278 layerNodes := []d2ast.MapNodeBox{}
279 scenarioNodes := []d2ast.MapNodeBox{}
280 stepNodes := []d2ast.MapNodeBox{}
281
282 prev := d2ast.Node(m)
283 for i := 0; i < len(m.Nodes); i++ {
284 nb := m.Nodes[i]
285 n := nb.Unbox()
286
287 if nb.IsBoardNode() {
288 switch nb.MapKey.Key.Path[0].Unbox().ScalarString() {
289 case "layers":
290 layerNodes = append(layerNodes, nb)
291 case "scenarios":
292 scenarioNodes = append(scenarioNodes, nb)
293 case "steps":
294 stepNodes = append(stepNodes, nb)
295 }
296 prev = n
297 continue
298 }
299
300
301 if i > 0 && (nb.Comment != nil || nb.BlockComment != nil) {
302 if n.GetRange().Start.Line == prev.GetRange().End.Line && n.GetRange().OneLine() {
303 p.sb.WriteByte(' ')
304 p.node(n)
305 continue
306 }
307 }
308
309 if !m.Range.OneLine() {
310 if prev != m {
311 if n.GetRange().Start.Line-prev.GetRange().End.Line > 1 {
312 p.sb.WriteByte('\n')
313 }
314 }
315 if !m.IsFileMap() || i > 0 {
316 p.newline()
317 }
318 } else if i > 0 {
319 p.sb.WriteString("; ")
320 }
321
322 p.node(n)
323 prev = n
324 }
325
326 boards := []d2ast.MapNodeBox{}
327 boards = append(boards, layerNodes...)
328 boards = append(boards, scenarioNodes...)
329 boards = append(boards, stepNodes...)
330
331
332 for i := 0; i < len(boards); i++ {
333 n := boards[i].Unbox()
334
335 if n.GetRange().Start.Line != 0 {
336 p.newline()
337 }
338
339 if i != 0 || len(m.Nodes) > len(boards) {
340 p.newline()
341 }
342 p.node(n)
343 prev = n
344 }
345
346 if !m.IsFileMap() {
347 if !m.Range.OneLine() {
348 p.deindent()
349 p.newline()
350 }
351 p.sb.WriteByte('}')
352 } else if len(m.Nodes) > 0 {
353
354 p.sb.WriteByte('\n')
355 }
356 }
357
358 func (p *printer) mapKey(mk *d2ast.Key) {
359 if mk.Ampersand {
360 p.sb.WriteByte('&')
361 }
362 if mk.Key != nil {
363 p.key(mk.Key)
364 }
365
366 if len(mk.Edges) > 0 {
367 if mk.Key != nil {
368 p.sb.WriteByte('.')
369 }
370
371 if mk.Key != nil || mk.EdgeIndex != nil || mk.EdgeKey != nil {
372 p.sb.WriteByte('(')
373 }
374 if mk.Edges[0].Src != nil {
375 p.key(mk.Edges[0].Src)
376 p.sb.WriteByte(' ')
377 }
378 for i, e := range mk.Edges {
379 p.edgeArrowAndDst(e)
380 if i < len(mk.Edges)-1 {
381 p.sb.WriteByte(' ')
382 }
383 }
384 if mk.Key != nil || mk.EdgeIndex != nil || mk.EdgeKey != nil {
385 p.sb.WriteByte(')')
386 }
387
388 if mk.EdgeIndex != nil {
389 p.edgeIndex(mk.EdgeIndex)
390 }
391 if mk.EdgeKey != nil {
392 p.sb.WriteByte('.')
393 p.key(mk.EdgeKey)
394 }
395 }
396
397 if mk.Primary.Unbox() != nil {
398 p.sb.WriteString(": ")
399 p.node(mk.Primary.Unbox())
400 }
401 if mk.Value.Map != nil && len(mk.Value.Map.Nodes) == 0 {
402 return
403 }
404 if mk.Value.Unbox() != nil {
405 if mk.Primary.Unbox() == nil {
406 p.sb.WriteString(": ")
407 } else {
408 p.sb.WriteByte(' ')
409 }
410 p.node(mk.Value.Unbox())
411 }
412 }
413
414 func (p *printer) key(k *d2ast.KeyPath) {
415 p.inKey = true
416 if k != nil {
417 p.path(k.Path)
418 }
419 p.inKey = false
420 }
421
422 func (p *printer) edge(e *d2ast.Edge) {
423 if e.Src != nil {
424 p.key(e.Src)
425 p.sb.WriteByte(' ')
426 }
427 p.edgeArrowAndDst(e)
428 }
429
430 func (p *printer) edgeArrowAndDst(e *d2ast.Edge) {
431 if e.SrcArrow == "" {
432 p.sb.WriteByte('-')
433 } else {
434 p.sb.WriteString(e.SrcArrow)
435 }
436 if e.DstArrow == "" {
437 p.sb.WriteByte('-')
438 } else {
439 if e.SrcArrow != "" {
440 p.sb.WriteByte('-')
441 }
442 p.sb.WriteString(e.DstArrow)
443 }
444 if e.Dst != nil {
445 p.sb.WriteByte(' ')
446 p.key(e.Dst)
447 }
448 }
449
450 func (p *printer) edgeIndex(ei *d2ast.EdgeIndex) {
451 p.sb.WriteByte('[')
452 if ei.Glob {
453 p.sb.WriteByte('*')
454 } else {
455 p.sb.WriteString(strconv.Itoa(*ei.Int))
456 }
457 p.sb.WriteByte(']')
458 }
459
460 func KeyPath(kp *d2ast.KeyPath) (ida []string) {
461 for _, s := range kp.Path {
462
463
464 n := &d2ast.KeyPath{
465 Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(s.Unbox().ScalarString(), true)).StringBox()},
466 }
467 ida = append(ida, Format(n))
468 }
469 return ida
470 }
471
View as plain text