...

Source file src/oss.terrastruct.com/d2/d2format/format.go

Documentation: oss.terrastruct.com/d2/d2format

     1  package d2format
     2  
     3  import (
     4  	"path"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"oss.terrastruct.com/d2/d2ast"
     9  )
    10  
    11  // TODO: edges with shared path should be fmted as <rel>.(x -> y)
    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  		// Handle inline comments.
   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  		// extract out layer, scenario, and step nodes and skip
   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  		// Handle inline comments.
   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  	// draw board nodes
   332  	for i := 0; i < len(boards); i++ {
   333  		n := boards[i].Unbox()
   334  		// if this board is the very first line of the file, don't add an extra newline
   335  		if n.GetRange().Start.Line != 0 {
   336  			p.newline()
   337  		}
   338  		// if scope only has boards, don't newline the first board
   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  		// Always write a trailing newline for nonempty file maps.
   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  		// We format each string of the key to ensure the resulting strings can be parsed
   463  		// correctly.
   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