merge with master

This commit is contained in:
Antoine Poivey 2023-03-17 18:54:12 +01:00
commit 58b596854b
No known key found for this signature in database
GPG key ID: 6AA1C83421F1A287
577 changed files with 57669 additions and 15998 deletions

View file

@ -41,7 +41,7 @@ https://user-images.githubusercontent.com/3120367/206125010-bd1fea8e-248a-43e7-8
- [Community plugins](#community-plugins)
- [Misc](#misc)
- [FAQ](#faq)
- [Open-source projects documenting with D2](#open-source-projects-documenting-with-d2)
- [Notable open-source projects documenting with D2](#notable-open-source-projects-documenting-with-d2)
## What does D2 look like?
@ -250,20 +250,24 @@ let us know and we'll be happy to include it here!
- I have a private inquiry.
- Please reach out at [hi@d2lang.com](hi@d2lang.com).
## Open-source projects documenting with D2
## Notable open-source projects documenting with D2
Do you have or see an open-source project with `.d2` files? Please submit a PR adding to
this list (ordered by star count, desc).
this selected list of featured projects using D2.
- [Block Protocol](https://github.com/blockprotocol/blockprotocol) - The Block Protocol is
an open standard for building and using data-driven blocks.
- [Ivy Wallet](https://github.com/Ivy-Apps/ivy-wallet) - Ivy Wallet is an open-source
money manager app for Android.
- [Learn EVM Attacks](https://github.com/coinspect/learn-evm-attacks) - Learn & Contribute
on previously exploited vulnerabilities across several EVM projects.
- [BYCEPS](https://github.com/byceps/byceps) - BYCEPS is a self-hosted web platform to run
LAN parties.
- [Re:Earth](https://github.com/reearth/reearth-web) - A free, open and highly extensible
WebGIS platform.
- [Terraform OCI VSCode Server](https://github.com/timoa/terraform-oci-vscode-server) -
Terraform project that deploys VSCode Server on Oracle Cloud Infrastructure.
- [ElasticSearch](https://github.com/elastic/beats/blob/main/libbeat/publisher/queue/proxy/diagrams/broker.d2)
- [UC
Berkeley](https://github.com/ucb-bar/hammer/blob/2b5c04d7b7d9ee3c73575efcd7ee0698bd5bfa88/doc/Hammer-Use/hier.d2)
- [Coronacheck](https://github.com/minvws/nl-covid19-coronacheck-app-ios/blob/e1567e9d1633b3273c537a105bff0e7d3a57ecfe/Diagrams/client-side-datamodel.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
- [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

View file

@ -3,5 +3,3 @@
#### Improvements 🧹
#### Bugfixes ⛑️
- Fixes `d2` erroring on malformed user paths (`fdopendir` error). [util-go#10](https://github.com/terrastruct/util-go/pull/10)

View file

@ -0,0 +1,28 @@
Customizations and layouts take a big leap forward with this release! Put together, these improvements make beautiful diagrams like these possible:
![mono](https://user-images.githubusercontent.com/3120367/225767298-73b6466c-c245-4df9-b9fd-9e2e4c7910c2.png)
> [Playground link](https://play.d2lang.com/?script=rJLRasMwDEXf_RX6gYWNvnmwXxkiEampaxlLXVdK_n3IidekHfRlT8GxonvvuUmkZy4HD1cH0FOMoHymMp8BBJViDErS3gDUhB5EudDwOaBiuzBHXePlQcuJ6tXk6kMLJjkGK9DdbYeXj_W1B6E0_ONMdcDJYkPmohhbnlPwcF0i7ekbR05T-8CyQS7ckwjfmCgXHOkBSH-Jwdg-p7Gsv-HuVp4twla4-1XMe04EkUdxk3MnaUUtDjIV4eQAarUe3navbc62Ll1365qPeCDoMcaHqQ2tzjBhb35mwRpu9at62JkU5gBC5evZqiFIjni5m7dgc4og6uZT6ybjSO9_Qp2ca0JbbrbyJuB-AgAA__8%3D&theme=300&sketch=0&layout=elk&)
#### Features 🚀
- New class of special themes, starting with `Terminal`, and `Terminal Grayscale`. See [docs](https://d2lang.com/tour/themes/#special-themes). [#1040](https://github.com/terrastruct/d2/pull/1040), [#1041](https://github.com/terrastruct/d2/pull/1041)
- `style.font: mono` to use a monospaced font for the text/label. See [docs](https://d2lang.com/tour/style/#font). [#1010](https://github.com/terrastruct/d2/pull/1010)
- `border-radius` is supported for both `class` and `sql_table` shapes. Thanks to second-time contributor @donglixiaoche ! [#982](https://github.com/terrastruct/d2/pull/982)
- Implements `style.fill-pattern`. See [docs](https://d2lang.com/tour/style#fill-pattern). [#1024](https://github.com/terrastruct/d2/pull/1024), [#1041](https://github.com/terrastruct/d2/pull/1041)
#### Improvements 🧹
- `dagre` layouts that have a connection where one endpoint is a container is much improved. [#1011](https://github.com/terrastruct/d2/pull/1011)
- `elk` layouts have less bends in the routes. [#1033](https://github.com/terrastruct/d2/pull/1033)
- `elk` layouts center nodes better. [#1028](https://github.com/terrastruct/d2/pull/1028)
- `elk` layouts have nicer margins between node boundaries and edges. [#1028](https://github.com/terrastruct/d2/pull/1028)
- `elk` layouts container contents are centered within. [#1038](https://github.com/terrastruct/d2/pull/1038)
- `elk` layouts container dimensions fit label. [#1038](https://github.com/terrastruct/d2/pull/1038)
- `sketch` draws connections with less roughness, which especially improves look of corner bends in ELK. [#1014](https://github.com/terrastruct/d2/pull/1014)
- CSS in SVGs are diagram-specific, which means you can embed multiple D2 diagrams on a web page without fear of style conflicts. [#1016](https://github.com/terrastruct/d2/pull/1016)
#### Bugfixes ⛑️
- Fixes `d2` erroring on malformed user paths (`fdopendir` error). [util-go#10](https://github.com/terrastruct/util-go/pull/10)
- Arrowhead labels being set without maps wasn't being picked up. [#1015](https://github.com/terrastruct/d2/pull/1015)
- Fixes a `dagre` layout error with connections to a container shape with a blockstring label. [#1032](https://github.com/terrastruct/d2/pull/1032)

View file

@ -3,23 +3,23 @@
// d2ast implements the d2 language's abstract syntax tree.
//
// Special characters to think about in parser:
// #
// """
// ;
// []
// {}
// |
// $
// '
// "
// \
// :
// .
// --
// <>
// *
// &
// ()
// #
// """
// ;
// []
// {}
// |
// $
// '
// "
// \
// :
// .
// --
// <>
// *
// &
// ()
package d2ast
import (
@ -159,9 +159,9 @@ func (r Range) Before(r2 Range) bool {
//
// note: Line and Column are zero indexed.
// note: Column and Byte are UTF-8 byte indexes unless byUTF16 was passed to Position.Advance in
// which they are UTF-16 code unit indexes.
// If intended for Javascript consumption like in the browser or via LSP, byUTF16 is
// set to true.
// . which they are UTF-16 code unit indexes.
// . If intended for Javascript consumption like in the browser or via LSP, byUTF16 is
// . set to true.
type Position struct {
Line int
Column int
@ -650,9 +650,14 @@ func (mk1 *Key) Equals(mk2 *Key) bool {
}
if mk1.Value.Unbox() != nil {
if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() {
if (mk1.Value.ScalarBox().Unbox() == nil) != (mk2.Value.ScalarBox().Unbox() == nil) {
return false
}
if mk1.Value.ScalarBox().Unbox() != nil {
if mk1.Value.ScalarBox().Unbox().ScalarString() != mk2.Value.ScalarBox().Unbox().ScalarString() {
return false
}
}
}
return true

View file

@ -273,8 +273,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
layout := plugin.Layout
opts := &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
}
if sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)
@ -432,6 +433,10 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
if err != nil {
return svg, err
}
out, err = png.AddExif(out)
if err != nil {
return svg, err
}
} else {
if len(out) > 0 && out[len(out)-1] != '\n' {
out = append(out, '\n')

View file

@ -392,6 +392,8 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "fill":
attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "fill-pattern":
attrs.Style.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "stroke-width":
attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
case "stroke-dash":
@ -488,9 +490,7 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
}
if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
if f.Map() != nil {
c.compileArrowheads(edge, f)
}
c.compileArrowheads(edge, f)
}
}
@ -508,21 +508,23 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
c.compileLabel(attrs, f)
}
for _, f2 := range f.Map().Fields {
keyword := strings.ToLower(f2.Name)
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(attrs, f2)
continue
} else if f2.Name == "style" {
if f2.Map() == nil {
if f.Map() != nil {
for _, f2 := range f.Map().Fields {
keyword := strings.ToLower(f2.Name)
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(attrs, f2)
continue
} else if f2.Name == "style" {
if f2.Map() == nil {
continue
}
c.compileStyle(attrs, f2.Map())
continue
} else {
c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
continue
}
c.compileStyle(attrs, f2.Map())
continue
} else {
c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
continue
}
}
}

View file

@ -242,6 +242,25 @@ containers: {
}
},
},
{
name: "fill-pattern",
text: `x: {
style: {
fill-pattern: dots
}
}
`,
},
{
name: "invalid-fill-pattern",
text: `x: {
style: {
fill-pattern: ddots
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain`,
},
{
name: "shape_unquoted_hex",
@ -971,6 +990,17 @@ x -> y: {
}
},
},
{
name: "edge_arrowhead_primary",
text: `x -> y: {
source-arrowhead: Reisner's Rule of Conceptual Inertia
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
},
},
{
name: "edge_arrowhead_fields",

View file

@ -9,6 +9,7 @@ import (
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/color"
)
@ -20,22 +21,25 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
if fontFamily == nil {
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
}
if g.Theme != nil && g.Theme.SpecialRules.Mono {
fontFamily = go2.Pointer(d2fonts.SourceCodePro)
}
diagram.FontFamily = fontFamily
diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range g.Objects {
diagram.Shapes[i] = toShape(g.Objects[i])
diagram.Shapes[i] = toShape(g.Objects[i], g.Theme)
}
diagram.Connections = make([]d2target.Connection, len(g.Edges))
for i := range g.Edges {
diagram.Connections[i] = toConnection(g.Edges[i])
diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
}
return diagram, nil
}
func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) {
shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText {
@ -46,6 +50,23 @@ func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
shape.SecondaryAccentColor = color.AA2
shape.NeutralAccentColor = color.N2
}
// Theme options that change more than color
if theme != nil {
if theme.SpecialRules.OuterContainerDoubleBorder {
if obj.Level() == 1 && len(obj.ChildrenArray) > 0 {
shape.DoubleBorder = true
}
}
if theme.SpecialRules.ContainerDots {
if len(obj.ChildrenArray) > 0 {
shape.FillPattern = "dots"
}
}
if theme.SpecialRules.Mono {
shape.FontFamily = "mono"
}
}
}
func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
@ -60,6 +81,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
} else if obj.Attributes.Shape.Value == d2target.ShapeText {
shape.Fill = "transparent"
}
if obj.Attributes.Style.FillPattern != nil {
shape.FillPattern = obj.Attributes.Style.FillPattern.Value
}
if obj.Attributes.Style.Stroke != nil {
shape.Stroke = obj.Attributes.Style.Stroke.Value
}
@ -99,7 +123,7 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) {
}
}
func toShape(obj *d2graph.Object) d2target.Shape {
func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value)
shape.ID = obj.AbsID()
@ -124,7 +148,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
}
applyStyles(shape, obj)
applyTheme(shape, obj)
applyTheme(shape, obj, theme)
shape.Color = text.GetColor(shape.Italic)
applyStyles(shape, obj)
@ -165,7 +189,7 @@ func toShape(obj *d2graph.Object) d2target.Shape {
return *shape
}
func toConnection(edge *d2graph.Edge) d2target.Connection {
func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
connection := d2target.BaseConnection()
connection.ID = edge.AbsID()
connection.ZIndex = edge.ZIndex
@ -205,7 +229,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
connection.DstLabel = edge.DstArrowhead.Label.Value
}
}
if theme != nil && theme.SpecialRules.NoCornerRadius {
connection.BorderRadius = 0
}
if edge.Attributes.Style.BorderRadius != nil {
connection.BorderRadius, _ = strconv.ParseFloat(edge.Attributes.Style.BorderRadius.Value, 64)
}
@ -255,6 +281,9 @@ func toConnection(edge *d2graph.Edge) d2target.Connection {
if edge.Attributes.Style.Bold != nil {
connection.Bold, _ = strconv.ParseBool(edge.Attributes.Style.Bold.Value)
}
if theme != nil && theme.SpecialRules.Mono {
connection.FontFamily = "mono"
}
if edge.Attributes.Style.Font != nil {
connection.FontFamily = edge.Attributes.Style.Font.Value
}

View file

@ -609,6 +609,14 @@ y
x -> y
# foo
y
`,
},
{
name: "less_than_edge#955",
in: `
x <= y
`,
exp: `x <- = y
`,
},
}

View file

@ -17,6 +17,8 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/shape"
@ -43,6 +45,8 @@ type Graph struct {
Layers []*Graph `json:"layers,omitempty"`
Scenarios []*Graph `json:"scenarios,omitempty"`
Steps []*Graph `json:"steps,omitempty"`
Theme *d2themes.Theme `json:"theme,omitempty"`
}
func NewGraph() *Graph {
@ -150,6 +154,7 @@ type Style struct {
Opacity *Scalar `json:"opacity,omitempty"`
Stroke *Scalar `json:"stroke,omitempty"`
Fill *Scalar `json:"fill,omitempty"`
FillPattern *Scalar `json:"fillPattern,omitempty"`
StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
StrokeDash *Scalar `json:"strokeDash,omitempty"`
BorderRadius *Scalar `json:"borderRadius,omitempty"`
@ -198,6 +203,14 @@ func (s *Style) Apply(key, value string) error {
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
}
s.Fill.Value = value
case "fill-pattern":
if s.FillPattern == nil {
break
}
if !go2.Contains(FillPatterns, strings.ToLower(value)) {
return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(FillPatterns, ", "))
}
s.FillPattern.Value = value
case "stroke-width":
if s.StrokeWidth == nil {
break
@ -259,10 +272,10 @@ func (s *Style) Apply(key, value string) error {
if s.Font == nil {
break
}
if !go2.Contains(systemFonts, strings.ToUpper(value)) {
if _, ok := d2fonts.D2_FONT_TO_FAMILY[strings.ToLower(value)]; !ok {
return fmt.Errorf(`"%v" is not a valid font in our system`, value)
}
s.Font.Value = strings.ToUpper(value)
s.Font.Value = strings.ToLower(value)
case "font-size":
if s.FontSize == nil {
break
@ -800,6 +813,11 @@ func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *
func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
if obj.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[obj.Attributes.Style.Font.Value]
fontFamily = &f
}
var dims *d2target.TextDimensions
switch shapeType {
case d2target.ShapeText:
@ -1171,6 +1189,10 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t
return d2target.NewTextDimensions(width, height), nil
}
if strings.TrimSpace(t.Text) == "" {
return d2target.NewTextDimensions(1, 1), nil
}
return nil, fmt.Errorf("text not pre-measured and no ruler provided")
}
@ -1240,6 +1262,11 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
}
}
if g.Theme != nil && g.Theme.SpecialRules.Mono {
tmp := d2fonts.SourceCodePro
fontFamily = &tmp
}
for _, obj := range g.Objects {
obj.Box = &geo.Box{}
@ -1280,6 +1307,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)
}
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
if err != nil {
return err
@ -1397,7 +1428,16 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
continue
}
dims := GetTextDimensions(mtexts, ruler, edge.Text(), fontFamily)
if g.Theme != nil && g.Theme.SpecialRules.CapsLock {
edge.Attributes.Label.Value = strings.ToUpper(edge.Attributes.Label.Value)
}
usedFont := fontFamily
if edge.Attributes.Style.Font != nil {
f := d2fonts.D2_FONT_TO_FAMILY[edge.Attributes.Style.Font.Value]
usedFont = &f
}
dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
if dims == nil {
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
}
@ -1412,9 +1452,15 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
func (g *Graph) Texts() []*d2target.MText {
var texts []*d2target.MText
capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
for _, obj := range g.Objects {
if obj.Attributes.Label.Value != "" {
texts = appendTextDedup(texts, obj.Text())
text := obj.Text()
if capsLock {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
}
if obj.Class != nil {
fontSize := d2fonts.FONT_SIZE_L
@ -1441,7 +1487,11 @@ func (g *Graph) Texts() []*d2target.MText {
}
for _, edge := range g.Edges {
if edge.Attributes.Label.Value != "" {
texts = appendTextDedup(texts, edge.Text())
text := edge.Text()
if capsLock {
text.Text = strings.ToUpper(text.Text)
}
texts = appendTextDedup(texts, text)
}
if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
t := edge.Text()
@ -1497,6 +1547,7 @@ var StyleKeywords = map[string]struct{}{
"opacity": {},
"stroke": {},
"fill": {},
"fill-pattern": {},
"stroke-width": {},
"stroke-dash": {},
"border-radius": {},
@ -1538,6 +1589,12 @@ var NearConstantsArray = []string{
}
var NearConstants map[string]struct{}
var FillPatterns = []string{
"dots",
"lines",
"grain",
}
// BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{
"layers": {},
@ -1633,3 +1690,15 @@ func (obj *Object) IsDescendantOf(ancestor *Object) bool {
}
return obj.Parent.IsDescendantOf(ancestor)
}
// ApplyTheme applies themes on the graph level
// This is different than on the render level, which only changes colors
// A theme applied on the graph level applies special rules that change the graph
func (g *Graph) ApplyTheme(themeID int64) error {
theme := d2themescatalog.Find(themeID)
if theme == (d2themes.Theme{}) {
return fmt.Errorf("theme %d not found", themeID)
}
g.Theme = &theme
return nil
}

View file

@ -1,10 +0,0 @@
package d2graph
var systemFonts = []string{
"DEFAULT",
"SERIOUS",
"DIGITAL",
"EDUCATIONAL",
"NEWSPAPER",
"MONO",
}

View file

@ -177,11 +177,27 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// 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 {
src = src.ChildrenArray[0]
// 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
@ -451,7 +467,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
// if an edge to a container runs into its label, stop the edge at the label instead
overlapsContainerLabel := false
if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" {
if edge.Dst.IsContainer() && edge.Dst.Attributes.Label.Value != "" && !dstShape.Is(shape.TEXT_TYPE) {
// assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label
labelWidth := float64(*edge.Dst.LabelWidth)
labelHeight := float64(*edge.Dst.LabelHeight)
@ -550,3 +566,66 @@ func generateAddParentLine(childID, parentID string) string {
func generateAddEdgeLine(fromID, toID, edgeID string, width, height int) string {
return fmt.Sprintf("g.setEdge({v:`%s`, w:`%s`, name:`%s`}, { width:%d, height:%d, labelpos: `c` });\n", escapeID(fromID), escapeID(toID), escapeID(edgeID), width, height)
}
// getLongestEdgeChainTail gets the node at the end of the longest edge chain, because that will be the end of the container
// and is what external connections should connect with
func getLongestEdgeChainTail(g *d2graph.Graph, container *d2graph.Object) *d2graph.Object {
rank := make(map[*d2graph.Object]int)
for _, obj := range container.ChildrenArray {
isHead := true
for _, e := range g.Edges {
if inContainer(e.Src, container) != nil && inContainer(e.Dst, obj) != nil {
isHead = false
break
}
}
if !isHead {
continue
}
rank[obj] = 1
// BFS
queue := []*d2graph.Object{obj}
visited := make(map[*d2graph.Object]struct{})
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
if _, ok := visited[curr]; ok {
continue
}
visited[curr] = struct{}{}
for _, e := range g.Edges {
child := inContainer(e.Dst, container)
if child == curr {
continue
}
if child != nil && inContainer(e.Src, curr) != nil {
rank[child] = go2.Max(rank[child], rank[curr]+1)
queue = append(queue, child)
}
}
}
}
max := int(math.MinInt32)
var tail *d2graph.Object
for _, obj := range container.ChildrenArray {
if rank[obj] >= max {
max = rank[obj]
tail = obj
}
}
return tail
}
func inContainer(obj, container *d2graph.Object) *d2graph.Object {
if obj == nil {
return nil
}
if obj == container {
return obj
}
if obj.Parent == container {
return obj
}
return inContainer(obj.Parent, container)
}

View file

@ -8,6 +8,7 @@ import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"math"
"strings"
@ -95,8 +96,11 @@ var DefaultOpts = ConfigurableOpts{
}
var port_spacing = 40.
var edge_node_spacing = 40
type elkOpts struct {
EdgeNode int `json:"elk.spacing.edgeNode,omitempty"`
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
Direction string `json:"elk.direction"`
@ -106,6 +110,7 @@ type elkOpts struct {
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
ContentAlignment string `json:"elk.contentAlignment,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
ConfigurableOpts
@ -140,8 +145,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
LayoutOptions: &elkOpts{
Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50,
EdgeNode: edge_node_spacing,
HierarchyHandling: "INCLUDE_CHILDREN",
FixedAlignment: "BALANCED",
ConsiderModelOrder: "NODES_AND_EDGES",
NodeSizeConstraints: "MINIMUM_SIZE",
ContentAlignment: "H_CENTER V_CENTER",
ConfigurableOpts: ConfigurableOpts{
Algorithm: opts.Algorithm,
NodeSpacing: opts.NodeSpacing,
@ -218,9 +227,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Thoroughness: 8,
EdgeEdgeBetweenLayersSpacing: 50,
HierarchyHandling: "INCLUDE_CHILDREN",
FixedAlignment: "BALANCED",
EdgeNode: edge_node_spacing,
ConsiderModelOrder: "NODES_AND_EDGES",
// Why is it (height, width)? I have no clue, but it works.
NodeSizeMinimum: fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width))),
NodeSizeConstraints: "MINIMUM_SIZE",
ContentAlignment: "H_CENTER V_CENTER",
ConfigurableOpts: ConfigurableOpts{
NodeSpacing: opts.NodeSpacing,
EdgeNodeSpacing: opts.EdgeNodeSpacing,
@ -228,11 +239,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Padding: opts.Padding,
},
}
// Only set if specified.
// There's a bug where if it's the node label dimensions that set the NodeSizeMinimum,
// then suddenly it's reversed back to (width, height). I must be missing something
if obj.Attributes.Width != nil || obj.Attributes.Height != nil {
n.LayoutOptions.NodeSizeConstraints = "MINIMUM_SIZE"
switch elkGraph.LayoutOptions.Direction {
case "DOWN", "UP":
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
case "RIGHT", "LEFT":
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
}
if n.LayoutOptions.Padding == DefaultOpts.Padding {
@ -252,6 +264,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
paddingTop,
)
}
} else {
n.LayoutOptions = &elkOpts{
// Margins: "[top=100,left=100,bottom=100,right=100]",
}
}
if obj.LabelWidth != nil && obj.LabelHeight != nil {
@ -303,7 +319,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
val, err := vm.RunString(`elk.layout(graph)
.then(s => s)
.catch(s => s)
.catch(err => err.message)
`)
if err != nil {
@ -324,7 +340,21 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
continue
}
jsonOut := promise.Result().Export().(map[string]interface{})
if promise.State() == goja.PromiseStateRejected {
return errors.New("ELK: something went wrong")
}
result := promise.Result().Export()
var jsonOut map[string]interface{}
switch out := result.(type) {
case string:
return fmt.Errorf("ELK layout error: %s", out)
case map[string]interface{}:
jsonOut = out
default:
return fmt.Errorf("ELK unexpected return: %v", out)
}
jsonBytes, err := json.Marshal(jsonOut)
if err != nil {
@ -417,5 +447,270 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
edge.Route = points
}
deleteBends(g)
return nil
}
// deleteBends is a shim for ELK to delete unnecessary bends
// see https://github.com/terrastruct/d2/issues/1030
func deleteBends(g *d2graph.Graph) {
// Get rid of S-shapes at the source and the target
// TODO there might be value in repeating this. removal of an S shape introducing another S shape that can still be removed
for _, isSource := range []bool{true, false} {
for ei, e := range g.Edges {
if len(e.Route) < 4 {
continue
}
if e.Src == e.Dst {
continue
}
var endpoint *d2graph.Object
var start *geo.Point
var corner *geo.Point
var end *geo.Point
if isSource {
start = e.Route[0]
corner = e.Route[1]
end = e.Route[2]
endpoint = e.Src
} else {
start = e.Route[len(e.Route)-1]
corner = e.Route[len(e.Route)-2]
end = e.Route[len(e.Route)-3]
endpoint = e.Dst
}
isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
// Make sure it's still attached
if isHorizontal {
if end.Y <= endpoint.TopLeft.Y+10 {
continue
}
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
continue
}
} else {
if end.X <= endpoint.TopLeft.X+10 {
continue
}
if end.X >= endpoint.TopLeft.X+endpoint.Width-10 {
continue
}
}
var newStart *geo.Point
if isHorizontal {
newStart = geo.NewPoint(start.X, end.Y)
} else {
newStart = geo.NewPoint(end.X, start.Y)
}
endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Attributes.Shape.Value)], endpoint.Box)
newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
// Check that the new segment doesn't collide with anything new
oldSegment := geo.NewSegment(start, corner)
newSegment := geo.NewSegment(newStart, end)
oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldSegment)
newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newSegment)
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount, oldOverlapsCount, oldCloseOverlapsCount, oldTouchingCount := countEdgeIntersects(g, g.Edges[ei], *oldSegment)
newCrossingsCount, newOverlapsCount, newCloseOverlapsCount, newTouchingCount := countEdgeIntersects(g, g.Edges[ei], *newSegment)
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
// commit
if isSource {
g.Edges[ei].Route = append(
[]*geo.Point{newStart},
e.Route[3:]...,
)
} else {
g.Edges[ei].Route = append(
e.Route[:len(e.Route)-3],
newStart,
)
}
}
}
// Get rid of ladders
// ELK likes to do these for some reason
// . ┌─
// . ┌─┘
// . │
// We want to transform these into L-shapes
for ei, e := range g.Edges {
if len(e.Route) < 6 {
continue
}
if e.Src == e.Dst {
continue
}
for i := 1; i < len(e.Route)-3; i++ {
before := e.Route[i-1]
start := e.Route[i]
corner := e.Route[i+1]
end := e.Route[i+2]
after := e.Route[i+3]
// S-shape on sources only concerned one segment, since the other was just along the bound of endpoint
// These concern two segments
var newCorner *geo.Point
if math.Ceil(start.X) == math.Ceil(corner.X) {
newCorner = geo.NewPoint(end.X, start.Y)
// not ladder
if (end.X > start.X) != (start.X > before.X) {
continue
}
if (end.Y > start.Y) != (after.Y > end.Y) {
continue
}
} else {
newCorner = geo.NewPoint(start.X, end.Y)
if (end.Y > start.Y) != (start.Y > before.Y) {
continue
}
if (end.X > start.X) != (after.X > end.X) {
continue
}
}
oldS1 := geo.NewSegment(start, corner)
oldS2 := geo.NewSegment(corner, end)
newS1 := geo.NewSegment(start, newCorner)
newS2 := geo.NewSegment(newCorner, end)
// Check that the new segments doesn't collide with anything new
oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldS1) + countObjectIntersects(g, e.Src, e.Dst, *oldS2)
newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newS1) + countObjectIntersects(g, e.Src, e.Dst, *newS2)
if newIntersects > oldIntersects {
continue
}
oldCrossingsCount1, oldOverlapsCount1, oldCloseOverlapsCount1, oldTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *oldS1)
oldCrossingsCount2, oldOverlapsCount2, oldCloseOverlapsCount2, oldTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *oldS2)
oldCrossingsCount := oldCrossingsCount1 + oldCrossingsCount2
oldOverlapsCount := oldOverlapsCount1 + oldOverlapsCount2
oldCloseOverlapsCount := oldCloseOverlapsCount1 + oldCloseOverlapsCount2
oldTouchingCount := oldTouchingCount1 + oldTouchingCount2
newCrossingsCount1, newOverlapsCount1, newCloseOverlapsCount1, newTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *newS1)
newCrossingsCount2, newOverlapsCount2, newCloseOverlapsCount2, newTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *newS2)
newCrossingsCount := newCrossingsCount1 + newCrossingsCount2
newOverlapsCount := newOverlapsCount1 + newOverlapsCount2
newCloseOverlapsCount := newCloseOverlapsCount1 + newCloseOverlapsCount2
newTouchingCount := newTouchingCount1 + newTouchingCount2
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
continue
}
if newTouchingCount > oldTouchingCount {
continue
}
// commit
g.Edges[ei].Route = append(append(
e.Route[:i],
newCorner,
),
e.Route[i+3:]...,
)
break
}
}
}
func countObjectIntersects(g *d2graph.Graph, src, dst *d2graph.Object, s geo.Segment) int {
count := 0
for i, o := range g.Objects {
if g.Objects[i] == src || g.Objects[i] == dst {
continue
}
if o.Intersects(s, float64(edge_node_spacing)-1) {
count++
}
}
return count
}
// countEdgeIntersects counts both crossings AND getting too close to a parallel segment
func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (int, int, int, int) {
isHorizontal := math.Ceil(s.Start.Y) == math.Ceil(s.End.Y)
crossingsCount := 0
overlapsCount := 0
closeOverlapsCount := 0
touchingCount := 0
for i, e := range g.Edges {
if g.Edges[i] == sEdge {
continue
}
for i := 0; i < len(e.Route)-1; i++ {
otherS := geo.NewSegment(e.Route[i], e.Route[i+1])
otherIsHorizontal := math.Ceil(otherS.Start.Y) == math.Ceil(otherS.End.Y)
if isHorizontal == otherIsHorizontal {
if s.Overlaps(*otherS, !isHorizontal, 0.) {
if isHorizontal {
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
touchingCount++
}
}
}
} else {
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
touchingCount++
}
}
}
}
}
} else {
if s.Intersects(*otherS) {
crossingsCount++
}
}
}
}
return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount
}

View file

@ -355,13 +355,13 @@ func (sd *sequenceDiagram) placeActors() {
}
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
// ┌──────────────┐
// │ actor │
// └──────┬───────┘
//
// │ lifeline
//
//
// . ┌──────────────┐
// . │ actor │
// . └──────┬───────┘
// .
// . │ lifeline
// .
// .
func (sd *sequenceDiagram) addLifelineEdges() {
endY := 0.
if len(sd.messages) > 0 {
@ -433,17 +433,17 @@ func (sd *sequenceDiagram) placeNotes() {
}
// placeSpans places spans over the object lifeline
// ┌──────────┐
// │ actor │
// └────┬─────┘
// ┌─┴──┐
// │ │
// |span|
// │ │
// └─┬──┘
//
// lifeline
//
// . ┌──────────┐
// . │ actor │
// . └────┬─────┘
// . ┌─┴──┐
// . │ │
// . |span|
// . │ │
// . └─┬──┘
// .
// . lifeline
// .
func (sd *sequenceDiagram) placeSpans() {
// quickly find the span center X
rankToX := make(map[int]float64)

View file

@ -22,6 +22,7 @@ type CompileOptions struct {
MeasuredTexts []*d2target.MText
Ruler *textmeasure.Ruler
Layout func(context.Context, *d2graph.Graph) error
ThemeID int64
// FontFamily controls the font family used for all texts that are not the following:
// - code
@ -51,6 +52,11 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
}
func compile(ctx context.Context, g *d2graph.Graph, opts *CompileOptions) (*d2target.Diagram, error) {
err := g.ApplyTheme(opts.ThemeID)
if err != nil {
return nil, err
}
if len(g.Objects) > 0 {
err := g.SetDimensions(opts.MeasuredTexts, opts.Ruler, opts.FontFamily)
if err != nil {

View file

@ -919,8 +919,7 @@ func deleteObject(g *d2graph.Graph, key *d2ast.KeyPath, obj *d2graph.Object) (*d
return !isSpecial
})
if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass {
ref.MapKey.Value.Map = nil
ref.MapKey.Primary = ref.MapKey.Value.ScalarBox()
deleteFromMap(ref.Scope, ref.MapKey)
} else if len(withoutSpecial) == 0 {
hoistRefChildren(g, key, ref)
deleteFromMap(ref.Scope, ref.MapKey)

View file

@ -1858,6 +1858,32 @@ func TestMove(t *testing.T) {
assert.JSON(t, g.Objects[1].ID, "c")
},
},
{
name: "duplicate",
text: `a: {
b: {
shape: cylinder
}
}
a: {
b: {
shape: cylinder
}
}
`,
key: `a.b`,
newKey: `b`,
exp: `a
a
b: {
shape: cylinder
}
`,
},
{
name: "rename_2",
@ -3399,6 +3425,44 @@ func TestDelete(t *testing.T) {
exp: `x.y.z -> y.b
`,
},
{
name: "table_refs",
text: `a: {
shape: sql_table
b
}
c: {
shape: sql_table
d
}
a.b
a.b -> c.d
`,
key: `a`,
exp: `c: {
shape: sql_table
d
}
c.d
`,
},
{
name: "class_refs",
text: `a: {
shape: class
b: int
}
a.b
`,
key: `a`,
exp: ``,
},
{
name: "edge_both_identical_childs",
@ -4740,7 +4804,6 @@ a -> b
shape: sql_table
id: int {constraint: primary_key}
}
disks.id
AWS S3 Vancouver -> disks

View file

@ -97,11 +97,12 @@ func ParseValue(value string) (d2ast.Value, error) {
}
// TODO: refactor parser to keep entire file in memory as []rune
// - trivial to then convert positions
// - lookahead is gone, just forward back as much as you want :)
// - streaming parser isn't really helpful.
// - just read into a string even and decode runes forward/back as needed
// - the whole file essentially exists within the parser as the AST anyway...
// - trivial to then convert positions
// - lookahead is gone, just forward back as much as you want :)
// - streaming parser isn't really helpful.
// - just read into a string even and decode runes forward/back as needed
// - the whole file essentially exists within the parser as the AST anyway...
//
// TODO: ast struct that combines map & errors and pass that around
type parser struct {
path string
@ -315,13 +316,15 @@ func (p *parser) commit() {
//
// TODO: make each parse function read its delimiter and return nil if not as expected
// TODO: lookahead *must* always be empty in between parse calls. you either commit or
// rewind in each function. if you don't, you pass a hint.
//
// rewind in each function. if you don't, you pass a hint.
//
// TODO: omg we don't need two buffers, just a single lookahead and an index...
// TODO: get rid of lookaheadPos or at least never use directly. maybe rename to beforePeekPos?
// or better yet keep positions in the lookahead buffer.
// ok so plan here is to get rid of lookaheadPos and add a rewindPos that stores
// the pos to rewind to.
//
// or better yet keep positions in the lookahead buffer.
// ok so plan here is to get rid of lookaheadPos and add a rewindPos that stores
// the pos to rewind to.
func (p *parser) rewind() {
if len(p.lookahead) == 0 {
return

View file

@ -363,6 +363,20 @@ not part of block string
name: "edge_group_value",
text: `
q.(x -> y).z: (rawr)
`,
},
{
name: "less_than_edge#955",
text: `
x <= y
`,
},
{
name: "merged_shapes_#322",
text: `
a-
b-
c-
`,
},
}

View file

@ -22,18 +22,18 @@ import (
// The layout plugin protocol works as follows.
//
// Info
// 1. The binary is invoked with info as the first argument.
// 2. The stdout of the binary is unmarshalled into PluginInfo.
// 1. The binary is invoked with info as the first argument.
// 2. The stdout of the binary is unmarshalled into PluginInfo.
//
// Layout
// 1. The binary is invoked with layout as the first argument and the json marshalled
// d2graph.Graph on stdin.
// 2. The stdout of the binary is unmarshalled into a d2graph.Graph
// 1. The binary is invoked with layout as the first argument and the json marshalled
// d2graph.Graph on stdin.
// 2. The stdout of the binary is unmarshalled into a d2graph.Graph
//
// PostProcess
// 1. The binary is invoked with postprocess as the first argument and the
// bytes of the SVG render on stdin.
// 2. The stdout of the binary is bytes of SVG with any post-processing.
// 1. The binary is invoked with postprocess as the first argument and the
// bytes of the SVG render on stdin.
// 2. The stdout of the binary is bytes of SVG with any post-processing.
//
// If any errors occur the binary will exit with a non zero status code and write
// the error to stderr.

View file

@ -132,12 +132,12 @@ func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
}
// FindPlugin finds the plugin with the given name.
// 1. It first searches the bundled plugins in the global plugins slice.
// 2. If not found, it then searches each directory in $PATH for a binary with the name
// d2plugin-<name>.
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
// 3. If such a binary is found, it builds an execPlugin in exec.go
// to get a plugin implementation around the binary and returns it.
// 1. It first searches the bundled plugins in the global plugins slice.
// 2. If not found, it then searches each directory in $PATH for a binary with the name
// d2plugin-<name>.
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
// 3. If such a binary is found, it builds an execPlugin in exec.go
// to get a plugin implementation around the binary and returns it.
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
for _, p := range ps {
info, err := p.Info(ctx)

View file

@ -211,3 +211,8 @@ func init() {
Style: FONT_STYLE_BOLD,
}] = b
}
var D2_FONT_TO_FAMILY = map[string]FontFamily{
"default": SourceSansPro,
"mono": SourceCodePro,
}

View file

@ -98,6 +98,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range paths {
@ -145,6 +146,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range pathsBigRect {
@ -192,6 +194,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range paths {
@ -242,6 +245,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range pathsBigCircle {
@ -292,6 +296,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
}
pathEl := d2themes.NewThemableElement("path")
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range sketchPaths {
@ -316,7 +321,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
}
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
roughness := 1.0
roughness := 0.5
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
paths, err := computeRoughPathData(r, js)
if err != nil {
@ -357,6 +362,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range paths {
@ -383,6 +389,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
pathEl = d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header"
for _, p := range paths {
pathEl.D = p
@ -462,6 +469,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
}
pathEl := d2themes.NewThemableElement("path")
pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
for _, p := range paths {
pathEl.D = p
output += pathEl.Render()
@ -496,6 +504,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
pathEl := d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill, pathEl.Stroke = d2themes.ShapeTheme(shape)
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "shape"
pathEl.Style = shape.CSSStyle()
for _, p := range paths {
@ -523,6 +532,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
pathEl = d2themes.NewThemableElement("path")
pathEl.SetTranslate(float64(shape.Pos.X), float64(shape.Pos.Y))
pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header"
for _, p := range paths {
pathEl.D = p
@ -576,6 +586,7 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
}
pathEl = d2themes.NewThemableElement("path")
pathEl.Fill = shape.Fill
pathEl.FillPattern = shape.FillPattern
pathEl.ClassName = "class_header"
for _, p := range paths {
pathEl.D = p

View file

@ -18,9 +18,11 @@ import (
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@ -39,6 +41,15 @@ func TestSketch(t *testing.T) {
script: `winter.snow -> summer.sun
`,
},
{
name: "elk corners",
engine: "elk",
script: `a -> b
b -> c
a -> c
c -> a
`,
},
{
name: "animated",
script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
@ -495,6 +506,54 @@ darker: {
style.font-color: "#fff"
style.fill: "#000"
}
`,
},
{
name: "terminal",
themeID: d2themescatalog.Terminal.ID,
script: `network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: { shape: hexagon }
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: { shape: page; style.multiple: true }
network.data processor -> api server
`,
},
{
@ -1003,6 +1062,185 @@ normal: {
nested normal
}
something
`,
},
{
name: "class_and_sqlTable_border_radius",
script: `
a: {
shape: sql_table
id: int {constraint: primary_key}
disk: int {constraint: foreign_key}
json: jsonb {constraint: unique}
last_updated: timestamp with time zone
style: {
fill: red
border-radius: 0
}
}
b: {
shape: class
field: "[]string"
method(a uint64): (x, y int)
style: {
border-radius: 0
}
}
c: {
shape: class
style: {
border-radius: 0
}
}
d: {
shape: sql_table
style: {
border-radius: 0
}
}
`,
},
{
name: "dots-real",
script: `
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
}
}
}
D2 Parser: {
style.fill-pattern: grain
shape: class
+reader: io.RuneReader
# Default visibility is + so no need to specify.
readerPos: d2ast.Position
# Private field.
-lookahead: "[]rune"
# Escape the # to prevent being parsed as comment
#lookaheadPos: d2ast.Position
# Or just wrap in quotes
"#peekn(n int)": (s string, eof bool)
+peek(): (r rune, eof bool)
rewind()
commit()
}
`,
},
{
name: "dots-3d",
script: `x: {style.3d: true; style.fill-pattern: dots}
y: {shape: hexagon; style.3d: true; style.fill-pattern: dots}
`,
},
{
name: "dots-multiple",
script: `
rectangle: {shape: "rectangle"; style.fill-pattern: dots; style.multiple: true}
square: {shape: "square"; style.fill-pattern: dots; style.multiple: true}
page: {shape: "page"; style.fill-pattern: dots; style.multiple: true}
parallelogram: {shape: "parallelogram"; style.fill-pattern: dots; style.multiple: true}
document: {shape: "document"; style.fill-pattern: dots; style.multiple: true}
cylinder: {shape: "cylinder"; style.fill-pattern: dots; style.multiple: true}
queue: {shape: "queue"; style.fill-pattern: dots; style.multiple: true}
package: {shape: "package"; style.fill-pattern: dots; style.multiple: true}
step: {shape: "step"; style.fill-pattern: dots; style.multiple: true}
callout: {shape: "callout"; style.fill-pattern: dots; style.multiple: true}
stored_data: {shape: "stored_data"; style.fill-pattern: dots; style.multiple: true}
person: {shape: "person"; style.fill-pattern: dots; style.multiple: true}
diamond: {shape: "diamond"; style.fill-pattern: dots; style.multiple: true}
oval: {shape: "oval"; style.fill-pattern: dots; style.multiple: true}
circle: {shape: "circle"; style.fill-pattern: dots; style.multiple: true}
hexagon: {shape: "hexagon"; style.fill-pattern: dots; style.multiple: true}
cloud: {shape: "cloud"; style.fill-pattern: dots; style.multiple: true}
rectangle -> square -> page
parallelogram -> document -> cylinder
queue -> package -> step
callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
`,
},
{
name: "dots-all",
script: `
rectangle: {shape: "rectangle"; style.fill-pattern: dots}
square: {shape: "square"; style.fill-pattern: dots}
page: {shape: "page"; style.fill-pattern: dots}
parallelogram: {shape: "parallelogram"; style.fill-pattern: dots}
document: {shape: "document"; style.fill-pattern: dots}
cylinder: {shape: "cylinder"; style.fill-pattern: dots}
queue: {shape: "queue"; style.fill-pattern: dots}
package: {shape: "package"; style.fill-pattern: dots}
step: {shape: "step"; style.fill-pattern: dots}
callout: {shape: "callout"; style.fill-pattern: dots}
stored_data: {shape: "stored_data"; style.fill-pattern: dots}
person: {shape: "person"; style.fill-pattern: dots}
diamond: {shape: "diamond"; style.fill-pattern: dots}
oval: {shape: "oval"; style.fill-pattern: dots}
circle: {shape: "circle"; style.fill-pattern: dots}
hexagon: {shape: "hexagon"; style.fill-pattern: dots}
cloud: {shape: "cloud"; style.fill-pattern: dots}
rectangle -> square -> page
parallelogram -> document -> cylinder
queue -> package -> step
callout -> stored_data -> person
diamond -> oval -> circle
hexagon -> cloud
`,
},
}
@ -1014,6 +1252,7 @@ type testCase struct {
themeID int64
script string
skip bool
engine string
}
func runa(t *testing.T, tcs []testCase) {
@ -1040,10 +1279,15 @@ func run(t *testing.T, tc testCase) {
return
}
layout := d2dagrelayout.DefaultLayout
if strings.EqualFold(tc.engine, "elk") {
layout = d2elklayout.DefaultLayout
}
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout,
Layout: layout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
ThemeID: tc.themeID,
})
if !tassert.Nil(t, err) {
return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 299 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 290 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 285 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 276 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 335 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 326 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 280 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 271 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 285 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 278 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 269 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 325 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 333 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 350 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 316 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 290 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 235 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 341 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 332 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 230 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 307 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 107 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 355 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 420 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 420 KiB

View file

@ -136,8 +136,9 @@ func run(t *testing.T, tc testCase) {
}
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout,
Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout,
ThemeID: tc.themeID,
})
if !tassert.Nil(t, err) {
return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 806 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 978 KiB

View file

@ -11,12 +11,16 @@ import (
"oss.terrastruct.com/d2/lib/svg"
)
func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
func classHeader(diagramHash string, shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
rectEl := d2themes.NewThemableElement("rect")
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y
rectEl.Width, rectEl.Height = box.Width, box.Height
rectEl.Fill = shape.Fill
rectEl.FillPattern = shape.FillPattern
rectEl.ClassName = "class_header"
if shape.BorderRadius != 0 {
rectEl.ClipPath = fmt.Sprintf("%v-%v", diagramHash, shape.ID)
}
str := rectEl.Render()
if text != "" {
@ -81,14 +85,19 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
return out
}
func drawClass(writer io.Writer, targetShape d2target.Shape) {
func drawClass(writer io.Writer, diagramHash string, targetShape d2target.Shape) {
el := d2themes.NewThemableElement("rect")
el.X = float64(targetShape.Pos.X)
el.Y = float64(targetShape.Pos.Y)
el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height)
el.Fill, el.Stroke = d2themes.ShapeTheme(targetShape)
el.FillPattern = targetShape.FillPattern
el.Style = targetShape.CSSStyle()
if targetShape.BorderRadius != 0 {
el.Rx = float64(targetShape.BorderRadius)
el.Ry = float64(targetShape.BorderRadius)
}
fmt.Fprint(writer, el.Render())
box := geo.NewBox(
@ -100,7 +109,7 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
fmt.Fprint(writer,
classHeader(targetShape, headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
classHeader(diagramHash, targetShape, headerBox, targetShape.Label, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
)
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
@ -113,8 +122,15 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) {
}
lineEl := d2themes.NewThemableElement("line")
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
if targetShape.BorderRadius != 0 && len(targetShape.Methods) == 0 {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X+float64(targetShape.BorderRadius), rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width-float64(targetShape.BorderRadius), rowBox.TopLeft.Y
} else {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
}
lineEl.Stroke = targetShape.Fill
lineEl.Style = "stroke-width:1"
fmt.Fprint(writer, lineEl.Render())

View file

@ -35,6 +35,7 @@ import (
"oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/svg"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
)
const (
@ -58,6 +59,15 @@ var baseStylesheet string
//go:embed github-markdown.css
var mdCSS string
//go:embed dots.txt
var dots string
//go:embed lines.txt
var lines string
//go:embed grain.txt
var grain string
type RenderOpts struct {
Pad int
Sketch bool
@ -558,6 +568,9 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
if connection.Label != "" {
fontClass := "text"
if connection.FontFamily == "mono" {
fontClass = "text-mono"
}
if connection.Bold {
fontClass += "-bold"
} else if connection.Italic {
@ -617,21 +630,22 @@ func renderArrowheadLabel(connection d2target.Connection, text string, position,
return textEl.Render()
}
func renderOval(tl *geo.Point, width, height float64, fill, stroke, style string) string {
func renderOval(tl *geo.Point, width, height float64, fill, fillPattern, stroke, style string) string {
el := d2themes.NewThemableElement("ellipse")
el.Rx = width / 2
el.Ry = height / 2
el.Cx = tl.X + el.Rx
el.Cy = tl.Y + el.Ry
el.Fill, el.Stroke = fill, stroke
el.FillPattern = fillPattern
el.ClassName = "shape"
el.Style = style
return el.Render()
}
func renderDoubleOval(tl *geo.Point, width, height float64, fill, stroke, style string) string {
func renderDoubleOval(tl *geo.Point, width, height float64, fill, fillStroke, stroke, style string) string {
var innerTL *geo.Point = tl.AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET))
return renderOval(tl, width, height, fill, stroke, style) + renderOval(innerTL, width-10, height-10, fill, stroke, style)
return renderOval(tl, width, height, fill, fillStroke, stroke, style) + renderOval(innerTL, width-10, height-10, fill, "", stroke, style)
}
func defineShadowFilter(writer io.Writer) {
@ -709,6 +723,7 @@ func render3dRect(targetShape d2target.Shape) string {
mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
mainShape.Fill = mainShapeFill
mainShape.FillPattern = targetShape.FillPattern
mainShape.Stroke = color.None
mainShape.Style = targetShape.CSSStyle()
mainShapeRendered := mainShape.Render()
@ -829,6 +844,7 @@ func render3dHexagon(targetShape d2target.Shape) string {
mainShape.Points = mainPointsPoly
mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
mainShape.FillPattern = targetShape.FillPattern
mainShape.Fill = mainShapeFill
mainShape.Stroke = color.None
mainShape.Style = targetShape.CSSStyle()
@ -865,7 +881,7 @@ func render3dHexagon(targetShape d2target.Shape) string {
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
func drawShape(writer io.Writer, diagramHash string, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
closingTag := "</g>"
if targetShape.Link != "" {
@ -877,6 +893,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if targetShape.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity)
}
// this clipPath must be defined outside `g` element
if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) {
fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape))
}
fmt.Fprintf(writer, `<g id="%s"%s>`, svg.EscapeText(targetShape.ID), opacityStyle)
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width)
@ -920,7 +941,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, out)
} else {
drawClass(writer, targetShape)
drawClass(writer, diagramHash, targetShape)
}
addAppendixItems(writer, targetShape)
fmt.Fprint(writer, `</g>`)
@ -934,7 +955,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, out)
} else {
drawTable(writer, targetShape)
drawTable(writer, diagramHash, targetShape)
}
addAppendixItems(writer, targetShape)
fmt.Fprint(writer, `</g>`)
@ -943,7 +964,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
case d2target.ShapeOval:
if targetShape.DoubleBorder {
if targetShape.Multiple {
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, stroke, style))
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style))
}
if sketchRunner != nil {
out, err := d2sketch.DoubleOval(sketchRunner, targetShape)
@ -952,11 +973,11 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, stroke, style))
fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
}
} else {
if targetShape.Multiple {
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, stroke, style))
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style))
}
if sketchRunner != nil {
out, err := d2sketch.Oval(sketchRunner, targetShape)
@ -965,7 +986,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
}
fmt.Fprint(writer, out)
} else {
fmt.Fprint(writer, renderOval(tl, width, height, fill, stroke, style))
fmt.Fprint(writer, renderOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
}
}
@ -1013,6 +1034,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height)
el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
el.Rx = targetShape.BorderRadius
@ -1027,6 +1049,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height)
el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
el.Rx = targetShape.BorderRadius
@ -1058,6 +1081,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Width = float64(targetShape.Width)
el.Height = float64(targetShape.Height)
el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
el.Rx = targetShape.BorderRadius
@ -1069,7 +1093,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
el.Y = float64(targetShape.Pos.Y + d2target.INNER_BORDER_OFFSET)
el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
el.Fill = fill
el.Fill = "transparent"
el.Stroke = stroke
el.Style = style
el.Rx = targetShape.BorderRadius
@ -1103,6 +1127,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
el := d2themes.NewThemableElement("path")
el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
for _, pathData := range s.GetSVGPathData() {
@ -1134,6 +1159,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
el := d2themes.NewThemableElement("path")
el.Fill = fill
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
for _, pathData := range s.GetSVGPathData() {
@ -1186,10 +1212,14 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
)
fontClass := "text"
if targetShape.Bold {
fontClass += "-bold"
} else if targetShape.Italic {
fontClass += "-italic"
if targetShape.FontFamily == "mono" {
fontClass = "text-mono"
} else {
if targetShape.Bold {
fontClass += "-bold"
} else if targetShape.Italic {
fontClass += "-italic"
}
}
if targetShape.Underline {
fontClass += " text-underline"
@ -1348,7 +1378,7 @@ func RenderText(text string, x, height float64) string {
return strings.Join(rendered, "")
}
func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily) {
func embedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily) {
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
appendOnTrigger(
@ -1360,13 +1390,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`class="md"`,
},
fmt.Sprintf(`
.text {
font-family: "font-regular";
.%s .text {
font-family: "%s-font-regular";
}
@font-face {
font-family: font-regular;
font-family: %s-font-regular;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR)],
),
)
@ -1419,13 +1452,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<strong>`,
},
fmt.Sprintf(`
.text-bold {
font-family: "font-bold";
.%s .text-bold {
font-family: "%s-font-bold";
}
@font-face {
font-family: font-bold;
font-family: %s-font-bold;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD)],
),
)
@ -1439,13 +1475,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<dfn>`,
},
fmt.Sprintf(`
.text-italic {
font-family: "font-italic";
.%s .text-italic {
font-family: "%s-font-italic";
}
@font-face {
font-family: font-italic;
font-family: %s-font-italic;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC)],
),
)
@ -1461,13 +1500,16 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
`<samp>`,
},
fmt.Sprintf(`
.text-mono {
font-family: "font-mono";
.%s .text-mono {
font-family: "%s-font-mono";
}
@font-face {
font-family: font-mono;
font-family: %s-font-mono;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR)],
),
)
@ -1476,16 +1518,19 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
buf,
source,
[]string{
`class="text-mono-bold"`,
`class="text-mono-bold`,
},
fmt.Sprintf(`
.text-mono-bold {
font-family: "font-mono-bold";
.%s .text-mono-bold {
font-family: "%s-font-mono-bold";
}
@font-face {
font-family: font-mono-bold;
font-family: %s-font-mono-bold;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)],
),
)
@ -1494,52 +1539,19 @@ func embedFonts(buf *bytes.Buffer, source string, fontFamily *d2fonts.FontFamily
buf,
source,
[]string{
`class="text-mono-italic"`,
`class="text-mono-italic`,
},
fmt.Sprintf(`
.text-mono-italic {
font-family: "font-mono-italic";
.%s .text-mono-italic {
font-family: "%s-font-mono-italic";
}
@font-face {
font-family: font-mono-italic;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)],
),
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-mono-bold"`,
},
fmt.Sprintf(`
.text-mono-bold {
font-family: "font-mono-bold";
}
@font-face {
font-family: font-mono-bold;
src: url("%s");
}`,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD)],
),
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-mono-italic"`,
},
fmt.Sprintf(`
.text-mono-italic {
font-family: "font-mono-italic";
}
@font-face {
font-family: font-mono-italic;
font-family: %s-font-mono-italic;
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
d2fonts.FontEncodings[d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC)],
),
)
@ -1644,10 +1656,12 @@ 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.
labelMaskID, err := diagram.HashID()
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
// 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
@ -1667,7 +1681,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, labelMaskID, c, markers, idToShape, sketchRunner)
labelMask, err := drawConnection(buf, diagramHash, c, markers, idToShape, sketchRunner)
if err != nil {
return nil, err
}
@ -1675,7 +1689,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
labelMasks = append(labelMasks, labelMask)
}
} else if s, is := obj.(d2target.Shape); is {
labelMask, err := drawShape(buf, s, sketchRunner)
labelMask, err := drawShape(buf, diagramHash, s, sketchRunner)
if err != nil {
return nil, err
} else if labelMask != "" {
@ -1690,7 +1704,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">`,
labelMaskID, left, top, w, h,
diagramHash, left, top, w, h,
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
left, top, w, h,
@ -1701,8 +1715,8 @@ 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, buf.String(), diagram.FontFamily) // embedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
themeStylesheet, err := themeCSS(themeID, darkThemeID)
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
}
@ -1716,8 +1730,14 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
}
if hasMarkdown {
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, mdCSS)
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)
}
if sketchRunner != nil {
d2sketch.DefineFillPatterns(upperBuf)
}
@ -1735,6 +1755,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
backgroundEl.Height = float64(h)
backgroundEl.Fill = diagram.Root.Fill
backgroundEl.Stroke = diagram.Root.Stroke
backgroundEl.FillPattern = diagram.Root.FillPattern
backgroundEl.Rx = diagram.Root.BorderRadius
if diagram.Root.StrokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(diagram.Root.StrokeWidth), diagram.Root.StrokeDash)
@ -1773,21 +1794,52 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
}
bufStr := buf.String()
patternDefs := ""
for _, pattern := range d2graph.FillPatterns {
if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern != "" {
if patternDefs == "" {
fmt.Fprint(upperBuf, `<style type="text/css"><![CDATA[`)
}
switch pattern {
case "dots":
patternDefs += dots
case "lines":
patternDefs += lines
case "grain":
patternDefs += grain
}
fmt.Fprint(upperBuf, fmt.Sprintf(`
.%s-overlay {
fill: url(#%s);
mix-blend-mode: multiply;
}`, pattern, pattern))
}
}
if patternDefs != "" {
fmt.Fprint(upperBuf, `]]></style>`)
fmt.Fprint(upperBuf, "<defs>")
fmt.Fprintf(upperBuf, patternDefs)
fmt.Fprint(upperBuf, "</defs>")
}
var dimensions string
if setDimensions {
dimensions = fmt.Sprintf(` width="%d" height="%d"`, w, h)
}
fitToScreenWrapper := fmt.Sprintf(`<svg %s preserveAspectRatio="xMinYMin meet" viewBox="0 0 %d %d"%s>`,
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,
)
// TODO minify
docRendered := fmt.Sprintf(`%s%s<svg id="d2-svg" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</svg></svg>`,
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,
diagramHash,
w, h, left, top, w, h,
doubleBorderElStr,
backgroundEl.Render(),
@ -1798,14 +1850,14 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
// TODO include only colors that are being used to reduce size
func themeCSS(themeID int64, darkThemeID *int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(themeID)
func themeCSS(diagramHash string, themeID int64, darkThemeID *int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(diagramHash, themeID)
if err != nil {
return "", err
}
if darkThemeID != nil {
darkOut, err := singleThemeRulesets(*darkThemeID)
darkOut, err := singleThemeRulesets(diagramHash, *darkThemeID)
if err != nil {
return "", err
}
@ -1815,30 +1867,66 @@ func themeCSS(themeID int64, darkThemeID *int64) (stylesheet string, err error)
return out, nil
}
func singleThemeRulesets(themeID int64) (rulesets string, err error) {
func singleThemeRulesets(diagramHash string, themeID int64) (rulesets string, err error) {
out := ""
theme := d2themescatalog.Find(themeID)
// Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} {
out += fmt.Sprintf(".%s-N1{%s:%s;}.%s-N2{%s:%s;}.%s-N3{%s:%s;}.%s-N4{%s:%s;}.%s-N5{%s:%s;}.%s-N6{%s:%s;}.%s-N7{%s:%s;}.%s-B1{%s:%s;}.%s-B2{%s:%s;}.%s-B3{%s:%s;}.%s-B4{%s:%s;}.%s-B5{%s:%s;}.%s-B6{%s:%s;}.%s-AA2{%s:%s;}.%s-AA4{%s:%s;}.%s-AA5{%s:%s;}.%s-AB4{%s:%s;}.%s-AB5{%s:%s;}",
out += fmt.Sprintf(`
.%s .%s-N1{%s:%s;}
.%s .%s-N2{%s:%s;}
.%s .%s-N3{%s:%s;}
.%s .%s-N4{%s:%s;}
.%s .%s-N5{%s:%s;}
.%s .%s-N6{%s:%s;}
.%s .%s-N7{%s:%s;}
.%s .%s-B1{%s:%s;}
.%s .%s-B2{%s:%s;}
.%s .%s-B3{%s:%s;}
.%s .%s-B4{%s:%s;}
.%s .%s-B5{%s:%s;}
.%s .%s-B6{%s:%s;}
.%s .%s-AA2{%s:%s;}
.%s .%s-AA4{%s:%s;}
.%s .%s-AA5{%s:%s;}
.%s .%s-AB4{%s:%s;}
.%s .%s-AB5{%s:%s;}`,
diagramHash,
property, property, theme.Colors.Neutrals.N1,
diagramHash,
property, property, theme.Colors.Neutrals.N2,
diagramHash,
property, property, theme.Colors.Neutrals.N3,
diagramHash,
property, property, theme.Colors.Neutrals.N4,
diagramHash,
property, property, theme.Colors.Neutrals.N5,
diagramHash,
property, property, theme.Colors.Neutrals.N6,
diagramHash,
property, property, theme.Colors.Neutrals.N7,
diagramHash,
property, property, theme.Colors.B1,
diagramHash,
property, property, theme.Colors.B2,
diagramHash,
property, property, theme.Colors.B3,
diagramHash,
property, property, theme.Colors.B4,
diagramHash,
property, property, theme.Colors.B5,
diagramHash,
property, property, theme.Colors.B6,
diagramHash,
property, property, theme.Colors.AA2,
diagramHash,
property, property, theme.Colors.AA4,
diagramHash,
property, property, theme.Colors.AA5,
diagramHash,
property, property, theme.Colors.AB4,
diagramHash,
property, property, theme.Colors.AB5,
)
}

View file

@ -430,6 +430,7 @@ func run(t *testing.T, tc testCase) {
Ruler: ruler,
Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
ThemeID: 200,
})
if !tassert.Nil(t, err) {
return

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 197 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 415 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 300 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 189 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View file

@ -0,0 +1,29 @@
<pattern id="dots" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="2" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="12" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="2" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="12" width="1" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.1">
<rect x="7" y="7" width="1" height="1" fill="#0A0F25"/>
</g>
</pattern>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
<pattern id="lines" x="0" y="0" width="15" height="15" patternUnits="userSpaceOnUse">
<g style="mix-blend-mode:multiply">
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="2" width="15" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="7" width="15" height="1" fill="#0A0F25"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.05">
<rect y="12" width="15" height="1" fill="#0A0F25"/>
</g>
</g>
</pattern>

View file

@ -12,12 +12,44 @@ import (
"oss.terrastruct.com/util-go/go2"
)
func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
// this func helps define a clipPath for shape class and sql_table to draw border-radius
func clipPathForBorderRadius(diagramHash string, shape d2target.Shape) string {
box := geo.NewBox(
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
float64(shape.Width),
float64(shape.Height),
)
topX, topY := box.TopLeft.X+box.Width, box.TopLeft.Y
out := fmt.Sprintf(`<clipPath id="%v-%v">`, diagramHash, shape.ID)
out += fmt.Sprintf(`<path d="M %f %f L %f %f S %f %f %f %f `, box.TopLeft.X, box.TopLeft.Y+float64(shape.BorderRadius), box.TopLeft.X, box.TopLeft.Y+float64(shape.BorderRadius), box.TopLeft.X, box.TopLeft.Y, box.TopLeft.X+float64(shape.BorderRadius), box.TopLeft.Y)
out += fmt.Sprintf(`L %f %f L %f %f `, box.TopLeft.X+box.Width-float64(shape.BorderRadius), box.TopLeft.Y, topX-float64(shape.BorderRadius), topY)
out += fmt.Sprintf(`S %f %f %f %f `, topX, topY, topX, topY+float64(shape.BorderRadius))
out += fmt.Sprintf(`L %f %f `, topX, topY+box.Height-float64(shape.BorderRadius))
if len(shape.Columns) != 0 {
out += fmt.Sprintf(`L %f %f L %f %f`, topX, topY+box.Height, box.TopLeft.X, box.TopLeft.Y+box.Height)
} else {
out += fmt.Sprintf(`S %f % f %f %f `, topX, topY+box.Height, topX-float64(shape.BorderRadius), topY+box.Height)
out += fmt.Sprintf(`L %f %f `, box.TopLeft.X+float64(shape.BorderRadius), box.TopLeft.Y+box.Height)
out += fmt.Sprintf(`S %f %f %f %f`, box.TopLeft.X, box.TopLeft.Y+box.Height, box.TopLeft.X, box.TopLeft.Y+box.Height-float64(shape.BorderRadius))
out += fmt.Sprintf(`L %f %f`, box.TopLeft.X, box.TopLeft.Y+float64(shape.BorderRadius))
}
out += fmt.Sprintf(`Z %f %f" `, box.TopLeft.X, box.TopLeft.Y)
return out + `fill="none" /> </clipPath>`
}
func tableHeader(diagramHash string, shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
rectEl := d2themes.NewThemableElement("rect")
rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y
rectEl.Width, rectEl.Height = box.Width, box.Height
rectEl.Fill = shape.Fill
rectEl.FillPattern = shape.FillPattern
rectEl.ClassName = "class_header"
if shape.BorderRadius != 0 {
rectEl.ClipPath = fmt.Sprintf("%v-%v", diagramHash, shape.ID)
}
str := rectEl.Render()
if text != "" {
@ -82,15 +114,20 @@ func tableRow(shape d2target.Shape, box *geo.Box, nameText, typeText, constraint
return out
}
func drawTable(writer io.Writer, targetShape d2target.Shape) {
func drawTable(writer io.Writer, diagramHash string, targetShape d2target.Shape) {
rectEl := d2themes.NewThemableElement("rect")
rectEl.X = float64(targetShape.Pos.X)
rectEl.Y = float64(targetShape.Pos.Y)
rectEl.Width = float64(targetShape.Width)
rectEl.Height = float64(targetShape.Height)
rectEl.Fill, rectEl.Stroke = d2themes.ShapeTheme(targetShape)
rectEl.FillPattern = targetShape.FillPattern
rectEl.ClassName = "shape"
rectEl.Style = targetShape.CSSStyle()
if targetShape.BorderRadius != 0 {
rectEl.Rx = float64(targetShape.BorderRadius)
rectEl.Ry = float64(targetShape.BorderRadius)
}
fmt.Fprint(writer, rectEl.Render())
box := geo.NewBox(
@ -102,7 +139,7 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) {
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
fmt.Fprint(writer,
tableHeader(targetShape, headerBox, targetShape.Label,
tableHeader(diagramHash, targetShape, headerBox, targetShape.Label,
float64(targetShape.LabelWidth), float64(targetShape.LabelHeight), float64(targetShape.FontSize)),
)
@ -113,15 +150,20 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) {
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight)
rowBox.TopLeft.Y += headerBox.Height
for _, f := range targetShape.Columns {
for idx, f := range targetShape.Columns {
fmt.Fprint(writer,
tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)),
)
rowBox.TopLeft.Y += rowHeight
lineEl := d2themes.NewThemableElement("line")
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
if idx == len(targetShape.Columns)-1 && targetShape.BorderRadius != 0 {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X+float64(targetShape.BorderRadius), rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width-float64(targetShape.BorderRadius), rowBox.TopLeft.Y
} else {
lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
}
lineEl.Stroke = targetShape.Fill
lineEl.Style = "stroke-width:2"
fmt.Fprint(writer, lineEl.Render())

View file

@ -176,8 +176,9 @@ type Shape struct {
BorderRadius float64 `json:"borderRadius"`
Fill string `json:"fill"`
Stroke string `json:"stroke"`
Fill string `json:"fill"`
FillPattern string `json:"fillPattern,omitempty"`
Stroke string `json:"stroke"`
Shadow bool `json:"shadow"`
ThreeDee bool `json:"3d"`

View file

@ -18,14 +18,10 @@ Run `d2 --help` or `man d2` for more.
# Color coding guide
To distinguish container nesting, objects get progressively lighter the more nested it is.
<img src="../docs/assets/themes_coding.png" />
# Color coding example
<img src="../docs/assets/themes_coding_example.png" />
# Container gradients
To distinguish container nesting, objects get progressively lighter the more nested it is.
<img src="../docs/assets/themes_gradients.png" width="300px" />

View file

@ -8,6 +8,16 @@ type Theme struct {
ID int64 `json:"id"`
Name string `json:"name"`
Colors ColorPalette `json:"colors"`
SpecialRules SpecialRules `json:"specialRules,omitempty"`
}
type SpecialRules struct {
Mono bool `json:"mono"`
NoCornerRadius bool `json:"noCornerRadius"`
OuterContainerDoubleBorder bool `json:"outerContainerDoubleBorder"`
ContainerDots bool `json:"containerDots"`
CapsLock bool `json:"capsLock"`
}
func (t *Theme) IsDark() bool {

View file

@ -22,6 +22,8 @@ var LightCatalog = []d2themes.Theme{
EarthTones,
EvergladeGreen,
ButteredToast,
Terminal,
TerminalGrayscale,
}
var DarkCatalog = []d2themes.Theme{

View file

@ -0,0 +1,42 @@
package d2themescatalog
import "oss.terrastruct.com/d2/d2themes"
var Terminal = d2themes.Theme{
ID: 300,
Name: "Terminal",
Colors: d2themes.ColorPalette{
Neutrals: TerminalNeutral,
B1: "#000410",
B2: "#0000E4",
B3: "#5AA4DC",
B4: "#E7E9EE",
B5: "#F5F6F9",
B6: "#FFFFFF",
AA2: "#008566",
AA4: "#45BBA5",
AA5: "#7ACCBD",
AB4: "#F1C759",
AB5: "#F9E088",
},
SpecialRules: d2themes.SpecialRules{
Mono: true,
NoCornerRadius: true,
OuterContainerDoubleBorder: true,
ContainerDots: true,
CapsLock: true,
},
}
var TerminalNeutral = d2themes.Neutral{
N1: "#000410",
N2: "#0000B8",
N3: "#9499AB",
N4: "#CFD2DD",
N5: "#C3DEF3",
N6: "#EEF1F8",
N7: "#FFFFFF",
}

View file

@ -0,0 +1,42 @@
package d2themescatalog
import "oss.terrastruct.com/d2/d2themes"
var TerminalGrayscale = d2themes.Theme{
ID: 301,
Name: "Terminal Grayscale",
Colors: d2themes.ColorPalette{
Neutrals: TerminalGrayscaleNeutral,
B1: "#000410",
B2: "#000410",
B3: "#FFFFFF",
B4: "#E7E9EE",
B5: "#F5F6F9",
B6: "#FFFFFF",
AA2: "#6D7284",
AA4: "#F5F6F9",
AA5: "#FFFFFF",
AB4: "#F5F6F9",
AB5: "#FFFFFF",
},
SpecialRules: d2themes.SpecialRules{
Mono: true,
NoCornerRadius: true,
OuterContainerDoubleBorder: true,
ContainerDots: true,
CapsLock: true,
},
}
var TerminalGrayscaleNeutral = d2themes.Neutral{
N1: "#000410",
N2: "#000410",
N3: "#9499AB",
N4: "#FFFFFF",
N5: "#FFFFFF",
N6: "#EEF1F8",
N7: "#FFFFFF",
}

View file

@ -45,7 +45,10 @@ type ThemableElement struct {
Style string
Attributes string
Content string
Content string
ClipPath string
FillPattern string
}
func NewThemableElement(tag string) *ThemableElement {
@ -84,6 +87,8 @@ func NewThemableElement(tag string) *ThemableElement {
"",
"",
"",
"",
"",
}
}
@ -201,10 +206,26 @@ func (el *ThemableElement) Render() string {
out += fmt.Sprintf(` %s`, el.Attributes)
}
if len(el.ClipPath) > 0 {
out += fmt.Sprintf(` clip-path="url(#%s)"`, el.ClipPath)
}
if len(el.Content) > 0 {
return fmt.Sprintf("%s>%s</%s>", out, el.Content, el.tag)
}
return out + " />"
out += " />"
if el.FillPattern != "" {
patternEl := el.Copy()
patternEl.Fill = ""
patternEl.Stroke = ""
patternEl.BackgroundColor = ""
patternEl.Color = ""
patternEl.ClassName = fmt.Sprintf("%s-overlay", el.FillPattern)
patternEl.FillPattern = ""
out += patternEl.Render()
}
return out
}
func calculateAxisRadius(borderRadius float64, sideLength float64) float64 {

View file

@ -0,0 +1,24 @@
package d2themes
import (
"fmt"
)
type PatternOverlay struct {
el *ThemableElement
pattern string
}
func NewPatternOverlay(el *ThemableElement, pattern string) *PatternOverlay {
return &PatternOverlay{
el,
pattern,
}
}
func (o *PatternOverlay) Render() (string, error) {
el := o.el.Copy()
el.Fill = ""
el.ClassName = fmt.Sprintf("%s-overlay", o.pattern)
return el.Render(), nil
}

View file

@ -18,6 +18,7 @@ func NewThemableSketchOverlay(el *ThemableElement, fill string) *ThemableSketchO
}
}
// TODO we can just call el.Copy() to prevent that
// WARNING: Do not reuse the element afterwards as this function changes the Class propery
func (o *ThemableSketchOverlay) Render() (string, error) {
if color.IsThemeColor(o.fill) {

View file

@ -66,8 +66,7 @@ git submodule update --recursive
## Logistics
- Use Go 1.18. Go 1.19's autofmt inexplicably strips spacing from ASCII art in comments.
We're working on it.
- Use Go 1.20.
- Please sign your commits
([https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730](https://github.com/terrastruct/d2/pull/557#issuecomment-1367468730)).
- D2 uses Issues as TODOs. No auto-closing on staleness.

View file

@ -158,7 +158,7 @@ You can always install from source:
go install oss.terrastruct.com/d2@latest
```
You need at least Go v1.18
You need at least Go v1.20
### Source Release
@ -175,7 +175,7 @@ fonts and icons. Furthermore, when installing a non versioned commit, installing
will ensure that `d2 --version` works correctly by embedding the commit hash into the `d2`
binary.
Remember, you need at least Go v1.18
Remember, you need at least Go v1.20
## Windows

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -17,6 +17,7 @@ import (
// Remember to add if err != nil checks in production.
func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
graph.ApplyTheme(d2themescatalog.NeutralDefault.ID)
ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler, nil)
_ = d2dagrelayout.Layout(context.Background(), graph, nil)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

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