...

Source file src/oss.terrastruct.com/d2/d2lib/d2.go

Documentation: oss.terrastruct.com/d2/d2lib

     1  package d2lib
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io/fs"
     7  	"os"
     8  	"strings"
     9  
    10  	"oss.terrastruct.com/d2/d2ast"
    11  	"oss.terrastruct.com/d2/d2compiler"
    12  	"oss.terrastruct.com/d2/d2exporter"
    13  	"oss.terrastruct.com/d2/d2graph"
    14  	"oss.terrastruct.com/d2/d2layouts"
    15  	"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
    16  	"oss.terrastruct.com/d2/d2parser"
    17  	"oss.terrastruct.com/d2/d2renderers/d2fonts"
    18  	"oss.terrastruct.com/d2/d2renderers/d2svg"
    19  	"oss.terrastruct.com/d2/d2target"
    20  	"oss.terrastruct.com/d2/d2themes/d2themescatalog"
    21  	"oss.terrastruct.com/d2/lib/textmeasure"
    22  	"oss.terrastruct.com/util-go/go2"
    23  )
    24  
    25  type CompileOptions struct {
    26  	UTF16Pos       bool
    27  	FS             fs.FS
    28  	MeasuredTexts  []*d2target.MText
    29  	Ruler          *textmeasure.Ruler
    30  	RouterResolver func(engine string) (d2graph.RouteEdges, error)
    31  	LayoutResolver func(engine string) (d2graph.LayoutGraph, error)
    32  
    33  	Layout *string
    34  
    35  	// FontFamily controls the font family used for all texts that are not the following:
    36  	// - code
    37  	// - latex
    38  	// - pre-measured (web setting)
    39  	// TODO maybe some will want to configure code font too, but that's much lower priority
    40  	FontFamily *d2fonts.FontFamily
    41  
    42  	InputPath string
    43  }
    44  
    45  func Parse(ctx context.Context, input string, compileOpts *CompileOptions) (*d2ast.Map, error) {
    46  	if compileOpts == nil {
    47  		compileOpts = &CompileOptions{}
    48  	}
    49  
    50  	ast, err := d2parser.Parse(compileOpts.InputPath, strings.NewReader(input), &d2parser.ParseOptions{
    51  		UTF16Pos: compileOpts.UTF16Pos,
    52  	})
    53  	return ast, err
    54  }
    55  
    56  func Compile(ctx context.Context, input string, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, *d2graph.Graph, error) {
    57  	if compileOpts == nil {
    58  		compileOpts = &CompileOptions{}
    59  	}
    60  	if renderOpts == nil {
    61  		renderOpts = &d2svg.RenderOpts{}
    62  	}
    63  
    64  	g, config, err := d2compiler.Compile(compileOpts.InputPath, strings.NewReader(input), &d2compiler.CompileOptions{
    65  		UTF16Pos: compileOpts.UTF16Pos,
    66  		FS:       compileOpts.FS,
    67  	})
    68  	if err != nil {
    69  		return nil, nil, err
    70  	}
    71  
    72  	applyConfigs(config, compileOpts, renderOpts)
    73  	applyDefaults(compileOpts, renderOpts)
    74  
    75  	d, err := compile(ctx, g, compileOpts, renderOpts)
    76  	if d != nil {
    77  		d.Config = config
    78  	}
    79  	return d, g, err
    80  }
    81  
    82  func compile(ctx context.Context, g *d2graph.Graph, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) (*d2target.Diagram, error) {
    83  	err := g.ApplyTheme(*renderOpts.ThemeID)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	if len(g.Objects) > 0 {
    89  		err := g.SetDimensions(compileOpts.MeasuredTexts, compileOpts.Ruler, compileOpts.FontFamily)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  
    94  		coreLayout, err := getLayout(compileOpts)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		edgeRouter, err := getEdgeRouter(compileOpts)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  
   103  		graphInfo := d2layouts.NestedGraphInfo(g.Root)
   104  		err = d2layouts.LayoutNested(ctx, g, graphInfo, coreLayout, edgeRouter)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  	}
   109  
   110  	d, err := d2exporter.Export(ctx, g, compileOpts.FontFamily)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	for _, l := range g.Layers {
   116  		ld, err := compile(ctx, l, compileOpts, renderOpts)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		d.Layers = append(d.Layers, ld)
   121  	}
   122  	for _, l := range g.Scenarios {
   123  		ld, err := compile(ctx, l, compileOpts, renderOpts)
   124  		if err != nil {
   125  			return nil, err
   126  		}
   127  		d.Scenarios = append(d.Scenarios, ld)
   128  	}
   129  	for _, l := range g.Steps {
   130  		ld, err := compile(ctx, l, compileOpts, renderOpts)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  		d.Steps = append(d.Steps, ld)
   135  	}
   136  	return d, nil
   137  }
   138  
   139  func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
   140  	if opts.Layout != nil {
   141  		return opts.LayoutResolver(*opts.Layout)
   142  	} else if os.Getenv("D2_LAYOUT") == "dagre" {
   143  		defaultLayout := func(ctx context.Context, g *d2graph.Graph) error {
   144  			return d2dagrelayout.Layout(ctx, g, nil)
   145  		}
   146  		return defaultLayout, nil
   147  	} else {
   148  		return nil, errors.New("no available layout")
   149  	}
   150  }
   151  
   152  func getEdgeRouter(opts *CompileOptions) (d2graph.RouteEdges, error) {
   153  	if opts.Layout != nil && opts.RouterResolver != nil {
   154  		router, err := opts.RouterResolver(*opts.Layout)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  		if router != nil {
   159  			return router, nil
   160  		}
   161  	}
   162  	return d2layouts.DefaultRouter, nil
   163  }
   164  
   165  // applyConfigs applies the configs read from D2 and applies it to passed in opts
   166  // It will only write to opt fields that are nil, as passed-in opts have precedence
   167  func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
   168  	if config == nil {
   169  		return
   170  	}
   171  
   172  	if compileOpts.Layout == nil {
   173  		compileOpts.Layout = config.LayoutEngine
   174  	}
   175  
   176  	if renderOpts.ThemeID == nil {
   177  		renderOpts.ThemeID = config.ThemeID
   178  	}
   179  	if renderOpts.DarkThemeID == nil {
   180  		renderOpts.DarkThemeID = config.DarkThemeID
   181  	}
   182  	if renderOpts.Sketch == nil {
   183  		renderOpts.Sketch = config.Sketch
   184  	}
   185  	if renderOpts.Pad == nil {
   186  		renderOpts.Pad = config.Pad
   187  	}
   188  	if renderOpts.Center == nil {
   189  		renderOpts.Center = config.Center
   190  	}
   191  	renderOpts.ThemeOverrides = config.ThemeOverrides
   192  	renderOpts.DarkThemeOverrides = config.DarkThemeOverrides
   193  }
   194  
   195  func applyDefaults(compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
   196  	if compileOpts.Layout == nil {
   197  		compileOpts.Layout = go2.Pointer("dagre")
   198  	}
   199  
   200  	if renderOpts.ThemeID == nil {
   201  		renderOpts.ThemeID = &d2themescatalog.NeutralDefault.ID
   202  	}
   203  	if renderOpts.Sketch == nil {
   204  		renderOpts.Sketch = go2.Pointer(false)
   205  	}
   206  	if *renderOpts.Sketch {
   207  		compileOpts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
   208  	}
   209  	if renderOpts.Pad == nil {
   210  		renderOpts.Pad = go2.Pointer(int64(d2svg.DEFAULT_PADDING))
   211  	}
   212  	if renderOpts.Center == nil {
   213  		renderOpts.Center = go2.Pointer(false)
   214  	}
   215  }
   216  

View as plain text