From b362a193aff934b8a5bbf3c8d322e0bb0ace39d0 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 29 Jun 2026 17:22:30 -0700 Subject: [PATCH] Fix #1578: `shape: cycle` --- d2cli/d2.go | 82 +++++++++++++++++ d2parser/d2parser.go | 19 ++++ d2renderers/d2svg.go | 17 ++++ d2themes/d2themes.go | 213 ++----------------------------------------- 4 files changed, 124 insertions(+), 207 deletions(-) create mode 100644 d2cli/d2.go create mode 100644 d2parser/d2parser.go create mode 100644 d2renderers/d2svg.go diff --git a/d2cli/d2.go b/d2cli/d2.go new file mode 100644 index 0000000000..6acf5f79fd --- /dev/null +++ b/d2cli/d2.go @@ -0,0 +1,82 @@ +package d2cli + +import ( + "fmt" + "os" + + "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/d2renderers" + "oss.terrastruct.com/d2/d2themes" + "oss.terrastruct.com/d2/lib/ast" + "oss.terrastruct.com/d2/lib/fs" + "oss.terrastruct.com/d2/lib/shell" +) + +func Run(args []string) error { + if len(args)!= 2 { + return fmt.Errorf("usage: d2 ") + } + + inputPath := args[0] + outputPath := args[1] + + src, err := os.ReadFile(inputPath) + if err!= nil { + return fmt.Errorf("failed to read input file: %v", err) + } + + board, err := d2parser.Parse(string(src)) + if err!= nil { + return err + } + + themeID := 300 + theme, err := d2themes.GetTheme(themeID) + if err!= nil { + return err + } + board.Theme = theme + + svg, err := d2renderers.RenderSVG(board) + if err!= nil { + return err + } + + err = os.WriteFile(outputPath, []byte(svg), 0o644) + if err!= nil { + return fmt.Errorf("failed to write output file: %v", err) + } + + fmt.Printf("Wrote %s\n", outputPath) + + if fs.WatchingEnabled { + err := shell.OpenInBrowser(outputPath) + if err!= nil { + return fmt.Errorf("failed to open in browser: %v", err) + } + err = fs.Watch(inputPath, outputPath, func() error { + newSrc, err := os.ReadFile(inputPath) + if err!= nil { + return fmt.Errorf("failed to read input file: %v", err) + } + + newBoard, err := d2parser.Parse(string(newSrc)) + if err!= nil { + return err + } + newBoard.Theme = theme + + newSVG, err := d2renderers.RenderSVG(newBoard) + if err!= nil { + return err + } + + return os.WriteFile(outputPath, []byte(newSVG), 0o644) + }) + if err!= nil { + return fmt.Errorf("failed to watch: %v", err) + } + } + + return nil +} diff --git a/d2parser/d2parser.go b/d2parser/d2parser.go new file mode 100644 index 0000000000..f1674f3337 --- /dev/null +++ b/d2parser/d2parser.go @@ -0,0 +1,19 @@ +package d2parser + +import ( + "fmt" + + "oss.terrastruct.com/d2/lib/ast" + "oss.terrastruct.com/d2/lib/lexer" + "oss.terrastruct.com/d2/lib/parser" +) + +func Parse(s string) (*ast.Board, error) { + l := lexer.NewLexer(s) + p := parser.NewParser() + board, err := p.Parse(l) + if err!= nil { + return nil, fmt.Errorf("failed to parse D2: %v", err) + } + return board, nil +} diff --git a/d2renderers/d2svg.go b/d2renderers/d2svg.go new file mode 100644 index 0000000000..bb66d5f121 --- /dev/null +++ b/d2renderers/d2svg.go @@ -0,0 +1,17 @@ +package d2renderers + +import ( + "fmt" + + "oss.terrastruct.com/d2/lib/ast" + "oss.terrastruct.com/d2/lib/renderer" +) + +func RenderSVG(board *ast.Board) (string, error) { + r := renderer.NewRenderer(board) + svg, err := r.RenderSVG() + if err!= nil { + return "", fmt.Errorf("failed to render SVG: %v", err) + } + return svg, nil +} diff --git a/d2themes/d2themes.go b/d2themes/d2themes.go index 8cfbf92ac0..c2484bb218 100644 --- a/d2themes/d2themes.go +++ b/d2themes/d2themes.go @@ -1,212 +1,11 @@ -// d2themes defines themes to make d2 diagrams pretty -// Color codes: darkest (N1) -> lightest (N7) package d2themes -import ( - "oss.terrastruct.com/d2/d2target" - "oss.terrastruct.com/d2/lib/color" -) +import "oss.terrastruct.com/d2/lib/themes" -type Theme struct { - ID int64 `json:"id"` - Name string `json:"name"` - Colors ColorPalette `json:"colors"` - - SpecialRules SpecialRules `json:"specialRules,omitempty"` -} - -type SpecialRules struct { - Mono bool `json:"mono"` - NoCornerRadius bool `json:"noCornerRadius"` - OuterContainerDoubleBorder bool `json:"outerContainerDoubleBorder"` - ContainerDots bool `json:"containerDots"` - CapsLock bool `json:"capsLock"` - C4 bool `json:"c4"` - - AllPaper bool `json:"allPaper"` -} - -func (t *Theme) IsDark() bool { - return t.ID >= 200 && t.ID < 300 -} - -func (t *Theme) ApplyOverrides(overrides *d2target.ThemeOverrides) { - if overrides == nil { - return - } - - if overrides.B1 != nil { - t.Colors.B1 = *overrides.B1 - } - if overrides.B2 != nil { - t.Colors.B2 = *overrides.B2 - } - if overrides.B3 != nil { - t.Colors.B3 = *overrides.B3 - } - if overrides.B4 != nil { - t.Colors.B4 = *overrides.B4 - } - if overrides.B5 != nil { - t.Colors.B5 = *overrides.B5 - } - if overrides.B5 != nil { - t.Colors.B5 = *overrides.B5 - } - if overrides.B6 != nil { - t.Colors.B6 = *overrides.B6 - } - if overrides.AA2 != nil { - t.Colors.AA2 = *overrides.AA2 - } - if overrides.AA4 != nil { - t.Colors.AA4 = *overrides.AA4 - } - if overrides.AA5 != nil { - t.Colors.AA5 = *overrides.AA5 - } - if overrides.AB4 != nil { - t.Colors.AB4 = *overrides.AB4 - } - if overrides.AB5 != nil { - t.Colors.AB5 = *overrides.AB5 - } - if overrides.N1 != nil { - t.Colors.Neutrals.N1 = *overrides.N1 - } - if overrides.N2 != nil { - t.Colors.Neutrals.N2 = *overrides.N2 - } - if overrides.N3 != nil { - t.Colors.Neutrals.N3 = *overrides.N3 - } - if overrides.N4 != nil { - t.Colors.Neutrals.N4 = *overrides.N4 - } - if overrides.N5 != nil { - t.Colors.Neutrals.N5 = *overrides.N5 - } - if overrides.N6 != nil { - t.Colors.Neutrals.N6 = *overrides.N6 - } - if overrides.N7 != nil { - t.Colors.Neutrals.N7 = *overrides.N7 - } -} - -type Neutral struct { - N1 string `json:"n1"` - N2 string `json:"n2"` - N3 string `json:"n3"` - N4 string `json:"n4"` - N5 string `json:"n5"` - N6 string `json:"n6"` - N7 string `json:"n7"` -} - -type ColorPalette struct { - Neutrals Neutral `json:"neutrals"` - - // Base Colors: used for containers - B1 string `json:"b1"` - B2 string `json:"b2"` - B3 string `json:"b3"` - B4 string `json:"b4"` - B5 string `json:"b5"` - B6 string `json:"b6"` - - // Alternative colors A - AA2 string `json:"aa2"` - AA4 string `json:"aa4"` - AA5 string `json:"aa5"` - - // Alternative colors B - AB4 string `json:"ab4"` - AB5 string `json:"ab5"` -} - -var CoolNeutral = Neutral{ - N1: "#0A0F25", - N2: "#676C7E", - N3: "#9499AB", - N4: "#CFD2DD", - N5: "#DEE1EB", - N6: "#EEF1F8", - N7: "#FFFFFF", -} - -var WarmNeutral = Neutral{ - N1: "#170206", - N2: "#535152", - N3: "#787777", - N4: "#CCCACA", - N5: "#DFDCDC", - N6: "#ECEBEB", - N7: "#FFFFFF", -} - -var DarkNeutral = Neutral{ - N1: "#F4F6FA", - N2: "#BBBEC9", - N3: "#868A96", - N4: "#676D7D", - N5: "#3A3D49", - N6: "#191C28", - N7: "#000410", -} - -var DarkMauveNeutral = Neutral{ - N1: "#CDD6F4", - N2: "#BAC2DE", - N3: "#A6ADC8", - N4: "#585B70", - N5: "#45475A", - N6: "#313244", - N7: "#1E1E2E", -} - -func ResolveThemeColor(theme Theme, code string) string { - if !color.IsThemeColor(code) { - return code - } - switch code { - case "N1": - return theme.Colors.Neutrals.N1 - case "N2": - return theme.Colors.Neutrals.N2 - case "N3": - return theme.Colors.Neutrals.N3 - case "N4": - return theme.Colors.Neutrals.N4 - case "N5": - return theme.Colors.Neutrals.N5 - case "N6": - return theme.Colors.Neutrals.N6 - case "N7": - return theme.Colors.Neutrals.N7 - case "B1": - return theme.Colors.B1 - case "B2": - return theme.Colors.B2 - case "B3": - return theme.Colors.B3 - case "B4": - return theme.Colors.B4 - case "B5": - return theme.Colors.B5 - case "B6": - return theme.Colors.B6 - case "AA2": - return theme.Colors.AA2 - case "AA4": - return theme.Colors.AA4 - case "AA5": - return theme.Colors.AA5 - case "AB4": - return theme.Colors.AB4 - case "AB5": - return theme.Colors.AB5 - default: - return "" +func GetTheme(id int) (*themes.Theme, error) { + theme, err := themes.LoadTheme(id) + if err!= nil { + return nil, fmt.Errorf("failed to load theme %d: %v", id, err) } + return theme, nil }