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
.Op Fl -watch Ar false
.Op Fl -theme Em 0
.Op Fl -salt Ar string
.Ar file.d2
.Op Ar file.svg | file.png
.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
Check that the specified files are formatted correctly
.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
Print usage information and exit
.Ns .

View file

@ -134,6 +134,8 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
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)
if err != nil {
return err
@ -324,6 +326,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
DarkThemeID: darkThemeFlag,
Scale: scale,
NoXMLTag: noXMLTagFlag,
Salt: saltFlag,
}
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)
if animateInterval > 0 {
masterID, err := diagram.HashID()
masterID, err := diagram.HashID(renderOpts.Salt)
if err != nil {
return nil, false, err
}
@ -865,7 +868,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
} else if toPNG {
scale = go2.Pointer(1.)
}
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
@ -875,8 +878,10 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
NoXMLTag: opts.NoXMLTag,
Salt: opts.Salt,
Scale: scale,
})
}
svg, err := d2svg.Render(diagram, renderOpts)
if err != nil {
return nil, err
}
@ -897,12 +902,12 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts
bundleErr = multierr.Combine(bundleErr, bundleErr2)
}
if forceAppendix && !toPNG {
svg = appendix.Append(diagram, ruler, svg)
svg = appendix.Append(diagram, renderOpts, ruler, svg)
}
out := svg
if toPNG {
svg := appendix.Append(diagram, ruler, svg)
svg := appendix.Append(diagram, renderOpts, ruler, svg)
if !bundle {
var bundleErr2 error
@ -960,7 +965,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
scale = go2.Pointer(1.)
}
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
@ -969,7 +974,8 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
})
}
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil {
return nil, err
}
@ -987,7 +993,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
if bundleErr != nil {
return svg, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {
@ -1066,7 +1072,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
var err error
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
@ -1075,7 +1081,8 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
})
}
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil {
return nil, err
}
@ -1094,7 +1101,7 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
return nil, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {
@ -1312,7 +1319,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
} else {
scale = go2.Pointer(1.)
}
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
renderOpts := &d2svg.RenderOpts{
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
@ -1321,7 +1328,8 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
DarkThemeID: opts.DarkThemeID,
ThemeOverrides: opts.ThemeOverrides,
DarkThemeOverrides: opts.DarkThemeOverrides,
})
}
svg, err = d2svg.Render(diagram, renderOpts)
if err != nil {
return nil, nil, err
}
@ -1340,7 +1348,7 @@ func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plug
return nil, nil, bundleErr
}
svg = appendix.Append(diagram, ruler, svg)
svg = appendix.Append(diagram, renderOpts, ruler, svg)
pngImg, err := ConvertSVG(ms, page, svg)
if err != nil {

View file

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

View file

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

View file

@ -63,7 +63,7 @@ func FindViewboxSlice(svg []byte) []string {
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)
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
})
diagramHash, err := diagram.HashID()
var salt *string
if renderOpts != nil {
salt = renderOpts.Salt
}
diagramHash, err := diagram.HashID(salt)
if err != nil {
return nil
}

View file

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

View file

@ -88,6 +88,7 @@ type RenderOpts struct {
// Currently, that's when multi-boards are collapsed
MasterID string
NoXMLTag *bool
Salt *string
}
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
diagramHash, err := diagram.HashID()
diagramHash, err := diagram.HashID(opts.Salt)
if err != nil {
return nil, err
}

View file

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

View file

@ -38,7 +38,7 @@ func TestConfigHash(t *testing.T) {
}
ctx := log.WithDefault(context.Background())
diagram, _, _ := d2lib.Compile(ctx, "x -> y", compileOpts, renderOpts)
hash1, err = diagram.HashID()
hash1, err = diagram.HashID(nil)
assert.Success(t, err)
}
@ -57,7 +57,52 @@ func TestConfigHash(t *testing.T) {
}
ctx := log.WithDefault(context.Background())
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)
}

View file

@ -250,7 +250,7 @@ func run(t *testing.T, tc testCase) {
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
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)
renderOpts.MasterID = masterID
}