2022-11-03 13:54:49 +00:00
package d2compiler
import (
2023-03-07 17:00:45 +00:00
"encoding/xml"
2022-11-03 13:54:49 +00:00
"fmt"
2024-09-25 15:53:46 +00:00
"html"
2022-11-03 13:54:49 +00:00
"io"
2023-06-04 22:39:00 +00:00
"io/fs"
2022-11-03 13:54:49 +00:00
"net/url"
2024-11-08 21:26:38 +00:00
"slices"
2022-11-03 13:54:49 +00:00
"strconv"
"strings"
2022-12-01 18:48:01 +00:00
"oss.terrastruct.com/util-go/go2"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
2023-01-22 07:49:07 +00:00
"oss.terrastruct.com/d2/d2ir"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2target"
2023-12-14 19:35:35 +00:00
"oss.terrastruct.com/d2/lib/color"
2025-03-04 19:05:39 +00:00
"oss.terrastruct.com/d2/lib/geo"
2023-03-07 17:00:45 +00:00
"oss.terrastruct.com/d2/lib/textmeasure"
2022-11-03 13:54:49 +00:00
)
type CompileOptions struct {
2023-08-02 17:26:45 +00:00
UTF16Pos bool
2023-06-04 22:39:00 +00:00
// FS is the file system used for resolving imports in the d2 text.
// It should correspond to the root path.
FS fs . FS
2022-11-03 13:54:49 +00:00
}
2023-08-02 17:26:45 +00:00
func Compile ( p string , r io . Reader , opts * CompileOptions ) ( * d2graph . Graph , * d2target . Config , error ) {
2022-11-03 13:54:49 +00:00
if opts == nil {
opts = & CompileOptions { }
}
2023-06-06 20:30:01 +00:00
ast , err := d2parser . Parse ( p , r , & d2parser . ParseOptions {
2023-08-02 17:26:45 +00:00
UTF16Pos : opts . UTF16Pos ,
2022-11-03 13:54:49 +00:00
} )
if err != nil {
2023-07-14 20:08:26 +00:00
return nil , nil , err
2023-01-22 07:49:07 +00:00
}
2023-12-25 05:48:14 +00:00
ir , _ , err := d2ir . Compile ( ast , & d2ir . CompileOptions {
2023-08-02 17:26:45 +00:00
UTF16Pos : opts . UTF16Pos ,
FS : opts . FS ,
2023-06-04 22:39:00 +00:00
} )
2023-01-22 07:49:07 +00:00
if err != nil {
2023-07-14 20:08:26 +00:00
return nil , nil , err
2023-01-22 07:49:07 +00:00
}
2023-01-28 01:19:12 +00:00
g , err := compileIR ( ast , ir )
2023-01-22 07:49:07 +00:00
if err != nil {
2023-07-14 20:08:26 +00:00
return nil , nil , err
2022-11-03 13:54:49 +00:00
}
2023-08-05 03:16:25 +00:00
g . FS = opts . FS
2023-02-02 20:24:48 +00:00
g . SortObjectsByAST ( )
2023-01-28 07:45:04 +00:00
g . SortEdgesByAST ( )
2023-12-14 19:35:35 +00:00
config , err := compileConfig ( ir )
if err != nil {
return nil , nil , err
}
return g , config , nil
2023-01-22 07:49:07 +00:00
}
2022-11-03 13:54:49 +00:00
2023-01-28 01:19:12 +00:00
func compileIR ( ast * d2ast . Map , m * d2ir . Map ) ( * d2graph . Graph , error ) {
2023-06-04 22:39:00 +00:00
c := & compiler {
err : & d2parser . ParseError { } ,
}
2023-01-24 06:45:21 +00:00
2023-01-28 01:19:12 +00:00
g := d2graph . NewGraph ( )
g . AST = ast
2024-06-03 03:31:42 +00:00
g . BaseAST = ast
2023-01-28 01:19:12 +00:00
c . compileBoard ( g , m )
2023-01-24 06:45:21 +00:00
if len ( c . err . Errors ) > 0 {
return nil , c . err
2023-01-22 07:49:07 +00:00
}
2023-03-01 22:53:58 +00:00
c . validateBoardLinks ( g )
2023-03-01 18:28:01 +00:00
if len ( c . err . Errors ) > 0 {
return nil , c . err
}
2023-01-24 06:45:21 +00:00
return g , nil
2022-11-03 13:54:49 +00:00
}
2023-01-28 01:19:12 +00:00
func ( c * compiler ) compileBoard ( g * d2graph . Graph , ir * d2ir . Map ) * d2graph . Graph {
2023-02-02 18:30:54 +00:00
ir = ir . Copy ( nil ) . ( * d2ir . Map )
// c.preprocessSeqDiagrams(ir)
2023-01-28 01:19:12 +00:00
c . compileMap ( g . Root , ir )
2025-03-02 20:26:58 +00:00
c . setDefaultShapes ( g )
2022-11-03 13:54:49 +00:00
if len ( c . err . Errors ) == 0 {
2023-01-28 01:19:12 +00:00
c . validateKeys ( g . Root , ir )
2022-11-03 13:54:49 +00:00
}
2023-09-18 20:16:50 +00:00
c . validateLabels ( g )
2022-11-03 13:54:49 +00:00
c . validateNear ( g )
2023-04-05 00:44:05 +00:00
c . validateEdges ( g )
2024-03-15 22:26:00 +00:00
c . validatePositionsCompatibility ( g )
2022-11-03 13:54:49 +00:00
2025-03-04 17:14:32 +00:00
c . compileLegend ( g , ir )
2023-01-28 01:19:12 +00:00
c . compileBoardsField ( g , ir , "layers" )
c . compileBoardsField ( g , ir , "scenarios" )
c . compileBoardsField ( g , ir , "steps" )
2023-02-27 21:34:03 +00:00
if d2ir . ParentMap ( ir ) . CopyBase ( nil ) . Equal ( ir . CopyBase ( nil ) ) {
if len ( g . Layers ) > 0 || len ( g . Scenarios ) > 0 || len ( g . Steps ) > 0 {
2023-02-27 22:33:33 +00:00
g . IsFolderOnly = true
2023-02-27 21:34:03 +00:00
}
}
2023-07-29 16:56:36 +00:00
if len ( g . Objects ) == 0 {
g . IsFolderOnly = true
}
2023-01-24 06:45:21 +00:00
return g
}
2025-03-04 17:14:32 +00:00
func ( c * compiler ) compileLegend ( g * d2graph . Graph , m * d2ir . Map ) {
varsField := m . GetField ( d2ast . FlatUnquotedString ( "vars" ) )
if varsField == nil || varsField . Map ( ) == nil {
return
}
legendField := varsField . Map ( ) . GetField ( d2ast . FlatUnquotedString ( "d2-legend" ) )
if legendField == nil || legendField . Map ( ) == nil {
return
}
legendGraph := d2graph . NewGraph ( )
c . compileMap ( legendGraph . Root , legendField . Map ( ) )
c . setDefaultShapes ( legendGraph )
objects := make ( [ ] * d2graph . Object , 0 )
for _ , obj := range legendGraph . Objects {
if obj . Style . Opacity != nil {
if opacity , err := strconv . ParseFloat ( obj . Style . Opacity . Value , 64 ) ; err == nil && opacity == 0 {
continue
}
}
2025-03-04 19:05:39 +00:00
obj . Box = & geo . Box { }
2025-03-04 21:18:58 +00:00
obj . TopLeft = geo . NewPoint ( 10 , 10 )
2025-03-04 21:21:33 +00:00
obj . Width = 100
obj . Height = 100
2025-03-04 17:14:32 +00:00
objects = append ( objects , obj )
}
2025-03-04 19:05:39 +00:00
for _ , edge := range legendGraph . Edges {
edge . Route = [ ] * geo . Point {
2025-03-04 21:18:58 +00:00
{ X : 10 , Y : 10 } ,
{ X : 110 , Y : 10 } ,
2025-03-04 19:05:39 +00:00
}
}
2025-03-04 17:14:32 +00:00
legend := & d2graph . Legend {
Objects : objects ,
Edges : legendGraph . Edges ,
}
if len ( legend . Objects ) > 0 || len ( legend . Edges ) > 0 {
g . Legend = legend
}
}
2023-01-28 01:19:12 +00:00
func ( c * compiler ) compileBoardsField ( g * d2graph . Graph , ir * d2ir . Map , fieldName string ) {
2024-11-24 18:45:06 +00:00
boards := ir . GetField ( d2ast . FlatUnquotedString ( fieldName ) )
2024-07-13 14:01:57 +00:00
if boards . Map ( ) == nil {
2023-01-24 06:45:21 +00:00
return
}
2024-07-13 14:01:57 +00:00
for _ , f := range boards . Map ( ) . Fields {
2024-10-23 23:40:12 +00:00
m := f . Map ( )
2023-01-24 06:45:21 +00:00
if f . Map ( ) == nil {
2024-10-23 23:40:12 +00:00
m = & d2ir . Map { }
2023-01-24 06:45:21 +00:00
}
2024-11-24 02:40:36 +00:00
if g . GetBoard ( f . Name . ScalarString ( ) ) != nil {
2024-11-24 02:56:18 +00:00
c . errorf ( f . References [ 0 ] . AST ( ) , "board name %v already used by another board" , f . Name . ScalarString ( ) )
2023-01-27 18:41:25 +00:00
continue
}
2023-01-28 01:19:12 +00:00
g2 := d2graph . NewGraph ( )
2023-03-01 18:43:53 +00:00
g2 . Parent = g
2024-10-23 23:40:12 +00:00
g2 . AST = m . AST ( ) . ( * d2ast . Map )
2024-07-13 14:01:57 +00:00
if g . BaseAST != nil {
g2 . BaseAST = findFieldAST ( g . BaseAST , f )
}
2024-10-23 23:40:12 +00:00
c . compileBoard ( g2 , m )
2024-10-30 02:38:21 +00:00
if f . Primary ( ) != nil {
c . compileLabel ( & g2 . Root . Attributes , f )
}
2024-11-24 02:40:36 +00:00
g2 . Name = f . Name . ScalarString ( )
2023-01-24 06:45:21 +00:00
switch fieldName {
case "layers" :
g . Layers = append ( g . Layers , g2 )
case "scenarios" :
g . Scenarios = append ( g . Scenarios , g2 )
case "steps" :
g . Steps = append ( g . Steps , g2 )
}
2022-11-03 13:54:49 +00:00
}
}
2023-06-19 17:28:44 +00:00
func findFieldAST ( ast * d2ast . Map , f * d2ir . Field ) * d2ast . Map {
path := [ ] string { }
2024-07-13 14:01:57 +00:00
curr := f
2023-06-19 17:28:44 +00:00
for {
2024-11-24 02:40:36 +00:00
path = append ( [ ] string { curr . Name . ScalarString ( ) } , path ... )
2023-06-19 17:28:44 +00:00
boardKind := d2ir . NodeBoardKind ( curr )
if boardKind == "" {
break
}
curr = d2ir . ParentField ( curr )
}
currAST := ast
for len ( path ) > 0 {
head := path [ 0 ]
found := false
for _ , n := range currAST . Nodes {
if n . MapKey == nil {
continue
}
if n . MapKey . Key == nil {
continue
}
if len ( n . MapKey . Key . Path ) != 1 {
continue
}
head2 := n . MapKey . Key . Path [ 0 ] . Unbox ( ) . ScalarString ( )
if head == head2 {
currAST = n . MapKey . Value . Map
2024-01-21 18:46:48 +00:00
// The BaseAST is only used for making edits to the AST (through d2oracle)
// If there's no Map for a given board, either it's an empty layer or set to an import
// Either way, in order to make edits, it needs to be expanded into a Map to add lines to
if currAST == nil {
n . MapKey . Value . Map = & d2ast . Map {
Range : d2ast . MakeRange ( ",1:0:0-1:0:0" ) ,
}
if n . MapKey . Value . Import != nil {
imp := & d2ast . Import {
Range : d2ast . MakeRange ( ",1:0:0-1:0:0" ) ,
Spread : true ,
Pre : n . MapKey . Value . Import . Pre ,
Path : n . MapKey . Value . Import . Path ,
}
n . MapKey . Value . Map . Nodes = append ( n . MapKey . Value . Map . Nodes , d2ast . MapNodeBox {
Import : imp ,
} )
}
currAST = n . MapKey . Value . Map
}
2023-06-19 17:28:44 +00:00
found = true
break
}
}
if ! found {
return nil
}
path = path [ 1 : ]
}
return currAST
}
2022-11-03 13:54:49 +00:00
type compiler struct {
2023-06-04 22:39:00 +00:00
err * d2parser . ParseError
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) errorf ( n d2ast . Node , f string , v ... interface { } ) {
2023-10-31 19:26:01 +00:00
err := d2parser . Errorf ( n , f , v ... ) . ( d2ast . Error )
if c . err . ErrorsLookup == nil {
c . err . ErrorsLookup = make ( map [ d2ast . Error ] struct { } )
}
if _ , ok := c . err . ErrorsLookup [ err ] ; ! ok {
c . err . Errors = append ( c . err . Errors , err )
c . err . ErrorsLookup [ err ] = struct { } { }
}
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileMap ( obj * d2graph . Object , m * d2ir . Map ) {
2024-11-24 18:45:06 +00:00
class := m . GetField ( d2ast . FlatUnquotedString ( "class" ) )
2023-02-06 21:32:08 +00:00
if class != nil {
2023-04-27 22:09:43 +00:00
var classNames [ ] string
if class . Primary ( ) != nil {
classNames = append ( classNames , class . Primary ( ) . String ( ) )
} else if class . Composite != nil {
if arr , ok := class . Composite . ( * d2ir . Array ) ; ok {
for _ , class := range arr . Values {
if scalar , ok := class . ( * d2ir . Scalar ) ; ok {
classNames = append ( classNames , scalar . Value . ScalarString ( ) )
} else {
c . errorf ( class . LastPrimaryKey ( ) , "invalid value in array" )
}
}
}
}
for _ , className := range classNames {
classMap := m . GetClassMap ( className )
2023-02-06 21:32:08 +00:00
if classMap != nil {
c . compileMap ( obj , classMap )
2023-06-07 03:24:47 +00:00
} else {
if strings . Contains ( className , "," ) {
split := strings . Split ( className , "," )
allFound := true
for _ , maybeClassName := range split {
maybeClassName = strings . TrimSpace ( maybeClassName )
if m . GetClassMap ( maybeClassName ) == nil {
allFound = false
break
}
}
if allFound {
c . errorf ( class . LastRef ( ) . AST ( ) , ` class "%s" not found. Did you mean to use ";" to separate array items? ` , className )
}
}
2023-02-06 21:32:08 +00:00
}
}
}
2024-11-24 18:45:06 +00:00
shape := m . GetField ( d2ast . FlatUnquotedString ( "shape" ) )
2023-01-24 05:48:43 +00:00
if shape != nil {
2023-06-16 23:50:16 +00:00
if shape . Composite != nil {
c . errorf ( shape . LastPrimaryKey ( ) , "reserved field shape does not accept composite" )
} else {
c . compileField ( obj , shape )
}
2023-01-24 05:48:43 +00:00
}
2023-01-22 07:49:07 +00:00
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
if f . Name . ScalarString ( ) == "shape" && f . Name . IsUnquoted ( ) {
2023-01-24 05:48:43 +00:00
continue
}
2024-11-24 02:40:36 +00:00
if _ , ok := d2ast . BoardKeywords [ f . Name . ScalarString ( ) ] ; ok && f . Name . IsUnquoted ( ) {
2023-01-28 01:19:12 +00:00
continue
}
2023-01-22 07:49:07 +00:00
c . compileField ( obj , f )
2022-11-03 13:54:49 +00:00
}
2023-01-22 08:21:03 +00:00
2023-04-26 19:38:41 +00:00
if ! m . IsClass ( ) {
switch obj . Shape . Value {
case d2target . ShapeClass :
c . compileClass ( obj )
case d2target . ShapeSQLTable :
c . compileSQLTable ( obj )
}
2023-01-22 08:21:03 +00:00
2023-04-26 19:38:41 +00:00
for _ , e := range m . Edges {
2023-06-26 17:39:38 +00:00
c . compileEdge ( obj , e )
2023-04-26 19:38:41 +00:00
}
2022-11-03 13:54:49 +00:00
}
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileField ( obj * d2graph . Object , f * d2ir . Field ) {
2024-11-24 02:40:36 +00:00
keyword := strings . ToLower ( f . Name . ScalarString ( ) )
2024-09-15 16:43:10 +00:00
_ , isStyleReserved := d2ast . StyleKeywords [ keyword ]
2024-11-24 02:40:36 +00:00
if isStyleReserved && f . Name . IsUnquoted ( ) {
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , "%v must be style.%v" , f . Name . ScalarString ( ) , f . Name . ScalarString ( ) )
2023-01-28 06:20:10 +00:00
return
}
2024-09-15 16:43:10 +00:00
_ , isReserved := d2ast . SimpleReservedKeywords [ keyword ]
2024-11-24 02:40:36 +00:00
isReserved = isReserved && f . Name . IsUnquoted ( )
if f . Name . ScalarString ( ) == "classes" && f . Name . IsUnquoted ( ) {
2023-02-06 21:32:08 +00:00
if f . Map ( ) != nil {
if len ( f . Map ( ) . Edges ) > 0 {
c . errorf ( f . Map ( ) . Edges [ 0 ] . LastRef ( ) . AST ( ) , "classes cannot contain an edge" )
}
for _ , classesField := range f . Map ( ) . Fields {
if classesField . Map ( ) == nil {
continue
}
for _ , cf := range classesField . Map ( ) . Fields {
2024-11-24 02:40:36 +00:00
if _ , ok := d2ast . ReservedKeywords [ cf . Name . ScalarString ( ) ] ; ! ( ok && f . Name . IsUnquoted ( ) ) {
2024-11-24 02:56:18 +00:00
c . errorf ( cf . LastRef ( ) . AST ( ) , "%s is an invalid class field, must be reserved keyword" , cf . Name . ScalarString ( ) )
2023-02-06 21:32:08 +00:00
}
2024-11-24 02:40:36 +00:00
if cf . Name . ScalarString ( ) == "class" && cf . Name . IsUnquoted ( ) {
2023-02-06 21:32:08 +00:00
c . errorf ( cf . LastRef ( ) . AST ( ) , ` "class" cannot appear within "classes" ` )
}
}
}
}
return
2024-11-24 02:40:36 +00:00
} else if f . Name . ScalarString ( ) == "vars" && f . Name . IsUnquoted ( ) {
2023-07-11 02:24:21 +00:00
return
2024-11-24 02:40:36 +00:00
} else if ( f . Name . ScalarString ( ) == "source-arrowhead" || f . Name . ScalarString ( ) == "target-arrowhead" ) && f . Name . IsUnquoted ( ) {
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , ` %#v can only be used on connections ` , f . Name . ScalarString ( ) )
2023-10-28 04:00:46 +00:00
return
2023-02-06 21:32:08 +00:00
} else if isReserved {
2023-04-14 03:04:55 +00:00
c . compileReserved ( & obj . Attributes , f )
2025-03-17 17:59:57 +00:00
return
2024-11-24 02:40:36 +00:00
} else if f . Name . ScalarString ( ) == "style" && f . Name . IsUnquoted ( ) {
2023-05-16 17:45:35 +00:00
if f . Map ( ) == nil || len ( f . Map ( ) . Fields ) == 0 {
c . errorf ( f . LastRef ( ) . AST ( ) , ` "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4" ` )
2022-11-03 13:54:49 +00:00
return
}
2025-03-22 19:22:54 +00:00
c . compileStyle ( & obj . Attributes . Style , f . Map ( ) )
2022-11-03 13:54:49 +00:00
return
}
2023-02-17 01:44:54 +00:00
if obj . Parent != nil {
2024-03-29 07:40:22 +00:00
if strings . EqualFold ( obj . Parent . Shape . Value , d2target . ShapeSQLTable ) {
2023-02-17 01:44:54 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , "sql_table columns cannot have children" )
return
}
2024-03-29 07:40:22 +00:00
if strings . EqualFold ( obj . Parent . Shape . Value , d2target . ShapeClass ) {
2023-02-17 01:44:54 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , "class fields cannot have children" )
return
}
}
2024-09-19 14:57:33 +00:00
parent := obj
2024-11-24 02:40:36 +00:00
obj = obj . EnsureChild ( ( [ ] d2ast . String { f . Name } ) )
2023-01-22 07:49:07 +00:00
if f . Primary ( ) != nil {
2023-04-14 03:04:55 +00:00
c . compileLabel ( & obj . Attributes , f )
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
if f . Map ( ) != nil {
c . compileMap ( obj , f . Map ( ) )
2022-11-03 13:54:49 +00:00
}
2023-01-22 09:43:25 +00:00
2023-04-14 03:04:55 +00:00
if obj . Label . MapKey == nil {
obj . Label . MapKey = f . LastPrimaryKey ( )
2023-01-24 11:09:40 +00:00
}
2023-01-24 05:48:43 +00:00
for _ , fr := range f . References {
2023-01-24 11:09:40 +00:00
if fr . Primary ( ) {
2023-08-17 21:10:09 +00:00
if fr . Context_ . Key . Value . Map != nil {
obj . Map = fr . Context_ . Key . Value . Map
2023-01-24 11:09:40 +00:00
}
2023-01-24 05:48:43 +00:00
}
2023-06-06 22:09:27 +00:00
r := d2graph . Reference {
2023-01-24 05:48:43 +00:00
Key : fr . KeyPath ,
KeyPathIndex : fr . KeyPathIndex ( ) ,
2023-01-22 09:43:25 +00:00
2023-08-17 21:10:09 +00:00
MapKey : fr . Context_ . Key ,
MapKeyEdgeIndex : fr . Context_ . EdgeIndex ( ) ,
Scope : fr . Context_ . Scope ,
ScopeAST : fr . Context_ . ScopeAST ,
2024-09-19 14:57:33 +00:00
ScopeObj : parent ,
2024-09-25 03:17:05 +00:00
IsVar : d2ir . IsVar ( fr . Context_ . ScopeMap ) ,
2023-06-06 22:09:27 +00:00
}
2023-08-17 21:10:09 +00:00
if fr . Context_ . ScopeMap != nil && ! d2ir . IsVar ( fr . Context_ . ScopeMap ) {
2024-11-24 18:45:06 +00:00
scopeObjIDA := d2ir . BoardIDA ( fr . Context_ . ScopeMap )
2023-06-12 18:26:51 +00:00
r . ScopeObj = obj . Graph . Root . EnsureChild ( scopeObjIDA )
2023-06-06 22:09:27 +00:00
}
obj . References = append ( obj . References , r )
2023-01-22 09:43:25 +00:00
}
2023-01-22 07:49:07 +00:00
}
2022-11-03 13:54:49 +00:00
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileLabel ( attrs * d2graph . Attributes , f d2ir . Node ) {
scalar := f . Primary ( ) . Value
switch scalar := scalar . ( type ) {
case * d2ast . BlockString :
2023-06-02 17:42:44 +00:00
if strings . TrimSpace ( scalar . ScalarString ( ) ) == "" {
2023-06-02 01:42:43 +00:00
c . errorf ( f . LastPrimaryKey ( ) , "block string cannot be empty" )
}
2023-01-22 07:49:07 +00:00
attrs . Language = scalar . Tag
fullTag , ok := ShortToFullLanguageAliases [ scalar . Tag ]
if ok {
attrs . Language = fullTag
2022-11-03 13:54:49 +00:00
}
2023-03-07 17:00:45 +00:00
switch attrs . Language {
case "markdown" :
rendered , err := textmeasure . RenderMarkdown ( scalar . ScalarString ( ) )
if err != nil {
c . errorf ( f . LastPrimaryKey ( ) , "malformed Markdown" )
}
rendered = "<div>" + rendered + "</div>"
var xmlParsed interface { }
err = xml . Unmarshal ( [ ] byte ( rendered ) , & xmlParsed )
if err != nil {
switch xmlErr := err . ( type ) {
case * xml . SyntaxError :
c . errorf ( f . LastPrimaryKey ( ) , "malformed Markdown: %s" , xmlErr . Msg )
default :
c . errorf ( f . LastPrimaryKey ( ) , "malformed Markdown: %s" , err . Error ( ) )
}
}
2023-01-22 07:49:07 +00:00
}
2023-01-28 07:05:42 +00:00
attrs . Label . Value = scalar . ScalarString ( )
2023-01-22 07:49:07 +00:00
default :
2025-03-17 17:59:57 +00:00
attrs . Label . Value = scalar . ScalarString ( )
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
attrs . Label . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
}
2023-06-22 23:13:08 +00:00
func ( c * compiler ) compilePosition ( attrs * d2graph . Attributes , f * d2ir . Field ) {
name := f . Name
if f . Map ( ) != nil {
for _ , f := range f . Map ( ) . Fields {
2024-11-24 02:40:36 +00:00
if f . Name . ScalarString ( ) == "near" && f . Name . IsUnquoted ( ) {
2023-06-22 23:13:08 +00:00
if f . Primary ( ) == nil {
c . errorf ( f . LastPrimaryKey ( ) , ` invalid "near" field ` )
} else {
scalar := f . Primary ( ) . Value
switch scalar := scalar . ( type ) {
case * d2ast . Null :
attrs . LabelPosition = nil
default :
2024-09-15 16:43:10 +00:00
if _ , ok := d2ast . LabelPositions [ scalar . ScalarString ( ) ] ; ! ok {
2023-06-22 23:13:08 +00:00
c . errorf ( f . LastPrimaryKey ( ) , ` invalid "near" field ` )
} else {
2024-11-24 02:40:36 +00:00
switch name . ScalarString ( ) {
2023-06-22 23:13:08 +00:00
case "label" :
attrs . LabelPosition = & d2graph . Scalar { }
attrs . LabelPosition . Value = scalar . ScalarString ( )
attrs . LabelPosition . MapKey = f . LastPrimaryKey ( )
case "icon" :
attrs . IconPosition = & d2graph . Scalar { }
attrs . IconPosition . Value = scalar . ScalarString ( )
attrs . IconPosition . MapKey = f . LastPrimaryKey ( )
}
}
}
}
} else {
if f . LastPrimaryKey ( ) != nil {
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastPrimaryKey ( ) , ` unexpected field %s ` , f . Name . ScalarString ( ) )
2023-06-22 23:13:08 +00:00
}
}
}
if len ( f . Map ( ) . Edges ) > 0 {
c . errorf ( f . LastPrimaryKey ( ) , "unexpected edges in map" )
}
}
}
2023-01-22 08:59:02 +00:00
func ( c * compiler ) compileReserved ( attrs * d2graph . Attributes , f * d2ir . Field ) {
2023-01-22 09:08:13 +00:00
if f . Primary ( ) == nil {
2023-05-03 11:42:31 +00:00
if f . Composite != nil {
2024-11-24 02:40:36 +00:00
switch f . Name . ScalarString ( ) {
2023-05-03 11:42:31 +00:00
case "class" :
if arr , ok := f . Composite . ( * d2ir . Array ) ; ok {
for _ , class := range arr . Values {
if scalar , ok := class . ( * d2ir . Scalar ) ; ok {
attrs . Classes = append ( attrs . Classes , scalar . Value . ScalarString ( ) )
}
2023-05-01 11:59:57 +00:00
}
}
2023-05-03 11:42:31 +00:00
case "constraint" :
if arr , ok := f . Composite . ( * d2ir . Array ) ; ok {
for _ , constraint := range arr . Values {
if scalar , ok := constraint . ( * d2ir . Scalar ) ; ok {
2023-10-12 18:48:27 +00:00
switch scalar . Value . ( type ) {
case * d2ast . Null :
attrs . Constraint = append ( attrs . Constraint , "null" )
default :
attrs . Constraint = append ( attrs . Constraint , scalar . Value . ScalarString ( ) )
}
2023-05-03 11:42:31 +00:00
}
2023-05-01 11:59:57 +00:00
}
}
2023-06-22 23:13:08 +00:00
case "label" , "icon" :
c . compilePosition ( attrs , f )
2023-05-03 11:42:31 +00:00
default :
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastPrimaryKey ( ) , "reserved field %v does not accept composite" , f . Name . ScalarString ( ) )
2023-05-01 11:59:57 +00:00
}
2024-12-15 05:12:41 +00:00
} else {
c . errorf ( f . LastRef ( ) . AST ( ) , ` reserved field "%v" must have a value ` , f . Name . ScalarString ( ) )
2023-01-24 05:48:43 +00:00
}
2023-01-22 09:08:13 +00:00
return
}
2023-01-22 07:49:07 +00:00
scalar := f . Primary ( ) . Value
2024-11-24 02:40:36 +00:00
switch f . Name . ScalarString ( ) {
2023-01-22 07:49:07 +00:00
case "label" :
2023-01-22 08:59:02 +00:00
c . compileLabel ( attrs , f )
2023-06-22 23:13:08 +00:00
c . compilePosition ( attrs , f )
2022-11-03 13:54:49 +00:00
case "shape" :
2025-02-13 17:26:59 +00:00
shapeVal := strings . ToLower ( scalar . ScalarString ( ) )
in := d2target . IsShape ( shapeVal )
_ , isArrowhead := d2target . Arrowheads [ shapeVal ]
2023-01-24 06:45:21 +00:00
if ! in && ! isArrowhead {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , "unknown shape %q" , scalar . ScalarString ( ) )
2022-11-03 13:54:49 +00:00
return
}
2025-02-13 17:26:59 +00:00
attrs . Shape . Value = shapeVal
2024-03-29 07:40:22 +00:00
if strings . EqualFold ( attrs . Shape . Value , d2target . ShapeCode ) {
2022-11-03 13:54:49 +00:00
// Explicit code shape is plaintext.
attrs . Language = d2target . ShapeText
}
2023-01-22 07:49:07 +00:00
attrs . Shape . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
case "icon" :
iconURL , err := url . Parse ( scalar . ScalarString ( ) )
if err != nil {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , "bad icon url %#v: %s" , scalar . ScalarString ( ) , err )
2022-11-03 13:54:49 +00:00
return
}
attrs . Icon = iconURL
2023-06-22 23:13:08 +00:00
c . compilePosition ( attrs , f )
2025-03-22 09:33:37 +00:00
if f . Map ( ) != nil {
for _ , ff := range f . Map ( ) . Fields {
if ff . Name . ScalarString ( ) == "style" && ff . Name . IsUnquoted ( ) {
if ff . Map ( ) == nil || len ( ff . Map ( ) . Fields ) == 0 {
c . errorf ( f . LastRef ( ) . AST ( ) , ` "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4" ` )
return
}
2025-03-22 19:22:54 +00:00
c . compileStyle ( & attrs . IconStyle , ff . Map ( ) )
2025-03-22 09:33:37 +00:00
}
}
}
2022-11-03 13:54:49 +00:00
case "near" :
nearKey , err := d2parser . ParseKey ( scalar . ScalarString ( ) )
if err != nil {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , "bad near key %#v: %s" , scalar . ScalarString ( ) , err )
2022-11-03 13:54:49 +00:00
return
}
2023-01-24 06:45:21 +00:00
nearKey . Range = scalar . GetRange ( )
2022-11-03 13:54:49 +00:00
attrs . NearKey = nearKey
case "tooltip" :
2023-02-20 22:15:47 +00:00
attrs . Tooltip = & d2graph . Scalar { }
attrs . Tooltip . Value = scalar . ScalarString ( )
attrs . Tooltip . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
case "width" :
_ , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , "non-integer width %#v: %s" , scalar . ScalarString ( ) , err )
2022-11-03 13:54:49 +00:00
return
}
2023-04-14 03:04:55 +00:00
attrs . WidthAttr = & d2graph . Scalar { }
attrs . WidthAttr . Value = scalar . ScalarString ( )
attrs . WidthAttr . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
case "height" :
_ , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , "non-integer height %#v: %s" , scalar . ScalarString ( ) , err )
2022-11-03 13:54:49 +00:00
return
}
2023-04-14 03:04:55 +00:00
attrs . HeightAttr = & d2graph . Scalar { }
attrs . HeightAttr . Value = scalar . ScalarString ( )
attrs . HeightAttr . MapKey = f . LastPrimaryKey ( )
2023-02-19 00:23:27 +00:00
case "top" :
2023-02-22 04:09:23 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
2023-02-18 22:54:10 +00:00
if err != nil {
2023-02-19 00:23:27 +00:00
c . errorf ( scalar , "non-integer top %#v: %s" , scalar . ScalarString ( ) , err )
2023-02-18 22:54:10 +00:00
return
}
2023-02-22 04:09:23 +00:00
if v < 0 {
c . errorf ( scalar , "top must be a non-negative integer: %#v" , scalar . ScalarString ( ) )
return
}
2023-02-18 22:54:10 +00:00
attrs . Top = & d2graph . Scalar { }
attrs . Top . Value = scalar . ScalarString ( )
attrs . Top . MapKey = f . LastPrimaryKey ( )
2023-02-19 00:23:27 +00:00
case "left" :
2023-02-22 04:09:23 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
2023-02-18 22:54:10 +00:00
if err != nil {
2023-02-19 00:23:27 +00:00
c . errorf ( scalar , "non-integer left %#v: %s" , scalar . ScalarString ( ) , err )
2023-02-18 22:54:10 +00:00
return
}
2023-02-22 04:09:23 +00:00
if v < 0 {
c . errorf ( scalar , "left must be a non-negative integer: %#v" , scalar . ScalarString ( ) )
return
}
2023-02-18 22:54:10 +00:00
attrs . Left = & d2graph . Scalar { }
attrs . Left . Value = scalar . ScalarString ( )
attrs . Left . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
case "link" :
2023-02-20 22:15:47 +00:00
attrs . Link = & d2graph . Scalar { }
2023-02-05 18:36:38 +00:00
attrs . Link . Value = scalar . ScalarString ( )
attrs . Link . MapKey = f . LastPrimaryKey ( )
2022-11-30 00:02:37 +00:00
case "direction" :
dirs := [ ] string { "up" , "down" , "right" , "left" }
2025-02-13 17:26:59 +00:00
val := strings . ToLower ( scalar . ScalarString ( ) )
if ! go2 . Contains ( dirs , val ) {
2023-01-22 07:49:07 +00:00
c . errorf ( scalar , ` direction must be one of %v, got %q ` , strings . Join ( dirs , ", " ) , scalar . ScalarString ( ) )
2022-11-29 05:39:36 +00:00
return
}
2025-02-13 17:26:59 +00:00
attrs . Direction . Value = val
2023-01-22 07:49:07 +00:00
attrs . Direction . MapKey = f . LastPrimaryKey ( )
2022-12-12 00:46:09 +00:00
case "constraint" :
2023-01-24 05:48:43 +00:00
if _ , ok := scalar . ( d2ast . String ) ; ! ok {
c . errorf ( f . LastPrimaryKey ( ) , "constraint value must be a string" )
return
}
2023-05-01 11:59:57 +00:00
attrs . Constraint = append ( attrs . Constraint , scalar . ScalarString ( ) )
2023-04-06 22:43:03 +00:00
case "grid-rows" :
2023-04-01 00:18:17 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-04-06 22:43:03 +00:00
c . errorf ( scalar , "non-integer grid-rows %#v: %s" , scalar . ScalarString ( ) , err )
2023-04-01 00:18:17 +00:00
return
}
2023-04-03 18:52:12 +00:00
if v <= 0 {
2023-04-06 22:43:03 +00:00
c . errorf ( scalar , "grid-rows must be a positive integer: %#v" , scalar . ScalarString ( ) )
2023-04-01 00:18:17 +00:00
return
}
2023-04-06 22:43:03 +00:00
attrs . GridRows = & d2graph . Scalar { }
attrs . GridRows . Value = scalar . ScalarString ( )
attrs . GridRows . MapKey = f . LastPrimaryKey ( )
case "grid-columns" :
2023-04-01 00:18:17 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-04-06 22:43:03 +00:00
c . errorf ( scalar , "non-integer grid-columns %#v: %s" , scalar . ScalarString ( ) , err )
2023-04-01 00:18:17 +00:00
return
}
2023-04-03 18:52:12 +00:00
if v <= 0 {
2023-04-06 22:43:03 +00:00
c . errorf ( scalar , "grid-columns must be a positive integer: %#v" , scalar . ScalarString ( ) )
2023-04-01 00:18:17 +00:00
return
}
2023-04-06 22:43:03 +00:00
attrs . GridColumns = & d2graph . Scalar { }
attrs . GridColumns . Value = scalar . ScalarString ( )
attrs . GridColumns . MapKey = f . LastPrimaryKey ( )
2023-04-11 01:18:48 +00:00
case "grid-gap" :
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
c . errorf ( scalar , "non-integer grid-gap %#v: %s" , scalar . ScalarString ( ) , err )
return
}
if v < 0 {
c . errorf ( scalar , "grid-gap must be a non-negative integer: %#v" , scalar . ScalarString ( ) )
return
}
attrs . GridGap = & d2graph . Scalar { }
attrs . GridGap . Value = scalar . ScalarString ( )
attrs . GridGap . MapKey = f . LastPrimaryKey ( )
2023-04-12 20:48:53 +00:00
case "vertical-gap" :
2023-04-11 01:18:48 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-04-12 20:48:53 +00:00
c . errorf ( scalar , "non-integer vertical-gap %#v: %s" , scalar . ScalarString ( ) , err )
2023-04-11 01:18:48 +00:00
return
}
if v < 0 {
2023-04-12 20:48:53 +00:00
c . errorf ( scalar , "vertical-gap must be a non-negative integer: %#v" , scalar . ScalarString ( ) )
2023-04-11 01:18:48 +00:00
return
}
2023-04-12 20:48:53 +00:00
attrs . VerticalGap = & d2graph . Scalar { }
attrs . VerticalGap . Value = scalar . ScalarString ( )
attrs . VerticalGap . MapKey = f . LastPrimaryKey ( )
case "horizontal-gap" :
2023-04-11 01:18:48 +00:00
v , err := strconv . Atoi ( scalar . ScalarString ( ) )
if err != nil {
2023-04-12 20:48:53 +00:00
c . errorf ( scalar , "non-integer horizontal-gap %#v: %s" , scalar . ScalarString ( ) , err )
2023-04-11 01:18:48 +00:00
return
}
if v < 0 {
2023-04-12 20:48:53 +00:00
c . errorf ( scalar , "horizontal-gap must be a non-negative integer: %#v" , scalar . ScalarString ( ) )
2023-04-11 01:18:48 +00:00
return
}
2023-04-12 20:48:53 +00:00
attrs . HorizontalGap = & d2graph . Scalar { }
attrs . HorizontalGap . Value = scalar . ScalarString ( )
attrs . HorizontalGap . MapKey = f . LastPrimaryKey ( )
2023-02-06 21:32:08 +00:00
case "class" :
2023-05-01 11:59:57 +00:00
attrs . Classes = append ( attrs . Classes , scalar . ScalarString ( ) )
2023-02-06 21:32:08 +00:00
case "classes" :
2022-11-03 13:54:49 +00:00
}
2023-03-26 16:45:05 +00:00
2024-09-17 03:18:11 +00:00
if attrs . Link != nil && attrs . Label . Value != "" {
u , err := url . ParseRequestURI ( attrs . Label . Value )
if err == nil && u . Host != "" {
c . errorf ( scalar , "Label cannot be set to URL when link is also set (for security)" )
}
}
2023-03-26 16:45:05 +00:00
if attrs . Link != nil && attrs . Tooltip != nil {
2023-06-09 21:07:24 +00:00
u , err := url . ParseRequestURI ( attrs . Tooltip . Value )
if err == nil && u . Host != "" {
2023-03-27 22:07:24 +00:00
c . errorf ( scalar , "Tooltip cannot be set to URL when link is also set (for security)" )
2023-03-26 16:45:05 +00:00
}
}
2023-01-22 07:49:07 +00:00
}
2022-11-03 13:54:49 +00:00
2025-03-22 19:22:54 +00:00
func ( c * compiler ) compileStyle ( styles * d2graph . Style , m * d2ir . Map ) {
2025-03-17 17:59:57 +00:00
for _ , f := range m . Fields {
2025-03-22 19:22:54 +00:00
c . compileStyleField ( styles , f )
2025-03-17 17:59:57 +00:00
}
}
2025-03-22 19:22:54 +00:00
func ( c * compiler ) compileStyleField ( styles * d2graph . Style , f * d2ir . Field ) {
2024-11-24 02:40:36 +00:00
if _ , ok := d2ast . StyleKeywords [ strings . ToLower ( f . Name . ScalarString ( ) ) ] ; ! ( ok && f . Name . IsUnquoted ( ) ) {
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , ` invalid style keyword: "%s" ` , f . Name . ScalarString ( ) )
2023-04-27 18:22:10 +00:00
return
}
2023-01-22 09:08:13 +00:00
if f . Primary ( ) == nil {
return
}
2025-03-22 19:22:54 +00:00
compileStyleFieldInit ( styles , f )
2025-03-25 17:28:45 +00:00
scalar := f . Primary ( ) . Value
2025-03-22 19:22:54 +00:00
err := styles . Apply ( f . Name . ScalarString ( ) , scalar . ScalarString ( ) )
2023-01-22 07:49:07 +00:00
if err != nil {
c . errorf ( scalar , err . Error ( ) )
2022-11-03 13:54:49 +00:00
return
}
2023-01-22 09:43:25 +00:00
}
2022-11-03 13:54:49 +00:00
2025-03-22 19:22:54 +00:00
func compileStyleFieldInit ( styles * d2graph . Style , f * d2ir . Field ) {
2024-11-24 02:40:36 +00:00
switch f . Name . ScalarString ( ) {
2023-01-22 07:49:07 +00:00
case "opacity" :
2025-03-22 19:22:54 +00:00
styles . Opacity = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "stroke" :
2025-03-22 19:22:54 +00:00
styles . Stroke = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "fill" :
2025-03-22 19:22:54 +00:00
styles . Fill = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-03-14 03:07:13 +00:00
case "fill-pattern" :
2025-03-22 19:22:54 +00:00
styles . FillPattern = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "stroke-width" :
2025-03-22 19:22:54 +00:00
styles . StrokeWidth = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "stroke-dash" :
2025-03-22 19:22:54 +00:00
styles . StrokeDash = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "border-radius" :
2025-03-22 19:22:54 +00:00
styles . BorderRadius = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "shadow" :
2025-03-22 19:22:54 +00:00
styles . Shadow = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "3d" :
2025-03-22 19:22:54 +00:00
styles . ThreeDee = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "multiple" :
2025-03-22 19:22:54 +00:00
styles . Multiple = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "font" :
2025-03-22 19:22:54 +00:00
styles . Font = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "font-size" :
2025-03-22 19:22:54 +00:00
styles . FontSize = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "font-color" :
2025-03-22 19:22:54 +00:00
styles . FontColor = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "animated" :
2025-03-22 19:22:54 +00:00
styles . Animated = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "bold" :
2025-03-22 19:22:54 +00:00
styles . Bold = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "italic" :
2025-03-22 19:22:54 +00:00
styles . Italic = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "underline" :
2025-03-22 19:22:54 +00:00
styles . Underline = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-22 07:49:07 +00:00
case "filled" :
2025-03-22 19:22:54 +00:00
styles . Filled = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-27 18:44:25 +00:00
case "double-border" :
2025-03-22 19:22:54 +00:00
styles . DoubleBorder = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-03-31 08:58:02 +00:00
case "text-transform" :
2025-03-22 19:22:54 +00:00
styles . TextTransform = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2025-03-18 18:04:43 +00:00
}
}
2023-06-26 17:39:38 +00:00
func ( c * compiler ) compileEdge ( obj * d2graph . Object , e * d2ir . Edge ) {
2024-11-24 18:45:06 +00:00
edge , err := obj . Connect ( e . ID . SrcPath , e . ID . DstPath , e . ID . SrcArrow , e . ID . DstArrow , "" )
2023-01-22 07:49:07 +00:00
if err != nil {
2023-01-22 08:59:02 +00:00
c . errorf ( e . References [ 0 ] . AST ( ) , err . Error ( ) )
2022-11-03 13:54:49 +00:00
return
}
2023-01-22 07:49:07 +00:00
if e . Primary ( ) != nil {
2023-04-14 03:04:55 +00:00
c . compileLabel ( & edge . Attributes , e )
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
if e . Map ( ) != nil {
2023-02-06 21:32:08 +00:00
c . compileEdgeMap ( edge , e . Map ( ) )
2022-11-03 13:54:49 +00:00
}
2023-01-22 09:43:25 +00:00
2023-04-14 03:04:55 +00:00
edge . Label . MapKey = e . LastPrimaryKey ( )
2023-01-22 09:43:25 +00:00
for _ , er := range e . References {
2023-06-06 22:09:27 +00:00
r := d2graph . EdgeReference {
2023-08-17 21:10:09 +00:00
Edge : er . Context_ . Edge ,
MapKey : er . Context_ . Key ,
MapKeyEdgeIndex : er . Context_ . EdgeIndex ( ) ,
Scope : er . Context_ . Scope ,
ScopeAST : er . Context_ . ScopeAST ,
2024-09-19 14:57:33 +00:00
ScopeObj : obj ,
2023-08-17 21:10:09 +00:00
}
if er . Context_ . ScopeMap != nil && ! d2ir . IsVar ( er . Context_ . ScopeMap ) {
2024-11-24 18:45:06 +00:00
scopeObjIDA := d2ir . BoardIDA ( er . Context_ . ScopeMap )
2023-06-12 18:26:51 +00:00
r . ScopeObj = edge . Src . Graph . Root . EnsureChild ( scopeObjIDA )
2023-06-06 22:09:27 +00:00
}
edge . References = append ( edge . References , r )
2023-02-06 21:32:08 +00:00
}
}
func ( c * compiler ) compileEdgeMap ( edge * d2graph . Edge , m * d2ir . Map ) {
2024-11-24 18:45:06 +00:00
class := m . GetField ( d2ast . FlatUnquotedString ( "class" ) )
2023-02-06 21:32:08 +00:00
if class != nil {
2023-04-27 22:09:43 +00:00
var classNames [ ] string
if class . Primary ( ) != nil {
classNames = append ( classNames , class . Primary ( ) . String ( ) )
} else if class . Composite != nil {
if arr , ok := class . Composite . ( * d2ir . Array ) ; ok {
for _ , class := range arr . Values {
if scalar , ok := class . ( * d2ir . Scalar ) ; ok {
classNames = append ( classNames , scalar . Value . ScalarString ( ) )
} else {
c . errorf ( class . LastPrimaryKey ( ) , "invalid value in array" )
}
}
}
}
for _ , className := range classNames {
classMap := m . GetClassMap ( className )
2023-02-06 21:32:08 +00:00
if classMap != nil {
c . compileEdgeMap ( edge , classMap )
}
}
}
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
_ , ok := d2ast . ReservedKeywords [ f . Name . ScalarString ( ) ]
if ! ( ok && f . Name . IsUnquoted ( ) ) {
2023-02-06 21:32:08 +00:00
c . errorf ( f . References [ 0 ] . AST ( ) , ` edge map keys must be reserved keywords ` )
continue
}
c . compileEdgeField ( edge , f )
2023-01-22 09:43:25 +00:00
}
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileEdgeField ( edge * d2graph . Edge , f * d2ir . Field ) {
2024-11-24 02:40:36 +00:00
keyword := strings . ToLower ( f . Name . ScalarString ( ) )
2024-09-15 16:43:10 +00:00
_ , isStyleReserved := d2ast . StyleKeywords [ keyword ]
2024-11-24 02:40:36 +00:00
isStyleReserved = isStyleReserved && f . Name . IsUnquoted ( )
2023-03-07 06:42:52 +00:00
if isStyleReserved {
2024-11-24 02:56:18 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , "%v must be style.%v" , f . Name . ScalarString ( ) , f . Name . ScalarString ( ) )
2023-03-07 06:42:52 +00:00
return
}
2024-09-15 16:43:10 +00:00
_ , isReserved := d2ast . SimpleReservedKeywords [ keyword ]
2023-01-22 07:49:07 +00:00
if isReserved {
2023-04-14 03:04:55 +00:00
c . compileReserved ( & edge . Attributes , f )
2023-01-22 07:49:07 +00:00
return
2024-11-24 02:40:36 +00:00
} else if f . Name . ScalarString ( ) == "style" {
2023-01-22 07:49:07 +00:00
if f . Map ( ) == nil {
return
}
2025-03-22 19:22:54 +00:00
c . compileStyle ( & edge . Attributes . Style , f . Map ( ) )
2022-11-03 13:54:49 +00:00
return
}
2024-11-24 02:40:36 +00:00
if ( f . Name . ScalarString ( ) == "source-arrowhead" || f . Name . ScalarString ( ) == "target-arrowhead" ) && f . Name . IsUnquoted ( ) {
2023-03-11 21:21:13 +00:00
c . compileArrowheads ( edge , f )
2022-11-03 13:54:49 +00:00
}
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileArrowheads ( edge * d2graph . Edge , f * d2ir . Field ) {
var attrs * d2graph . Attributes
2024-11-24 02:40:36 +00:00
if f . Name . ScalarString ( ) == "source-arrowhead" {
2023-06-02 00:11:11 +00:00
if edge . SrcArrowhead == nil {
edge . SrcArrowhead = & d2graph . Attributes { }
}
2023-01-22 07:49:07 +00:00
attrs = edge . SrcArrowhead
} else {
2023-06-02 00:11:11 +00:00
if edge . DstArrowhead == nil {
edge . DstArrowhead = & d2graph . Attributes { }
}
2023-01-22 07:49:07 +00:00
attrs = edge . DstArrowhead
2022-11-03 13:54:49 +00:00
}
2023-01-24 06:45:21 +00:00
if f . Primary ( ) != nil {
c . compileLabel ( attrs , f )
}
2023-03-11 21:21:13 +00:00
if f . Map ( ) != nil {
for _ , f2 := range f . Map ( ) . Fields {
2024-11-24 02:40:36 +00:00
keyword := strings . ToLower ( f2 . Name . ScalarString ( ) )
2024-09-15 16:43:10 +00:00
_ , isReserved := d2ast . SimpleReservedKeywords [ keyword ]
2024-11-24 02:40:36 +00:00
isReserved = isReserved && f2 . Name . IsUnquoted ( )
2023-03-11 21:21:13 +00:00
if isReserved {
c . compileReserved ( attrs , f2 )
continue
2024-11-24 02:40:36 +00:00
} else if f2 . Name . ScalarString ( ) == "style" && f2 . Name . IsUnquoted ( ) {
2023-03-11 21:21:13 +00:00
if f2 . Map ( ) == nil {
continue
}
2025-03-22 19:22:54 +00:00
c . compileStyle ( & attrs . Style , f2 . Map ( ) )
2023-03-11 21:21:13 +00:00
continue
} else {
c . errorf ( f2 . LastRef ( ) . AST ( ) , ` source-arrowhead/target-arrowhead map keys must be reserved keywords ` )
2023-01-22 07:49:07 +00:00
continue
}
2022-11-03 13:54:49 +00:00
}
}
}
// TODO add more, e.g. C, bash
var ShortToFullLanguageAliases = map [ string ] string {
2022-11-27 21:54:41 +00:00
"md" : "markdown" ,
"tex" : "latex" ,
"js" : "javascript" ,
"go" : "golang" ,
"py" : "python" ,
"rb" : "ruby" ,
"ts" : "typescript" ,
2022-11-03 13:54:49 +00:00
}
var FullToShortLanguageAliases map [ string ] string
2023-01-22 08:21:03 +00:00
func ( c * compiler ) compileClass ( obj * d2graph . Object ) {
2022-11-03 13:54:49 +00:00
obj . Class = & d2target . Class { }
for _ , f := range obj . ChildrenArray {
2023-04-13 17:19:58 +00:00
visibility := "public"
2022-11-03 13:54:49 +00:00
name := f . IDVal
// See https://www.uml-diagrams.org/visibility.html
if name != "" {
switch name [ 0 ] {
case '+' :
name = name [ 1 : ]
case '-' :
2023-04-13 17:19:58 +00:00
visibility = "private"
2022-11-03 13:54:49 +00:00
name = name [ 1 : ]
case '#' :
2023-04-13 17:19:58 +00:00
visibility = "protected"
2022-11-03 13:54:49 +00:00
name = name [ 1 : ]
}
}
if ! strings . Contains ( f . IDVal , "(" ) {
2023-04-14 03:04:55 +00:00
typ := f . Label . Value
2022-11-03 13:54:49 +00:00
if typ == f . IDVal {
typ = ""
}
obj . Class . Fields = append ( obj . Class . Fields , d2target . ClassField {
Name : name ,
Type : typ ,
2023-04-13 17:19:58 +00:00
Visibility : visibility ,
2022-11-03 13:54:49 +00:00
} )
} else {
// TODO: Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name.
2023-04-14 03:04:55 +00:00
returnType := f . Label . Value
2022-11-03 13:54:49 +00:00
if returnType == f . IDVal {
returnType = "void"
}
obj . Class . Methods = append ( obj . Class . Methods , d2target . ClassMethod {
Name : name ,
Return : returnType ,
2023-04-13 17:19:58 +00:00
Visibility : visibility ,
2022-11-03 13:54:49 +00:00
} )
}
}
2023-01-22 08:21:03 +00:00
2023-01-24 05:48:43 +00:00
for _ , ch := range obj . ChildrenArray {
for i := 0 ; i < len ( obj . Graph . Objects ) ; i ++ {
if obj . Graph . Objects [ i ] == ch {
obj . Graph . Objects = append ( obj . Graph . Objects [ : i ] , obj . Graph . Objects [ i + 1 : ] ... )
i --
}
}
}
2023-01-22 08:21:03 +00:00
obj . Children = nil
obj . ChildrenArray = nil
2022-11-03 13:54:49 +00:00
}
func ( c * compiler ) compileSQLTable ( obj * d2graph . Object ) {
obj . SQLTable = & d2target . SQLTable { }
for _ , col := range obj . ChildrenArray {
2023-04-14 03:04:55 +00:00
typ := col . Label . Value
2022-11-03 13:54:49 +00:00
if typ == col . IDVal {
// Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name.
typ = ""
}
d2Col := d2target . SQLColumn {
2023-05-01 11:59:57 +00:00
Name : d2target . Text { Label : col . IDVal } ,
Type : d2target . Text { Label : typ } ,
Constraint : col . Constraint ,
2022-11-03 13:54:49 +00:00
}
obj . SQLTable . Columns = append ( obj . SQLTable . Columns , d2Col )
}
2023-01-22 08:21:03 +00:00
2023-01-24 05:48:43 +00:00
for _ , ch := range obj . ChildrenArray {
for i := 0 ; i < len ( obj . Graph . Objects ) ; i ++ {
if obj . Graph . Objects [ i ] == ch {
obj . Graph . Objects = append ( obj . Graph . Objects [ : i ] , obj . Graph . Objects [ i + 1 : ] ... )
i --
}
}
}
2023-01-22 08:21:03 +00:00
obj . Children = nil
obj . ChildrenArray = nil
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) validateKeys ( obj * d2graph . Object , m * d2ir . Map ) {
2023-01-22 08:59:02 +00:00
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
if _ , ok := d2ast . BoardKeywords [ f . Name . ScalarString ( ) ] ; ok && f . Name . IsUnquoted ( ) {
2023-01-28 01:19:12 +00:00
continue
}
2023-01-22 07:49:07 +00:00
c . validateKey ( obj , f )
2022-11-03 13:54:49 +00:00
}
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) validateKey ( obj * d2graph . Object , f * d2ir . Field ) {
2024-11-24 02:40:36 +00:00
keyword := strings . ToLower ( f . Name . ScalarString ( ) )
2024-09-15 16:43:10 +00:00
_ , isReserved := d2ast . ReservedKeywords [ keyword ]
2024-11-24 02:40:36 +00:00
isReserved = isReserved && f . Name . IsUnquoted ( )
2023-01-22 07:49:07 +00:00
if isReserved {
2023-04-14 03:04:55 +00:00
switch obj . Shape . Value {
2023-01-22 07:49:07 +00:00
case d2target . ShapeCircle , d2target . ShapeSquare :
2023-04-14 03:04:55 +00:00
checkEqual := ( keyword == "width" && obj . HeightAttr != nil ) || ( keyword == "height" && obj . WidthAttr != nil )
if checkEqual && obj . WidthAttr . Value != obj . HeightAttr . Value {
c . errorf ( f . LastPrimaryKey ( ) , "width and height must be equal for %s shapes" , obj . Shape . Value )
2023-01-22 07:49:07 +00:00
}
2022-12-29 04:47:40 +00:00
}
2022-11-03 13:54:49 +00:00
2024-11-24 02:40:36 +00:00
switch f . Name . ScalarString ( ) {
2023-01-24 11:09:40 +00:00
case "style" :
2023-04-14 03:04:55 +00:00
if obj . Style . ThreeDee != nil {
if ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeSquare ) && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeRectangle ) && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeHexagon ) {
c . errorf ( obj . Style . ThreeDee . MapKey , ` key "3d" can only be applied to squares, rectangles, and hexagons ` )
2023-01-24 11:09:40 +00:00
}
2023-01-22 07:49:07 +00:00
}
2023-04-14 03:04:55 +00:00
if obj . Style . DoubleBorder != nil {
2024-03-29 07:40:22 +00:00
if obj . Shape . Value != "" && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeSquare ) && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeRectangle ) && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeCircle ) && ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeOval ) {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . Style . DoubleBorder . MapKey , ` key "double-border" can only be applied to squares, rectangles, circles, ovals ` )
2023-01-27 18:44:25 +00:00
}
}
2023-01-22 07:49:07 +00:00
case "shape" :
2024-03-29 07:40:22 +00:00
if strings . EqualFold ( obj . Shape . Value , d2target . ShapeImage ) && obj . Icon == nil {
2023-01-24 11:09:40 +00:00
c . errorf ( f . LastPrimaryKey ( ) , ` image shape must include an "icon" field ` )
2023-01-22 08:21:03 +00:00
}
2023-04-14 03:04:55 +00:00
in := d2target . IsShape ( obj . Shape . Value )
_ , arrowheadIn := d2target . Arrowheads [ obj . Shape . Value ]
2023-01-22 07:49:07 +00:00
if ! in && arrowheadIn {
2023-04-14 03:04:55 +00:00
c . errorf ( f . LastPrimaryKey ( ) , fmt . Sprintf ( ` invalid shape, can only set "%s" for arrowheads ` , obj . Shape . Value ) )
2023-01-22 07:49:07 +00:00
}
2023-06-08 18:07:46 +00:00
case "constraint" :
2024-03-29 07:40:22 +00:00
if ! strings . EqualFold ( obj . Shape . Value , d2target . ShapeSQLTable ) {
2023-06-08 18:07:46 +00:00
c . errorf ( f . LastPrimaryKey ( ) , ` "constraint" keyword can only be used in "sql_table" shapes ` )
}
2022-12-29 05:14:25 +00:00
}
2022-11-03 13:54:49 +00:00
return
}
2024-08-17 01:04:01 +00:00
if strings . EqualFold ( obj . Shape . Value , d2target . ShapeImage ) && obj . OuterSequenceDiagram ( ) == nil {
2023-01-24 06:45:21 +00:00
c . errorf ( f . LastRef ( ) . AST ( ) , "image shapes cannot have children." )
2023-01-22 07:49:07 +00:00
return
2022-11-03 13:54:49 +00:00
}
2024-11-24 02:40:36 +00:00
obj , ok := obj . HasChild ( [ ] string { f . Name . ScalarString ( ) } )
2023-01-22 08:59:02 +00:00
if ok && f . Map ( ) != nil {
2023-01-22 07:49:07 +00:00
c . validateKeys ( obj , f . Map ( ) )
2022-11-03 13:54:49 +00:00
}
}
2023-09-18 20:16:50 +00:00
func ( c * compiler ) validateLabels ( g * d2graph . Graph ) {
for _ , obj := range g . Objects {
2024-08-13 14:36:22 +00:00
if strings . EqualFold ( obj . Shape . Value , d2target . ShapeText ) {
if obj . Attributes . Language != "" {
// blockstrings have already been validated
continue
}
if strings . TrimSpace ( obj . Label . Value ) == "" {
c . errorf ( obj . Label . MapKey , "shape text must have a non-empty label" )
}
} else if strings . EqualFold ( obj . Shape . Value , d2target . ShapeSQLTable ) {
if strings . Contains ( obj . Label . Value , "\n" ) {
2024-08-14 00:40:46 +00:00
c . errorf ( obj . Label . MapKey , "shape sql_table cannot have newlines in label" )
2024-08-13 14:36:22 +00:00
}
2023-09-18 20:16:50 +00:00
}
}
}
2022-11-03 13:54:49 +00:00
func ( c * compiler ) validateNear ( g * d2graph . Graph ) {
for _ , obj := range g . Objects {
2023-04-14 03:04:55 +00:00
if obj . NearKey != nil {
nearObj , isKey := g . Root . HasChild ( d2graph . Key ( obj . NearKey ) )
2024-09-15 16:43:10 +00:00
_ , isConst := d2ast . NearConstants [ d2graph . Key ( obj . NearKey ) [ 0 ] ]
2023-02-19 16:15:59 +00:00
if isKey {
// Doesn't make sense to set near to an ancestor or descendant
nearIsAncestor := false
for curr := obj ; curr != nil ; curr = curr . Parent {
if curr == nearObj {
nearIsAncestor = true
break
}
}
if nearIsAncestor {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . NearKey , "near keys cannot be set to an ancestor" )
2023-02-19 16:15:59 +00:00
continue
}
nearIsDescendant := false
for curr := nearObj ; curr != nil ; curr = curr . Parent {
if curr == obj {
nearIsDescendant = true
break
}
}
if nearIsDescendant {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . NearKey , "near keys cannot be set to an descendant" )
2023-02-19 16:15:59 +00:00
continue
}
2023-02-25 03:05:23 +00:00
if nearObj . OuterSequenceDiagram ( ) != nil {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . NearKey , "near keys cannot be set to an object within sequence diagrams" )
2023-02-25 03:05:23 +00:00
continue
}
2023-04-14 03:04:55 +00:00
if nearObj . NearKey != nil {
2024-09-15 16:43:10 +00:00
_ , nearObjNearIsConst := d2ast . NearConstants [ d2graph . Key ( nearObj . NearKey ) [ 0 ] ]
2023-03-28 18:37:40 +00:00
if nearObjNearIsConst {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . NearKey , "near keys cannot be set to an object with a constant near key" )
2023-03-28 18:37:40 +00:00
continue
}
}
2024-02-27 21:16:22 +00:00
if nearObj . ClosestGridDiagram ( ) != nil {
c . errorf ( obj . NearKey , "near keys cannot be set to descendants of special objects, like grid cells" )
continue
}
if nearObj . OuterSequenceDiagram ( ) != nil {
c . errorf ( obj . NearKey , "near keys cannot be set to descendants of special objects, like sequence diagram actors" )
continue
}
2023-02-19 16:15:59 +00:00
} else if isConst {
if obj . Parent != g . Root {
2023-04-14 03:04:55 +00:00
c . errorf ( obj . NearKey , "constant near keys can only be set on root level shapes" )
2023-02-19 16:15:59 +00:00
continue
}
} else {
2024-09-15 16:43:10 +00:00
c . errorf ( obj . NearKey , "near key %#v must be the absolute path to a shape or one of the following constants: %s" , d2format . Format ( obj . NearKey ) , strings . Join ( d2ast . NearConstantsArray , ", " ) )
2023-02-19 16:15:59 +00:00
continue
2022-12-27 05:22:23 +00:00
}
2022-11-03 13:54:49 +00:00
}
}
2023-03-31 14:31:43 +00:00
for _ , edge := range g . Edges {
2023-09-29 01:29:49 +00:00
if edge . Src . IsConstantNear ( ) && edge . Dst . IsDescendantOf ( edge . Src ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from constant near %#v cannot enter itself" , edge . Src . AbsID ( ) )
continue
}
if edge . Dst . IsConstantNear ( ) && edge . Src . IsDescendantOf ( edge . Dst ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from constant near %#v cannot enter itself" , edge . Dst . AbsID ( ) )
continue
}
2023-03-31 14:31:43 +00:00
}
2022-11-03 13:54:49 +00:00
}
2024-03-15 22:26:00 +00:00
func ( c * compiler ) validatePositionsCompatibility ( g * d2graph . Graph ) {
for _ , o := range g . Objects {
for _ , pos := range [ ] * d2graph . Scalar { o . Top , o . Left } {
if pos != nil {
if o . Parent != nil {
if strings . EqualFold ( o . Parent . Shape . Value , d2target . ShapeHierarchy ) {
c . errorf ( pos . MapKey , ` position keywords cannot be used with shape "hierarchy" ` )
}
if o . OuterSequenceDiagram ( ) != nil {
c . errorf ( pos . MapKey , ` position keywords cannot be used inside shape "sequence_diagram" ` )
}
if o . Parent . GridColumns != nil || o . Parent . GridRows != nil {
c . errorf ( pos . MapKey , ` position keywords cannot be used with grids ` )
}
}
}
}
}
}
2023-04-05 00:44:05 +00:00
func ( c * compiler ) validateEdges ( g * d2graph . Graph ) {
for _ , edge := range g . Edges {
2023-09-28 18:17:46 +00:00
// edges from a grid to something outside is ok
// grid -> outside : ok
// grid -> grid.cell : not ok
// grid -> grid.cell.inner : not ok
if edge . Src . IsGridDiagram ( ) && edge . Dst . IsDescendantOf ( edge . Src ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from grid diagram %#v cannot enter itself" , edge . Src . AbsID ( ) )
continue
}
if edge . Dst . IsGridDiagram ( ) && edge . Src . IsDescendantOf ( edge . Dst ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from grid diagram %#v cannot enter itself" , edge . Dst . AbsID ( ) )
2023-09-29 01:08:49 +00:00
continue
}
if edge . Src . Parent . IsGridDiagram ( ) && edge . Dst . IsDescendantOf ( edge . Src ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from grid cell %#v cannot enter itself" , edge . Src . AbsID ( ) )
continue
}
if edge . Dst . Parent . IsGridDiagram ( ) && edge . Src . IsDescendantOf ( edge . Dst ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from grid cell %#v cannot enter itself" , edge . Dst . AbsID ( ) )
2023-09-28 18:17:46 +00:00
continue
}
2023-09-29 23:21:36 +00:00
if edge . Src . IsSequenceDiagram ( ) && edge . Dst . IsDescendantOf ( edge . Src ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from sequence diagram %#v cannot enter itself" , edge . Src . AbsID ( ) )
continue
}
if edge . Dst . IsSequenceDiagram ( ) && edge . Src . IsDescendantOf ( edge . Dst ) {
c . errorf ( edge . GetAstEdge ( ) , "edge from sequence diagram %#v cannot enter itself" , edge . Dst . AbsID ( ) )
continue
}
2023-04-05 00:44:05 +00:00
}
}
2023-03-01 22:53:58 +00:00
func ( c * compiler ) validateBoardLinks ( g * d2graph . Graph ) {
2023-02-05 18:36:38 +00:00
for _ , obj := range g . Objects {
2023-04-14 03:04:55 +00:00
if obj . Link == nil {
2023-02-05 18:36:38 +00:00
continue
}
2023-04-14 03:04:55 +00:00
linkKey , err := d2parser . ParseKey ( obj . Link . Value )
2023-02-05 18:36:38 +00:00
if err != nil {
continue
}
2024-09-25 15:53:46 +00:00
u , err := url . Parse ( html . UnescapeString ( obj . Link . Value ) )
2025-03-20 15:53:24 +00:00
isRemote := err == nil && ( u . Scheme != "" || strings . HasPrefix ( u . Path , "/" ) )
2024-09-25 15:53:46 +00:00
if isRemote {
continue
}
2023-03-01 22:53:58 +00:00
if linkKey . Path [ 0 ] . Unbox ( ) . ScalarString ( ) != "root" {
2024-09-25 15:53:46 +00:00
obj . Link = nil
2023-02-05 18:36:38 +00:00
continue
}
2023-03-03 01:56:32 +00:00
if ! hasBoard ( g . RootBoard ( ) , linkKey . IDA ( ) ) {
2024-08-26 23:22:25 +00:00
obj . Link = nil
2023-02-05 18:36:38 +00:00
continue
}
2024-11-08 21:26:38 +00:00
2024-11-24 19:03:03 +00:00
if slices . Equal ( linkKey . StringIDA ( ) , obj . Graph . IDA ( ) ) {
2024-11-08 21:26:38 +00:00
obj . Link = nil
continue
}
2023-03-01 18:28:01 +00:00
}
2023-03-03 01:56:32 +00:00
for _ , b := range g . Layers {
c . validateBoardLinks ( b )
}
for _ , b := range g . Scenarios {
c . validateBoardLinks ( b )
}
for _ , b := range g . Steps {
2023-03-01 22:53:58 +00:00
c . validateBoardLinks ( b )
2023-03-01 19:27:18 +00:00
}
2023-03-01 18:28:01 +00:00
}
2024-11-24 18:45:06 +00:00
func hasBoard ( root * d2graph . Graph , ida [ ] d2ast . String ) bool {
2023-03-03 03:32:31 +00:00
if len ( ida ) == 0 {
return true
}
2024-11-24 18:45:06 +00:00
if ida [ 0 ] . ScalarString ( ) == "root" && ida [ 0 ] . IsUnquoted ( ) {
2023-03-03 03:32:31 +00:00
return hasBoard ( root , ida [ 1 : ] )
}
id := ida [ 0 ]
if len ( ida ) == 1 {
2024-11-24 18:45:06 +00:00
return root . Name == id . ScalarString ( )
2023-03-03 03:32:31 +00:00
}
nextID := ida [ 1 ]
2024-11-24 18:45:06 +00:00
switch id . ScalarString ( ) {
2023-03-03 03:32:31 +00:00
case "layers" :
for _ , b := range root . Layers {
2024-11-24 18:45:06 +00:00
if b . Name == nextID . ScalarString ( ) {
2023-03-03 03:32:31 +00:00
return hasBoard ( b , ida [ 2 : ] )
2023-03-01 22:53:58 +00:00
}
2023-03-03 03:32:31 +00:00
}
case "scenarios" :
for _ , b := range root . Scenarios {
2024-11-24 18:45:06 +00:00
if b . Name == nextID . ScalarString ( ) {
2023-03-03 03:32:31 +00:00
return hasBoard ( b , ida [ 2 : ] )
2023-03-01 22:53:58 +00:00
}
2023-03-03 03:32:31 +00:00
}
case "steps" :
for _ , b := range root . Steps {
2024-11-24 18:45:06 +00:00
if b . Name == nextID . ScalarString ( ) {
2023-03-03 03:32:31 +00:00
return hasBoard ( b , ida [ 2 : ] )
2023-03-01 18:28:01 +00:00
}
}
2023-02-05 18:36:38 +00:00
}
2023-03-01 22:53:58 +00:00
return false
2023-02-05 18:36:38 +00:00
}
2022-11-03 13:54:49 +00:00
func init ( ) {
FullToShortLanguageAliases = make ( map [ string ] string , len ( ShortToFullLanguageAliases ) )
for k , v := range ShortToFullLanguageAliases {
FullToShortLanguageAliases [ v ] = k
}
}
2023-01-24 05:48:43 +00:00
2023-02-02 18:30:54 +00:00
// Unused for now until shape: edge_group
func ( c * compiler ) preprocessSeqDiagrams ( m * d2ir . Map ) {
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
if f . Name . ScalarString ( ) == "shape" && f . Name . IsUnquoted ( ) && f . Primary_ . Value . ScalarString ( ) == d2target . ShapeSequenceDiagram {
2023-02-02 18:30:54 +00:00
c . preprocessEdgeGroup ( m , m )
return
}
if f . Map ( ) != nil {
c . preprocessSeqDiagrams ( f . Map ( ) )
}
}
}
func ( c * compiler ) preprocessEdgeGroup ( seqDiagram , m * d2ir . Map ) {
// Any child of a sequence diagram can be either an actor, edge group or a span.
// 1. Actors are shapes without edges inside them defined at the top level scope of a
// sequence diagram.
// 2. Spans are the children of actors. For our purposes we can ignore them.
// 3. Edge groups are defined as having at least one connection within them and also not
// being connected to anything. All direct children of an edge group are either edge
// groups or top level actors.
// Go through all the fields and hoist actors from edge groups while also processing
// the edge groups recursively.
for _ , f := range m . Fields {
if isEdgeGroup ( f ) {
if f . Map ( ) != nil {
c . preprocessEdgeGroup ( seqDiagram , f . Map ( ) )
}
} else {
if m == seqDiagram {
// Ignore for root.
continue
}
hoistActor ( seqDiagram , f )
}
}
// We need to adjust all edges recursively to point to actual actors instead.
for _ , e := range m . Edges {
if isCrossEdgeGroupEdge ( m , e ) {
c . errorf ( e . References [ 0 ] . AST ( ) , "illegal edge between edge groups" )
continue
}
if m == seqDiagram {
// Root edges between actors directly do not require hoisting.
continue
}
srcParent := seqDiagram
for i , el := range e . ID . SrcPath {
f := srcParent . GetField ( el )
if ! isEdgeGroup ( f ) {
for j := 0 ; j < i + 1 ; j ++ {
2024-11-24 18:45:06 +00:00
e . ID . SrcPath = append ( [ ] d2ast . String { d2ast . FlatUnquotedString ( "_" ) } , e . ID . SrcPath ... )
e . ID . DstPath = append ( [ ] d2ast . String { d2ast . FlatUnquotedString ( "_" ) } , e . ID . DstPath ... )
2023-02-02 18:30:54 +00:00
}
break
}
srcParent = f . Map ( )
}
}
}
func hoistActor ( seqDiagram * d2ir . Map , f * d2ir . Field ) {
2024-11-24 18:45:06 +00:00
f2 := seqDiagram . GetField ( f . Name )
2023-02-02 18:30:54 +00:00
if f2 == nil {
seqDiagram . Fields = append ( seqDiagram . Fields , f . Copy ( seqDiagram ) . ( * d2ir . Field ) )
} else {
d2ir . OverlayField ( f2 , f )
2024-11-24 02:40:36 +00:00
d2ir . ParentMap ( f ) . DeleteField ( f . Name . ScalarString ( ) )
2023-02-02 18:30:54 +00:00
}
}
func isCrossEdgeGroupEdge ( m * d2ir . Map , e * d2ir . Edge ) bool {
srcParent := m
for _ , el := range e . ID . SrcPath {
f := srcParent . GetField ( el )
if f == nil {
// Hoisted already.
break
}
if isEdgeGroup ( f ) {
return true
}
srcParent = f . Map ( )
}
dstParent := m
for _ , el := range e . ID . DstPath {
f := dstParent . GetField ( el )
if f == nil {
// Hoisted already.
break
}
if isEdgeGroup ( f ) {
return true
}
dstParent = f . Map ( )
}
return false
}
func isEdgeGroup ( n d2ir . Node ) bool {
return n . Map ( ) . EdgeCountRecursive ( ) > 0
}
func parentSeqDiagram ( n d2ir . Node ) * d2ir . Map {
for {
m := d2ir . ParentMap ( n )
if m == nil {
return nil
}
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
if f . Name . ScalarString ( ) == "shape" && f . Name . IsUnquoted ( ) && f . Primary_ . Value . ScalarString ( ) == d2target . ShapeSequenceDiagram {
2023-02-02 18:30:54 +00:00
return m
}
}
n = m
}
}
2023-07-14 20:08:26 +00:00
2023-12-14 19:35:35 +00:00
func compileConfig ( ir * d2ir . Map ) ( * d2target . Config , error ) {
2024-11-24 18:45:06 +00:00
f := ir . GetField ( d2ast . FlatUnquotedString ( "vars" ) , d2ast . FlatUnquotedString ( "d2-config" ) )
2023-07-14 20:08:26 +00:00
if f == nil || f . Map ( ) == nil {
2023-12-14 19:35:35 +00:00
return nil , nil
2023-07-14 20:08:26 +00:00
}
configMap := f . Map ( )
config := & d2target . Config { }
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "sketch" ) )
2023-07-14 20:08:26 +00:00
if f != nil {
val , _ := strconv . ParseBool ( f . Primary ( ) . Value . ScalarString ( ) )
config . Sketch = & val
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "theme-id" ) )
2023-07-14 20:08:26 +00:00
if f != nil {
val , _ := strconv . Atoi ( f . Primary ( ) . Value . ScalarString ( ) )
config . ThemeID = go2 . Pointer ( int64 ( val ) )
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "dark-theme-id" ) )
2023-07-14 20:08:26 +00:00
if f != nil {
val , _ := strconv . Atoi ( f . Primary ( ) . Value . ScalarString ( ) )
config . DarkThemeID = go2 . Pointer ( int64 ( val ) )
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "pad" ) )
2023-07-14 20:08:26 +00:00
if f != nil {
val , _ := strconv . Atoi ( f . Primary ( ) . Value . ScalarString ( ) )
config . Pad = go2 . Pointer ( int64 ( val ) )
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "layout-engine" ) )
2023-07-14 20:08:26 +00:00
if f != nil {
config . LayoutEngine = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
}
2025-02-18 18:37:49 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "center" ) )
if f != nil {
val , _ := strconv . ParseBool ( f . Primary ( ) . Value . ScalarString ( ) )
config . Center = & val
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "theme-overrides" ) )
2023-12-13 20:17:22 +00:00
if f != nil {
2023-12-14 19:35:35 +00:00
overrides , err := compileThemeOverrides ( f . Map ( ) )
if err != nil {
return nil , err
}
config . ThemeOverrides = overrides
2023-12-13 20:17:22 +00:00
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "dark-theme-overrides" ) )
2023-12-13 20:17:22 +00:00
if f != nil {
2023-12-14 19:35:35 +00:00
overrides , err := compileThemeOverrides ( f . Map ( ) )
if err != nil {
return nil , err
}
config . DarkThemeOverrides = overrides
2023-12-13 20:17:22 +00:00
}
2024-11-24 18:45:06 +00:00
f = configMap . GetField ( d2ast . FlatUnquotedString ( "data" ) )
2024-10-15 18:56:39 +00:00
if f != nil && f . Map ( ) != nil {
2024-10-15 22:37:49 +00:00
config . Data = make ( map [ string ] interface { } )
2024-10-15 18:56:39 +00:00
for _ , f := range f . Map ( ) . Fields {
2024-10-15 22:37:49 +00:00
if f . Primary ( ) != nil {
2024-11-24 02:40:36 +00:00
config . Data [ f . Name . ScalarString ( ) ] = f . Primary ( ) . Value . ScalarString ( )
2024-10-15 22:37:49 +00:00
} else if f . Composite != nil {
var arr [ ] interface { }
switch c := f . Composite . ( type ) {
case * d2ir . Array :
for _ , f := range c . Values {
switch c := f . ( type ) {
case * d2ir . Scalar :
arr = append ( arr , c . String ( ) )
}
}
}
2024-11-24 02:40:36 +00:00
config . Data [ f . Name . ScalarString ( ) ] = arr
2024-10-15 22:37:49 +00:00
}
2024-10-15 18:56:39 +00:00
}
}
2023-12-13 20:17:22 +00:00
2023-12-14 19:35:35 +00:00
return config , nil
2023-07-14 20:08:26 +00:00
}
2023-12-13 20:17:22 +00:00
2023-12-14 19:35:35 +00:00
func compileThemeOverrides ( m * d2ir . Map ) ( * d2target . ThemeOverrides , error ) {
2023-12-13 20:17:22 +00:00
if m == nil {
2023-12-14 19:35:35 +00:00
return nil , nil
2023-12-13 20:17:22 +00:00
}
themeOverrides := d2target . ThemeOverrides { }
2023-12-14 19:35:35 +00:00
err := & d2parser . ParseError { }
FOR :
for _ , f := range m . Fields {
2024-11-24 02:40:36 +00:00
switch strings . ToUpper ( f . Name . ScalarString ( ) ) {
2023-12-14 19:35:35 +00:00
case "N1" :
themeOverrides . N1 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N2" :
themeOverrides . N2 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N3" :
themeOverrides . N3 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N4" :
themeOverrides . N4 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N5" :
themeOverrides . N5 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N6" :
themeOverrides . N6 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "N7" :
themeOverrides . N7 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B1" :
themeOverrides . B1 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B2" :
themeOverrides . B2 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B3" :
themeOverrides . B3 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B4" :
themeOverrides . B4 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B5" :
themeOverrides . B5 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "B6" :
themeOverrides . B6 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "AA2" :
themeOverrides . AA2 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "AA4" :
themeOverrides . AA4 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "AA5" :
themeOverrides . AA5 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "AB4" :
themeOverrides . AB4 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
case "AB5" :
themeOverrides . AB5 = go2 . Pointer ( f . Primary ( ) . Value . ScalarString ( ) )
default :
2024-11-24 02:56:18 +00:00
err . Errors = append ( err . Errors , d2parser . Errorf ( f . LastPrimaryKey ( ) , fmt . Sprintf ( ` "%s" is not a valid theme code ` , f . Name . ScalarString ( ) ) ) . ( d2ast . Error ) )
2023-12-14 19:35:35 +00:00
continue FOR
}
if ! go2 . Contains ( color . NamedColors , strings . ToLower ( f . Primary ( ) . Value . ScalarString ( ) ) ) && ! color . ColorHexRegex . MatchString ( f . Primary ( ) . Value . ScalarString ( ) ) {
2024-11-24 02:56:18 +00:00
err . Errors = append ( err . Errors , d2parser . Errorf ( f . LastPrimaryKey ( ) , fmt . Sprintf ( ` expected "%s" to be a valid named color ("orange") or a hex code ("#f0ff3a") ` , f . Name . ScalarString ( ) ) ) . ( d2ast . Error ) )
2023-12-14 19:35:35 +00:00
}
2023-12-13 20:17:22 +00:00
}
2023-12-14 19:35:35 +00:00
if ! err . Empty ( ) {
return nil , err
2023-12-13 20:17:22 +00:00
}
if themeOverrides != ( d2target . ThemeOverrides { } ) {
2023-12-14 19:35:35 +00:00
return & themeOverrides , nil
2023-12-13 20:17:22 +00:00
}
2023-12-14 19:35:35 +00:00
return nil , nil
2023-12-13 20:17:22 +00:00
}
2025-03-02 20:26:58 +00:00
func ( c * compiler ) setDefaultShapes ( g * d2graph . Graph ) {
for _ , obj := range g . Objects {
if obj . Shape . Value == "" {
if obj . OuterSequenceDiagram ( ) != nil {
obj . Shape . Value = d2target . ShapeRectangle
} else if obj . Language == "latex" {
obj . Shape . Value = d2target . ShapeText
} else if obj . Language == "markdown" {
obj . Shape . Value = d2target . ShapeText
} else if obj . Language != "" {
obj . Shape . Value = d2target . ShapeCode
} else {
obj . Shape . Value = d2target . ShapeRectangle
}
}
}
}