...

Source file src/oss.terrastruct.com/d2/d2exporter/export.go

Documentation: oss.terrastruct.com/d2/d2exporter

     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  	// Theme options that change more than color
    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  		// The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size
   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  			// If any part of it is blank, "x > > y" looks stupid, so just use the last
   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