1 package d2cli
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "os/exec"
12 "os/user"
13 "path/filepath"
14 "strconv"
15 "strings"
16 "time"
17
18 "github.com/playwright-community/playwright-go"
19 "github.com/spf13/pflag"
20 "go.uber.org/multierr"
21
22 "oss.terrastruct.com/util-go/go2"
23 "oss.terrastruct.com/util-go/xmain"
24
25 "oss.terrastruct.com/d2/d2ast"
26 "oss.terrastruct.com/d2/d2graph"
27 "oss.terrastruct.com/d2/d2lib"
28 "oss.terrastruct.com/d2/d2parser"
29 "oss.terrastruct.com/d2/d2plugin"
30 "oss.terrastruct.com/d2/d2renderers/d2animate"
31 "oss.terrastruct.com/d2/d2renderers/d2fonts"
32 "oss.terrastruct.com/d2/d2renderers/d2svg"
33 "oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
34 "oss.terrastruct.com/d2/d2target"
35 "oss.terrastruct.com/d2/d2themes"
36 "oss.terrastruct.com/d2/d2themes/d2themescatalog"
37 "oss.terrastruct.com/d2/lib/background"
38 "oss.terrastruct.com/d2/lib/imgbundler"
39 ctxlog "oss.terrastruct.com/d2/lib/log"
40 "oss.terrastruct.com/d2/lib/pdf"
41 "oss.terrastruct.com/d2/lib/png"
42 "oss.terrastruct.com/d2/lib/pptx"
43 "oss.terrastruct.com/d2/lib/simplelog"
44 "oss.terrastruct.com/d2/lib/textmeasure"
45 timelib "oss.terrastruct.com/d2/lib/time"
46 "oss.terrastruct.com/d2/lib/version"
47 "oss.terrastruct.com/d2/lib/xgif"
48
49 "cdr.dev/slog"
50 "cdr.dev/slog/sloggers/sloghuman"
51 )
52
53 func Run(ctx context.Context, ms *xmain.State) (err error) {
54
55 ctx = DiscardSlog(ctx)
56
57
58 watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).")
59 if err != nil {
60 return err
61 }
62 hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch")
63 portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch")
64 bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file")
65 if err != nil {
66 return err
67 }
68 forceAppendixFlag, err := ms.Opts.Bool("D2_FORCE_APPENDIX", "force-appendix", "", false, "an appendix for tooltips and links is added to PNG exports since they are not interactive. --force-appendix adds an appendix to SVG exports as well")
69 if err != nil {
70 return err
71 }
72 debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.")
73 if err != nil {
74 ms.Log.Warn.Printf("Invalid DEBUG flag value ignored")
75 debugFlag = go2.Pointer(false)
76 }
77 imgCacheFlag, err := ms.Opts.Bool("IMG_CACHE", "img-cache", "", true, "in watch mode, images used in icons are cached for subsequent compilations. This should be disabled if images might change.")
78 if err != nil {
79 return err
80 }
81 layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used`)
82 themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID")
83 if err != nil {
84 return err
85 }
86 darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
87 if err != nil {
88 return err
89 }
90 padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
91 if err != nil {
92 return err
93 }
94 animateIntervalFlag, err := ms.Opts.Int64("D2_ANIMATE_INTERVAL", "animate-interval", "", 0, "if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports.")
95 if err != nil {
96 return err
97 }
98 timeoutFlag, err := ms.Opts.Int64("D2_TIMEOUT", "timeout", "", 120, "the maximum number of seconds that D2 runs for before timing out and exiting. When rendering a large diagram, it is recommended to increase this value")
99 if err != nil {
100 return err
101 }
102
103 versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
104 if err != nil {
105 return err
106 }
107 sketchFlag, err := ms.Opts.Bool("D2_SKETCH", "sketch", "s", false, "render the diagram to look like it was sketched by hand")
108 if err != nil {
109 return err
110 }
111 browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
112 centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
113 if err != nil {
114 return err
115 }
116 scaleFlag, err := ms.Opts.Float64("SCALE", "scale", "", -1, "scale the output. E.g., 0.5 to halve the default size. Default -1 means that SVG's will fit to screen and all others will use their default render size. Setting to 1 turns off SVG fitting to screen.")
117 if err != nil {
118 return err
119 }
120 targetFlag := ms.Opts.String("", "target", "", "*", "target board to render. Pass an empty string to target root board. If target ends with '*', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. --target='' to render root board only or --target='layers.x.*' to render layer 'x' with all of its children.")
121
122 fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.")
123 fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.")
124 fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
125 fontSemiboldFlag := ms.Opts.String("D2_FONT_SEMIBOLD", "font-semibold", "", "", "path to .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.")
126
127 plugins, err := d2plugin.ListPlugins(ctx)
128 if err != nil {
129 return err
130 }
131 err = populateLayoutOpts(ctx, ms, plugins)
132 if err != nil {
133 return err
134 }
135
136 err = ms.Opts.Flags.Parse(ms.Opts.Args)
137 if !errors.Is(err, pflag.ErrHelp) && err != nil {
138 return xmain.UsageErrorf("failed to parse flags: %v", err)
139 }
140
141 if errors.Is(err, pflag.ErrHelp) {
142 help(ms)
143 return nil
144 }
145
146 fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag, *fontSemiboldFlag)
147 if err != nil {
148 return xmain.UsageErrorf("failed to load specified fonts: %v", err)
149 }
150
151 if len(ms.Opts.Flags.Args()) > 0 {
152 switch ms.Opts.Flags.Arg(0) {
153 case "init-playwright":
154 return initPlaywright()
155 case "layout":
156 return layoutCmd(ctx, ms, plugins)
157 case "themes":
158 themesCmd(ctx, ms)
159 return nil
160 case "fmt":
161 return fmtCmd(ctx, ms)
162 case "version":
163 if len(ms.Opts.Flags.Args()) > 1 {
164 return xmain.UsageErrorf("version subcommand accepts no arguments")
165 }
166 fmt.Println(version.Version)
167 return nil
168 }
169 }
170
171 if *debugFlag {
172 ms.Env.Setenv("DEBUG", "1")
173 }
174 if *imgCacheFlag {
175 ms.Env.Setenv("IMG_CACHE", "1")
176 }
177 if *browserFlag != "" {
178 ms.Env.Setenv("BROWSER", *browserFlag)
179 }
180 if timeoutFlag != nil {
181 os.Setenv("D2_TIMEOUT", fmt.Sprintf("%d", *timeoutFlag))
182 }
183
184 var inputPath string
185 var outputPath string
186
187 if len(ms.Opts.Flags.Args()) == 0 {
188 if versionFlag != nil && *versionFlag {
189 fmt.Println(version.Version)
190 return nil
191 }
192 help(ms)
193 return nil
194 } else if len(ms.Opts.Flags.Args()) >= 3 {
195 return xmain.UsageErrorf("too many arguments passed")
196 }
197
198 if len(ms.Opts.Flags.Args()) >= 1 {
199 inputPath = ms.Opts.Flags.Arg(0)
200 }
201 if len(ms.Opts.Flags.Args()) >= 2 {
202 outputPath = ms.Opts.Flags.Arg(1)
203 } else {
204 if inputPath == "-" {
205 outputPath = "-"
206 } else {
207 outputPath = renameExt(inputPath, ".svg")
208 }
209 }
210 if inputPath != "-" {
211 inputPath = ms.AbsPath(inputPath)
212 d, err := os.Stat(inputPath)
213 if err == nil && d.IsDir() {
214 inputPath = filepath.Join(inputPath, "index.d2")
215 }
216 }
217 if filepath.Ext(outputPath) == ".ppt" {
218 return xmain.UsageErrorf("D2 does not support ppt exports, did you mean \"pptx\"?")
219 }
220 outputFormat := getExportExtension(outputPath)
221 if outputPath != "-" {
222 outputPath = ms.AbsPath(outputPath)
223 if *animateIntervalFlag > 0 && !outputFormat.supportsAnimation() {
224 return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG or GIF.\nYou provided: %s", filepath.Ext(outputPath))
225 } else if *animateIntervalFlag <= 0 && outputFormat.requiresAnimationInterval() {
226 return xmain.UsageErrorf("-animate-interval must be greater than 0 for %s outputs.\nYou provided: %d", outputFormat, *animateIntervalFlag)
227 }
228 }
229
230 match := d2themescatalog.Find(*themeFlag)
231 if match == (d2themes.Theme{}) {
232 return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag)
233 }
234 ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
235
236
237
238 flagSet := make(map[string]struct{})
239 ms.Opts.Flags.Visit(func(f *pflag.Flag) {
240 flagSet[f.Name] = struct{}{}
241 })
242 if ms.Env.Getenv("D2_LAYOUT") == "" {
243 if _, ok := flagSet["layout"]; !ok {
244 layoutFlag = nil
245 }
246 }
247 if ms.Env.Getenv("D2_THEME") == "" {
248 if _, ok := flagSet["theme"]; !ok {
249 themeFlag = nil
250 }
251 }
252 if ms.Env.Getenv("D2_SKETCH") == "" {
253 if _, ok := flagSet["sketch"]; !ok {
254 sketchFlag = nil
255 }
256 }
257 if ms.Env.Getenv("D2_PAD") == "" {
258 if _, ok := flagSet["pad"]; !ok {
259 padFlag = nil
260 }
261 }
262 if ms.Env.Getenv("D2_CENTER") == "" {
263 if _, ok := flagSet["center"]; !ok {
264 centerFlag = nil
265 }
266 }
267
268 if *darkThemeFlag == -1 {
269 darkThemeFlag = nil
270 }
271 if darkThemeFlag != nil {
272 match = d2themescatalog.Find(*darkThemeFlag)
273 if match == (d2themes.Theme{}) {
274 return xmain.UsageErrorf("--dark-theme could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *darkThemeFlag)
275 }
276 ms.Log.Debug.Printf("using dark theme %s (ID: %d)", match.Name, *darkThemeFlag)
277 }
278 var scale *float64
279 if scaleFlag != nil && *scaleFlag > 0. {
280 scale = scaleFlag
281 }
282
283 if !outputFormat.supportsDarkTheme() {
284 if darkThemeFlag != nil {
285 ms.Log.Warn.Printf("--dark-theme cannot be used while exporting to another format other than .svg")
286 darkThemeFlag = nil
287 }
288 }
289 var pw png.Playwright
290 if outputFormat.requiresPNGRenderer() {
291 pw, err = png.InitPlaywright()
292 if err != nil {
293 return err
294 }
295 defer func() {
296 cleanupErr := pw.Cleanup()
297 if err == nil {
298 err = cleanupErr
299 }
300 }()
301 }
302
303 renderOpts := d2svg.RenderOpts{
304 Pad: padFlag,
305 Sketch: sketchFlag,
306 Center: centerFlag,
307 ThemeID: themeFlag,
308 DarkThemeID: darkThemeFlag,
309 Scale: scale,
310 }
311
312 if *watchFlag {
313 if inputPath == "-" {
314 return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
315 }
316 if *targetFlag != "*" {
317 return xmain.UsageErrorf("-w[atch] cannot be combined with --target")
318 }
319 w, err := newWatcher(ctx, ms, watcherOpts{
320 plugins: plugins,
321 layout: layoutFlag,
322 renderOpts: renderOpts,
323 animateInterval: *animateIntervalFlag,
324 host: *hostFlag,
325 port: *portFlag,
326 inputPath: inputPath,
327 outputPath: outputPath,
328 bundle: *bundleFlag,
329 forceAppendix: *forceAppendixFlag,
330 pw: pw,
331 fontFamily: fontFamily,
332 })
333 if err != nil {
334 return err
335 }
336 return w.run()
337 }
338
339 var boardPath []string
340 var noChildren bool
341 switch *targetFlag {
342 case "*":
343 case "":
344 noChildren = true
345 default:
346 target := *targetFlag
347 if strings.HasSuffix(target, ".*") {
348 target = target[:len(target)-2]
349 } else {
350 noChildren = true
351 }
352 key, err := d2parser.ParseKey(target)
353 if err != nil {
354 return xmain.UsageErrorf("invalid target: %s", *targetFlag)
355 }
356 boardPath = key.IDA()
357 }
358
359 ctx, cancel := timelib.WithTimeout(ctx, time.Minute*2)
360 defer cancel()
361
362 _, written, err := compile(ctx, ms, plugins, nil, layoutFlag, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, boardPath, noChildren, *bundleFlag, *forceAppendixFlag, pw.Page)
363 if err != nil {
364 if written {
365 return fmt.Errorf("failed to fully compile (partial render written) %s: %w", ms.HumanPath(inputPath), err)
366 }
367 return fmt.Errorf("failed to compile %s: %w", ms.HumanPath(inputPath), err)
368 }
369 return nil
370 }
371
372 func LayoutResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.LayoutGraph, error) {
373 cached := make(map[string]d2graph.LayoutGraph)
374 return func(engine string) (d2graph.LayoutGraph, error) {
375 if c, ok := cached[engine]; ok {
376 return c, nil
377 }
378
379 plugin, err := d2plugin.FindPlugin(ctx, plugins, engine)
380 if err != nil {
381 if errors.Is(err, exec.ErrNotFound) {
382 return nil, layoutNotFound(ctx, plugins, engine)
383 }
384 return nil, err
385 }
386
387 err = d2plugin.HydratePluginOpts(ctx, ms, plugin)
388 if err != nil {
389 return nil, err
390 }
391
392 cached[engine] = plugin.Layout
393 return plugin.Layout, nil
394 }
395 }
396
397 func RouterResolver(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin) func(engine string) (d2graph.RouteEdges, error) {
398 cached := make(map[string]d2graph.RouteEdges)
399 return func(engine string) (d2graph.RouteEdges, error) {
400 if c, ok := cached[engine]; ok {
401 return c, nil
402 }
403
404 plugin, err := d2plugin.FindPlugin(ctx, plugins, engine)
405 if err != nil {
406 if errors.Is(err, exec.ErrNotFound) {
407 return nil, layoutNotFound(ctx, plugins, engine)
408 }
409 return nil, err
410 }
411
412 pluginInfo, err := plugin.Info(ctx)
413 if err != nil {
414 return nil, err
415 }
416 hasRouter := false
417 for _, feat := range pluginInfo.Features {
418 if feat == d2plugin.ROUTES_EDGES {
419 hasRouter = true
420 break
421 }
422 }
423 if !hasRouter {
424 return nil, nil
425 }
426 routingPlugin, ok := plugin.(d2plugin.RoutingPlugin)
427 if !ok {
428 return nil, fmt.Errorf("plugin has routing feature but does not implement RoutingPlugin")
429 }
430
431 routeEdges := d2graph.RouteEdges(routingPlugin.RouteEdges)
432 cached[engine] = routeEdges
433 return routeEdges, nil
434 }
435 }
436
437 func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs fs.FS, layout *string, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, boardPath []string, noChildren, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
438 start := time.Now()
439 input, err := ms.ReadPath(inputPath)
440 if err != nil {
441 return nil, false, err
442 }
443
444 ruler, err := textmeasure.NewRuler()
445 if err != nil {
446 return nil, false, err
447 }
448
449 opts := &d2lib.CompileOptions{
450 Ruler: ruler,
451 FontFamily: fontFamily,
452 InputPath: inputPath,
453 LayoutResolver: LayoutResolver(ctx, ms, plugins),
454 Layout: layout,
455 RouterResolver: RouterResolver(ctx, ms, plugins),
456 FS: fs,
457 }
458
459 if os.Getenv("D2_LSP_MODE") == "1" {
460
461 ast, err := d2lib.Parse(ctx, string(input), opts)
462 if err != nil {
463 return nil, false, err
464 }
465
466 type LspOutputData struct {
467 Ast *d2ast.Map
468 Err error
469 }
470 jsonOutput, err := json.Marshal(LspOutputData{Ast: ast, Err: err})
471 if err != nil {
472 return nil, false, err
473 }
474 fmt.Print(string(jsonOutput))
475 os.Exit(42)
476 return nil, false, nil
477 }
478
479 cancel := background.Repeat(func() {
480 ms.Log.Info.Printf("compiling & running layout algorithms...")
481 }, time.Second*5)
482 defer cancel()
483
484 diagram, g, err := d2lib.Compile(ctx, string(input), opts, &renderOpts)
485 if err != nil {
486 return nil, false, err
487 }
488 cancel()
489
490 diagram = diagram.GetBoard(boardPath)
491 if diagram == nil {
492 return nil, false, fmt.Errorf(`render target "%s" not found`, strings.Join(boardPath, "."))
493 }
494 if noChildren {
495 diagram.Layers = nil
496 diagram.Scenarios = nil
497 diagram.Steps = nil
498 }
499
500 plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
501
502 if animateInterval > 0 {
503 masterID, err := diagram.HashID()
504 if err != nil {
505 return nil, false, err
506 }
507 renderOpts.MasterID = masterID
508 }
509
510 pinfo, err := plugin.Info(ctx)
511 if err != nil {
512 return nil, false, err
513 }
514 plocation := pinfo.Type
515 if pinfo.Type == "binary" {
516 plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
517 }
518 ms.Log.Debug.Printf("using layout plugin %s (%s)", *opts.Layout, plocation)
519
520 pluginInfo, err := plugin.Info(ctx)
521 if err != nil {
522 return nil, false, err
523 }
524
525 err = d2plugin.FeatureSupportCheck(pluginInfo, g)
526 if err != nil {
527 return nil, false, err
528 }
529
530 ext := getExportExtension(outputPath)
531 switch ext {
532 case GIF:
533 svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
534 if err != nil {
535 return nil, false, err
536 }
537 out, err := AnimatePNGs(ms, pngs, int(animateInterval))
538 if err != nil {
539 return nil, false, err
540 }
541 err = os.MkdirAll(filepath.Dir(outputPath), 0755)
542 if err != nil {
543 return nil, false, err
544 }
545 err = ms.WritePath(outputPath, out)
546 if err != nil {
547 return nil, false, err
548 }
549 dur := time.Since(start)
550 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
551 return svg, true, nil
552 case PDF:
553 pageMap := buildBoardIDToIndex(diagram, nil, nil)
554 path := []pdf.BoardTitle{
555 {Name: diagram.Root.Label, BoardID: "root"},
556 }
557 pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, path, pageMap, diagram.Root.Label != "")
558 if err != nil {
559 return pdf, false, err
560 }
561 dur := time.Since(start)
562 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
563 return pdf, true, nil
564 case PPTX:
565 var username string
566 if user, err := user.Current(); err == nil {
567 username = user.Username
568 }
569 description := "Presentation generated with D2 - https://d2lang.com"
570 rootName := getFileName(outputPath)
571
572 p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers(), diagram.Root.Label != "")
573
574 boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil)
575 path := []pptx.BoardTitle{
576 {Name: "root", BoardID: "root", LinkToSlide: boardIdToIndex["root"] + 1},
577 }
578 svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, path, boardIdToIndex)
579 if err != nil {
580 return nil, false, err
581 }
582 err = p.SaveTo(outputPath)
583 if err != nil {
584 return nil, false, err
585 }
586 dur := time.Since(start)
587 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
588 return svg, true, nil
589 default:
590 compileDur := time.Since(start)
591 if animateInterval <= 0 {
592
593 linkToOutput, err := resolveLinks("root", outputPath, diagram)
594 if err != nil {
595 return nil, false, err
596 }
597 err = relink("root", diagram, linkToOutput)
598 if err != nil {
599 return nil, false, err
600 }
601 }
602
603 var boards [][]byte
604 var err error
605 if noChildren {
606 boards, err = renderSingle(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
607 } else {
608 boards, err = render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
609 }
610 if err != nil {
611 return nil, false, err
612 }
613 var out []byte
614 if len(boards) > 0 {
615 out = boards[0]
616 if animateInterval > 0 {
617 out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
618 if err != nil {
619 return nil, false, err
620 }
621 err = os.MkdirAll(filepath.Dir(outputPath), 0755)
622 if err != nil {
623 return nil, false, err
624 }
625 err = ms.WritePath(outputPath, out)
626 if err != nil {
627 return nil, false, err
628 }
629 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
630 }
631 }
632 return out, true, nil
633 }
634 }
635
636 func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram) (linkToOutput map[string]string, err error) {
637 if diagram.Name != "" {
638 ext := filepath.Ext(outputPath)
639 outputPath = strings.TrimSuffix(outputPath, ext)
640 outputPath = filepath.Join(outputPath, diagram.Name)
641 outputPath += ext
642 }
643
644 boardOutputPath := outputPath
645 if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
646 ext := filepath.Ext(boardOutputPath)
647 boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
648 boardOutputPath = filepath.Join(boardOutputPath, "index")
649 boardOutputPath += ext
650 }
651
652 layersOutputPath := outputPath
653 if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
654 ext := filepath.Ext(layersOutputPath)
655 layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
656 layersOutputPath = filepath.Join(layersOutputPath, "layers")
657 layersOutputPath += ext
658 }
659 scenariosOutputPath := outputPath
660 if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
661 ext := filepath.Ext(scenariosOutputPath)
662 scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
663 scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
664 scenariosOutputPath += ext
665 }
666 stepsOutputPath := outputPath
667 if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
668 ext := filepath.Ext(stepsOutputPath)
669 stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
670 stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
671 stepsOutputPath += ext
672 }
673
674 linkToOutput = map[string]string{currDiagramPath: boardOutputPath}
675
676 for _, dl := range diagram.Layers {
677 m, err := resolveLinks(strings.Join([]string{currDiagramPath, "layers", dl.Name}, "."), layersOutputPath, dl)
678 if err != nil {
679 return nil, err
680 }
681 for k, v := range m {
682 linkToOutput[k] = v
683 }
684 }
685 for _, dl := range diagram.Scenarios {
686 m, err := resolveLinks(strings.Join([]string{currDiagramPath, "scenarios", dl.Name}, "."), scenariosOutputPath, dl)
687 if err != nil {
688 return nil, err
689 }
690 for k, v := range m {
691 linkToOutput[k] = v
692 }
693 }
694 for _, dl := range diagram.Steps {
695 m, err := resolveLinks(strings.Join([]string{currDiagramPath, "steps", dl.Name}, "."), stepsOutputPath, dl)
696 if err != nil {
697 return nil, err
698 }
699 for k, v := range m {
700 linkToOutput[k] = v
701 }
702 }
703
704 return linkToOutput, nil
705 }
706
707 func relink(currDiagramPath string, d *d2target.Diagram, linkToOutput map[string]string) error {
708 for i, shape := range d.Shapes {
709 if shape.Link != "" {
710 for k, v := range linkToOutput {
711 if shape.Link == k {
712 rel, err := filepath.Rel(filepath.Dir(linkToOutput[currDiagramPath]), v)
713 if err != nil {
714 return err
715 }
716 d.Shapes[i].Link = rel
717 break
718 }
719 }
720 }
721 }
722 for _, board := range d.Layers {
723 err := relink(strings.Join([]string{currDiagramPath, "layers", board.Name}, "."), board, linkToOutput)
724 if err != nil {
725 return err
726 }
727 }
728 for _, board := range d.Scenarios {
729 err := relink(strings.Join([]string{currDiagramPath, "scenarios", board.Name}, "."), board, linkToOutput)
730 if err != nil {
731 return err
732 }
733 }
734 for _, board := range d.Steps {
735 err := relink(strings.Join([]string{currDiagramPath, "steps", board.Name}, "."), board, linkToOutput)
736 if err != nil {
737 return err
738 }
739 }
740 return nil
741 }
742
743 func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
744 if diagram.Name != "" {
745 ext := filepath.Ext(outputPath)
746 outputPath = strings.TrimSuffix(outputPath, ext)
747 outputPath = filepath.Join(outputPath, diagram.Name)
748 outputPath += ext
749 }
750
751 boardOutputPath := outputPath
752 if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
753 if outputPath == "-" {
754
755 return nil, fmt.Errorf("multiboard output cannot be written to stdout")
756 }
757
758 ext := filepath.Ext(boardOutputPath)
759 boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
760 os.RemoveAll(boardOutputPath)
761 boardOutputPath = filepath.Join(boardOutputPath, "index")
762 boardOutputPath += ext
763 }
764
765 layersOutputPath := outputPath
766 if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
767 ext := filepath.Ext(layersOutputPath)
768 layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
769 layersOutputPath = filepath.Join(layersOutputPath, "layers")
770 layersOutputPath += ext
771 }
772 scenariosOutputPath := outputPath
773 if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
774 ext := filepath.Ext(scenariosOutputPath)
775 scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
776 scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
777 scenariosOutputPath += ext
778 }
779 stepsOutputPath := outputPath
780 if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
781 ext := filepath.Ext(stepsOutputPath)
782 stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
783 stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
784 stepsOutputPath += ext
785 }
786
787 var boards [][]byte
788 for _, dl := range diagram.Layers {
789 childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
790 if err != nil {
791 return nil, err
792 }
793 boards = append(boards, childrenBoards...)
794 }
795 for _, dl := range diagram.Scenarios {
796 childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
797 if err != nil {
798 return nil, err
799 }
800 boards = append(boards, childrenBoards...)
801 }
802 for _, dl := range diagram.Steps {
803 childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
804 if err != nil {
805 return nil, err
806 }
807 boards = append(boards, childrenBoards...)
808 }
809
810 if !diagram.IsFolderOnly {
811 start := time.Now()
812 out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
813 if err != nil {
814 return boards, err
815 }
816 dur := compileDur + time.Since(start)
817 if opts.MasterID == "" {
818 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
819 }
820 boards = append([][]byte{out}, boards...)
821 }
822
823 return boards, nil
824 }
825
826 func renderSingle(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
827 start := time.Now()
828 out, err := _render(ctx, ms, plugin, opts, outputPath, bundle, forceAppendix, page, ruler, diagram)
829 if err != nil {
830 return [][]byte{}, err
831 }
832 dur := compileDur + time.Since(start)
833 if opts.MasterID == "" {
834 ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
835 }
836 return [][]byte{out}, nil
837 }
838
839 func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
840 toPNG := getExportExtension(outputPath) == PNG
841 var scale *float64
842 if opts.Scale != nil {
843 scale = opts.Scale
844 } else if toPNG {
845 scale = go2.Pointer(1.)
846 }
847 svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
848 Pad: opts.Pad,
849 Sketch: opts.Sketch,
850 Center: opts.Center,
851 ThemeID: opts.ThemeID,
852 DarkThemeID: opts.DarkThemeID,
853 MasterID: opts.MasterID,
854 ThemeOverrides: opts.ThemeOverrides,
855 DarkThemeOverrides: opts.DarkThemeOverrides,
856 Scale: scale,
857 })
858 if err != nil {
859 return nil, err
860 }
861
862 svg, err = plugin.PostProcess(ctx, svg)
863 if err != nil {
864 return svg, err
865 }
866
867 cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
868 l := simplelog.FromCmdLog(ms.Log)
869 svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
870 if bundle {
871 var bundleErr2 error
872 svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
873 bundleErr = multierr.Combine(bundleErr, bundleErr2)
874 }
875 if forceAppendix && !toPNG {
876 svg = appendix.Append(diagram, ruler, svg)
877 }
878
879 out := svg
880 if toPNG {
881 svg := appendix.Append(diagram, ruler, svg)
882
883 if !bundle {
884 var bundleErr2 error
885 svg, bundleErr2 = imgbundler.BundleRemote(ctx, l, svg, cacheImages)
886 bundleErr = multierr.Combine(bundleErr, bundleErr2)
887 }
888
889 out, err = ConvertSVG(ms, page, svg)
890 if err != nil {
891 return svg, err
892 }
893 out, err = png.AddExif(out)
894 if err != nil {
895 return svg, err
896 }
897 } else {
898 if len(out) > 0 && out[len(out)-1] != '\n' {
899 out = append(out, '\n')
900 }
901 }
902
903 if opts.MasterID == "" {
904 err = os.MkdirAll(filepath.Dir(outputPath), 0755)
905 if err != nil {
906 return svg, err
907 }
908 err = ms.WritePath(outputPath, out)
909 if err != nil {
910 return svg, err
911 }
912 }
913 if bundleErr != nil {
914 return svg, bundleErr
915 }
916 return svg, nil
917 }
918
919 func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, doc *pdf.GoFPDF, boardPath []pdf.BoardTitle, pageMap map[string]int, includeNav bool) (svg []byte, err error) {
920 var isRoot bool
921 if doc == nil {
922 doc = pdf.Init()
923 isRoot = true
924 }
925
926 if !diagram.IsFolderOnly {
927 rootFill := diagram.Root.Fill
928
929
930 diagram.Root.Fill = "transparent"
931
932 var scale *float64
933 if opts.Scale != nil {
934 scale = opts.Scale
935 } else {
936 scale = go2.Pointer(1.)
937 }
938
939 svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
940 Pad: opts.Pad,
941 Sketch: opts.Sketch,
942 Center: opts.Center,
943 Scale: scale,
944 })
945 if err != nil {
946 return nil, err
947 }
948
949 svg, err = plugin.PostProcess(ctx, svg)
950 if err != nil {
951 return svg, err
952 }
953
954 cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
955 l := simplelog.FromCmdLog(ms.Log)
956 svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
957 svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
958 bundleErr = multierr.Combine(bundleErr, bundleErr2)
959 if bundleErr != nil {
960 return svg, bundleErr
961 }
962 svg = appendix.Append(diagram, ruler, svg)
963
964 pngImg, err := ConvertSVG(ms, page, svg)
965 if err != nil {
966 return svg, err
967 }
968
969 viewboxSlice := appendix.FindViewboxSlice(svg)
970 viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
971 if err != nil {
972 return svg, err
973 }
974 viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
975 if err != nil {
976 return svg, err
977 }
978 err = doc.AddPDFPage(pngImg, boardPath, *opts.ThemeID, rootFill, diagram.Shapes, *opts.Pad, viewboxX, viewboxY, pageMap, includeNav)
979 if err != nil {
980 return svg, err
981 }
982 }
983
984 for _, dl := range diagram.Layers {
985 path := append(boardPath, pdf.BoardTitle{
986 Name: dl.Root.Label,
987 BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, "."),
988 })
989 _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
990 if err != nil {
991 return nil, err
992 }
993 }
994 for _, dl := range diagram.Scenarios {
995 path := append(boardPath, pdf.BoardTitle{
996 Name: dl.Root.Label,
997 BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, "."),
998 })
999 _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
1000 if err != nil {
1001 return nil, err
1002 }
1003 }
1004 for _, dl := range diagram.Steps {
1005 path := append(boardPath, pdf.BoardTitle{
1006 Name: dl.Root.Label,
1007 BoardID: strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, "."),
1008 })
1009 _, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, doc, path, pageMap, includeNav)
1010 if err != nil {
1011 return nil, err
1012 }
1013 }
1014
1015 if isRoot {
1016 err := doc.Export(outputPath)
1017 if err != nil {
1018 return nil, err
1019 }
1020 }
1021
1022 return svg, nil
1023 }
1024
1025 func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []pptx.BoardTitle, boardIDToIndex map[string]int) ([]byte, error) {
1026 var svg []byte
1027 if !diagram.IsFolderOnly {
1028
1029
1030 diagram.Root.Fill = "transparent"
1031
1032 var scale *float64
1033 if opts.Scale != nil {
1034 scale = opts.Scale
1035 } else {
1036 scale = go2.Pointer(1.)
1037 }
1038
1039 var err error
1040
1041 svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
1042 Pad: opts.Pad,
1043 Sketch: opts.Sketch,
1044 Center: opts.Center,
1045 Scale: scale,
1046 })
1047 if err != nil {
1048 return nil, err
1049 }
1050
1051 svg, err = plugin.PostProcess(ctx, svg)
1052 if err != nil {
1053 return nil, err
1054 }
1055
1056 cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
1057 l := simplelog.FromCmdLog(ms.Log)
1058 svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
1059 svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
1060 bundleErr = multierr.Combine(bundleErr, bundleErr2)
1061 if bundleErr != nil {
1062 return nil, bundleErr
1063 }
1064
1065 svg = appendix.Append(diagram, ruler, svg)
1066
1067 pngImg, err := ConvertSVG(ms, page, svg)
1068 if err != nil {
1069 return nil, err
1070 }
1071
1072 slide, err := presentation.AddSlide(pngImg, boardPath)
1073 if err != nil {
1074 return nil, err
1075 }
1076
1077 viewboxSlice := appendix.FindViewboxSlice(svg)
1078 viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
1079 if err != nil {
1080 return nil, err
1081 }
1082 viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
1083 if err != nil {
1084 return nil, err
1085 }
1086
1087
1088 for _, shape := range diagram.Shapes {
1089 if shape.Link == "" {
1090 continue
1091 }
1092
1093 linkX := png.SCALE * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth))
1094 linkY := png.SCALE * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth))
1095 linkWidth := png.SCALE * (float64(shape.Width) + float64(shape.StrokeWidth*2))
1096 linkHeight := png.SCALE * (float64(shape.Height) + float64(shape.StrokeWidth*2))
1097 link := &pptx.Link{
1098 Left: int(linkX),
1099 Top: int(linkY),
1100 Width: int(linkWidth),
1101 Height: int(linkHeight),
1102 Tooltip: shape.Link,
1103 }
1104 slide.AddLink(link)
1105 key, err := d2parser.ParseKey(shape.Link)
1106 if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
1107
1108 link.ExternalUrl = shape.Link
1109 } else if pageNum, ok := boardIDToIndex[shape.Link]; ok {
1110
1111 link.SlideIndex = pageNum + 1
1112 }
1113 }
1114 }
1115
1116 for _, dl := range diagram.Layers {
1117 boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, LAYERS, dl.Name}, ".")
1118 path := append(boardPath, pptx.BoardTitle{
1119 Name: dl.Name,
1120 BoardID: boardID,
1121 LinkToSlide: boardIDToIndex[boardID] + 1,
1122 })
1123 _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
1124 if err != nil {
1125 return nil, err
1126 }
1127 }
1128 for _, dl := range diagram.Scenarios {
1129 boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, SCENARIOS, dl.Name}, ".")
1130 path := append(boardPath, pptx.BoardTitle{
1131 Name: dl.Name,
1132 BoardID: boardID,
1133 LinkToSlide: boardIDToIndex[boardID] + 1,
1134 })
1135 _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
1136 if err != nil {
1137 return nil, err
1138 }
1139 }
1140 for _, dl := range diagram.Steps {
1141 boardID := strings.Join([]string{boardPath[len(boardPath)-1].BoardID, STEPS, dl.Name}, ".")
1142 path := append(boardPath, pptx.BoardTitle{
1143 Name: dl.Name,
1144 BoardID: boardID,
1145 LinkToSlide: boardIDToIndex[boardID] + 1,
1146 })
1147 _, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, path, boardIDToIndex)
1148 if err != nil {
1149 return nil, err
1150 }
1151 }
1152
1153 return svg, nil
1154 }
1155
1156
1157 func renameExt(fp string, newExt string) string {
1158 ext := filepath.Ext(fp)
1159 if ext == "" {
1160 return fp + newExt
1161 } else {
1162 return strings.TrimSuffix(fp, ext) + newExt
1163 }
1164 }
1165
1166 func getFileName(path string) string {
1167 ext := filepath.Ext(path)
1168 return strings.TrimSuffix(filepath.Base(path), ext)
1169 }
1170
1171
1172 func DiscardSlog(ctx context.Context) context.Context {
1173 return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
1174 }
1175
1176 func populateLayoutOpts(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
1177 pluginFlags, err := d2plugin.ListPluginFlags(ctx, ps)
1178 if err != nil {
1179 return err
1180 }
1181
1182 for _, f := range pluginFlags {
1183 f.AddToOpts(ms.Opts)
1184
1185 ms.Opts.Flags.MarkHidden(f.Name)
1186 }
1187
1188 return nil
1189 }
1190
1191 func initPlaywright() error {
1192 pw, err := png.InitPlaywright()
1193 if err != nil {
1194 return err
1195 }
1196 return pw.Cleanup()
1197 }
1198
1199 func loadFont(ms *xmain.State, path string) ([]byte, error) {
1200 if filepath.Ext(path) != ".ttf" {
1201 return nil, fmt.Errorf("expected .ttf file but %s has extension %s", path, filepath.Ext(path))
1202 }
1203 ttf, err := os.ReadFile(path)
1204 if err != nil {
1205 return nil, fmt.Errorf("failed to read font at %s: %v", path, err)
1206 }
1207 ms.Log.Info.Printf("font %s loaded", filepath.Base(path))
1208 return ttf, nil
1209 }
1210
1211 func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold string) (*d2fonts.FontFamily, error) {
1212 if pathToRegular == "" && pathToItalic == "" && pathToBold == "" && pathToSemibold == "" {
1213 return nil, nil
1214 }
1215
1216 var regularTTF []byte
1217 var italicTTF []byte
1218 var boldTTF []byte
1219 var semiboldTTF []byte
1220
1221 var err error
1222 if pathToRegular != "" {
1223 regularTTF, err = loadFont(ms, pathToRegular)
1224 if err != nil {
1225 return nil, err
1226 }
1227 }
1228 if pathToItalic != "" {
1229 italicTTF, err = loadFont(ms, pathToItalic)
1230 if err != nil {
1231 return nil, err
1232 }
1233 }
1234 if pathToBold != "" {
1235 boldTTF, err = loadFont(ms, pathToBold)
1236 if err != nil {
1237 return nil, err
1238 }
1239 }
1240 if pathToSemibold != "" {
1241 semiboldTTF, err = loadFont(ms, pathToSemibold)
1242 if err != nil {
1243 return nil, err
1244 }
1245 }
1246
1247 return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
1248 }
1249
1250 const LAYERS = "layers"
1251 const STEPS = "steps"
1252 const SCENARIOS = "scenarios"
1253
1254
1255
1256 func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int {
1257 newPath := append(path, diagram.Name)
1258 if dictionary == nil {
1259 dictionary = map[string]int{}
1260 newPath[0] = "root"
1261 }
1262
1263 key := strings.Join(newPath, ".")
1264 dictionary[key] = len(dictionary)
1265
1266 for _, dl := range diagram.Layers {
1267 buildBoardIDToIndex(dl, dictionary, append(newPath, LAYERS))
1268 }
1269 for _, dl := range diagram.Scenarios {
1270 buildBoardIDToIndex(dl, dictionary, append(newPath, SCENARIOS))
1271 }
1272 for _, dl := range diagram.Steps {
1273 buildBoardIDToIndex(dl, dictionary, append(newPath, STEPS))
1274 }
1275
1276 return dictionary
1277 }
1278
1279 func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
1280 if !diagram.IsFolderOnly {
1281
1282 var scale *float64
1283 if opts.Scale != nil {
1284 scale = opts.Scale
1285 } else {
1286 scale = go2.Pointer(1.)
1287 }
1288 svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
1289 Pad: opts.Pad,
1290 Sketch: opts.Sketch,
1291 Center: opts.Center,
1292 Scale: scale,
1293 })
1294 if err != nil {
1295 return nil, nil, err
1296 }
1297
1298 svg, err = plugin.PostProcess(ctx, svg)
1299 if err != nil {
1300 return nil, nil, err
1301 }
1302
1303 cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
1304 l := simplelog.FromCmdLog(ms.Log)
1305 svg, bundleErr := imgbundler.BundleLocal(ctx, l, svg, cacheImages)
1306 svg, bundleErr2 := imgbundler.BundleRemote(ctx, l, svg, cacheImages)
1307 bundleErr = multierr.Combine(bundleErr, bundleErr2)
1308 if bundleErr != nil {
1309 return nil, nil, bundleErr
1310 }
1311
1312 svg = appendix.Append(diagram, ruler, svg)
1313
1314 pngImg, err := ConvertSVG(ms, page, svg)
1315 if err != nil {
1316 return nil, nil, err
1317 }
1318 pngs = append(pngs, pngImg)
1319 }
1320
1321 for _, dl := range diagram.Layers {
1322 _, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
1323 if err != nil {
1324 return nil, nil, err
1325 }
1326 pngs = append(pngs, layerPNGs...)
1327 }
1328 for _, dl := range diagram.Scenarios {
1329 _, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
1330 if err != nil {
1331 return nil, nil, err
1332 }
1333 pngs = append(pngs, scenarioPNGs...)
1334 }
1335 for _, dl := range diagram.Steps {
1336 _, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
1337 if err != nil {
1338 return nil, nil, err
1339 }
1340 pngs = append(pngs, stepsPNGs...)
1341 }
1342
1343 return svg, pngs, nil
1344 }
1345
1346 func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
1347 cancel := background.Repeat(func() {
1348 ms.Log.Info.Printf("converting to PNG...")
1349 }, time.Second*5)
1350 defer cancel()
1351
1352 return png.ConvertSVG(page, svg)
1353 }
1354
1355 func AnimatePNGs(ms *xmain.State, pngs [][]byte, animIntervalMs int) ([]byte, error) {
1356 cancel := background.Repeat(func() {
1357 ms.Log.Info.Printf("generating GIF...")
1358 }, time.Second*5)
1359 defer cancel()
1360
1361 return xgif.AnimatePNGs(pngs, animIntervalMs)
1362 }
1363
1364 func init() {
1365 ctxlog.Init()
1366 }
1367
View as plain text