merge with master

This commit is contained in:
Antoine Poivey 2023-04-06 19:04:53 +02:00
commit e59a50fd08
No known key found for this signature in database
GPG key ID: 6AA1C83421F1A287
500 changed files with 41253 additions and 3939 deletions

3
.gitignore vendored
View file

@ -1,8 +1,7 @@
.make-log
.changed-files
.make-log.txt
*.got.json
*.got.svg
*.got.*
e2e_report.html
bin
out

View file

@ -262,12 +262,14 @@ this selected list of featured projects using D2.
- Official app of the Netherlands for coronavirus entry passes.
- [Block
Protocol](https://github.com/blockprotocol/blockprotocol/blob/db4cf8d422b881e52113aa52467d53115270e2b3/libs/%40blockprotocol/type-system/crate/assets/overview.d2)
- The Block Protocol is an open standard for building and using data-driven blocks.
- [Dagger](https://github.com/dagger/dagger/tree/main/cmd/dagger-graph) - A programmable
CI/CD engine that runs your pipelines in containers
- The Block Protocol is an open standard for building and using data-driven blocks (1.2k
stars).
- [Dagger](https://github.com/dagger/dagger/tree/main/cmd/dagger-graph)
- A programmable CI/CD engine that runs your pipelines in containers (8k stars).
- [Ivy
Wallet](https://github.com/Ivy-Apps/ivy-wallet/blob/8062624bfa65175ec143cdc4038de27a84d38b57/assets/calc_algo.d2)
- Ivy Wallet is an open-source money manager app for Android.
- [Shed
Skin](https://github.com/shedskin/shedskin/blob/c7929e5fe0290d734ffb7e34e4cfc2cf731c7f98/docs/assets/diagrams/shedskin.d2)
- Python to C++ compiler
- Open-source money manager app for Android (1.1k stars).
- [LocalStack](https://docs.localstack.cloud/references/network-troubleshooting/)
- Cloud service emulator (46k stars)
- [Queue Library](https://github.com/golang-queue/queue/tree/master/images)
- Queue is a Golang library for spawning and managing a Goroutine pool

View file

@ -1,5 +1,16 @@
#### Features 🚀
- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
#### Improvements 🧹
- Labels on parallel `dagre` connections include a gap between them [#1134](https://github.com/terrastruct/d2/pull/1134)
#### Bugfixes ⛑️
- Fix a bug in 32bit builds [#1115](https://github.com/terrastruct/d2/issues/1115)
- Fix a bug in ID parsing [#322](https://github.com/terrastruct/d2/issues/322)
- Fix a bug in watch mode parsing SVG [#1119](https://github.com/terrastruct/d2/issues/1119)
- Namespace transitions so that multiple animated D2 diagrams can exist on the same page [#1123](https://github.com/terrastruct/d2/issues/1123)
- Fix a bug in vertical alignment of appendix lines [#1104](https://github.com/terrastruct/d2/issues/1104)
- Fix precision difference for sketch mode running on different architectures [#921](https://github.com/terrastruct/d2/issues/921)

View file

@ -0,0 +1,17 @@
#### Features 🚀
- `--center` flag centers the SVG in the containing viewbox. [#1056](https://github.com/terrastruct/d2/pull/1056)
- Strikethrough in Markdown with `~~`. [#1059](https://github.com/terrastruct/d2/pull/1059)
#### Improvements 🧹
- `elk` layout containers no longer overlap the label with children. [#1055](https://github.com/terrastruct/d2/pull/1055)
- `--browser` flag on CLI controls `BROWSER` environment variable for not opening browser in watch mode. [#1052](https://github.com/terrastruct/d2/pull/1052)
- Message emitted by CLI when a particular stage is taking a long time. [#1058](https://github.com/terrastruct/d2/pull/1058)
- `<title>` attribute of HTML in watch mode is the base file name, instead of the whole path. [#1054](https://github.com/terrastruct/d2/pull/1054)
#### Bugfixes ⛑️
- Code blocks are not affected by uppercasing from special themes like Terminal. [#1053](https://github.com/terrastruct/d2/pull/1053)
- Fixes `fill-pattern` replacement in the API. [#1051](https://github.com/terrastruct/d2/pull/1051)
- Fixes multiple `<br/>` elements in a row being mismeasured in Markdown blocks. [#1060](https://github.com/terrastruct/d2/pull/1060)

View file

@ -0,0 +1,34 @@
D2 0.3 is here!
## Major updates:
- SVG sizes are ~**5%** of what they were in D2 0.2.
- The disproportionately largest contributor to this size was the font. Instead of encoding the entire font, D2 now only bundles only the used part of it (e.g. if you don't use the letter "b", the font encoding for "b" won't be included).
- The first practical applications of multi-board compositions are here: animations. Composition is among D2's most powerful features, and these first applications are just the tip of the iceberg. Stay tuned for more in upcoming 0.3.x releases. See [docs](https://d2lang.com/tour/composition).
![animated](https://user-images.githubusercontent.com/3120367/228722320-65a42558-55b5-40f0-8616-53510b57202f.svg)
- Customizable fonts. You can pass in whatever you want to use through the command line.
<img width="300" alt="Screen Shot 2023-03-29 at 8 27 45 PM" src="https://user-images.githubusercontent.com/3120367/228721122-577c8d28-5fbf-473e-924c-35f6f1e98fa1.png">
### Other
- New "Origami" theme
<img width="550" alt="Screen Shot 2023-03-29 at 7 59 31 PM" src="https://user-images.githubusercontent.com/3120367/228721029-2136e162-e303-4b87-9da3-d8e6ad02af92.png">
#### Features 🚀
- Flags to set a custom font are supported. See [docs](https://d2lang.com/tour/fonts). [#1108](https://github.com/terrastruct/d2/pull/1108)
- `--animate-interval` can be passed as a flag to animate multi-board diagrams. See [docs](https://d2lang.com/tour/composition). [#1088](https://github.com/terrastruct/d2/pull/1088)
- New `fill-pattern`: `paper` [#1070](https://github.com/terrastruct/d2/pull/1070)
- Fonts are subsetted to only include what's necessary [#1089](https://github.com/terrastruct/d2/pull/1089)
- New theme: Origami [#1110](https://github.com/terrastruct/d2/pull/1110)
#### Improvements 🧹
- Prevent `tooltip` being set to a URL when `link` is already set (for security) [#1091](https://github.com/terrastruct/d2/pull/1091)
- Scale arrowhead sizes appropriately to `stroke-width`. [#1101](https://github.com/terrastruct/d2/pull/1101)
#### Bugfixes ⛑️
- Prevents an object's `near` from targeting another object with `near` set to a constant [#1100](https://github.com/terrastruct/d2/pull/1100)
- Fixes inaccurate bold edge label padding [#1108](https://github.com/terrastruct/d2/pull/1108)
- Prevents Latex blocks from being uppercased in special themes [#1111](https://github.com/terrastruct/d2/pull/1111)

View file

@ -77,9 +77,27 @@ making style maps in D2 light/dark mode specific. See
.It Fl s , -sketch Ar false
Renders the diagram to look like it was sketched by hand
.Ns .
.It Fl -center Ar flag
Center the SVG in the containing viewbox, such as your browser screen
.Ns .
.It Fl -font-regular
Path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used
.Ns .
.It Fl -font-italic
Path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used
.Ns .
.It Fl -font-bold
Path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used
.Ns .
.It Fl -pad Ar 100
Pixels padded around the rendered diagram
.Ns .
.It Fl -animate-interval Ar 0
If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports
.Ns .
.It Fl -browser Ar true
Browser executable that watch opens. Setting to 0 opens no browser
.Ns .
.It Fl l , -layout Ar dagre
Set the diagram layout engine to the passed string. For a list of available options, run
.Ar layout

2
ci/sub

@ -1 +1 @@
Subproject commit 690bc39e545cae76314fa32effe343a088e2a52e
Subproject commit 771c618dfa0372f8f1dd7975497151ed55223c60

View file

@ -21,12 +21,14 @@ import (
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2animate"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2renderers/d2svg/appendix"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/background"
"oss.terrastruct.com/d2/lib/imgbundler"
ctxlog "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/pdf"
@ -67,7 +69,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "The theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
darkThemeFlag, err := ms.Opts.Int64("D2_DARK_THEME", "dark-theme", "", -1, "the theme to use when the viewer's browser is in dark mode. When left unset -theme is used for both light and dark mode. Be aware that explicit styles set in D2 code will still be applied and this may produce unexpected results. We plan on resolving this by making style maps in D2 light/dark mode specific. See https://github.com/terrastruct/d2/issues/831.")
if err != nil {
return err
}
@ -75,6 +77,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
animateIntervalFlag, err := ms.Opts.Int64("D2_ANIMATE_INTERVAL", "animate-interval", "", 0, "if given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds). Can only be used with SVG exports.")
if err != nil {
return err
}
versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version")
if err != nil {
return err
@ -83,6 +89,15 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if err != nil {
return err
}
browserFlag := ms.Opts.String("BROWSER", "browser", "", "", "browser executable that watch opens. Setting to 0 opens no browser.")
centerFlag, err := ms.Opts.Bool("D2_CENTER", "center", "c", false, "center the SVG in the containing viewbox, such as your browser screen")
if err != nil {
return err
}
fontRegularFlag := ms.Opts.String("D2_FONT_REGULAR", "font-regular", "", "", "path to .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.")
fontItalicFlag := ms.Opts.String("D2_FONT_ITALIC", "font-italic", "", "", "path to .ttf file to use for the italic font. If none provided, Source Sans Pro Regular-Italic is used.")
fontBoldFlag := ms.Opts.String("D2_FONT_BOLD", "font-bold", "", "", "path to .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.")
ps, err := d2plugin.ListPlugins(ctx)
if err != nil {
@ -103,6 +118,11 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
return nil
}
fontFamily, err := loadFonts(ms, *fontRegularFlag, *fontItalicFlag, *fontBoldFlag)
if err != nil {
return xmain.UsageErrorf("failed to load specified fonts: %v", err)
}
if len(ms.Opts.Flags.Args()) > 0 {
switch ms.Opts.Flags.Arg(0) {
case "init-playwright":
@ -126,6 +146,9 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
if *debugFlag {
ms.Env.Setenv("DEBUG", "1")
}
if *browserFlag != "" {
ms.Env.Setenv("BROWSER", *browserFlag)
}
var inputPath string
var outputPath string
@ -162,6 +185,12 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}
if outputPath != "-" {
outputPath = ms.AbsPath(outputPath)
if *animateIntervalFlag > 0 {
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
}
}
}
match := d2themescatalog.Find(*themeFlag)
@ -222,23 +251,30 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}()
}
renderOpts := d2svg.RenderOpts{
Pad: int(*padFlag),
Sketch: *sketchFlag,
Center: *centerFlag,
ThemeID: *themeFlag,
DarkThemeID: darkThemeFlag,
}
if *watchFlag {
if inputPath == "-" {
return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin")
}
w, err := newWatcher(ctx, ms, watcherOpts{
layoutPlugin: plugin,
sketch: *sketchFlag,
themeID: *themeFlag,
darkThemeID: darkThemeFlag,
pad: *padFlag,
host: *hostFlag,
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
forceAppendix: *forceAppendixFlag,
pw: pw,
layoutPlugin: plugin,
renderOpts: renderOpts,
animateInterval: *animateIntervalFlag,
host: *hostFlag,
port: *portFlag,
inputPath: inputPath,
outputPath: outputPath,
bundle: *bundleFlag,
forceAppendix: *forceAppendixFlag,
pw: pw,
fontFamily: fontFamily,
})
if err != nil {
return err
@ -249,7 +285,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, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
_, written, err := compile(ctx, ms, plugin, renderOpts, fontFamily, *animateIntervalFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page)
if err != nil {
if written {
return fmt.Errorf("failed to fully compile (partial render written): %w", err)
@ -259,7 +295,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, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, renderOpts d2svg.RenderOpts, fontFamily *d2fonts.FontFamily, animateInterval int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) {
start := time.Now()
input, err := ms.ReadPath(inputPath)
if err != nil {
@ -273,17 +309,33 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
layout := plugin.Layout
opts := &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
Layout: layout,
Ruler: ruler,
ThemeID: renderOpts.ThemeID,
FontFamily: fontFamily,
}
if sketch {
if renderOpts.Sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
}
cancel := background.Repeat(func() {
ms.Log.Info.Printf("compiling & running layout algorithms...")
}, time.Second*5)
defer cancel()
diagram, g, err := d2lib.Compile(ctx, string(input), opts)
if err != nil {
return nil, false, err
}
cancel()
if animateInterval > 0 {
masterID, err := diagram.HashID()
if err != nil {
return nil, false, err
}
renderOpts.MasterID = masterID
}
pluginInfo, err := plugin.Info(ctx)
if err != nil {
@ -295,27 +347,144 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
return nil, false, err
}
var svg []byte
if filepath.Ext(outputPath) == ".pdf" {
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
svg, err = renderPDF(ctx, ms, plugin, sketch, pad, themeID, outputPath, page, ruler, diagram, nil, nil, pageMap)
} else {
compileDur := time.Since(start)
svg, err = render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
}
if err != nil {
return svg, false, err
}
if filepath.Ext(outputPath) == ".pdf" {
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
if err != nil {
return pdf, false, err
}
dur := time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
}
return pdf, true, nil
} else {
compileDur := time.Since(start)
if animateInterval <= 0 {
// Rename all the "root.layers.x" to the paths that the boards get output to
linkToOutput, err := resolveLinks("root", outputPath, diagram)
if err != nil {
return nil, false, err
}
relink(diagram, linkToOutput)
}
return svg, true, nil
boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return nil, false, err
}
out := boards[0]
if animateInterval > 0 {
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
if err != nil {
return nil, false, err
}
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return nil, false, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return nil, false, err
}
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), time.Since(start))
}
return out, true, nil
}
}
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, sketch bool, pad int64, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram) (linkToOutput map[string]string, err error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
outputPath = strings.TrimSuffix(outputPath, ext)
outputPath = filepath.Join(outputPath, diagram.Name)
outputPath += ext
}
boardOutputPath := outputPath
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
ext := filepath.Ext(boardOutputPath)
boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
boardOutputPath = filepath.Join(boardOutputPath, "index")
boardOutputPath += ext
}
layersOutputPath := outputPath
if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
ext := filepath.Ext(layersOutputPath)
layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
layersOutputPath = filepath.Join(layersOutputPath, "layers")
layersOutputPath += ext
}
scenariosOutputPath := outputPath
if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
ext := filepath.Ext(scenariosOutputPath)
scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
scenariosOutputPath += ext
}
stepsOutputPath := outputPath
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
ext := filepath.Ext(stepsOutputPath)
stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
stepsOutputPath += ext
}
linkToOutput = map[string]string{currDiagramPath: boardOutputPath}
for _, dl := range diagram.Layers {
m, err := resolveLinks(strings.Join([]string{currDiagramPath, "layers", dl.Name}, "."), layersOutputPath, dl)
if err != nil {
return nil, err
}
for k, v := range m {
linkToOutput[k] = v
}
}
for _, dl := range diagram.Scenarios {
m, err := resolveLinks(strings.Join([]string{currDiagramPath, "scenarios", dl.Name}, "."), scenariosOutputPath, dl)
if err != nil {
return nil, err
}
for k, v := range m {
linkToOutput[k] = v
}
}
for _, dl := range diagram.Steps {
m, err := resolveLinks(strings.Join([]string{currDiagramPath, "steps", dl.Name}, "."), stepsOutputPath, dl)
if err != nil {
return nil, err
}
for k, v := range m {
linkToOutput[k] = v
}
}
return linkToOutput, nil
}
func relink(d *d2target.Diagram, linkToOutput map[string]string) {
for i, shape := range d.Shapes {
if shape.Link != "" {
for k, v := range linkToOutput {
if shape.Link == k {
d.Shapes[i].Link = v
break
}
}
}
}
for _, board := range d.Layers {
relink(board, linkToOutput)
}
for _, board := range d.Scenarios {
relink(board, linkToOutput)
}
for _, board := range d.Steps {
relink(board, linkToOutput)
}
}
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
outputPath = strings.TrimSuffix(outputPath, ext)
@ -326,6 +495,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
boardOutputPath := outputPath
if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
if outputPath == "-" {
// TODO it can if composed into one
return nil, fmt.Errorf("multiboard output cannot be written to stdout")
}
// Boards with subboards must be self-contained folders.
@ -358,46 +528,55 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
stepsOutputPath += ext
}
var boards [][]byte
for _, dl := range diagram.Layers {
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, layersOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, scenariosOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
_, err := render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
childrenBoards, err := render(ctx, ms, compileDur, plugin, opts, inputPath, stepsOutputPath, bundle, forceAppendix, page, ruler, dl)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
if !diagram.IsFolderOnly {
start := time.Now()
svg, err := _render(ctx, ms, plugin, sketch, pad, themeID, darkThemeID, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
out, err := _render(ctx, ms, plugin, opts, boardOutputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return svg, err
return boards, err
}
dur := compileDur + time.Since(start)
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
return svg, nil
if opts.MasterID == "" {
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(boardOutputPath), dur)
}
boards = append([][]byte{out}, boards...)
return boards, nil
}
return nil, nil
}
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad int64, themeID int64, darkThemeID *int64, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
toPNG := filepath.Ext(outputPath) == ".png"
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: int(pad),
Sketch: sketch,
ThemeID: themeID,
DarkThemeID: darkThemeID,
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
ThemeID: opts.ThemeID,
DarkThemeID: opts.DarkThemeID,
MasterID: opts.MasterID,
SetDimensions: toPNG,
})
if err != nil {
@ -443,13 +622,15 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
}
}
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return svg, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return svg, err
if opts.MasterID == "" {
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
if err != nil {
return svg, err
}
err = ms.WritePath(outputPath, out)
if err != nil {
return svg, err
}
}
if bundleErr != nil {
return svg, bundleErr
@ -457,7 +638,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
return svg, nil
}
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
var isRoot bool
if pdf == nil {
pdf = pdflib.Init()
@ -483,8 +664,9 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
diagram.Root.Fill = "transparent"
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: int(pad),
Sketch: sketch,
Pad: opts.Pad,
Sketch: opts.Sketch,
Center: opts.Center,
SetDimensions: true,
})
if err != nil {
@ -518,26 +700,26 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
if err != nil {
return svg, err
}
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY, pageMap)
err = pdf.AddPDFPage(pngImg, currBoardPath, opts.ThemeID, rootFill, diagram.Shapes, int64(opts.Pad), viewboxX, viewboxY, pageMap)
if err != nil {
return svg, err
}
}
for _, dl := range diagram.Layers {
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Scenarios {
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
}
for _, dl := range diagram.Steps {
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
_, err := renderPDF(ctx, ms, plugin, opts, "", page, ruler, dl, pdf, currBoardPath, pageMap)
if err != nil {
return nil, err
}
@ -590,3 +772,47 @@ func initPlaywright() error {
}
return pw.Cleanup()
}
func loadFont(ms *xmain.State, path string) ([]byte, error) {
if filepath.Ext(path) != ".ttf" {
return nil, fmt.Errorf("expected .ttf file but %s has extension %s", path, filepath.Ext(path))
}
ttf, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read font at %s: %v", path, err)
}
ms.Log.Info.Printf("font %s loaded", filepath.Base(path))
return ttf, nil
}
func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold string) (*d2fonts.FontFamily, error) {
if pathToRegular == "" && pathToItalic == "" && pathToBold == "" {
return nil, nil
}
var regularTTF []byte
var italicTTF []byte
var boldTTF []byte
var err error
if pathToRegular != "" {
regularTTF, err = loadFont(ms, pathToRegular)
if err != nil {
return nil, err
}
}
if pathToItalic != "" {
italicTTF, err = loadFont(ms, pathToItalic)
if err != nil {
return nil, err
}
}
if pathToBold != "" {
boldTTF, err = loadFont(ms, pathToBold)
if err != nil {
return nil, err
}
}
return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF)
}

View file

@ -25,14 +25,9 @@ function init(reconnectDelay) {
console.debug("watch websocket received data");
}
if (msg.svg) {
// We could turn d2SVG into an actual SVG element and use outerHTML to fully replace it
// with the result from the renderer but unfortunately that overwrites the #d2-svg ID.
// Even if you add another line to set it afterwards. The parsing/interpretation of outerHTML must be async.
//
// There's no way around that short of parsing out the top level svg tag in the msg and
// setting innerHTML to only the actual svg innards. However then you also need to parse
// out the width, height and viewbox out of the top level SVG tag and update those manually.
d2SVG.innerHTML = msg.svg;
// we can't just set `d2SVG.innerHTML = msg.svg` need to parse this as xml not html
const parsedXML = new DOMParser().parseFromString(msg.svg, "text/xml");
d2SVG.replaceChildren(parsedXML.documentElement);
const svgEl = d2SVG.querySelector("#d2-svg");
// just use inner SVG in watch mode

View file

@ -26,6 +26,8 @@ import (
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/lib/png"
)
@ -39,19 +41,18 @@ var devMode = false
var staticFS embed.FS
type watcherOpts struct {
layoutPlugin d2plugin.Plugin
themeID int64
darkThemeID *int64
pad int64
sketch bool
host string
port string
inputPath string
outputPath string
pwd string
bundle bool
forceAppendix bool
pw png.Playwright
layoutPlugin d2plugin.Plugin
renderOpts d2svg.RenderOpts
animateInterval int64
host string
port string
inputPath string
outputPath string
pwd string
bundle bool
forceAppendix bool
pw png.Playwright
fontFamily *d2fonts.FontFamily
}
type watcher struct {
@ -359,7 +360,7 @@ func (w *watcher) compileLoop(ctx context.Context) error {
w.pw = newPW
}
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.renderOpts, w.fontFamily, w.animateInterval, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page)
errs := ""
if err != nil {
if len(svg) > 0 {
@ -433,7 +434,7 @@ func (w *watcher) handleRoot(hw http.ResponseWriter, r *http.Request) {
<div id="d2-err" style="display: none"></div>
<div id="d2-svg-container"></div>
</body>
</html>`, w.outputPath, w.devMode)
</html>`, filepath.Base(w.outputPath), w.devMode)
}
func (w *watcher) handleWatch(hw http.ResponseWriter, r *http.Request) error {

View file

@ -363,6 +363,13 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.Constraint.Value = scalar.ScalarString()
attrs.Constraint.MapKey = f.LastPrimaryKey()
}
if attrs.Link != nil && attrs.Tooltip != nil {
_, err := url.ParseRequestURI(attrs.Tooltip.Value)
if err == nil {
c.errorf(scalar, "Tooltip cannot be set to URL when link is also set (for security)")
}
}
}
func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
@ -719,6 +726,13 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an object within sequence diagrams")
continue
}
if nearObj.Attributes.NearKey != nil {
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.Attributes.NearKey)[0]]
if nearObjNearIsConst {
c.errorf(obj.Attributes.NearKey, "near keys cannot be set to an object with a constant near key")
continue
}
}
} else if isConst {
is := false
for _, e := range g.Edges {

View file

@ -259,7 +259,7 @@ containers: {
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain, paper`,
},
{
name: "shape_unquoted_hex",
@ -1457,6 +1457,40 @@ x -> y: {
}
},
},
{
name: "url_tooltip",
text: `x: {tooltip: https://google.com}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Tooltip.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value)
}
},
},
{
name: "no_url_link_and_url_tooltip_concurrently",
text: `x: {link: https://not-google.com; tooltip: https://google.com}`,
expErr: `d2/testdata/d2compiler/TestCompile/no_url_link_and_url_tooltip_concurrently.d2:1:44: Tooltip cannot be set to URL when link is also set (for security)`,
},
{
name: "url_link_and_not_url_tooltip_concurrently",
text: `x: {link: https://google.com; tooltip: hello world}`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].Attributes.Link.Value != "https://google.com" {
t.Fatal(g.Objects[0].Attributes.Link.Value)
}
if g.Objects[0].Attributes.Tooltip.Value != "hello world" {
t.Fatal(g.Objects[0].Attributes.Tooltip.Value)
}
},
},
{
name: "nil_scope_obj_regression",
@ -2245,6 +2279,19 @@ x: {
}`,
expErr: `d2/testdata/d2compiler/TestCompile/border-radius-more-than-100-percent.d2:3:24: expected "border-radius" to be an integer if greater than 1`,
},
{
name: "near_near_const",
text: `
title: Title {
near: top-center
}
obj {
near: title
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
},
}
for _, tc := range testCases {

View file

@ -62,6 +62,8 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Them
if len(obj.ChildrenArray) > 0 {
shape.FillPattern = "dots"
}
} else if theme.SpecialRules.AllPaper {
shape.FillPattern = "paper"
}
if theme.SpecialRules.Mono {
shape.FontFamily = "mono"

View file

@ -1039,10 +1039,14 @@ func (e *Edge) Text() *d2target.MText {
if e.Attributes.Style.FontSize != nil {
fontSize, _ = strconv.Atoi(e.Attributes.Style.FontSize.Value)
}
isBold := false
if e.Attributes.Style.Bold != nil {
isBold, _ = strconv.ParseBool(e.Attributes.Style.Bold.Value)
}
return &d2target.MText{
Text: e.Attributes.Label.Value,
FontSize: fontSize,
IsBold: false,
IsBold: isBold,
IsItalic: true,
Dimensions: e.LabelDimensions,
@ -1307,8 +1311,10 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value)
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" {
obj.Attributes.Label.Value = strings.ToUpper(obj.Attributes.Label.Value)
}
}
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
@ -1457,8 +1463,10 @@ func (g *Graph) Texts() []*d2target.MText {
for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" {
text := obj.Text()
if capsLock {
text.Text = strings.ToUpper(text.Text)
if capsLock && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeCode) {
if obj.Attributes.Language != "latex" {
text.Text = strings.ToUpper(text.Text)
}
}
texts = appendTextDedup(texts, text)
}
@ -1593,6 +1601,7 @@ var FillPatterns = []string{
"dots",
"lines",
"grain",
"paper",
}
// BoardKeywords contains the keywords that create new boards.

View file

@ -33,6 +33,7 @@ var dagreJS string
const (
MIN_SEGMENT_LEN = 10
MIN_RANK_SEP = 60
EDGE_LABEL_GAP = 20
)
type ConfigurableOpts struct {
@ -173,37 +174,30 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
for _, edge := range g.Edges {
// dagre doesn't work with edges to containers so we connect container edges to their first child instead (going all the way down)
// we will chop the edge where it intersects the container border so it only shows the edge from the container
src := edge.Src
for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil {
// We want to get the bottom node of sources, setting its rank higher than all children
src = getLongestEdgeChainTail(g, src)
}
dst := edge.Dst
for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
dst = dst.ChildrenArray[0]
src, dst := getEdgeEndpoints(g, edge)
// We want to get the top node of destinations
for _, child := range dst.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, child) != nil && inContainer(e.Dst, dst) != nil {
isHead = false
break
}
}
if isHead {
dst = child
break
}
width := edge.LabelDimensions.Width
height := edge.LabelDimensions.Height
numEdges := 0
for _, e := range g.Edges {
otherSrc, otherDst := getEdgeEndpoints(g, e)
if (otherSrc == src && otherDst == dst) || (otherSrc == dst && otherDst == src) {
numEdges++
}
}
if edge.SrcArrow && !edge.DstArrow {
// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
src, dst = dst, src
// We want to leave some gap between multiple edges
if numEdges > 1 {
switch g.Root.Attributes.Direction.Value {
case "down", "up", "":
width += EDGE_LABEL_GAP
case "left", "right":
height += EDGE_LABEL_GAP
}
}
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), edge.LabelDimensions.Width, edge.LabelDimensions.Height)
loadScript += generateAddEdgeLine(src.AbsID(), dst.AbsID(), edge.AbsID(), width, height)
}
if debugJS {
@ -528,6 +522,40 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
return nil
}
func getEdgeEndpoints(g *d2graph.Graph, edge *d2graph.Edge) (*d2graph.Object, *d2graph.Object) {
// dagre doesn't work with edges to containers so we connect container edges to their first child instead (going all the way down)
// we will chop the edge where it intersects the container border so it only shows the edge from the container
src := edge.Src
for len(src.Children) > 0 && src.Class == nil && src.SQLTable == nil {
// We want to get the bottom node of sources, setting its rank higher than all children
src = getLongestEdgeChainTail(g, src)
}
dst := edge.Dst
for len(dst.Children) > 0 && dst.Class == nil && dst.SQLTable == nil {
dst = dst.ChildrenArray[0]
// We want to get the top node of destinations
for _, child := range dst.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, child) != nil && inContainer(e.Dst, dst) != nil {
isHead = false
break
}
}
if isHead {
dst = child
break
}
}
}
if edge.SrcArrow && !edge.DstArrow {
// for `b <- a`, edge.Edge is `a -> b` and we expect this routing result
src, dst = dst, src
}
return src, dst
}
func setGraphAttrs(attrs dagreOpts) string {
return fmt.Sprintf(`g.setGraph({
ranksep: %d,

View file

@ -248,20 +248,31 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
if n.LayoutOptions.Padding == DefaultOpts.Padding {
// Default
paddingTop := 50
labelHeight := 0
if obj.LabelHeight != nil {
paddingTop = go2.Max(paddingTop, *obj.LabelHeight+label.PADDING)
labelHeight = *obj.LabelHeight + label.PADDING
}
n.Height += 100 + float64(labelHeight)
n.Width += 100
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
paddingTop := n.Height - s.GetInnerBox().Height
n.Height -= (100 + float64(labelHeight))
n.Width -= 100
iconHeight := 0
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
paddingTop = go2.Max(paddingTop, iconSize+label.PADDING*2)
iconHeight = d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft)) + label.PADDING*2
}
paddingTop += float64(go2.Max(labelHeight, iconHeight))
n.LayoutOptions.Padding = fmt.Sprintf("[top=%d,left=50,bottom=50,right=50]",
paddingTop,
// Default padding
go2.Max(int(math.Ceil(paddingTop)), 50),
)
}
} else {

View file

@ -433,6 +433,11 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
attrs.Style.Underline.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "fill-pattern":
if attrs.Style.FillPattern != nil {
attrs.Style.FillPattern.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
}
case "label":
if attrs.Label.MapKey != nil {
@ -638,6 +643,18 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
return g, nil
}
// Usually ignore the object when generating, but if a sibling has the same ID, can't ignore
ignored := obj
for _, ch := range obj.ChildrenArray {
if ch.ID == obj.ID {
ignored = nil
break
}
}
// Keep a list of newly generated IDs, so that generateUniqueKey considers them for conflict
var newIDs []string
// If we already renamed the key from another reference, no need to touch
dedupedRenames := map[string]struct{}{}
for _, ref := range obj.References {
var absKeys []*d2ast.KeyPath
@ -681,7 +698,6 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
absKeys = append(absKeys, absKey)
}
var newIDs []string
renames := make(map[string]string)
for _, absKey := range absKeys {
ida := d2graph.Key(absKey)
@ -689,6 +705,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
if _, ok := dedupedRenames[absKeyStr]; ok {
continue
}
// Stale reference
dedupedRenames[absKeyStr] = struct{}{}
// Do not consider the parent for conflicts, assume the parent will be deleted
if ida[len(ida)-1] == ida[len(ida)-2] {
@ -702,7 +719,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
hoistedAbsKey.Path = append(hoistedAbsKey.Path, ref.Key.Path[:ref.KeyPathIndex]...)
hoistedAbsKey.Path = append(hoistedAbsKey.Path, absKey.Path[len(absKey.Path)-1])
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(hoistedAbsKey), "."), obj, newIDs)
uniqueKeyStr, _, err := generateUniqueKey(g, strings.Join(d2graph.Key(hoistedAbsKey), "."), ignored, newIDs)
if err != nil {
return nil, err
}
@ -719,6 +736,7 @@ func renameConflictsToParent(g *d2graph.Graph, key *d2ast.KeyPath) (*d2graph.Gra
if absKeyStr != renamedKeyStr {
renames[absKeyStr] = renamedKeyStr
}
dedupedRenames[renamedKeyStr] = struct{}{}
}
// We need to rename in a conflict-free order
// E.g. imagine you have children `Text 4` and `Text`.
@ -1853,6 +1871,14 @@ func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]strin
return nil, nil
}
ignored := obj
for _, ch := range obj.ChildrenArray {
if ch.ID == obj.ID {
ignored = nil
break
}
}
for _, ch := range obj.ChildrenArray {
chMK, err := d2parser.ParseMapKey(ch.AbsID())
if err != nil {
@ -1881,7 +1907,7 @@ func MoveIDDeltas(g *d2graph.Graph, key, newKey string) (deltas map[string]strin
}
if _, ok := g.Root.HasChild(d2graph.Key(hoistedMK.Key)); ok || conflictsWithNewID {
newKey, _, err := generateUniqueKey(g, hoistedAbsID, nil, newIDs)
newKey, _, err := generateUniqueKey(g, hoistedAbsID, ignored, newIDs)
if err != nil {
return nil, err
}
@ -2022,7 +2048,35 @@ func DeleteIDDeltas(g *d2graph.Graph, key string) (deltas map[string]string, err
if !ok {
return nil, nil
}
ignored := obj
for _, ch := range obj.ChildrenArray {
if ch.ID == obj.ID {
ignored = nil
break
}
}
for _, ch := range obj.ChildrenArray {
// Record siblings as the unique key generated should not conflict with any siblings either
var siblingsToBeHoisted []string
for _, ch2 := range obj.ChildrenArray {
if ch2 != ch {
chMK, err := d2parser.ParseMapKey(ch2.AbsID())
if err != nil {
return nil, err
}
ida := d2graph.Key(chMK.Key)
if ida[len(ida)-1] == ida[len(ida)-2] {
continue
}
hoistedAbsID := ch2.ID
if obj.Parent != g.Root {
hoistedAbsID = obj.Parent.AbsID() + "." + ch2.ID
}
siblingsToBeHoisted = append(siblingsToBeHoisted, hoistedAbsID)
}
}
chMK, err := d2parser.ParseMapKey(ch.AbsID())
if err != nil {
return nil, err
@ -2049,7 +2103,7 @@ func DeleteIDDeltas(g *d2graph.Graph, key string) (deltas map[string]string, err
}
if conflictingObj, ok := g.Root.HasChild(d2graph.Key(hoistedMK.Key)); (ok && conflictingObj != obj) || conflictsWithNewID {
newKey, _, err := generateUniqueKey(g, hoistedAbsID, obj, newIDs)
newKey, _, err := generateUniqueKey(g, hoistedAbsID, ignored, append(newIDs, siblingsToBeHoisted...))
if err != nil {
return nil, err
}

View file

@ -838,6 +838,27 @@ square.style.opacity: 0.2
style.stroke-width: 1
style.stroke-dash: 3
}
`,
},
{
name: "set_fill_pattern",
text: `square`,
key: `square.style.fill-pattern`,
value: go2.Pointer(`grain`),
exp: `square: {style.fill-pattern: grain}
`,
},
{
name: "replace_fill_pattern",
text: `square: {
style.fill-pattern: lines
}
`,
key: `square.style.fill-pattern`,
value: go2.Pointer(`grain`),
exp: `square: {
style.fill-pattern: grain
}
`,
},
{
@ -1882,6 +1903,33 @@ a
b: {
shape: cylinder
}
`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
newKey: `y.x 3`,
exp: `x
x 2
x 3
x 5
x 4
y: {
x 3
}
`,
},
{
@ -1917,6 +1965,42 @@ z: ""
exp: `x -> z.y (z)
z: ""
`,
},
{
name: "middle_container_generated_conflict",
text: `a.Square.Text 3 -> a.Square.Text 2
a.Square -> a.Text
a: {
Text
Square: {
Text 2
Text 3
}
Square
Text 2
}
`,
key: `a.Square`,
newKey: `Square`,
exp: `a.Text 3 -> a.Text 4
Square -> a.Text
a: {
Text
Text 4
Text 3
Text 2
}
Square
`,
},
{
@ -3351,6 +3435,25 @@ b: {
exp: `b: {
a
}
`,
},
{
name: "container_conflicts_generated",
text: `Square 2: "" {
Square: ""
}
Square: ""
Square 3
`,
key: `Square 2`,
newKey: `Square 3.Square 2`,
exp: `Square 2: ""
Square: ""
Square 3: {
Square 2: ""
}
`,
},
}
@ -3423,6 +3526,29 @@ func TestDelete(t *testing.T) {
key: `x`,
exp: `x.y.z -> y.b
`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
exp: `x
x 2
x 3
x 5
x 4
y
`,
},
{
@ -4894,10 +5020,10 @@ Square: {
exp: `Text 4
Text: {
Text 2: {
Text 2
}
Text 2
Text
`,
},
{
@ -4918,6 +5044,7 @@ Text
Text 2
`,
},
{
name: "drop_value",
text: `a.b.c: "c label"
@ -5257,6 +5384,43 @@ x.a -> x.b
"x.(a -> b)[0]": "(a 2 -> b)[0]",
"x.a": "a 2",
"x.b": "b"
}`,
},
{
name: "container_conflicts_generated",
text: `Square 2: "" {
Square: ""
}
Square: ""
Square 3
`,
key: `Square 2`,
newKey: `Square 3.Square 2`,
exp: `{
"Square 2": "Square 3.Square 2",
"Square 2.Square": "Square 2"
}`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
newKey: `y.x 3`,
exp: `{
"x 3": "y.x 3",
"x 3.x 3": "x 3",
"x 3.x 4": "x 5"
}`,
},
}
@ -5362,6 +5526,24 @@ x.y.z.w.e.p.l -> x.y.z.1.2.3.4
exp: `{
"x.x": "x"
}`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
exp: `{
"x 3.x 3": "x 3",
"x 3.x 4": "x 5"
}`,
},
{
@ -5523,9 +5705,9 @@ Square: {
key: "Square",
exp: `{
"Square.Text": "Text 2",
"Square.Text 4": "Text",
"Square.Text 4.Text 2": "Text.Text 2"
"Square.Text": "Text",
"Square.Text 4": "Text 2",
"Square.Text 4.Text 2": "Text 2.Text 2"
}`,
},
{

View file

@ -14,7 +14,7 @@ func GetParentID(g *d2graph.Graph, absID string) (string, error) {
}
obj, ok := g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return "", fmt.Errorf("%v parent not found", absID)
return "", fmt.Errorf("%v not found", absID)
}
return obj.Parent.AbsID(), nil

View file

@ -1063,6 +1063,15 @@ func (p *parser) parseUnquotedString(inKey bool) (s *d2ast.UnquotedString) {
if eof {
return s
}
switch r2 {
case '\n', ';', '#', '{', '}', '[', ']':
p.rewind()
p.peek()
p.commit()
sb.WriteRune(r)
rawb.WriteRune(r)
return s
}
if r2 == '-' || r2 == '>' || r2 == '*' {
p.rewind()
return s

View file

@ -8,6 +8,7 @@ import (
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"oss.terrastruct.com/util-go/xdefer"
@ -83,6 +84,25 @@ func (p *execPlugin) HydrateOpts(opts []byte) error {
allString[k] = vt
case int64:
allString[k] = strconv.Itoa(int(vt))
case []interface{}:
str := make([]string, len(vt))
for i, v := range vt {
switch vt := v.(type) {
case string:
str[i] = vt
case int64:
str[i] = strconv.Itoa(int(vt))
case float64:
str[i] = strconv.Itoa(int(vt))
}
}
allString[k] = strings.Join(str, ",")
case []int64:
str := make([]string, len(vt))
for i, v := range vt {
str[i] = strconv.Itoa(int(v))
}
allString[k] = strings.Join(str, ",")
case float64:
allString[k] = strconv.Itoa(int(vt))
}

View file

@ -44,6 +44,23 @@ func (f *PluginSpecificFlag) AddToOpts(opts *xmain.Opts) {
val = int64(defaultType)
}
opts.Int64("", f.Name, "", val, f.Usage)
case "[]int64":
var slice []int64
switch defaultType := f.Default.(type) {
case []int64:
slice = defaultType
case []interface{}:
for _, v := range defaultType {
switch defaultType := v.(type) {
case int64:
slice = append(slice, defaultType)
case float64:
// json unmarshals numbers to float64
slice = append(slice, int64(defaultType))
}
}
}
opts.Int64Slice("", f.Name, "", slice, f.Usage)
}
}
@ -178,6 +195,9 @@ func HydratePluginOpts(ctx context.Context, ms *xmain.State, plugin Plugin) erro
case "int64":
val, _ := ms.Opts.Flags.GetInt64(f.Name)
opts[f.Tag] = val
case "[]int64":
val, _ := ms.Opts.Flags.GetInt64Slice(f.Name)
opts[f.Tag] = val
}
}

View file

@ -0,0 +1,116 @@
package d2animate
import (
"bytes"
"fmt"
"math"
"strings"
"oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/version"
)
var transitionDurationMS = 1
func makeKeyframe(delayMS, durationMS, totalMS, identifier int, diagramHash string) string {
percentageBefore := (math.Max(0, float64(delayMS-transitionDurationMS)) / float64(totalMS)) * 100.
percentageStart := (float64(delayMS) / float64(totalMS)) * 100.
percentageEnd := (float64(delayMS+durationMS-transitionDurationMS) / float64(totalMS)) * 100.
if int(math.Ceil(percentageEnd)) == 100 {
return fmt.Sprintf(`@keyframes d2Transition-%s-%d {
0%%, %f%% {
opacity: 0;
}
%f%%, %f%% {
opacity: 1;
}
}`, diagramHash, identifier, percentageBefore, percentageStart, math.Ceil(percentageEnd))
}
percentageAfter := (float64(delayMS+durationMS) / float64(totalMS)) * 100.
return fmt.Sprintf(`@keyframes d2Transition-%s-%d {
0%%, %f%% {
opacity: 0;
}
%f%%, %f%% {
opacity: 1;
}
%f%%, 100%% {
opacity: 0;
}
}`, diagramHash, identifier, percentageBefore, percentageStart, percentageEnd, percentageAfter)
}
func Wrap(rootDiagram *d2target.Diagram, svgs [][]byte, renderOpts d2svg.RenderOpts, intervalMS int) ([]byte, error) {
buf := &bytes.Buffer{}
// TODO account for stroke width of root border
tl, br := rootDiagram.NestedBoundingBox()
left := tl.X - renderOpts.Pad
top := tl.Y - renderOpts.Pad
width := br.X - tl.X + renderOpts.Pad*2
height := br.Y - tl.Y + renderOpts.Pad*2
fitToScreenWrapperOpening := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d">`,
version.Version,
width, height,
)
fmt.Fprint(buf, fitToScreenWrapperOpening)
innerOpening := fmt.Sprintf(`<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">`,
width, height, left, top, width, height)
fmt.Fprint(buf, innerOpening)
svgsStr := ""
for _, svg := range svgs {
svgsStr += string(svg) + " "
}
diagramHash, err := rootDiagram.HashID()
if err != nil {
return nil, err
}
d2svg.EmbedFonts(buf, diagramHash, svgsStr, rootDiagram.FontFamily, rootDiagram.GetNestedCorpus())
themeStylesheet, err := d2svg.ThemeCSS(diagramHash, renderOpts.ThemeID, renderOpts.DarkThemeID)
if err != nil {
return nil, err
}
fmt.Fprintf(buf, `<style type="text/css"><![CDATA[%s%s]]></style>`, d2svg.BaseStylesheet, themeStylesheet)
if rootDiagram.HasShape(func(s d2target.Shape) bool {
return s.Label != "" && s.Type == d2target.ShapeText
}) {
css := d2svg.MarkdownCSS
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, css)
}
if renderOpts.Sketch {
d2sketch.DefineFillPatterns(buf)
}
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
for i := range svgs {
fmt.Fprint(buf, makeKeyframe(i*intervalMS, intervalMS, len(svgs)*intervalMS, i, diagramHash))
}
fmt.Fprint(buf, `]]></style>`)
for i, svg := range svgs {
str := string(svg)
str = strings.Replace(str, "<g", fmt.Sprintf(`<g style="animation: d2Transition-%s-%d %dms infinite"`, diagramHash, i, len(svgs)*intervalMS), 1)
buf.Write([]byte(str))
}
fmt.Fprint(buf, "</svg>")
fmt.Fprint(buf, "</svg>")
return buf.Bytes(), nil
}

View file

@ -6,7 +6,13 @@ package d2fonts
import (
"embed"
"encoding/base64"
"fmt"
"strings"
"github.com/jung-kurt/gofpdf"
fontlib "oss.terrastruct.com/d2/lib/font"
)
type FontFamily string
@ -26,6 +32,29 @@ func (f FontFamily) Font(size int, style FontStyle) Font {
}
}
func (f Font) GetEncodedSubset(corpus string) string {
var uniqueChars string
uniqueMap := make(map[rune]bool)
for _, char := range corpus {
if _, exists := uniqueMap[char]; !exists {
uniqueMap[char] = true
uniqueChars = uniqueChars + string(char)
}
}
fontBuf := make([]byte, len(FontFaces[f]))
copy(fontBuf, FontFaces[f])
fontBuf = gofpdf.UTF8CutFont(fontBuf, uniqueChars)
fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
if err != nil {
// If subset fails, return full encoding
return FontEncodings[f]
}
return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf))
}
const (
FONT_SIZE_XS = 13
FONT_SIZE_S = 14
@ -216,3 +245,78 @@ var D2_FONT_TO_FAMILY = map[string]FontFamily{
"default": SourceSansPro,
"mono": SourceCodePro,
}
func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
FontFaces[font] = ttf
woff, err := fontlib.Sfnt2Woff(ttf)
if err != nil {
return fmt.Errorf("failed to encode ttf to woff: %v", err)
}
encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff))
FontEncodings[font] = encodedWoff
return nil
}
func AddFontFamily(name string, regularTTF, italicTTF, boldTTF []byte) (*FontFamily, error) {
customFontFamily := FontFamily(name)
regularFont := Font{
Family: customFontFamily,
Style: FONT_STYLE_REGULAR,
}
if regularTTF != nil {
err := AddFontStyle(regularFont, FONT_STYLE_REGULAR, regularTTF)
if err != nil {
return nil, err
}
} else {
fallbackFont := Font{
Family: SourceSansPro,
Style: FONT_STYLE_REGULAR,
}
FontFaces[regularFont] = FontFaces[fallbackFont]
FontEncodings[regularFont] = FontEncodings[fallbackFont]
}
italicFont := Font{
Family: customFontFamily,
Style: FONT_STYLE_ITALIC,
}
if italicTTF != nil {
err := AddFontStyle(italicFont, FONT_STYLE_ITALIC, italicTTF)
if err != nil {
return nil, err
}
} else {
fallbackFont := Font{
Family: SourceSansPro,
Style: FONT_STYLE_ITALIC,
}
FontFaces[italicFont] = FontFaces[fallbackFont]
FontEncodings[italicFont] = FontEncodings[fallbackFont]
}
boldFont := Font{
Family: customFontFamily,
Style: FONT_STYLE_BOLD,
}
if boldTTF != nil {
err := AddFontStyle(boldFont, FONT_STYLE_BOLD, boldTTF)
if err != nil {
return nil, err
}
} else {
fallbackFont := Font{
Family: SourceSansPro,
Style: FONT_STYLE_BOLD,
}
FontFaces[boldFont] = FontFaces[fallbackFont]
FontEncodings[boldFont] = FontEncodings[fallbackFont]
}
FontFamilies = append(FontFamilies, customFontFamily)
return &customFontFamily, nil
}

View file

@ -0,0 +1,23 @@
package d2fonts
import (
"path/filepath"
"testing"
"github.com/jung-kurt/gofpdf"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
)
func TestCutFont(t *testing.T) {
f := Font{
Family: SourceCodePro,
Style: FONT_STYLE_BOLD,
}
fontBuf := make([]byte, len(FontFaces[f]))
copy(fontBuf, FontFaces[f])
fontBuf = gofpdf.UTF8CutFont(fontBuf, " 1")
err := diff.Testdata(filepath.Join("testdata", "d2fonts", "cut"), ".txt", fontBuf)
assert.Success(t, err)
}

Binary file not shown.

View file

@ -1107,6 +1107,58 @@ something
}
`,
},
{
name: "paper-real",
script: `style.fill-pattern: paper
style.fill: "#947A6D"
NETWORK: {
style: {
stroke: black
fill-pattern: dots
double-border: true
fill: "#E7E9EE"
font: mono
}
CELL TOWER: {
style: {
stroke: black
fill-pattern: dots
fill: "#F5F6F9"
font: mono
}
satellites: SATELLITES {
shape: stored_data
style: {
font: mono
fill: white
stroke: black
multiple: true
}
}
transmitter: TRANSMITTER {
style: {
font: mono
fill: white
stroke: black
}
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
satellites -> transmitter: SEND {
style.stroke: black
style.font: mono
}
}
}
`},
{
name: "dots-real",
script: `

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 67 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 125 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 161 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 152 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 89 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 493 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 307 KiB

After

Width:  |  Height:  |  Size: 100 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 355 KiB

After

Width:  |  Height:  |  Size: 108 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 181 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View file

@ -11,6 +11,7 @@ import (
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target"
@ -162,6 +163,31 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by
return []byte(svg)
}
// transformInternalLink turns
// "root.layers.x.layers.y"
// into
// "root > x > y"
func transformInternalLink(link string) string {
if link == "" || !strings.HasPrefix(link, "root") {
return link
}
mk, err := d2parser.ParseMapKey(link)
if err != nil {
return ""
}
key := d2graph.Key(mk.Key)
if len(key) > 1 {
for i := 1; i < len(key); i += 2 {
key[i] = ">"
}
}
return strings.Join(key, " ")
}
func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) {
tl, br := diagram.BoundingBox()
@ -171,7 +197,7 @@ func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg s
i := 1
for _, s := range diagram.Shapes {
for _, txt := range []string{s.Tooltip, s.Link} {
for _, txt := range []string{s.Tooltip, transformInternalLink(s.Link)} {
if txt != "" {
line, w, h := generateLine(i, br.Y+(PAD_TOP*2)+totalHeight, txt, ruler)
i++
@ -212,7 +238,7 @@ func generateLine(i, y int, text string, ruler *textmeasure.Ruler) (string, int,
0, y, generateNumberedIcon(i, 0, 0))
line += fmt.Sprintf(`<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
ICON_RADIUS*3, y, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
ICON_RADIUS*3, y+5, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
return line, dims.Width + ICON_RADIUS*3, go2.IntMax(dims.Height, ICON_RADIUS*2)
}

View file

@ -90,6 +90,23 @@ x -> y
script: `x: { link: https://d2lang.com }
y: { link: https://fosny.eu; tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! }
x -> y
`,
},
{
name: "internal-links",
script: `x: { link: layers.x }
layers: {
x: {
gooo
home.link: _
next.link: steps.next
steps: {
next: {
hi
}
}
}
}
`,
},
{

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 676 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 657 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 661 KiB

View file

@ -54,10 +54,10 @@ var TooltipIcon string
var LinkIcon string
//go:embed style.css
var baseStylesheet string
var BaseStylesheet string
//go:embed github-markdown.css
var mdCSS string
var MarkdownCSS string
//go:embed dots.txt
var dots string
@ -68,13 +68,22 @@ var lines string
//go:embed grain.txt
var grain string
//go:embed paper.txt
var paper string
type RenderOpts struct {
Pad int
Sketch bool
Center bool
ThemeID int64
DarkThemeID *int64
Font string
// disables the fit to screen behavior and ensures the exported svg has the exact dimensions
SetDimensions bool
// MasterID is passed when the diagram should use something other than its own hash for unique targeting
// Currently, that's when multi-boards are collapsed
MasterID string
}
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
@ -101,34 +110,46 @@ func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string {
}
func arrowheadDimensions(arrowhead d2target.Arrowhead, strokeWidth float64) (width, height float64) {
var widthMultiplier float64
var heightMultiplier float64
var baseWidth, baseHeight float64
var widthMultiplier, heightMultiplier float64
switch arrowhead {
case d2target.ArrowArrowhead:
widthMultiplier = 5
heightMultiplier = 5
baseWidth = 4
baseHeight = 4
widthMultiplier = 4
heightMultiplier = 4
case d2target.TriangleArrowhead:
widthMultiplier = 5
heightMultiplier = 6
baseWidth = 4
baseHeight = 4
widthMultiplier = 3
heightMultiplier = 4
case d2target.LineArrowhead:
widthMultiplier = 5
heightMultiplier = 8
case d2target.FilledDiamondArrowhead:
widthMultiplier = 11
heightMultiplier = 7
baseWidth = 11
baseHeight = 7
widthMultiplier = 5.5
heightMultiplier = 3.5
case d2target.DiamondArrowhead:
widthMultiplier = 11
heightMultiplier = 9
baseWidth = 11
baseHeight = 9
widthMultiplier = 5.5
heightMultiplier = 4.5
case d2target.FilledCircleArrowhead, d2target.CircleArrowhead:
widthMultiplier = 12
heightMultiplier = 12
baseWidth = 8
baseHeight = 8
widthMultiplier = 5
heightMultiplier = 5
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
widthMultiplier = 9
heightMultiplier = 9
baseWidth = 9
baseHeight = 9
widthMultiplier = 4.5
heightMultiplier = 4.5
}
clippedStrokeWidth := go2.Max(MIN_ARROWHEAD_STROKE_WIDTH, strokeWidth)
return clippedStrokeWidth * widthMultiplier, clippedStrokeWidth * heightMultiplier
return baseWidth + clippedStrokeWidth*widthMultiplier, baseHeight + clippedStrokeWidth*heightMultiplier
}
func arrowheadMarker(isTarget bool, id string, connection d2target.Connection) string {
@ -1382,7 +1403,7 @@ func RenderText(text string, x, height float64) string {
return strings.Join(rendered, "")
}
func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily) {
func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily, corpus string) {
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
appendOnTrigger(
@ -1404,7 +1425,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)],
fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
),
)
@ -1466,7 +1487,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)],
fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
),
)
@ -1489,7 +1510,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)],
fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
),
)
@ -1514,7 +1535,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR)],
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
),
)
@ -1535,7 +1556,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)],
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
),
)
@ -1556,7 +1577,7 @@ func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)],
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
),
)
@ -1658,14 +1679,16 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
}
// Mask URLs are global. So when multiple SVGs attach to a DOM, they share
// the same namespace for mask URLs.
// Apply hash on IDs for targeting, to be specific for this diagram
diagramHash, err := diagram.HashID()
if err != nil {
return nil, err
}
// CSS names can't start with numbers, so prepend a little something
diagramHash = "d2-" + diagramHash
// Some targeting is still per-board, like masks for connections
isolatedDiagramHash := diagramHash
if opts != nil && opts.MasterID != "" {
diagramHash = opts.MasterID
}
// SVG has no notion of z-index. The z-index is effectively the order it's drawn.
// So draw from the least nested to most nested
@ -1685,7 +1708,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
markers := map[string]struct{}{}
for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is {
labelMask, err := drawConnection(buf, diagramHash, c, markers, idToShape, sketchRunner)
labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, sketchRunner)
if err != nil {
return nil, err
}
@ -1708,7 +1731,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
left, top, w, h := dimensions(diagram, pad)
fmt.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
diagramHash, left, top, w, h,
isolatedDiagramHash, left, top, w, h,
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
left, top, w, h,
@ -1719,31 +1742,33 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// generate style elements that will be appended to the SVG tag
upperBuf := &bytes.Buffer{}
embedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily) // embedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
themeStylesheet, err := themeCSS(diagramHash, themeID, darkThemeID)
if err != nil {
return nil, err
}
fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, baseStylesheet, themeStylesheet)
hasMarkdown := false
for _, s := range diagram.Shapes {
if s.Label != "" && s.Type == d2target.ShapeText {
hasMarkdown = true
break
if opts.MasterID == "" {
EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus()) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
themeStylesheet, err := ThemeCSS(diagramHash, themeID, darkThemeID)
if err != nil {
return nil, err
}
}
if hasMarkdown {
css := mdCSS
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
}
fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, BaseStylesheet, themeStylesheet)
if sketchRunner != nil {
d2sketch.DefineFillPatterns(upperBuf)
hasMarkdown := false
for _, s := range diagram.Shapes {
if s.Label != "" && s.Type == d2target.ShapeText {
hasMarkdown = true
break
}
}
if hasMarkdown {
css := MarkdownCSS
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
}
if sketchRunner != nil {
d2sketch.DefineFillPatterns(upperBuf)
}
}
// This shift is for background el to envelop the diagram
@ -1801,7 +1826,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
bufStr := buf.String()
patternDefs := ""
for _, pattern := range d2graph.FillPatterns {
if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern != "" {
if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern == pattern {
if patternDefs == "" {
fmt.Fprint(upperBuf, `<style type="text/css"><![CDATA[`)
}
@ -1812,6 +1837,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
patternDefs += lines
case "grain":
patternDefs += grain
case "paper":
patternDefs += paper
}
fmt.Fprint(upperBuf, fmt.Sprintf(`
.%s-overlay {
@ -1832,29 +1859,49 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
dimensions = fmt.Sprintf(` width="%d" height="%d"`, w, h)
}
fitToScreenWrapper := fmt.Sprintf(`<svg %s d2Version="%s" preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d"%s>`,
`xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`,
version.Version,
w, h,
dimensions,
)
alignment := "xMinYMin"
if opts.Center {
alignment = "xMidYMid"
}
fitToScreenWrapperOpening := ""
xmlTag := ""
fitToScreenWrapperClosing := ""
idAttr := ""
tag := "g"
// Many things change when this is rendering for animation
if opts.MasterID == "" {
fitToScreenWrapperOpening = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>`,
version.Version,
alignment,
w, h,
dimensions,
)
xmlTag = `<?xml version="1.0" encoding="utf-8"?>`
fitToScreenWrapperClosing = "</svg>"
idAttr = `id="d2-svg"`
tag = "svg"
}
// TODO minify
docRendered := fmt.Sprintf(`%s%s<svg id="d2-svg" class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</svg></svg>`,
`<?xml version="1.0" encoding="utf-8"?>`,
fitToScreenWrapper,
docRendered := fmt.Sprintf(`%s%s<%s %s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s`,
xmlTag,
fitToScreenWrapperOpening,
tag,
idAttr,
diagramHash,
w, h, left, top, w, h,
doubleBorderElStr,
backgroundEl.Render(),
upperBuf.String(),
buf.String(),
tag,
fitToScreenWrapperClosing,
)
return []byte(docRendered), nil
}
// TODO include only colors that are being used to reduce size
func themeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
func ThemeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(diagramHash, themeID)
if err != nil {
return "", err

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 81 KiB

1060
d2renderers/d2svg/paper.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -51,18 +51,101 @@ type Diagram struct {
Steps []*Diagram `json:"steps,omitempty"`
}
func (diagram Diagram) HashID() (string, error) {
func (diagram Diagram) Bytes() ([]byte, error) {
b1, err := json.Marshal(diagram.Shapes)
if err != nil {
return "", err
return nil, err
}
b2, err := json.Marshal(diagram.Connections)
if err != nil {
return nil, err
}
base := append(b1, b2...)
for _, d := range diagram.Layers {
slices, err := d.Bytes()
if err != nil {
return nil, err
}
base = append(base, slices...)
}
for _, d := range diagram.Scenarios {
slices, err := d.Bytes()
if err != nil {
return nil, err
}
base = append(base, slices...)
}
for _, d := range diagram.Steps {
slices, err := d.Bytes()
if err != nil {
return nil, err
}
base = append(base, slices...)
}
return base, nil
}
func (diagram Diagram) HasShape(condition func(Shape) bool) bool {
for _, d := range diagram.Layers {
if d.HasShape(condition) {
return true
}
}
for _, d := range diagram.Scenarios {
if d.HasShape(condition) {
return true
}
}
for _, d := range diagram.Steps {
if d.HasShape(condition) {
return true
}
}
for _, s := range diagram.Shapes {
if condition(s) {
return true
}
}
return false
}
func (diagram Diagram) HashID() (string, error) {
bytes, err := diagram.Bytes()
if err != nil {
return "", err
}
h := fnv.New32a()
h.Write(append(b1, b2...))
return fmt.Sprint(h.Sum32()), nil
h.Write(bytes)
// CSS names can't start with numbers, so prepend a little something
return fmt.Sprintf("d2-%d", h.Sum32()), nil
}
func (diagram Diagram) NestedBoundingBox() (topLeft, bottomRight Point) {
tl, br := diagram.BoundingBox()
for _, d := range diagram.Layers {
tl2, br2 := d.NestedBoundingBox()
tl.X = go2.Min(tl.X, tl2.X)
tl.Y = go2.Min(tl.Y, tl2.Y)
br.X = go2.Max(br.X, br2.X)
br.Y = go2.Max(br.Y, br2.Y)
}
for _, d := range diagram.Scenarios {
tl2, br2 := d.NestedBoundingBox()
tl.X = go2.Min(tl.X, tl2.X)
tl.Y = go2.Min(tl.Y, tl2.Y)
br.X = go2.Max(br.X, br2.X)
br.Y = go2.Max(br.Y, br2.Y)
}
for _, d := range diagram.Steps {
tl2, br2 := d.NestedBoundingBox()
tl.X = go2.Min(tl.X, tl2.X)
tl.Y = go2.Min(tl.Y, tl2.Y)
br.X = go2.Max(br.X, br2.X)
br.Y = go2.Max(br.Y, br2.Y)
}
return tl, br
}
func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
@ -154,6 +237,62 @@ func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
return Point{x1, y1}, Point{x2, y2}
}
func (diagram Diagram) GetNestedCorpus() string {
corpus := diagram.GetCorpus()
for _, d := range diagram.Layers {
corpus += d.GetNestedCorpus()
}
for _, d := range diagram.Scenarios {
corpus += d.GetNestedCorpus()
}
for _, d := range diagram.Steps {
corpus += d.GetNestedCorpus()
}
return corpus
}
func (diagram Diagram) GetCorpus() string {
var corpus string
appendixCount := 0
for _, s := range diagram.Shapes {
corpus += s.Label
if s.Tooltip != "" {
corpus += s.Tooltip
appendixCount++
corpus += fmt.Sprint(appendixCount)
}
if s.Link != "" {
corpus += s.Link
appendixCount++
corpus += fmt.Sprint(appendixCount)
}
if s.Type == ShapeClass {
for _, cf := range s.Fields {
corpus += cf.Text(0).Text + cf.VisibilityToken()
}
for _, cm := range s.Methods {
corpus += cm.Text(0).Text + cm.VisibilityToken()
}
}
if s.Type == ShapeSQLTable {
for _, c := range s.Columns {
for _, t := range c.Texts(0) {
corpus += t.Text
}
corpus += c.ConstraintAbbr()
}
}
}
for _, c := range diagram.Connections {
corpus += c.Label
corpus += c.SrcLabel
corpus += c.DstLabel
}
return corpus
}
func NewDiagram() *Diagram {
return &Diagram{
Root: Shape{

View file

@ -18,6 +18,8 @@ type SpecialRules struct {
OuterContainerDoubleBorder bool `json:"outerContainerDoubleBorder"`
ContainerDots bool `json:"containerDots"`
CapsLock bool `json:"capsLock"`
AllPaper bool `json:"allPaper"`
}
func (t *Theme) IsDark() bool {

View file

@ -24,6 +24,7 @@ var LightCatalog = []d2themes.Theme{
ButteredToast,
Terminal,
TerminalGrayscale,
Origami,
}
var DarkCatalog = []d2themes.Theme{

View file

@ -0,0 +1,40 @@
package d2themescatalog
import "oss.terrastruct.com/d2/d2themes"
var Origami = d2themes.Theme{
ID: 302,
Name: "Origami",
Colors: d2themes.ColorPalette{
Neutrals: OrigamiNeutral,
B1: "#170206",
B2: "#A62543",
B3: "#E07088",
B4: "#F3E0D2",
B5: "#FAF1E6",
B6: "#FFFBF8",
AA2: "#0A4EA6",
AA4: "#3182CD",
AA5: "#68A8E4",
AB4: "#E07088",
AB5: "#F19CAE",
},
SpecialRules: d2themes.SpecialRules{
NoCornerRadius: true,
OuterContainerDoubleBorder: true,
AllPaper: true,
},
}
var OrigamiNeutral = d2themes.Neutral{
N1: "#170206",
N2: "#6F0019",
N3: "#FFFFFF",
N4: "#E07088",
N5: "#D2B098",
N6: "#FFFFFF",
N7: "#FFFFFF",
}

Binary file not shown.

View file

@ -21,6 +21,7 @@ func TestCLI_E2E(t *testing.T) {
tca := []struct {
name string
skipCI bool
skip bool
run func(t *testing.T, ctx context.Context, dir string, env *xos.Env)
}{
{
@ -45,6 +46,94 @@ func TestCLI_E2E(t *testing.T) {
testdataIgnoreDiff(t, ".png", png)
},
},
{
name: "center",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--center=true", "hello-world.d2")
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "animation.d2", `Chicken's plan: {
style.font-size: 35
near: top-center
shape: text
}
steps: {
1: {
Approach road
}
2: {
Approach road -> Cross road
}
3: {
Cross road -> Make you wonder why
}
}
`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
assert.Success(t, err)
svg := readFile(t, dir, "animation.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "linked-path",
// TODO tempdir is random, resulting in different test results each time with the links
skip: true,
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "linked.d2", `cat: how does the cat go? {
link: layers.cat
}
layers: {
cat: {
home: {
link: _
}
the cat -> meow: goes
scenarios: {
big cat: {
the cat -> roar: goes
}
}
}
}
`)
err := runTestMain(t, ctx, dir, env, "linked.d2")
assert.Success(t, err)
assert.TestdataDir(t, filepath.Join(dir, "linked"))
},
},
{
name: "with-font",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "font.d2", `a: Why do computers get sick often?
b: Because their Windows are always open!
a -> b: italic font
`)
err := runTestMain(t, ctx, dir, env, "--font-bold=./RockSalt-Regular.ttf", "font.d2")
assert.Success(t, err)
svg := readFile(t, dir, "font.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "incompatible-animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "x.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=2", "x.d2", "x.png")
assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: -animate-interval can only be used when exporting to SVG.
You provided: .png`)
},
},
{
name: "hello_world_png_sketch",
skipCI: true,
@ -182,6 +271,9 @@ layers: {
if tc.skipCI && os.Getenv("CI") != "" {
t.SkipNow()
}
if tc.skip {
t.SkipNow()
}
ctx, cancel := context.WithTimeout(ctx, time.Minute*5)
defer cancel()

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,890 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 514 665"><svg id="d2-svg" width="514" height="665" viewBox="-206 -166 514 665"><style type="text/css"><![CDATA[
.d2-1015877328 .text {
font-family: "d2-1015877328-font-regular";
}
@font-face {
font-family: d2-1015877328-font-regular;
src: url("data:application/font-woff;base64,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");
}
.d2-1015877328 .text-bold {
font-family: "d2-1015877328-font-bold";
}
@font-face {
font-family: d2-1015877328-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-1015877328 .fill-N1{fill:#0A0F25;}
.d2-1015877328 .fill-N2{fill:#676C7E;}
.d2-1015877328 .fill-N3{fill:#9499AB;}
.d2-1015877328 .fill-N4{fill:#CFD2DD;}
.d2-1015877328 .fill-N5{fill:#DEE1EB;}
.d2-1015877328 .fill-N6{fill:#EEF1F8;}
.d2-1015877328 .fill-N7{fill:#FFFFFF;}
.d2-1015877328 .fill-B1{fill:#0D32B2;}
.d2-1015877328 .fill-B2{fill:#0D32B2;}
.d2-1015877328 .fill-B3{fill:#E3E9FD;}
.d2-1015877328 .fill-B4{fill:#E3E9FD;}
.d2-1015877328 .fill-B5{fill:#EDF0FD;}
.d2-1015877328 .fill-B6{fill:#F7F8FE;}
.d2-1015877328 .fill-AA2{fill:#4A6FF3;}
.d2-1015877328 .fill-AA4{fill:#EDF0FD;}
.d2-1015877328 .fill-AA5{fill:#F7F8FE;}
.d2-1015877328 .fill-AB4{fill:#EDF0FD;}
.d2-1015877328 .fill-AB5{fill:#F7F8FE;}
.d2-1015877328 .stroke-N1{stroke:#0A0F25;}
.d2-1015877328 .stroke-N2{stroke:#676C7E;}
.d2-1015877328 .stroke-N3{stroke:#9499AB;}
.d2-1015877328 .stroke-N4{stroke:#CFD2DD;}
.d2-1015877328 .stroke-N5{stroke:#DEE1EB;}
.d2-1015877328 .stroke-N6{stroke:#EEF1F8;}
.d2-1015877328 .stroke-N7{stroke:#FFFFFF;}
.d2-1015877328 .stroke-B1{stroke:#0D32B2;}
.d2-1015877328 .stroke-B2{stroke:#0D32B2;}
.d2-1015877328 .stroke-B3{stroke:#E3E9FD;}
.d2-1015877328 .stroke-B4{stroke:#E3E9FD;}
.d2-1015877328 .stroke-B5{stroke:#EDF0FD;}
.d2-1015877328 .stroke-B6{stroke:#F7F8FE;}
.d2-1015877328 .stroke-AA2{stroke:#4A6FF3;}
.d2-1015877328 .stroke-AA4{stroke:#EDF0FD;}
.d2-1015877328 .stroke-AA5{stroke:#F7F8FE;}
.d2-1015877328 .stroke-AB4{stroke:#EDF0FD;}
.d2-1015877328 .stroke-AB5{stroke:#F7F8FE;}
.d2-1015877328 .background-color-N1{background-color:#0A0F25;}
.d2-1015877328 .background-color-N2{background-color:#676C7E;}
.d2-1015877328 .background-color-N3{background-color:#9499AB;}
.d2-1015877328 .background-color-N4{background-color:#CFD2DD;}
.d2-1015877328 .background-color-N5{background-color:#DEE1EB;}
.d2-1015877328 .background-color-N6{background-color:#EEF1F8;}
.d2-1015877328 .background-color-N7{background-color:#FFFFFF;}
.d2-1015877328 .background-color-B1{background-color:#0D32B2;}
.d2-1015877328 .background-color-B2{background-color:#0D32B2;}
.d2-1015877328 .background-color-B3{background-color:#E3E9FD;}
.d2-1015877328 .background-color-B4{background-color:#E3E9FD;}
.d2-1015877328 .background-color-B5{background-color:#EDF0FD;}
.d2-1015877328 .background-color-B6{background-color:#F7F8FE;}
.d2-1015877328 .background-color-AA2{background-color:#4A6FF3;}
.d2-1015877328 .background-color-AA4{background-color:#EDF0FD;}
.d2-1015877328 .background-color-AA5{background-color:#F7F8FE;}
.d2-1015877328 .background-color-AB4{background-color:#EDF0FD;}
.d2-1015877328 .background-color-AB5{background-color:#F7F8FE;}
.d2-1015877328 .color-N1{color:#0A0F25;}
.d2-1015877328 .color-N2{color:#676C7E;}
.d2-1015877328 .color-N3{color:#9499AB;}
.d2-1015877328 .color-N4{color:#CFD2DD;}
.d2-1015877328 .color-N5{color:#DEE1EB;}
.d2-1015877328 .color-N6{color:#EEF1F8;}
.d2-1015877328 .color-N7{color:#FFFFFF;}
.d2-1015877328 .color-B1{color:#0D32B2;}
.d2-1015877328 .color-B2{color:#0D32B2;}
.d2-1015877328 .color-B3{color:#E3E9FD;}
.d2-1015877328 .color-B4{color:#E3E9FD;}
.d2-1015877328 .color-B5{color:#EDF0FD;}
.d2-1015877328 .color-B6{color:#F7F8FE;}
.d2-1015877328 .color-AA2{color:#4A6FF3;}
.d2-1015877328 .color-AA4{color:#EDF0FD;}
.d2-1015877328 .color-AA5{color:#F7F8FE;}
.d2-1015877328 .color-AB4{color:#EDF0FD;}
.d2-1015877328 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><style type="text/css">.md em,
.md dfn {
font-family: "d2-1015877328-font-italic";
}
.md b,
.md strong {
font-family: "d2-1015877328-font-bold";
}
.md code,
.md kbd,
.md pre,
.md samp {
font-family: "d2-1015877328-font-mono";
font-size: 1em;
}
.md {
tab-size: 4;
}
/* variables are provided in d2renderers/d2svg/d2svg.go */
.md {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: var(--color-fg-default);
background-color: transparent; /* we don't want to define the background color */
font-family: "d2-1015877328-font-regular";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.md details,
.md figcaption,
.md figure {
display: block;
}
.md summary {
display: list-item;
}
.md [hidden] {
display: none !important;
}
.md a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.md a:active,
.md a:hover {
outline-width: 0;
}
.md abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.md dfn {
font-style: italic;
}
.md h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.md mark {
background-color: var(--color-attention-subtle);
color: var(--color-text-primary);
}
.md small {
font-size: 90%;
}
.md sub,
.md sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.md sub {
bottom: -0.25em;
}
.md sup {
top: -0.5em;
}
.md img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.md figure {
margin: 1em 40px;
}
.md hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.md input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.md [type="button"],
.md [type="reset"],
.md [type="submit"] {
-webkit-appearance: button;
}
.md [type="button"]::-moz-focus-inner,
.md [type="reset"]::-moz-focus-inner,
.md [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.md [type="button"]:-moz-focusring,
.md [type="reset"]:-moz-focusring,
.md [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.md [type="checkbox"],
.md [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.md [type="number"]::-webkit-inner-spin-button,
.md [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.md [type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.md [type="search"]::-webkit-search-cancel-button,
.md [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.md ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.md ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.md a:hover {
text-decoration: underline;
}
.md hr::before {
display: table;
content: "";
}
.md hr::after {
display: table;
clear: both;
content: "";
}
.md table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.md td,
.md th {
padding: 0;
}
.md details summary {
cursor: pointer;
}
.md details:not([open]) > *:not(summary) {
display: none !important;
}
.md kbd {
display: inline-block;
padding: 3px 5px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.md h1,
.md h2,
.md h3,
.md h4,
.md h5,
.md h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
font-family: "d2-1015877328-font-regular";
}
.md h2 {
font-weight: 600;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.md h3 {
font-weight: 600;
font-size: 1.25em;
}
.md h4 {
font-weight: 600;
font-size: 1em;
}
.md h5 {
font-weight: 600;
font-size: 0.875em;
}
.md h6 {
font-weight: 600;
font-size: 0.85em;
color: var(--color-fg-muted);
}
.md p {
margin-top: 0;
margin-bottom: 10px;
}
.md blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: 0.25em solid var(--color-border-default);
}
.md ul,
.md ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.md ol ol,
.md ul ol {
list-style-type: lower-roman;
}
.md ul ul ol,
.md ul ol ol,
.md ol ul ol,
.md ol ol ol {
list-style-type: lower-alpha;
}
.md dd {
margin-left: 0;
}
.md pre {
margin-top: 0;
margin-bottom: 0;
word-wrap: normal;
}
.md ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.md input::-webkit-outer-spin-button,
.md input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.md::before {
display: table;
content: "";
}
.md::after {
display: table;
clear: both;
content: "";
}
.md > *:first-child {
margin-top: 0 !important;
}
.md > *:last-child {
margin-bottom: 0 !important;
}
.md a:not([href]) {
color: inherit;
text-decoration: none;
}
.md .absent {
color: var(--color-danger-fg);
}
.md .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.md .anchor:focus {
outline: none;
}
.md p,
.md blockquote,
.md ul,
.md ol,
.md dl,
.md table,
.md pre,
.md details {
margin-top: 0;
margin-bottom: 16px;
}
.md blockquote > :first-child {
margin-top: 0;
}
.md blockquote > :last-child {
margin-bottom: 0;
}
.md sup > a::before {
content: "[";
}
.md sup > a::after {
content: "]";
}
.md h1:hover .anchor,
.md h2:hover .anchor,
.md h3:hover .anchor,
.md h4:hover .anchor,
.md h5:hover .anchor,
.md h6:hover .anchor {
text-decoration: none;
}
.md h1 tt,
.md h1 code,
.md h2 tt,
.md h2 code,
.md h3 tt,
.md h3 code,
.md h4 tt,
.md h4 code,
.md h5 tt,
.md h5 code,
.md h6 tt,
.md h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.md ul.no-list,
.md ol.no-list {
padding: 0;
list-style-type: none;
}
.md ol[type="1"] {
list-style-type: decimal;
}
.md ol[type="a"] {
list-style-type: lower-alpha;
}
.md ol[type="i"] {
list-style-type: lower-roman;
}
.md div > ol:not([type]) {
list-style-type: decimal;
}
.md ul ul,
.md ul ol,
.md ol ol,
.md ol ul {
margin-top: 0;
margin-bottom: 0;
}
.md li > p {
margin-top: 16px;
}
.md li + li {
margin-top: 0.25em;
}
.md dl {
padding: 0;
}
.md dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.md dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.md table th {
font-weight: 600;
}
.md table th,
.md table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.md table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.md table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.md table img {
background-color: transparent;
}
.md img[align="right"] {
padding-left: 20px;
}
.md img[align="left"] {
padding-right: 20px;
}
.md span.frame {
display: block;
overflow: hidden;
}
.md span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.md span.frame span img {
display: block;
float: left;
}
.md span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.md span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.md span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.md span.align-center span img {
margin: 0 auto;
text-align: center;
}
.md span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.md span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.md span.align-right span img {
margin: 0;
text-align: right;
}
.md span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.md span.float-left span {
margin: 13px 0 0;
}
.md span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.md span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.md code,
.md tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.md code br,
.md tt br {
display: none;
}
.md del code {
text-decoration: inherit;
}
.md pre code {
font-size: 100%;
}
.md pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.md .highlight {
margin-bottom: 16px;
}
.md .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.md .highlight pre,
.md pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.md pre code,
.md pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.md .csv-data td,
.md .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.md .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.md .csv-data tr {
border-top: 0;
}
.md .csv-data th {
font-weight: 600;
background: var(--color-canvas-subtle);
border-top: 0;
}
.md .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.md .footnotes ol {
padding-left: 16px;
}
.md .footnotes li {
position: relative;
}
.md .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.md .footnotes li:target {
color: var(--color-fg-default);
}
.md .task-list-item {
list-style-type: none;
}
.md .task-list-item label {
font-weight: 400;
}
.md .task-list-item.enabled label {
cursor: pointer;
}
.md .task-list-item + .task-list-item {
margin-top: 3px;
}
.md .task-list-item .handle {
display: none;
}
.md .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.md .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
</style><style type="text/css"><![CDATA[@keyframes d2Transition-d2-1015877328-0 {
0%, 0.000000% {
opacity: 0;
}
0.000000%, 24.982143% {
opacity: 1;
}
25.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1015877328-1 {
0%, 24.982143% {
opacity: 0;
}
25.000000%, 49.982143% {
opacity: 1;
}
50.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1015877328-2 {
0%, 49.982143% {
opacity: 0;
}
50.000000%, 74.982143% {
opacity: 1;
}
75.000000%, 100% {
opacity: 0;
}
}@keyframes d2Transition-d2-1015877328-3 {
0%, 74.982143% {
opacity: 0;
}
75.000000%, 100.000000% {
opacity: 1;
}
}]]></style><g style="animation: d2Transition-d2-1015877328-0 5600ms infinite" class="d2-1015877328" width="412" height="247" viewBox="-206 -166 412 247"><rect x="-206.000000" y="-166.000000" width="412.000000" height="247.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="0.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken&#39;s plan</text></g><mask id="d2-1015877328" maskUnits="userSpaceOnUse" x="-206" y="-166" width="412" height="247">
<rect x="-206" y="-166" width="412" height="247" fill="white"></rect>
</mask></g><g style="animation: d2Transition-d2-1015877328-1 5600ms infinite" class="d2-1015877328" width="412" height="333" viewBox="-131 -166 412 333"><rect x="-131.000000" y="-166.000000" width="412.000000" height="333.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="0.000000" y="0.000000" width="150.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="75.000000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Approach road</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="75.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken&#39;s plan</text></g><mask id="d2-1041619556" maskUnits="userSpaceOnUse" x="-131" y="-166" width="412" height="333">
<rect x="-131" y="-166" width="412" height="333" fill="white"></rect>
</mask></g><g style="animation: d2Transition-d2-1015877328-2 5600ms infinite" class="d2-1015877328" width="412" height="499" viewBox="-131 -166 412 499"><rect x="-131.000000" y="-166.000000" width="412.000000" height="499.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="0.000000" y="0.000000" width="150.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="75.000000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Approach road</text></g><g id="Cross road"><g class="shape" ><rect x="15.000000" y="166.000000" width="120.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="75.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Cross road</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="75.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken&#39;s plan</text></g><g id="(Approach road -&gt; Cross road)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 75.000000 68.000000 C 75.000000 106.000000 75.000000 126.000000 75.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-681643259)" /></g><mask id="d2-681643259" maskUnits="userSpaceOnUse" x="-131" y="-166" width="412" height="499">
<rect x="-131" y="-166" width="412" height="499" fill="white"></rect>
</mask></g><g style="animation: d2Transition-d2-1015877328-3 5600ms infinite" class="d2-1015877328" width="412" height="665" viewBox="-104 -166 412 665"><rect x="-104.000000" y="-166.000000" width="412.000000" height="665.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><g id="Approach road"><g class="shape" ><rect x="27.000000" y="0.000000" width="150.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="102.000000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Approach road</text></g><g id="Cross road"><g class="shape" ><rect x="42.000000" y="166.000000" width="120.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="102.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Cross road</text></g><g id="Make you wonder why"><g class="shape" ><rect x="0.000000" y="332.000000" width="203.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="101.500000" y="370.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">Make you wonder why</text></g><g id="&#34;Chicken&#39;s plan&#34;"><g class="shape" ></g><text x="102.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken&#39;s plan</text></g><g id="(Approach road -&gt; Cross road)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 101.500000 68.000000 C 101.500000 106.000000 101.500000 126.000000 101.500000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2302375566)" /></g><g id="(Cross road -&gt; Make you wonder why)[0]"><path d="M 101.500000 234.000000 C 101.500000 272.000000 101.500000 292.000000 101.500000 328.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2302375566)" /></g><mask id="d2-2302375566" maskUnits="userSpaceOnUse" x="-104" y="-166" width="412" height="665">
<rect x="-104" y="-166" width="412" height="665" fill="white"></rect>
</mask></g></svg></svg>

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.3.0-HEAD" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 434"><svg id="d2-svg" class="d2-2448830429" width="256" height="434" viewBox="-101 -101 256 434"><rect x="-101.000000" y="-101.000000" width="256.000000" height="434.000000" rx="0.000000" class=" fill-N7" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-2448830429 .text-bold {
font-family: "d2-2448830429-font-bold";
}
@font-face {
font-family: d2-2448830429-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-2448830429 .fill-N1{fill:#0A0F25;}
.d2-2448830429 .fill-N2{fill:#676C7E;}
.d2-2448830429 .fill-N3{fill:#9499AB;}
.d2-2448830429 .fill-N4{fill:#CFD2DD;}
.d2-2448830429 .fill-N5{fill:#DEE1EB;}
.d2-2448830429 .fill-N6{fill:#EEF1F8;}
.d2-2448830429 .fill-N7{fill:#FFFFFF;}
.d2-2448830429 .fill-B1{fill:#0D32B2;}
.d2-2448830429 .fill-B2{fill:#0D32B2;}
.d2-2448830429 .fill-B3{fill:#E3E9FD;}
.d2-2448830429 .fill-B4{fill:#E3E9FD;}
.d2-2448830429 .fill-B5{fill:#EDF0FD;}
.d2-2448830429 .fill-B6{fill:#F7F8FE;}
.d2-2448830429 .fill-AA2{fill:#4A6FF3;}
.d2-2448830429 .fill-AA4{fill:#EDF0FD;}
.d2-2448830429 .fill-AA5{fill:#F7F8FE;}
.d2-2448830429 .fill-AB4{fill:#EDF0FD;}
.d2-2448830429 .fill-AB5{fill:#F7F8FE;}
.d2-2448830429 .stroke-N1{stroke:#0A0F25;}
.d2-2448830429 .stroke-N2{stroke:#676C7E;}
.d2-2448830429 .stroke-N3{stroke:#9499AB;}
.d2-2448830429 .stroke-N4{stroke:#CFD2DD;}
.d2-2448830429 .stroke-N5{stroke:#DEE1EB;}
.d2-2448830429 .stroke-N6{stroke:#EEF1F8;}
.d2-2448830429 .stroke-N7{stroke:#FFFFFF;}
.d2-2448830429 .stroke-B1{stroke:#0D32B2;}
.d2-2448830429 .stroke-B2{stroke:#0D32B2;}
.d2-2448830429 .stroke-B3{stroke:#E3E9FD;}
.d2-2448830429 .stroke-B4{stroke:#E3E9FD;}
.d2-2448830429 .stroke-B5{stroke:#EDF0FD;}
.d2-2448830429 .stroke-B6{stroke:#F7F8FE;}
.d2-2448830429 .stroke-AA2{stroke:#4A6FF3;}
.d2-2448830429 .stroke-AA4{stroke:#EDF0FD;}
.d2-2448830429 .stroke-AA5{stroke:#F7F8FE;}
.d2-2448830429 .stroke-AB4{stroke:#EDF0FD;}
.d2-2448830429 .stroke-AB5{stroke:#F7F8FE;}
.d2-2448830429 .background-color-N1{background-color:#0A0F25;}
.d2-2448830429 .background-color-N2{background-color:#676C7E;}
.d2-2448830429 .background-color-N3{background-color:#9499AB;}
.d2-2448830429 .background-color-N4{background-color:#CFD2DD;}
.d2-2448830429 .background-color-N5{background-color:#DEE1EB;}
.d2-2448830429 .background-color-N6{background-color:#EEF1F8;}
.d2-2448830429 .background-color-N7{background-color:#FFFFFF;}
.d2-2448830429 .background-color-B1{background-color:#0D32B2;}
.d2-2448830429 .background-color-B2{background-color:#0D32B2;}
.d2-2448830429 .background-color-B3{background-color:#E3E9FD;}
.d2-2448830429 .background-color-B4{background-color:#E3E9FD;}
.d2-2448830429 .background-color-B5{background-color:#EDF0FD;}
.d2-2448830429 .background-color-B6{background-color:#F7F8FE;}
.d2-2448830429 .background-color-AA2{background-color:#4A6FF3;}
.d2-2448830429 .background-color-AA4{background-color:#EDF0FD;}
.d2-2448830429 .background-color-AA5{background-color:#F7F8FE;}
.d2-2448830429 .background-color-AB4{background-color:#EDF0FD;}
.d2-2448830429 .background-color-AB5{background-color:#F7F8FE;}
.d2-2448830429 .color-N1{color:#0A0F25;}
.d2-2448830429 .color-N2{color:#676C7E;}
.d2-2448830429 .color-N3{color:#9499AB;}
.d2-2448830429 .color-N4{color:#CFD2DD;}
.d2-2448830429 .color-N5{color:#DEE1EB;}
.d2-2448830429 .color-N6{color:#EEF1F8;}
.d2-2448830429 .color-N7{color:#FFFFFF;}
.d2-2448830429 .color-B1{color:#0D32B2;}
.d2-2448830429 .color-B2{color:#0D32B2;}
.d2-2448830429 .color-B3{color:#E3E9FD;}
.d2-2448830429 .color-B4{color:#E3E9FD;}
.d2-2448830429 .color-B5{color:#EDF0FD;}
.d2-2448830429 .color-B6{color:#F7F8FE;}
.d2-2448830429 .color-AA2{color:#4A6FF3;}
.d2-2448830429 .color-AA4{color:#EDF0FD;}
.d2-2448830429 .color-AA5{color:#F7F8FE;}
.d2-2448830429 .color-AB4{color:#EDF0FD;}
.d2-2448830429 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><g id="x"><g class="shape" ><rect x="1.000000" y="0.000000" width="53.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.500000" y="38.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">x</text></g><g id="y"><g class="shape" ><rect x="0.000000" y="166.000000" width="54.000000" height="66.000000" class=" stroke-B1 fill-B6" style="stroke-width:2;" /></g><text x="27.000000" y="204.500000" class="text-bold fill-N1" style="text-anchor:middle;font-size:16px">y</text></g><g id="(x -&gt; y)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 27.000000 68.000000 C 27.000000 106.000000 27.000000 126.000000 27.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-2448830429)" /></g><mask id="d2-2448830429" maskUnits="userSpaceOnUse" x="-101" y="-101" width="256" height="434">
<rect x="-101" y="-101" width="256" height="434" fill="white"></rect>
</mask></svg></svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 329 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Some files were not shown because too many files have changed in this diff Show more