package d2lib
import (
"context"
"errors"
"fmt"
"io/fs"
"strings"
"github.com/pancsta/d2/d2ast"
"github.com/pancsta/d2/d2compiler"
"github.com/pancsta/d2/d2exporter"
"github.com/pancsta/d2/d2graph"
"github.com/pancsta/d2/d2layouts"
"github.com/pancsta/d2/d2parser"
"github.com/pancsta/d2/d2renderers/d2fonts"
"github.com/pancsta/d2/d2renderers/d2svg"
"github.com/pancsta/d2/d2target"
"github.com/pancsta/d2/d2themes/d2themescatalog"
"github.com/pancsta/d2/lib/textmeasure"
"github.com/d2lang/util-go/go2"
)
type CompileOptions struct {
UTF16Pos bool
MaxVariableExpansion int64
MaxGlobExpansion int64
MaxEdgeExpansion int64
MaxEdgeExpansionWork int64
FS fs .FS
MeasuredTexts []*d2target .MText
Ruler *textmeasure .Ruler
RouterResolver func (engine string ) (d2graph .RouteEdges , error )
LayoutResolver func (engine string ) (d2graph .LayoutGraph , error )
LayoutReuse bool
Layout *string
FontFamily *d2fonts .FontFamily
MonoFontFamily *d2fonts .FontFamily
InputPath string
}
func Parse (ctx context .Context , input string , compileOpts *CompileOptions ) (*d2ast .Map , error ) {
if compileOpts == nil {
compileOpts = &CompileOptions {}
}
ast , err := d2parser .ParseContext (ctx , compileOpts .InputPath , strings .NewReader (input ), &d2parser .ParseOptions {
UTF16Pos : compileOpts .UTF16Pos ,
})
return ast , err
}
func Compile (ctx context .Context , input string , compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts ) (*d2target .Diagram , *d2graph .Graph , error ) {
return compileInput (ctx , input , compileOpts , renderOpts )
}
func compileInput(ctx context .Context , input string , compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts ) (*d2target .Diagram , *d2graph .Graph , error ) {
if compileOpts == nil {
compileOpts = &CompileOptions {}
}
if renderOpts == nil {
renderOpts = &d2svg .RenderOpts {}
}
g , config , err := d2compiler .Compile (compileOpts .InputPath , strings .NewReader (input ), &d2compiler .CompileOptions {
Context : ctx ,
UTF16Pos : compileOpts .UTF16Pos ,
MaxVariableExpansion : compileOpts .MaxVariableExpansion ,
MaxGlobExpansion : compileOpts .MaxGlobExpansion ,
MaxEdgeExpansion : compileOpts .MaxEdgeExpansion ,
MaxEdgeExpansionWork : compileOpts .MaxEdgeExpansionWork ,
FS : compileOpts .FS ,
})
if err != nil {
return nil , nil , err
}
applyConfigs (config , compileOpts , renderOpts )
isometricConfig := renderOpts .Isometric
if isometricConfig != nil && !*isometricConfig {
isometricConfig = nil
}
applyDefaults (compileOpts , renderOpts )
if *renderOpts .Sketch && *renderOpts .Isometric {
return nil , nil , errors .New ("sketch cannot be combined with isometric rendering" )
}
if config != nil {
applyGraphData (g , config .Data )
}
d , err := compile (ctx , g , compileOpts , renderOpts )
if d != nil {
if config == nil {
config = &d2target .Config {}
}
config .ThemeID = renderOpts .ThemeID
config .DarkThemeID = renderOpts .DarkThemeID
config .Sketch = renderOpts .Sketch
config .Isometric = isometricConfig
d .Config = config
}
return d , g , err
}
func applyGraphData(g *d2graph .Graph , data map [string ]interface {}) {
if g == nil {
return
}
g .Data = cloneGraphData (data )
for _ , boards := range [][]*d2graph .Graph {g .Layers , g .Scenarios , g .Steps } {
for _ , board := range boards {
applyGraphData (board , data )
}
}
}
func cloneGraphData(data map [string ]interface {}) map [string ]interface {} {
if data == nil {
return nil
}
clone := make (map [string ]interface {}, len (data ))
for key , value := range data {
clone [key ] = cloneGraphDataValue (value )
}
return clone
}
func cloneGraphDataValue(value interface {}) interface {} {
switch value := value .(type ) {
case map [string ]interface {}:
return cloneGraphData (value )
case []interface {}:
clone := make ([]interface {}, len (value ))
for i , entry := range value {
clone [i ] = cloneGraphDataValue (entry )
}
return clone
default :
return value
}
}
func compile(ctx context .Context , g *d2graph .Graph , compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts ) (*d2target .Diagram , error ) {
d , _ , err := compileBoard (ctx , g , compileOpts , renderOpts , nil , false )
return d , err
}
func compileBoard(ctx context .Context , g *d2graph .Graph , compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts , reuse *layoutGeometry , captureForCaller bool ) (*d2target .Diagram , *layoutGeometry , error ) {
err := g .ApplyTheme (*renderOpts .ThemeID )
if err != nil {
return nil , nil , err
}
var geometry *layoutGeometry
if len (g .Objects ) > 0 {
err := g .SetDimensions (compileOpts .MeasuredTexts , compileOpts .Ruler , compileOpts .FontFamily , compileOpts .MonoFontFamily )
if err != nil {
return nil , nil , err
}
needsGeometry := compileOpts .LayoutReuse && (reuse != nil || captureForCaller || len (g .Scenarios ) > 0 || len (g .Steps ) > 0 )
var signature layoutInputSignature
reusable := false
if needsGeometry {
engine := ""
if compileOpts .Layout != nil {
engine = *compileOpts .Layout
}
signature , reusable , err = newLayoutInputSignature (g , engine )
if err != nil {
return nil , nil , err
}
}
reused := ctx .Err () == nil && reusable && compileOpts .LayoutReuse && reuse .apply (g , signature )
if !reused {
coreLayout , err := getLayout (compileOpts )
if err != nil {
return nil , nil , err
}
edgeRouter , err := getEdgeRouter (compileOpts )
if err != nil {
return nil , nil , err
}
graphInfo := d2layouts .NestedGraphInfo (g .Root )
err = d2layouts .LayoutNested (ctx , g , graphInfo , coreLayout , edgeRouter )
if err != nil {
return nil , nil , err
}
}
if reusable && (captureForCaller || len (g .Scenarios ) > 0 || len (g .Steps ) > 0 ) {
geometry = captureLayoutGeometry (g , signature )
}
}
d , err := d2exporter .Export (ctx , g , compileOpts .FontFamily , compileOpts .MonoFontFamily )
if err != nil {
return nil , nil , err
}
for _ , l := range g .Layers {
ld , _ , err := compileBoard (ctx , l , compileOpts , renderOpts , nil , false )
if err != nil {
return nil , nil , err
}
d .Layers = append (d .Layers , ld )
}
for _ , l := range g .Scenarios {
ld , _ , err := compileBoard (ctx , l , compileOpts , renderOpts , geometry , false )
if err != nil {
return nil , nil , err
}
d .Scenarios = append (d .Scenarios , ld )
}
previous := geometry
for i , l := range g .Steps {
ld , stepGeometry , err := compileBoard (ctx , l , compileOpts , renderOpts , previous , i +1 < len (g .Steps ))
if err != nil {
return nil , nil , err
}
d .Steps = append (d .Steps , ld )
previous = stepGeometry
}
return d , geometry , nil
}
func getLayout(opts *CompileOptions ) (d2graph .LayoutGraph , error ) {
if opts .Layout == nil {
return nil , errors .New ("no available layout" )
}
if opts .LayoutResolver == nil {
return nil , fmt .Errorf ("no layout resolver configured for layout engine %q" , *opts .Layout )
}
return opts .LayoutResolver (*opts .Layout )
}
func getEdgeRouter(opts *CompileOptions ) (d2graph .RouteEdges , error ) {
if opts .Layout != nil && opts .RouterResolver != nil {
router , err := opts .RouterResolver (*opts .Layout )
if err != nil {
return nil , err
}
if router != nil {
return router , nil
}
}
return d2layouts .DefaultRouter , nil
}
func applyConfigs(config *d2target .Config , compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts ) {
if config == nil {
return
}
if compileOpts .Layout == nil {
compileOpts .Layout = config .LayoutEngine
}
if renderOpts .ThemeID == nil {
renderOpts .ThemeID = config .ThemeID
}
if renderOpts .DarkThemeID == nil {
renderOpts .DarkThemeID = config .DarkThemeID
}
if renderOpts .Sketch == nil {
renderOpts .Sketch = config .Sketch
}
if renderOpts .Isometric == nil {
renderOpts .Isometric = config .Isometric
}
if renderOpts .Pad == nil {
renderOpts .Pad = config .Pad
}
if renderOpts .Center == nil {
renderOpts .Center = config .Center
}
if renderOpts .ThemeOverrides == nil {
renderOpts .ThemeOverrides = config .ThemeOverrides
}
if renderOpts .DarkThemeOverrides == nil {
renderOpts .DarkThemeOverrides = config .DarkThemeOverrides
}
}
func applyDefaults(compileOpts *CompileOptions , renderOpts *d2svg .RenderOpts ) {
if compileOpts .Layout == nil {
compileOpts .Layout = go2 .Pointer ("dagre" )
}
if renderOpts .ThemeID == nil {
renderOpts .ThemeID = &d2themescatalog .NeutralDefault .ID
}
if renderOpts .Sketch == nil {
renderOpts .Sketch = go2 .Pointer (false )
}
if renderOpts .Isometric == nil {
renderOpts .Isometric = go2 .Pointer (false )
}
if *renderOpts .Sketch && compileOpts .FontFamily == nil {
compileOpts .FontFamily = go2 .Pointer (d2fonts .HandDrawn )
}
if renderOpts .Pad == nil {
renderOpts .Pad = go2 .Pointer (int64 (d2svg .DEFAULT_PADDING ))
}
if renderOpts .Center == nil {
renderOpts .Center = go2 .Pointer (false )
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .