merge with master
3
.gitignore
vendored
|
|
@ -1,8 +1,7 @@
|
|||
.make-log
|
||||
.changed-files
|
||||
.make-log.txt
|
||||
*.got.json
|
||||
*.got.svg
|
||||
*.got.*
|
||||
e2e_report.html
|
||||
bin
|
||||
out
|
||||
|
|
|
|||
16
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
17
ci/release/changelogs/v0.2.6.md
Normal 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)
|
||||
34
ci/release/changelogs/v0.3.0.md
Normal 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).
|
||||

|
||||
- 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)
|
||||
|
|
@ -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
|
||||
344
d2cli/main.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
116
d2renderers/d2animate/d2animate.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
23
d2renderers/d2fonts/d2fonts_test.go
Normal 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)
|
||||
}
|
||||
BIN
d2renderers/d2fonts/testdata/d2fonts/cut.exp.txt
vendored
Normal 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: `
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 56 KiB |
1213
d2renderers/d2sketch/testdata/paper-real/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 181 KiB |
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 676 KiB |
115
d2renderers/d2svg/appendix/testdata/internal-links/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 657 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 661 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 81 KiB |
1060
d2renderers/d2svg/paper.txt
Normal 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{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ var LightCatalog = []d2themes.Theme{
|
|||
ButteredToast,
|
||||
Terminal,
|
||||
TerminalGrayscale,
|
||||
Origami,
|
||||
}
|
||||
|
||||
var DarkCatalog = []d2themes.Theme{
|
||||
|
|
|
|||
40
d2themes/d2themescatalog/origami.go
Normal 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",
|
||||
}
|
||||
BIN
e2etests-cli/RockSalt-Regular.ttf
Normal 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()
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 9.3 KiB |
890
e2etests-cli/testdata/TestCLI_E2E/animation.exp.svg
vendored
Normal 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,d09GRgABAAAAAAusAAoAAAAAEggAAguFAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAAA9AAAAGAAAABgXxHXrmNtYXAAAAFUAAAAkQAAAMADlQPeZ2x5ZgAAAegAAAVpAAAG4Mx7UqRoZWFkAAAHVAAAADYAAAA2G38e1GhoZWEAAAeMAAAAJAAAACQKfwXXaG10eAAAB7AAAABgAAAAYC0lA+5sb2NhAAAIEAAAADIAAAAyFv4VQm1heHAAAAhEAAAAIAAAACAAMAD3bmFtZQAACGQAAAMoAAAIKgjwVkFwb3N0AAALjAAAAB0AAAAg/9EAMgADAioCvAAFAAACigJYAAAASwKKAlgAAAFeADIBKQAAAgsHAwMEAwICBGAAAvcAAAADAAAAAAAAAABBREJPACAAIP//Au7/BgAAA9gBESAAAZ8AAAAAAfAClAAAACAAA3icfM1NSgIBAIbhZ5rpf5qmv2206xzRukNEBEURUUR0lkod9A5u9ShewCt8guDCje/2WbwolArUKh0utUqNK9du3Lpz79GzV+8+ffn2k7DmD568ePOx8swyzzSTjDPKMF0G6aeX//zld3nbVOHCllJl245de/YdOFQ70jjWOnHqzDkLAAAA//8BAAD//y9UJ1sAAAB4nFyUW2wbWRnHv3M8nhM7TpzxeGZsx/cTz9i5OI3H9jTNxXVuTrrOXUl22SZZohW7q7RJ1U3ZsELaF7qC3VQVOEiFAC0SSCC1lSpeoCggkGiRmre29IVLESivtVCEaOWM0dhuk/bBsh+s7/v//v//+cAMUwB4BW+DCSxgBwcIACoX4iKqolCiqZpGJZOmII5MYYf+858pMSYWY1qD1wKfLi+j8SW8fXju3fGVlf8u9/ToP/nNXf0K+vguAC6/AMCDeAsswAHwRFVkWaEsa+JVniqU7Dd9aW9obmBs7hd7d/Z+FL0fRWd6e7vW1OR5/TLeOtzY2QEAQBAvH+AT+Bo0A5jDspxKptNqQpSILNMwywpOUU2kNYlFizNfzM5dmcm8H5pwa7R9rG1+NJpxTczY8t8/f+4H02p4SfIllgbev9DiPvseIBgHwLfwFgQMXpUXRUlNpzVe5aixQqOEUEWhfiwI4z/9yOqwMlbO+sGNz4nFxKQWpxeTDFNH8Jb+d2+/39/vReHDjWfByanAzvPnO4GpyeAzAAyt5QP0CJXADRRAChvitYpuolQoBI4anmiJtJaqsPxuaOpbBUxjgdMtqc7VU8tf27QygVydO8JP9AZsC5mJt+0hxSV81deydlH/t+qlFyV+wdrmc0kVr1rKB2gXlcDzplc0fOQUi9zD69nRrw/Fc95hGkxlMidccf5UZN7Wd2lmdqPPLy378tnT44L9vWCzkQEGpXyASngXeAi+5KgMVlLqMQK5tuY/Z9d7lpOxk262sGllPCPYpTj4NidNd9q+/Mb0pX6vK//Lw8EuD910uh84GgdzY8OAK9r/iUrgMhI5pl4UnCwJiaKaMLSb1KSxBQVyFwcGz/XkFjsZrD+xjnSl0l3y0g9/pbSH07b+jZnpjUxmdYiPWNJq6B2PH52KpTqrfcoaQHgXnJXcBfIyCK4ymHDZAvG+lZgeK/iC3qgL7958x922uqjvoVA66pb0O1AugwYAf8MPsQwiABCQ4ItXs/14F2yV2ZyqqYSnChGyV5kf37j92+sXMnhXX/vTnv7XP+Q+Nf5fPkAOvAv2qqucyr0K6c/5ngJnMRPWYYvY3n0L08MnkgOh82bykgGVagxGcd9g2LQywfFXEKiY8Xe8xlD1GxNUAvsbL8vwm1US6VSyFicSM+tDQ+uZzNrQ0FqmIx7viHd01LrStzE7c6nvk/HT2bxRGWNutjyKRVQCHvwA0pE6J8vSsKxIAm/MpmEiiKKh0zemfOXD3uV0sNdjnpTT822tzuiv8S+6PPQ7H89tZprdk99FLSP5zzseOBprHqOrqASO4+y1c1Alb87LgtfqanA3efucqLiQ6DKbP2OYWEJ/CgiE8gG6jkqgVDxXNKNZBqysxHEqeTRMcIqSHwtO9mHXB/JAOBMI+X1xj78n+tFc90JgwJP0dHfLwb7YhzY5cNbdLPGcyFttLd2x4XnF9bZTVFzuxnraHR9crPaut3yA/oeKRmavZc3VntBfpscK/qBXFgub9abAGdvqIkrq/0jFPD40qjcNR9oBgQsAF1ERQgCqSZVqN0s79stEa3eWkO1vfu8Ea2UZ0mDRPjtpsROGWEjntz+52UEaCEPqSTsq7kdGZfkM3a98j0b29aZ7dCQaHaH3Kppt5X50iIpGQ4680rTjq02NeFMM2T3EUReJWsnvt3P1DitTx1l6r9yUTk7+kWUuIHOLz4P+9Tg8EqE5+liv759rrXqSRyvwFN8GMwCvKCohaz7zttmHVu5fvny/mjU8QkUwVd9TtoCKehOg8i3cDbP4IdQDcJVrVC1YJB6PROJx3N1Kaavxgf8DAAD//wEAAP//VmN0NQAAAAABAAAAAguFYS7IJ18PPPUAAQPoAAAAANhdoIQAAAAA3WYvNv43/sQIbQPxAAEAAwACAAAAAAAAAAEAAAPY/u8AAAiY/jf+NwhtAAEAAAAAAAAAAAAAAAAAAAAYArIAUADIAAACPf/6AkYALgL6AE0CDwAqAdMAJAI9ACcCBgAkAjsAQQEUADcCJABBAR4AQQI8AEECKwAkAj0AQQGOAEEBuwAVAjgAPAMIABgCCQAMASwATAEUAEEAAP+tAAAALAAsAFAAfACuAOYBEgFEAXgBmgGmAb4B2gH8AigCWAJ4ArQC1gMOAz4DTgNaA3AAAAABAAAAGACQAAwAYwAHAAEAAAAAAAAAAAAAAAAABAADeJyclM9uG1UUxn9ObNMKwQJFVbqJ7oJFkejYVEnVNiuH1IpFFAePC0JCSBPP+I8ynhl5Jg7hCVjzFrxFVzwEz4FYo/l87NgF0SaKknx37vnznXO+c4Ed/mabSvUh8Ec9MVxhr35ueIsH9RPD27TrW4arPKn9abhGWJsbrvN5rWf4I95WfzP8gP3qT4YfslttG/6YZ9Udw59sO/4y/Cn7vF3gCrzgV8MVdskMb7HDj4a3eYTFrFR5RNNwjc/YM1xnD+gzoSBmQsIIx5AJI66YEZHjEzFjwpCIEEeHFjGFviYEQo7Rf34N8CmYESjimAJHjE9MQM7YIv4ir5RzZRzqNLO7FgVjAi7kcUlAgiNlREpCxKXiFBRkvKJBg5yB+GYU5HjkTIjxSJkxokGXNqf0GTMhx9FWpJKZT8qQgmsC5XdmUXZmQERCbqyuSAjF04lfJO8Opzi6ZLJdj3y6EeFLHN/Ju+SWyvYrPP26NWabeZdsAubqZ6yuxLq51gTHui3ztvhWuOAV7l792WTy/h6F+l8o8gVXmn+oSSVikuDcLi18Kch3j3Ec6dzBV0e+p0OfE7q8oa9zix49WpzRp8Nr+Xbp4fiaLmccy6MjvLhrSzFn/IDjGzqyKWNH1p/FxCJ+JjN15+I4Ux1TMvW8ZO6p1kgV3n3C5Q6lG+rI5TPQHpWWTvNLtGcBI1NFJoZT9XKpjdz6F5oipqqlnO3tfbkNc9u95RbfkGqHS7UuOJWTWzB631S9dzRzrR+PgJCUC1kMSJnSoOBGvM8JuCLGcazunWhLClornzLPjVQSMRWDDonizMj0NzDd+MZ9sKF7Z29JKP+S6eWqqvtkcerV7YzeqHvLO9+6HK1NoGFTTdfUNBDXxLQfaafW+fvyzfW6pTzliJSY8F8vwDM8muxzwCFjZRjoZm6vQ1MvRJOXHKr6SyJZDaXnyCIc4PGcAw54yfN3+rhk4oyLW3FZz93imCO6HH5QFQv7Lke8Xn37/6y/i2lTtTierk4v7j3FJ3dQ6xfas9v3sqeJlZOYW7TbrTgjYFpycbvrNbnHeP8AAAD//wEAAP//9LdPUXicYmBmAIP/5xiMGLAAAAAAAP//AQAA//8vAQIDAAAA");
|
||||
}]]></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=""Chicken's plan""><g class="shape" ></g><text x="0.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken'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=""Chicken's plan""><g class="shape" ></g><text x="75.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken'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=""Chicken's plan""><g class="shape" ></g><text x="75.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken's plan</text></g><g id="(Approach road -> 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=""Chicken's plan""><g class="shape" ></g><text x="102.000000" y="-30.000000" class="text fill-N1" style="text-anchor:middle;font-size:35px">Chicken's plan</text></g><g id="(Approach road -> 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 -> 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 |
95
e2etests-cli/testdata/TestCLI_E2E/center.exp.svg
vendored
Normal 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 -> 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 |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 9.5 KiB |