Skip to content

Commit

Permalink
Merge pull request #1777 from alixander/customize-themes
Browse files Browse the repository at this point in the history
customize theme colors in d2-config
  • Loading branch information
alixander authored Dec 14, 2023
2 parents 6aaf795 + 16ac8d2 commit 37bd766
Show file tree
Hide file tree
Showing 28 changed files with 4,422 additions and 443 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#### Features 🚀

- Themes can be customized via `d2-config` vars. [#1777](https://github.com/terrastruct/d2/pull/1777)
- Icons can be added for special objects (sql_table, class, code, markdown, latex). [#1774](https://github.com/terrastruct/d2/pull/1774)

#### Improvements 🧹
Expand Down
16 changes: 9 additions & 7 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,13 +845,15 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
scale = go2.Pointer(1.)
}
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
ThemeID: opts.ThemeID,
DarkThemeID: opts.DarkThemeID,
MasterID: opts.MasterID,
Scale: scale,
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
ThemeID: opts.ThemeID,
DarkThemeID: opts.DarkThemeID,
MasterID: opts.MasterID,
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
Scale: scale,
})
if err != nil {
return nil, err
Expand Down
95 changes: 91 additions & 4 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"oss.terrastruct.com/d2/d2ir"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/textmeasure"
)

Expand Down Expand Up @@ -54,7 +55,11 @@ func Compile(p string, r io.Reader, opts *CompileOptions) (*d2graph.Graph, *d2ta
g.FS = opts.FS
g.SortObjectsByAST()
g.SortEdgesByAST()
return g, compileConfig(ir), nil
config, err := compileConfig(ir)
if err != nil {
return nil, nil, err
}
return g, config, nil
}

func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
Expand Down Expand Up @@ -1336,10 +1341,10 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map {
}
}

func compileConfig(ir *d2ir.Map) *d2target.Config {
func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
f := ir.GetField("vars", "d2-config")
if f == nil || f.Map() == nil {
return nil
return nil, nil
}

configMap := f.Map()
Expand Down Expand Up @@ -1375,5 +1380,87 @@ func compileConfig(ir *d2ir.Map) *d2target.Config {
config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString())
}

return config
f = configMap.GetField("theme-overrides")
if f != nil {
overrides, err := compileThemeOverrides(f.Map())
if err != nil {
return nil, err
}
config.ThemeOverrides = overrides
}
f = configMap.GetField("dark-theme-overrides")
if f != nil {
overrides, err := compileThemeOverrides(f.Map())
if err != nil {
return nil, err
}
config.DarkThemeOverrides = overrides
}

return config, nil
}

func compileThemeOverrides(m *d2ir.Map) (*d2target.ThemeOverrides, error) {
if m == nil {
return nil, nil
}
themeOverrides := d2target.ThemeOverrides{}

err := &d2parser.ParseError{}
FOR:
for _, f := range m.Fields {
switch strings.ToUpper(f.Name) {
case "N1":
themeOverrides.N1 = go2.Pointer(f.Primary().Value.ScalarString())
case "N2":
themeOverrides.N2 = go2.Pointer(f.Primary().Value.ScalarString())
case "N3":
themeOverrides.N3 = go2.Pointer(f.Primary().Value.ScalarString())
case "N4":
themeOverrides.N4 = go2.Pointer(f.Primary().Value.ScalarString())
case "N5":
themeOverrides.N5 = go2.Pointer(f.Primary().Value.ScalarString())
case "N6":
themeOverrides.N6 = go2.Pointer(f.Primary().Value.ScalarString())
case "N7":
themeOverrides.N7 = go2.Pointer(f.Primary().Value.ScalarString())
case "B1":
themeOverrides.B1 = go2.Pointer(f.Primary().Value.ScalarString())
case "B2":
themeOverrides.B2 = go2.Pointer(f.Primary().Value.ScalarString())
case "B3":
themeOverrides.B3 = go2.Pointer(f.Primary().Value.ScalarString())
case "B4":
themeOverrides.B4 = go2.Pointer(f.Primary().Value.ScalarString())
case "B5":
themeOverrides.B5 = go2.Pointer(f.Primary().Value.ScalarString())
case "B6":
themeOverrides.B6 = go2.Pointer(f.Primary().Value.ScalarString())
case "AA2":
themeOverrides.AA2 = go2.Pointer(f.Primary().Value.ScalarString())
case "AA4":
themeOverrides.AA4 = go2.Pointer(f.Primary().Value.ScalarString())
case "AA5":
themeOverrides.AA5 = go2.Pointer(f.Primary().Value.ScalarString())
case "AB4":
themeOverrides.AB4 = go2.Pointer(f.Primary().Value.ScalarString())
case "AB5":
themeOverrides.AB5 = go2.Pointer(f.Primary().Value.ScalarString())
default:
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`"%s" is not a valid theme code`, f.Name)).(d2ast.Error))
continue FOR
}
if !go2.Contains(color.NamedColors, strings.ToLower(f.Primary().Value.ScalarString())) && !color.ColorHexRegex.MatchString(f.Primary().Value.ScalarString()) {
err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`expected "%s" to be a valid named color ("orange") or a hex code ("#f0ff3a")`, f.Name)).(d2ast.Error))
}
}

if !err.Empty() {
return nil, err
}

if themeOverrides != (d2target.ThemeOverrides{}) {
return &themeOverrides, nil
}
return nil, nil
}
15 changes: 15 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2812,6 +2812,21 @@ g: |md
d2/testdata/d2compiler/TestCompile/text_no_label.d2:15:1: block string cannot be empty
d2/testdata/d2compiler/TestCompile/text_no_label.d2:4:1: shape text must have a non-empty label
d2/testdata/d2compiler/TestCompile/text_no_label.d2:7:1: shape text must have a non-empty label`,
},
{
name: "var-not-color",
text: `vars: {
d2-config: {
theme-overrides: {
B1: potato
potato: B1
}
}
}
a
`,
expErr: `d2/testdata/d2compiler/TestCompile/var-not-color.d2:4:7: expected "B1" to be a valid named color ("orange") or a hex code ("#f0ff3a")
d2/testdata/d2compiler/TestCompile/var-not-color.d2:5:4: "potato" is not a valid theme code`,
},
{
name: "no_arrowheads_in_shape",
Expand Down
159 changes: 0 additions & 159 deletions d2graph/color_helper.go

This file was deleted.

6 changes: 3 additions & 3 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,15 @@ func (s *Style) Apply(key, value string) error {
if s.Stroke == nil {
break
}
if !go2.Contains(namedColors, strings.ToLower(value)) && !colorHexRegex.MatchString(value) {
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "stroke" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
}
s.Stroke.Value = value
case "fill":
if s.Fill == nil {
break
}
if !go2.Contains(namedColors, strings.ToLower(value)) && !colorHexRegex.MatchString(value) {
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
}
s.Fill.Value = value
Expand Down Expand Up @@ -347,7 +347,7 @@ func (s *Style) Apply(key, value string) error {
if s.FontColor == nil {
break
}
if !go2.Contains(namedColors, strings.ToLower(value)) && !colorHexRegex.MatchString(value) {
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "font-color" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
}
s.FontColor.Value = value
Expand Down
4 changes: 2 additions & 2 deletions d2ir/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (c *compiler) validateConfigs(configs *Field) {
for _, f := range configs.Map().Fields {
var val string
if f.Primary() == nil {
if f.Name != "theme-colors" {
if f.Name != "theme-overrides" && f.Name != "dark-theme-overrides" {
c.errorf(f.LastRef().AST(), `"%s" needs a value`, f.Name)
continue
}
Expand All @@ -184,7 +184,7 @@ func (c *compiler) validateConfigs(configs *Field) {
c.errorf(f.LastRef().AST(), `expected a boolean for "%s", got "%s"`, f.Name, val)
continue
}
case "theme-colors":
case "theme-overrides", "dark-theme-overrides":
if f.Map() == nil {
c.errorf(f.LastRef().AST(), `"%s" needs a map`, f.Name)
continue
Expand Down
2 changes: 2 additions & 0 deletions d2lib/d2.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ func applyConfigs(config *d2target.Config, compileOpts *CompileOptions, renderOp
if renderOpts.Center == nil {
renderOpts.Center = config.Center
}
renderOpts.ThemeOverrides = config.ThemeOverrides
renderOpts.DarkThemeOverrides = config.DarkThemeOverrides
}

func applyDefaults(compileOpts *CompileOptions, renderOpts *d2svg.RenderOpts) {
Expand Down
2 changes: 1 addition & 1 deletion d2renderers/d2animate/d2animate.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO

d2svg.EmbedFonts(buf, diagramHash, svgsStr, rootDiagram.FontFamily, rootDiagram.GetNestedCorpus())

themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID)
themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID, renderOpts.ThemeOverrides, renderOpts.DarkThemeOverrides)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 37bd766

Please sign in to comment.