add diagram hash salt

This commit is contained in:
Alexander Wang 2025-01-30 14:48:06 -07:00
parent 0ce066d7ff
commit f711a76ade
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
10 changed files with 91 additions and 27 deletions

View file

@ -8,6 +8,7 @@
.Nm d2 .Nm d2
.Op Fl -watch Ar false .Op Fl -watch Ar false
.Op Fl -theme Em 0 .Op Fl -theme Em 0
.Op Fl -salt Ar string
.Ar file.d2 .Ar file.d2
.Op Ar file.svg | file.png .Op Ar file.svg | file.png
.Nm d2 .Nm d2
@ -128,6 +129,9 @@ The maximum number of seconds that D2 runs for before timing out and exiting. Wh
.It Fl -check Ar false .It Fl -check Ar false
Check that the specified files are formatted correctly Check that the specified files are formatted correctly
.Ns . .Ns .
.It Fl -salt Ar string
Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate id's do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output.
.Ns .
.It Fl h , -help .It Fl h , -help
Print usage information and exit Print usage information and exit
.Ns . .Ns .

View file

@ -134,6 +134,8 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return err return err
} }
saltFlag := ms.Opts.String("", "salt", "", "", "Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate IDs do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output.")
plugins, err := d2plugin.ListPlugins(ctx) plugins, err := d2plugin.ListPlugins(ctx)
if err != nil { if err != nil {
return err return err
@ -324,6 +326,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
DarkThemeID: darkThemeFlag, DarkThemeID: darkThemeFlag,
Scale: scale, Scale: scale,
NoXMLTag: noXMLTagFlag, NoXMLTag: noXMLTagFlag,
Salt: saltFlag,
} }
if *watchFlag { if *watchFlag {
@ -517,7 +520,7 @@ func compile(ctx context.Context, ms *xmain.State, plugins []d2plugin.Plugin, fs
plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout) plugin, _ := d2plugin.FindPlugin(ctx, plugins, *opts.Layout)
if animateInterval > 0 { if animateInterval > 0 {
masterID, err := diagram.HashID() masterID, err := diagram.HashID(renderOpts.Salt)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -865,7 +868,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
} else if toPNG { } else if toPNG {
scale = go2.Pointer(1.) scale = go2.Pointer(1.)
} }
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{ renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad, Pad: opts.Pad,
Sketch: opts.Sketch, Sketch: opts.Sketch,
Center: opts.Center, Center: opts.Center,
@ -875,8 +878,10 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
ThemeOverrides: opts.ThemeOverrides, ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides, DarkThemeOverrides: opts.DarkThemeOverrides,
NoXMLTag: opts.NoXMLTag, NoXMLTag: opts.NoXMLTag,
Salt: opts.Salt,
Scale: scale, Scale: scale,
}) }
svg, err := d2svg.Render(diagram, renderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -897,12 +902,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
bundleErr = multierr.Combine(bundleErr, bundleErr2) bundleErr = multierr.Combine(bundleErr, bundleErr2)
} }
if forceAppendix && !toPNG { if forceAppendix && !toPNG {
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, renderOpts, ruler, svg)
} }
out := svg out := svg
if toPNG { if toPNG {
svg := appendix.Append(diagram, ruler, svg) svg := appendix.Append(diagram, renderOpts, ruler, svg)
if !bundle { if !bundle {
var bundleErr2 error var bundleErr2 error
@ -960,7 +965,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
scale = go2.Pointer(1.) scale = go2.Pointer(1.)
} }
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad, Pad: opts.Pad,
Sketch: opts.Sketch, Sketch: opts.Sketch,
Center: opts.Center, Center: opts.Center,
@ -969,7 +974,8 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
DarkThemeID: opts.DarkThemeID, DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides, ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides, DarkThemeOverrides: opts.DarkThemeOverrides,
}) }
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -987,7 +993,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
if bundleErr != nil { if bundleErr != nil {
return svg, bundleErr return svg, bundleErr
} }
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
@ -1066,7 +1072,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
var err error var err error
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad, Pad: opts.Pad,
Sketch: opts.Sketch, Sketch: opts.Sketch,
Center: opts.Center, Center: opts.Center,
@ -1075,7 +1081,8 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
DarkThemeID: opts.DarkThemeID, DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides, ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides, DarkThemeOverrides: opts.DarkThemeOverrides,
}) }
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1094,7 +1101,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
return nil, bundleErr return nil, bundleErr
} }
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {
@ -1312,7 +1319,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
} else { } else {
scale = go2.Pointer(1.) scale = go2.Pointer(1.)
} }
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{ renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad, Pad: opts.Pad,
Sketch: opts.Sketch, Sketch: opts.Sketch,
Center: opts.Center, Center: opts.Center,
@ -1321,7 +1328,8 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
DarkThemeID: opts.DarkThemeID, DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides, ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides, DarkThemeOverrides: opts.DarkThemeOverrides,
}) }
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -1340,7 +1348,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
return nil, nil, bundleErr return nil, nil, bundleErr
} }
svg = appendix.Append(diagram, ruler, svg) svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg) pngImg, err := ConvertSVG(ms, page, svg)
if err != nil { if err != nil {

View file

@ -303,10 +303,10 @@ a -> b
db, err := compile(ctx, bString) db, err := compile(ctx, bString)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
hashA, err := da.HashID() hashA, err := da.HashID(nil)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
hashB, err := db.HashID() hashB, err := db.HashID(nil)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
assert.NotEqual(t, hashA, hashB) assert.NotEqual(t, hashA, hashB)

View file

@ -77,7 +77,7 @@ func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderO
svgsStr += string(svg) + " " svgsStr += string(svg) + " "
} }
diagramHash, err := rootDiagram.HashID() diagramHash, err := rootDiagram.HashID(renderOpts.Salt)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -63,7 +63,7 @@ func FindViewboxSlice(svg []byte) []string {
return strings.Split(viewboxRaw, " ") return strings.Split(viewboxRaw, " ")
} }
func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte { func Append(diagram *d2target.Diagram, renderOpts *d2svg.RenderOpts, ruler *textmeasure.Ruler, in []byte) []byte {
svg := string(in) svg := string(in)
appendix, w, h := generateAppendix(diagram, ruler, svg) appendix, w, h := generateAppendix(diagram, ruler, svg)
@ -177,7 +177,11 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
return renderOrder[i].shape.Level < renderOrder[j].shape.Level return renderOrder[i].shape.Level < renderOrder[j].shape.Level
}) })
diagramHash, err := diagram.HashID() var salt *string
if renderOpts != nil {
salt = renderOpts.Salt
}
diagramHash, err := diagram.HashID(salt)
if err != nil { if err != nil {
return nil return nil
} }

View file

@ -3,7 +3,6 @@ package appendix_test
import ( import (
"context" "context"
"encoding/xml" "encoding/xml"
"io/ioutil"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@ -172,11 +171,11 @@ func run(t *testing.T, tc testCase) {
svgBytes, err := d2svg.Render(diagram, renderOpts) svgBytes, err := d2svg.Render(diagram, renderOpts)
assert.Success(t, err) assert.Success(t, err)
svgBytes = appendix.Append(diagram, ruler, svgBytes) svgBytes = appendix.Append(diagram, nil, ruler, svgBytes)
err = os.MkdirAll(dataPath, 0755) err = os.MkdirAll(dataPath, 0755)
assert.Success(t, err) assert.Success(t, err)
err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600) err = os.WriteFile(pathGotSVG, svgBytes, 0600)
assert.Success(t, err) assert.Success(t, err)
defer os.Remove(pathGotSVG) defer os.Remove(pathGotSVG)

View file

@ -88,6 +88,7 @@ type RenderOpts struct {
// Currently, that's when multi-boards are collapsed // Currently, that's when multi-boards are collapsed
MasterID string MasterID string
NoXMLTag *bool NoXMLTag *bool
Salt *string
} }
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) { func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
@ -1908,7 +1909,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
} }
// Apply hash on IDs for targeting, to be specific for this diagram // Apply hash on IDs for targeting, to be specific for this diagram
diagramHash, err := diagram.HashID() diagramHash, err := diagram.HashID(opts.Salt)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -223,13 +223,16 @@ func (diagram Diagram) HasShape(condition func(Shape) bool) bool {
return false return false
} }
func (diagram Diagram) HashID() (string, error) { func (diagram Diagram) HashID(salt *string) (string, error) {
bytes, err := diagram.Bytes() bytes, err := diagram.Bytes()
if err != nil { if err != nil {
return "", err return "", err
} }
h := fnv.New32a() h := fnv.New32a()
h.Write(bytes) h.Write(bytes)
if salt != nil {
h.Write([]byte(*salt))
}
// CSS names can't start with numbers, so prepend a little something // CSS names can't start with numbers, so prepend a little something
return fmt.Sprintf("d2-%d", h.Sum32()), nil return fmt.Sprintf("d2-%d", h.Sum32()), nil
} }

View file

@ -38,7 +38,7 @@ func TestConfigHash(t *testing.T) {
} }
ctx := log.WithDefault(context.Background()) ctx := log.WithDefault(context.Background())
diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts) diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts)
hash1, err = diagram.HashID() hash1, err = diagram.HashID(nil)
assert.Success(t, err) assert.Success(t, err)
} }
@ -57,7 +57,52 @@ func TestConfigHash(t *testing.T) {
} }
ctx := log.WithDefault(context.Background()) ctx := log.WithDefault(context.Background())
diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts) diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts)
hash2, err = diagram.HashID() hash2, err = diagram.HashID(nil)
assert.Success(t, err)
}
assert.NotEqual(t, hash1, hash2)
}
func TestHashSalt(t *testing.T) {
var hash1, hash2 string
var err error
{
ruler, _ := textmeasure.NewRuler()
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
return d2dagrelayout.DefaultLayout, nil
}
renderOpts := &d2svg.RenderOpts{
Pad: go2.Pointer(int64(5)),
ThemeID: &d2themescatalog.GrapeSoda.ID,
}
compileOpts := &d2lib.CompileOptions{
LayoutResolver: layoutResolver,
Ruler: ruler,
}
ctx := log.WithDefault(context.Background())
diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts)
hash1, err = diagram.HashID(nil)
assert.Success(t, err)
}
{
ruler, _ := textmeasure.NewRuler()
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
return d2dagrelayout.DefaultLayout, nil
}
renderOpts := &d2svg.RenderOpts{
Pad: go2.Pointer(int64(5)),
ThemeID: &d2themescatalog.GrapeSoda.ID,
}
compileOpts := &d2lib.CompileOptions{
LayoutResolver: layoutResolver,
Ruler: ruler,
}
ctx := log.WithDefault(context.Background())
diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts)
hash2, err = diagram.HashID(go2.Pointer("asdf"))
assert.Success(t, err) assert.Success(t, err)
} }

View file

@ -250,7 +250,7 @@ func run(t *testing.T, tc testCase) {
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 { if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
masterID, err := diagram.HashID() masterID, err := diagram.HashID(nil)
assert.Success(t, err) assert.Success(t, err)
renderOpts.MasterID = masterID renderOpts.MasterID = masterID
} }