Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add diagram hash salt #2316

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: d2chaos
Expand Down
4 changes: 4 additions & 0 deletions ci/release/template/man/d2.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 .
Expand Down
36 changes: 22 additions & 14 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -324,6 +326,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
DarkThemeID: darkThemeFlag,
Scale: scale,
NoXMLTag: noXMLTagFlag,
Salt: saltFlag,
}

if *watchFlag {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions d2exporter/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion d2renderers/d2animate/d2animate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 6 additions & 2 deletions d2renderers/d2svg/appendix/appendix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 2 additions & 3 deletions d2renderers/d2svg/appendix/appendix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package appendix_test
import (
"context"
"encoding/xml"
"io/ioutil"
"log/slog"
"os"
"path/filepath"
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion d2renderers/d2svg/d2svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion d2target/d2target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
49 changes: 47 additions & 2 deletions docs/examples/lib/1-d2lib/d2lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion e2etests/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down