add diagram hash salt
This commit is contained in:
parent
0ce066d7ff
commit
f711a76ade
10 changed files with 91 additions and 27 deletions
|
|
@ -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 .
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue