diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index a6c2afea2..cd0f299a3 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -27,6 +27,7 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2target"
+ "oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
@@ -53,7 +54,7 @@ var TooltipIcon string
var LinkIcon string
//go:embed style.css
-var styleCSS string
+var baseStylesheet string
//go:embed sketchstyle.css
var sketchStyleCSS string
@@ -62,10 +63,10 @@ var sketchStyleCSS string
var mdCSS string
type RenderOpts struct {
- Pad int
- Sketch bool
- SketchBg bool
- ThemeID int64
+ Pad int
+ Sketch bool
+ ThemeID int64
+ DarkThemeID int64
}
func dimensions(writer io.Writer, diagram *d2target.Diagram, pad int) (width, height int, topLeft, bottomRight d2target.Point) {
@@ -1148,16 +1149,19 @@ var fitToScreenScript string
const (
BG_COLOR = color.N7
FG_COLOR = color.N1
+
+ DEFAULT_THEME int64 = 0
+ DEFAULT_DARK_THEME int64 = math.MaxInt64 // no theme selected
)
// TODO minify output at end
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
var sketchRunner *d2sketch.Runner
pad := DEFAULT_PADDING
- sketchBg := true
+ themeID := DEFAULT_THEME
+ darkThemeID := DEFAULT_DARK_THEME
if opts != nil {
pad = opts.Pad
- sketchBg = opts.SketchBg
if opts.Sketch {
var err error
sketchRunner, err = d2sketch.InitSketchVM()
@@ -1165,6 +1169,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
return nil, err
}
}
+ themeID = opts.ThemeID
+ darkThemeID = opts.DarkThemeID
}
buf := &bytes.Buffer{}
@@ -1245,11 +1251,12 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// containerEl.Color = color.N1 TODO this is useless as this element has no children
// generate elements that will be appended to the SVG tag
- styleCSS2 := ""
+ themeStylesheet := themeCSS(themeID, darkThemeID)
+ sketchStylesheet := ""
if sketchRunner != nil {
- styleCSS2 = "\n" + sketchStyleCSS
+ sketchStylesheet = "\n" + sketchStyleCSS
}
- svgOut := fmt.Sprintf(``, styleCSS, styleCSS2)
+ svgOut := fmt.Sprintf(``, baseStylesheet, themeStylesheet, sketchStylesheet)
// this script won't run in --watch mode because script tags are ignored when added via el.innerHTML = element
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
svgOut += fmt.Sprintf(``, fitToScreenScript)
@@ -1263,7 +1270,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
if hasMarkdown {
svgOut += fmt.Sprintf(``, mdCSS)
}
- if sketchRunner != nil && sketchBg {
+ if sketchRunner != nil {
svgOut += d2sketch.DefineFillPattern()
}
svgOut += embedFonts(buf, diagram.FontFamily)
@@ -1278,6 +1285,44 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
return []byte(docRendered), nil
}
+func themeCSS(themeID, darkThemeID int64) (stylesheet string) {
+ out := singleThemeRulesets(themeID)
+
+ if darkThemeID != math.MaxInt64 {
+ out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", singleThemeRulesets(darkThemeID))
+ }
+
+ return out
+}
+
+func singleThemeRulesets(themeID int64) (rulesets string) {
+ out := ""
+ theme := d2themescatalog.Find(themeID)
+ for _, property := range []string{"fill", "stroke", "background-color", "color"} {
+ out += fmt.Sprintf(".%s-N1{%s:%s;}.%s-N2{%s:%s;}.%s-N3{%s:%s;}.%s-N4{%s:%s;}.%s-N5{%s:%s;}.%s-N6{%s:%s;}.%s-N7{%s:%s;}.%s-B1{%s:%s;}.%s-B2{%s:%s;}.%s-B3{%s:%s;}.%s-B4{%s:%s;}.%s-B5{%s:%s;}.%s-B6{%s:%s;}.%s-AA2{%s:%s;}.%s-AA4{%s:%s;}.%s-AA5{%s:%s;}.%s-AB4{%s:%s;}.%s-AB5{%s:%s;}",
+ property, property, theme.Colors.Neutrals.N1,
+ property, property, theme.Colors.Neutrals.N2,
+ property, property, theme.Colors.Neutrals.N3,
+ property, property, theme.Colors.Neutrals.N4,
+ property, property, theme.Colors.Neutrals.N5,
+ property, property, theme.Colors.Neutrals.N6,
+ property, property, theme.Colors.Neutrals.N7,
+ property, property, theme.Colors.B1,
+ property, property, theme.Colors.B2,
+ property, property, theme.Colors.B3,
+ property, property, theme.Colors.B4,
+ property, property, theme.Colors.B5,
+ property, property, theme.Colors.B6,
+ property, property, theme.Colors.AA2,
+ property, property, theme.Colors.AA4,
+ property, property, theme.Colors.AA5,
+ property, property, theme.Colors.AB4,
+ property, property, theme.Colors.AB5,
+ )
+ }
+ return out
+}
+
type DiagramObject interface {
GetID() string
GetZIndex() int
diff --git a/d2renderers/d2svg/style.css b/d2renderers/d2svg/style.css
index a5a8e6c39..7312ba8de 100644
--- a/d2renderers/d2svg/style.css
+++ b/d2renderers/d2svg/style.css
@@ -10,453 +10,3 @@
mix-Blend-mode: multiply;
opacity: 0.5;
}
-
-/*
-.fill
-.stroke
-
-.background-color
-.color
-*/
-
-.fill-N1 {
- fill: #0A0F25;
-}
-.fill-N2 {
- fill: #676C7E;
-}
-.fill-N3 {
- fill: #9499AB;
-}
-.fill-N4 {
- fill: #CFD2DD;
-}
-.fill-N5 {
- fill: #DEE1EB;
-}
-.fill-N6 {
- fill: #EEF1F8;
-}
-.fill-N7 {
- fill: #FFFFFF;
-}
-.fill-B1 {
- fill: #0D32B2;
-}
-.fill-B2 {
- fill: #0D32B2;
-}
-.fill-B3 {
- fill: #E3E9FD;
-}
-.fill-B4 {
- fill: #E3E9FD;
-}
-.fill-B5 {
- fill: #EDF0FD;
-}
-.fill-B6 {
- fill: #F7F8FE;
-}
-.fill-AA2 {
- fill: #4A6FF3;
-}
-.fill-AA4 {
- fill: #EDF0FD;
-}
-.fill-AA5 {
- fill: #F7F8FE;
-}
-.fill-AB4 {
- fill: #DEE1EB;
-}
-.fill-AB5 {
- fill: #F7F8FE;
-}
-
-.stroke-N1 {
- stroke: #0A0F25;
-}
-.stroke-N2 {
- stroke: #676C7E;
-}
-.stroke-N3 {
- stroke: #9499AB;
-}
-.stroke-N4 {
- stroke: #CFD2DD;
-}
-.stroke-N5 {
- stroke: #DEE1EB;
-}
-.stroke-N6 {
- stroke: #EEF1F8;
-}
-.stroke-N7 {
- stroke: #FFFFFF;
-}
-.stroke-B1 {
- stroke: #0D32B2;
-}
-.stroke-B2 {
- stroke: #0D32B2;
-}
-.stroke-B3 {
- stroke: #E3E9FD;
-}
-.stroke-B4 {
- stroke: #E3E9FD;
-}
-.stroke-B5 {
- stroke: #EDF0FD;
-}
-.stroke-B6 {
- stroke: #F7F8FE;
-}
-.stroke-AA2 {
- stroke: #4A6FF3;
-}
-.stroke-AA4 {
- stroke: #EDF0FD;
-}
-.stroke-AA5 {
- stroke: #F7F8FE;
-}
-.stroke-AB4 {
- stroke: #DEE1EB;
-}
-.stroke-AB5 {
- stroke: #F7F8FE;
-}
-
-.background-color-N1 {
- background-color: #0A0F25;
-}
-.background-color-N2 {
- background-color: #676C7E;
-}
-.background-color-N3 {
- background-color: #9499AB;
-}
-.background-color-N4 {
- background-color: #CFD2DD;
-}
-.background-color-N5 {
- background-color: #DEE1EB;
-}
-.background-color-N6 {
- background-color: #EEF1F8;
-}
-.background-color-N7 {
- background-color: #FFFFFF;
-}
-.background-color-B1 {
- background-color: #0D32B2;
-}
-.background-color-B2 {
- background-color: #0D32B2;
-}
-.background-color-B3 {
- background-color: #E3E9FD;
-}
-.background-color-B4 {
- background-color: #E3E9FD;
-}
-.background-color-B5 {
- background-color: #EDF0FD;
-}
-.background-color-B6 {
- background-color: #F7F8FE;
-}
-.background-color-AA2 {
- background-color: #4A6FF3;
-}
-.background-color-AA4 {
- background-color: #EDF0FD;
-}
-.background-color-AA5 {
- background-color: #F7F8FE;
-}
-.background-color-AB4 {
- background-color: #DEE1EB;
-}
-.background-color-AB5 {
- background-color: #F7F8FE;
-}
-
-.color-N1 {
- color: #0A0F25;
-}
-.color-N2 {
- color: #676C7E;
-}
-.color-N3 {
- color: #9499AB;
-}
-.color-N4 {
- color: #CFD2DD;
-}
-.color-N5 {
- color: #DEE1EB;
-}
-.color-N6 {
- color: #EEF1F8;
-}
-.color-N7 {
- color: #FFFFFF;
-}
-.color-B1 {
- color: #0D32B2;
-}
-.color-B2 {
- color: #0D32B2;
-}
-.color-B3 {
- color: #E3E9FD;
-}
-.color-B4 {
- color: #E3E9FD;
-}
-.color-B5 {
- color: #EDF0FD;
-}
-.color-B6 {
- color: #F7F8FE;
-}
-.color-AA2 {
- color: #4A6FF3;
-}
-.color-AA4 {
- color: #EDF0FD;
-}
-.color-AA5 {
- color: #F7F8FE;
-}
-.color-AB4 {
- color: #DEE1EB;
-}
-.color-AB5 {
- color: #F7F8FE;
-}
-
-@media screen and (prefers-color-scheme: dark) {
- .fill-N1 {
- fill: #cdd6f4;
- }
- .fill-N2 {
- fill: #bac2de;
- }
- .fill-N3 {
- fill: #a6adc8;
- }
- .fill-N4 {
- fill: #585b70;
- }
- .fill-N5 {
- fill: #45475a;
- }
- .fill-N6 {
- fill: #313244;
- }
- .fill-N7 {
- fill: #1e1e2e;
- }
- .fill-B1 {
- fill: #cba6f7;
- }
- .fill-B2 {
- fill: #cba6f7;
- }
- .fill-B3 {
- fill: #6c7086;
- }
- .fill-B4 {
- fill: #585b70;
- }
- .fill-B5 {
- fill: #45475a;
- }
- .fill-B6 {
- fill: #313244;
- }
- .fill-AA2 {
- fill: #f38ba8;
- }
- .fill-AA4 {
- fill: #45475a;
- }
- .fill-AA5 {
- fill: #313244;
- }
- .fill-AB4 {
- fill: #45475a;
- }
- .fill-AB5 {
- fill: #313244;
- }
-
- .stroke-N1 {
- stroke: #cdd6f4;
- }
- .stroke-N2 {
- stroke: #bac2de;
- }
- .stroke-N3 {
- stroke: #a6adc8;
- }
- .stroke-N4 {
- stroke: #585b70;
- }
- .stroke-N5 {
- stroke: #45475a;
- }
- .stroke-N6 {
- stroke: #313244;
- }
- .stroke-N7 {
- stroke: #1e1e2e;
- }
- .stroke-B1 {
- stroke: #cba6f7;
- }
- .stroke-B2 {
- stroke: #cba6f7;
- }
- .stroke-B3 {
- stroke: #6c7086;
- }
- .stroke-B4 {
- stroke: #585b70;
- }
- .stroke-B5 {
- stroke: #45475a;
- }
- .stroke-B6 {
- stroke: #313244;
- }
- .stroke-AA2 {
- stroke: #f38ba8;
- }
- .stroke-AA4 {
- stroke: #45475a;
- }
- .stroke-AA5 {
- stroke: #313244;
- }
- .stroke-AB4 {
- stroke: #45475a;
- }
- .stroke-AB5 {
- stroke: #313244;
- }
-
- .background-color-N1 {
- background-color: #cdd6f4;
- }
- .background-color-N2 {
- background-color: #bac2de;
- }
- .background-color-N3 {
- background-color: #a6adc8;
- }
- .background-color-N4 {
- background-color: #585b70;
- }
- .background-color-N5 {
- background-color: #45475a;
- }
- .background-color-N6 {
- background-color: #313244;
- }
- .background-color-N7 {
- background-color: #1e1e2e;
- }
- .background-color-B1 {
- background-color: #cba6f7;
- }
- .background-color-B2 {
- background-color: #cba6f7;
- }
- .background-color-B3 {
- background-color: #6c7086;
- }
- .background-color-B4 {
- background-color: #585b70;
- }
- .background-color-B5 {
- background-color: #45475a;
- }
- .background-color-B6 {
- background-color: #313244;
- }
- .background-color-AA2 {
- background-color: #f38ba8;
- }
- .background-color-AA4 {
- background-color: #45475a;
- }
- .background-color-AA5 {
- background-color: #313244;
- }
- .background-color-AB4 {
- background-color: #45475a;
- }
- .background-color-AB5 {
- background-color: #313244;
- }
-
- .color-N1 {
- color: #cdd6f4;
- }
- .color-N2 {
- color: #bac2de;
- }
- .color-N3 {
- color: #a6adc8;
- }
- .color-N4 {
- color: #585b70;
- }
- .color-N5 {
- color: #45475a;
- }
- .color-N6 {
- color: #313244;
- }
- .color-N7 {
- color: #1e1e2e;
- }
- .color-B1 {
- color: #cba6f7;
- }
- .color-B2 {
- color: #cba6f7;
- }
- .color-B3 {
- color: #6c7086;
- }
- .color-B4 {
- color: #585b70;
- }
- .color-B5 {
- color: #45475a;
- }
- .color-B6 {
- color: #313244;
- }
- .color-AA2 {
- color: #f38ba8;
- }
- .color-AA4 {
- color: #45475a;
- }
- .color-AA5 {
- color: #313244;
- }
- .color-AB4 {
- color: #45475a;
- }
- .color-AB5 {
- color: #313244;
- }
-}
diff --git a/main.go b/main.go
index 948c59357..5afbbaf67 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
+ "math"
"os/exec"
"path/filepath"
"strings"
@@ -62,6 +63,10 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
+ darkThemeFlag, err := ms.Opts.Int64("D2_D_THEME", "dark_theme", "", math.MaxInt64, "the diagram dark theme ID. When left unset only the theme will be applied")
+ if err != nil {
+ return err
+ }
padFlag, err := ms.Opts.Int64("D2_PAD", "pad", "", d2svg.DEFAULT_PADDING, "pixels padded around the rendered diagram")
if err != nil {
return err
@@ -75,11 +80,6 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return err
}
- sketchBgFlag, err := ms.Opts.Bool("D2_SKT_BG", "sketch_bg", "", true, "make the background look like it was sketched too")
- if err != nil {
- return err
- }
-
ps, err := d2plugin.ListPlugins(ctx)
if err != nil {
return err
@@ -151,6 +151,14 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
}
ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
+ if *darkThemeFlag != math.MaxInt64 {
+ match = d2themescatalog.Find(*darkThemeFlag)
+ if match == (d2themes.Theme{}) {
+ return xmain.UsageErrorf("--dark_theme could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *darkThemeFlag)
+ }
+ ms.Log.Debug.Printf("using dark theme %s (ID: %d)", match.Name, *darkThemeFlag)
+ }
+
plugin, err := d2plugin.FindPlugin(ctx, ps, *layoutFlag)
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
@@ -176,6 +184,9 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
var pw png.Playwright
if filepath.Ext(outputPath) == ".png" {
+ if *darkThemeFlag != math.MaxInt64 {
+ return xmain.UsageErrorf("--dark_theme cannot be used while exporting to another format other than .svg")
+ }
pw, err = png.InitPlaywright()
if err != nil {
return err
@@ -197,6 +208,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
layoutPlugin: plugin,
sketch: *sketchFlag,
themeID: *themeFlag,
+ darkThemeID: *darkThemeFlag,
pad: *padFlag,
host: *hostFlag,
port: *portFlag,
@@ -214,7 +226,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
defer cancel()
- _, written, err := compile(ctx, ms, plugin, *sketchFlag, *sketchBgFlag, *padFlag, *themeFlag, inputPath, outputPath, *bundleFlag, pw.Page)
+ _, written, err := compile(ctx, ms, plugin, *sketchFlag, *padFlag, *themeFlag, *darkThemeFlag, inputPath, outputPath, *bundleFlag, pw.Page)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
@@ -225,7 +237,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return nil
}
-func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, sketchBg bool, pad, themeID int64, inputPath, outputPath string, bundle bool, page playwright.Page) (_ []byte, written bool, _ error) {
+func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID, themeDarkID int64, inputPath, outputPath string, bundle bool, page playwright.Page) (_ []byte, written bool, _ error) {
input, err := ms.ReadPath(inputPath)
if err != nil {
return nil, false, err
@@ -250,10 +262,10 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
}
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
- Pad: int(pad),
- Sketch: sketch,
- SketchBg: sketchBg,
- ThemeID: themeID,
+ Pad: int(pad),
+ Sketch: sketch,
+ ThemeID: themeID,
+ DarkThemeID: themeDarkID,
})
if err != nil {
return nil, false, err
diff --git a/watch.go b/watch.go
index 530ad7d70..d98a447f0 100644
--- a/watch.go
+++ b/watch.go
@@ -41,9 +41,9 @@ var staticFS embed.FS
type watcherOpts struct {
layoutPlugin d2plugin.Plugin
themeID int64
+ darkThemeID int64
pad int64
sketch bool
- sketchBg bool
host string
port string
inputPath string
@@ -357,7 +357,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
w.pw = newPW
}
- svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.sketchBg, w.pad, w.themeID, w.inputPath, w.outputPath, w.bundle, w.pw.Page)
+ svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.pw.Page)
errs := ""
if err != nil {
if len(svg) > 0 {