2022-11-03 13:54:49 +00:00
package d2compiler
import (
"fmt"
"io"
"net/url"
"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"
)
type CompileOptions struct {
UTF16 bool
}
func Compile ( path string , r io . RuneReader , opts * CompileOptions ) ( * d2graph . Graph , error ) {
if opts == nil {
opts = & CompileOptions { }
}
ast , err := d2parser . Parse ( path , r , & d2parser . ParseOptions {
UTF16 : opts . UTF16 ,
} )
if err != nil {
2023-01-22 07:49:07 +00:00
return nil , err
}
ir , err := d2ir . Compile ( ast )
if err != nil {
return nil , err
}
2023-01-28 01:19:12 +00:00
g , err := compileIR ( ast , ir )
2023-01-22 07:49:07 +00:00
if err != nil {
return nil , err
2022-11-03 13:54:49 +00:00
}
2023-02-02 20:24:48 +00:00
g . SortObjectsByAST ( )
2023-01-28 07:45:04 +00:00
g . SortEdgesByAST ( )
2023-01-28 07:05:42 +00:00
return g , 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 ) {
c := & compiler { }
2023-01-24 06:45:21 +00:00
2023-01-28 01:19:12 +00:00
g := d2graph . NewGraph ( )
g . AST = ast
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 )
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
}
c . validateNear ( g )
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-01-24 06:45:21 +00:00
return g
}
2023-01-28 01:19:12 +00:00
func ( c * compiler ) compileBoardsField ( g * d2graph . Graph , ir * d2ir . Map , fieldName string ) {
2023-01-24 06:45:21 +00:00
layers := ir . GetField ( fieldName )
if layers . Map ( ) == nil {
return
}
for _ , f := range layers . Map ( ) . Fields {
if f . Map ( ) == nil {
continue
}
2023-01-28 01:19:12 +00:00
if g . GetBoard ( f . Name ) != nil {
c . errorf ( f . References [ 0 ] . AST ( ) , "board name %v already used by another board" , f . Name )
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
2023-01-28 01:19:12 +00:00
g2 . AST = g . AST
c . compileBoard ( g2 , f . Map ( ) )
2023-01-24 06:45:21 +00:00
g2 . Name = f . Name
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
}
}
type compiler struct {
2023-02-02 18:30:54 +00:00
inEdgeGroup bool
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 { } ) {
c . err . Errors = append ( c . err . Errors , d2parser . Errorf ( n , f , v ... ) . ( d2ast . Error ) )
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 ) {
2023-01-24 05:48:43 +00:00
shape := m . GetField ( "shape" )
if shape != nil {
c . compileField ( obj , shape )
}
2023-01-22 07:49:07 +00:00
for _ , f := range m . Fields {
2023-01-24 05:48:43 +00:00
if f . Name == "shape" {
continue
}
2023-01-28 01:19:12 +00:00
if _ , ok := d2graph . BoardKeywords [ f . Name ] ; ok {
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
switch obj . Attributes . Shape . Value {
case d2target . ShapeClass :
2023-01-22 08:59:02 +00:00
c . compileClass ( obj )
2023-01-22 08:21:03 +00:00
case d2target . ShapeSQLTable :
2023-01-22 08:59:02 +00:00
c . compileSQLTable ( obj )
2023-01-22 08:21:03 +00:00
}
2023-01-22 07:49:07 +00:00
for _ , e := range m . Edges {
2023-01-22 08:59:02 +00:00
c . compileEdge ( obj , e )
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 ) {
keyword := strings . ToLower ( f . Name )
2023-01-28 06:20:10 +00:00
_ , isStyleReserved := d2graph . StyleKeywords [ keyword ]
if isStyleReserved {
c . errorf ( f . LastRef ( ) . AST ( ) , "%v must be style.%v" , f . Name , f . Name )
return
}
2023-01-22 09:43:25 +00:00
_ , isReserved := d2graph . SimpleReservedKeywords [ keyword ]
2023-01-22 07:49:07 +00:00
if isReserved {
c . compileReserved ( obj . Attributes , f )
2022-11-03 13:54:49 +00:00
return
2023-01-22 07:49:07 +00:00
} else if f . Name == "style" {
if f . Map ( ) == nil {
2022-11-03 13:54:49 +00:00
return
}
2023-01-22 07:49:07 +00:00
c . compileStyle ( obj . Attributes , f . Map ( ) )
2023-01-24 06:45:21 +00:00
if obj . Attributes . Style . Animated != nil {
c . errorf ( obj . Attributes . Style . Animated . MapKey , ` key "animated" can only be applied to edges ` )
}
2022-11-03 13:54:49 +00:00
return
}
2023-02-17 01:44:54 +00:00
if obj . Parent != nil {
if obj . Parent . Attributes . Shape . Value == d2target . ShapeSQLTable {
c . errorf ( f . LastRef ( ) . AST ( ) , "sql_table columns cannot have children" )
return
}
if obj . Parent . Attributes . Shape . Value == d2target . ShapeClass {
c . errorf ( f . LastRef ( ) . AST ( ) , "class fields cannot have children" )
return
}
}
2023-01-24 05:48:43 +00:00
obj = obj . EnsureChild ( d2graphIDA ( [ ] string { f . Name } ) )
2023-01-22 07:49:07 +00:00
if f . Primary ( ) != nil {
2023-01-22 08:59:02 +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-01-24 11:09:40 +00:00
if obj . Attributes . Label . MapKey == nil {
obj . Attributes . Label . MapKey = f . LastPrimaryKey ( )
}
2023-01-24 05:48:43 +00:00
for _ , fr := range f . References {
2023-01-24 11:09:40 +00:00
if fr . Primary ( ) {
if fr . Context . Key . Value . Map != nil {
obj . Map = fr . Context . Key . Value . Map
}
2023-01-24 05:48:43 +00:00
}
2023-03-03 01:56:32 +00:00
scopeObjIDA := d2ir . BoardIDA ( fr . Context . ScopeMap )
2023-02-23 17:46:28 +00:00
scopeObj := obj . Graph . Root . EnsureChildIDVal ( scopeObjIDA )
2023-01-22 09:43:25 +00:00
obj . References = append ( obj . References , 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-01-24 05:48:43 +00:00
MapKey : fr . Context . Key ,
MapKeyEdgeIndex : fr . Context . EdgeIndex ( ) ,
Scope : fr . Context . Scope ,
ScopeObj : scopeObj ,
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 . Null :
// TODO: Delete instaed.
attrs . Label . Value = scalar . ScalarString ( )
case * d2ast . BlockString :
attrs . Language = scalar . Tag
fullTag , ok := ShortToFullLanguageAliases [ scalar . Tag ]
if ok {
attrs . Language = fullTag
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
if attrs . Language == "markdown" || attrs . Language == "latex" {
attrs . Shape . Value = d2target . ShapeText
} else {
attrs . Shape . Value = d2target . ShapeCode
}
2023-01-28 07:05:42 +00:00
attrs . Label . Value = scalar . ScalarString ( )
2023-01-22 07:49:07 +00:00
default :
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-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-01-24 05:48:43 +00:00
if f . Composite != nil {
c . errorf ( f . LastPrimaryKey ( ) , "reserved field %v does not accept composite" , f . Name )
}
2023-01-22 09:08:13 +00:00
return
}
2023-01-22 07:49:07 +00:00
scalar := f . Primary ( ) . Value
switch f . Name {
case "label" :
2023-01-22 08:59:02 +00:00
c . compileLabel ( attrs , f )
2022-11-03 13:54:49 +00:00
case "shape" :
in := d2target . IsShape ( scalar . ScalarString ( ) )
2023-01-24 06:45:21 +00:00
_ , isArrowhead := d2target . Arrowheads [ scalar . ScalarString ( ) ]
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
}
2023-01-22 07:49:07 +00:00
attrs . Shape . Value = scalar . ScalarString ( )
2022-11-03 13:54:49 +00:00
if attrs . Shape . Value == d2target . ShapeCode {
// 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
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-01-22 07:49:07 +00:00
attrs . Width = & d2graph . Scalar { }
2022-11-03 13:54:49 +00:00
attrs . Width . Value = scalar . ScalarString ( )
2023-01-22 07:49:07 +00:00
attrs . Width . 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-01-22 07:49:07 +00:00
attrs . Height = & d2graph . Scalar { }
2022-11-03 13:54:49 +00:00
attrs . Height . Value = scalar . ScalarString ( )
2023-01-22 07:49:07 +00:00
attrs . Height . 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" }
if ! go2 . Contains ( dirs , scalar . ScalarString ( ) ) {
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
}
2022-11-30 00:02:37 +00:00
attrs . Direction . Value = scalar . ScalarString ( )
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
}
attrs . Constraint . Value = scalar . ScalarString ( )
attrs . Constraint . MapKey = f . LastPrimaryKey ( )
2022-11-03 13:54:49 +00:00
}
2023-01-22 07:49:07 +00:00
}
2022-11-03 13:54:49 +00:00
2023-01-22 08:59:02 +00:00
func ( c * compiler ) compileStyle ( attrs * d2graph . Attributes , m * d2ir . Map ) {
for _ , f := range m . Fields {
c . compileStyleField ( attrs , f )
}
}
func ( c * compiler ) compileStyleField ( attrs * d2graph . Attributes , f * d2ir . Field ) {
2023-01-22 09:08:13 +00:00
if f . Primary ( ) == nil {
return
}
2023-01-22 09:43:25 +00:00
compileStyleFieldInit ( attrs , f )
2023-01-22 07:49:07 +00:00
scalar := f . Primary ( ) . Value
err := attrs . Style . Apply ( f . Name , scalar . ScalarString ( ) )
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
2023-01-22 09:43:25 +00:00
func compileStyleFieldInit ( attrs * d2graph . Attributes , f * d2ir . Field ) {
2023-01-22 07:49:07 +00:00
switch f . Name {
case "opacity" :
attrs . Style . Opacity = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "stroke" :
attrs . Style . Stroke = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "fill" :
attrs . Style . Fill = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "stroke-width" :
attrs . Style . StrokeWidth = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "stroke-dash" :
attrs . Style . StrokeDash = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "border-radius" :
attrs . Style . BorderRadius = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "shadow" :
attrs . Style . Shadow = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "3d" :
attrs . Style . ThreeDee = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "multiple" :
attrs . Style . Multiple = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "font" :
attrs . Style . Font = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "font-size" :
attrs . Style . FontSize = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "font-color" :
attrs . Style . FontColor = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "animated" :
attrs . Style . Animated = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "bold" :
attrs . Style . Bold = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "italic" :
attrs . Style . Italic = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "underline" :
attrs . Style . Underline = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "filled" :
attrs . Style . Filled = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "width" :
attrs . Width = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "height" :
attrs . Height = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-02-18 22:54:10 +00:00
case "top" :
attrs . Top = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
case "left" :
attrs . Left = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2023-01-27 18:44:25 +00:00
case "double-border" :
attrs . Style . DoubleBorder = & d2graph . Scalar { MapKey : f . LastPrimaryKey ( ) }
2022-11-03 13:54:49 +00:00
}
}
2023-01-22 07:49:07 +00:00
func ( c * compiler ) compileEdge ( obj * d2graph . Object , e * d2ir . Edge ) {
2023-01-24 05:48:43 +00:00
edge , err := obj . Connect ( d2graphIDA ( e . ID . SrcPath ) , d2graphIDA ( 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 {
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 {
for _ , f := range e . Map ( ) . Fields {
_ , ok := d2graph . ReservedKeywords [ f . Name ]
if ! ok {
2023-01-22 08:59:02 +00:00
c . errorf ( f . References [ 0 ] . AST ( ) , ` edge map keys must be reserved keywords ` )
2022-11-03 13:54:49 +00:00
continue
}
2023-01-22 07:49:07 +00:00
c . compileEdgeField ( edge , f )
2022-11-03 13:54:49 +00:00
}
}
2023-01-22 09:43:25 +00:00
2023-01-24 11:09:40 +00:00
edge . Attributes . Label . MapKey = e . LastPrimaryKey ( )
2023-01-22 09:43:25 +00:00
for _ , er := range e . References {
2023-03-03 01:56:32 +00:00
scopeObjIDA := d2ir . BoardIDA ( er . Context . ScopeMap )
2023-02-23 17:46:28 +00:00
scopeObj := edge . Src . Graph . Root . EnsureChildIDVal ( scopeObjIDA )
2023-01-22 09:43:25 +00:00
edge . References = append ( edge . References , d2graph . EdgeReference {
2023-01-24 05:48:43 +00:00
Edge : er . Context . Edge ,
MapKey : er . Context . Key ,
2023-01-22 09:43:25 +00:00
MapKeyEdgeIndex : er . Context . EdgeIndex ( ) ,
2023-01-24 05:48:43 +00:00
Scope : er . Context . Scope ,
ScopeObj : scopeObj ,
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 ) {
keyword := strings . ToLower ( f . Name )
2023-01-22 09:43:25 +00:00
_ , isReserved := d2graph . SimpleReservedKeywords [ keyword ]
2023-01-22 07:49:07 +00:00
if isReserved {
c . compileReserved ( edge . Attributes , f )
return
} else if f . Name == "style" {
if f . Map ( ) == nil {
return
}
c . compileStyle ( edge . Attributes , f . Map ( ) )
2022-11-03 13:54:49 +00:00
return
}
2023-01-22 07:49:07 +00:00
if f . Name == "source-arrowhead" || f . Name == "target-arrowhead" {
if f . Map ( ) != nil {
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
if f . Name == "source-arrowhead" {
edge . SrcArrowhead = & d2graph . Attributes { }
attrs = edge . SrcArrowhead
} else {
edge . DstArrowhead = & d2graph . Attributes { }
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-01-22 07:49:07 +00:00
for _ , f2 := range f . Map ( ) . Fields {
2023-01-22 08:59:02 +00:00
keyword := strings . ToLower ( f2 . Name )
2023-01-22 09:43:25 +00:00
_ , isReserved := d2graph . SimpleReservedKeywords [ keyword ]
2023-01-22 07:49:07 +00:00
if isReserved {
c . compileReserved ( attrs , f2 )
continue
} else if f2 . Name == "style" {
if f2 . Map ( ) == nil {
continue
}
c . compileStyle ( attrs , f2 . Map ( ) )
continue
} else {
2023-01-22 08:59:02 +00:00
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 {
visiblity := "public"
name := f . IDVal
// See https://www.uml-diagrams.org/visibility.html
if name != "" {
switch name [ 0 ] {
case '+' :
name = name [ 1 : ]
case '-' :
visiblity = "private"
name = name [ 1 : ]
case '#' :
visiblity = "protected"
name = name [ 1 : ]
}
}
if ! strings . Contains ( f . IDVal , "(" ) {
typ := f . Attributes . Label . Value
if typ == f . IDVal {
typ = ""
}
obj . Class . Fields = append ( obj . Class . Fields , d2target . ClassField {
Name : name ,
Type : typ ,
Visibility : visiblity ,
} )
} else {
// TODO: Not great, AST should easily allow specifying alternate primary field
// as an explicit label should change the name.
returnType := f . Attributes . Label . Value
if returnType == f . IDVal {
returnType = "void"
}
obj . Class . Methods = append ( obj . Class . Methods , d2target . ClassMethod {
Name : name ,
Return : returnType ,
Visibility : visiblity ,
} )
}
}
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 {
typ := col . Attributes . Label . Value
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 {
2022-12-20 01:14:39 +00:00
Name : d2target . Text { Label : col . IDVal } ,
Type : d2target . Text { Label : typ } ,
2022-11-03 13:54:49 +00:00
}
2023-01-24 05:48:43 +00:00
if col . Attributes . Constraint . Value != "" {
d2Col . Constraint = col . Attributes . Constraint . Value
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 {
2023-01-28 01:19:12 +00:00
if _ , ok := d2graph . BoardKeywords [ f . Name ] ; ok {
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 ) {
2023-01-22 09:43:25 +00:00
keyword := strings . ToLower ( f . Name )
2023-01-24 11:09:40 +00:00
_ , isReserved := d2graph . ReservedKeywords [ keyword ]
2023-01-22 07:49:07 +00:00
if isReserved {
switch obj . Attributes . Shape . Value {
case d2target . ShapeCircle , d2target . ShapeSquare :
2023-01-24 11:09:40 +00:00
checkEqual := ( keyword == "width" && obj . Attributes . Height != nil ) || ( keyword == "height" && obj . Attributes . Width != nil )
2023-01-22 07:49:07 +00:00
if checkEqual && obj . Attributes . Width . Value != obj . Attributes . Height . Value {
c . errorf ( f . LastPrimaryKey ( ) , "width and height must be equal for %s shapes" , obj . Attributes . Shape . Value )
}
2022-12-29 04:47:40 +00:00
}
2022-11-03 13:54:49 +00:00
2023-01-22 07:49:07 +00:00
switch f . Name {
2023-01-24 11:09:40 +00:00
case "style" :
if obj . Attributes . Style . ThreeDee != nil {
2023-02-25 03:16:30 +00:00
if ! strings . EqualFold ( obj . Attributes . Shape . Value , d2target . ShapeSquare ) && ! strings . EqualFold ( obj . Attributes . Shape . Value , d2target . ShapeRectangle ) && ! strings . EqualFold ( obj . Attributes . Shape . Value , d2target . ShapeHexagon ) {
c . errorf ( obj . Attributes . 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-01-27 18:44:25 +00:00
if obj . Attributes . Style . DoubleBorder != nil {
if obj . Attributes . Shape . Value != "" && obj . Attributes . Shape . Value != d2target . ShapeSquare && obj . Attributes . Shape . Value != d2target . ShapeRectangle && obj . Attributes . Shape . Value != d2target . ShapeCircle && obj . Attributes . Shape . Value != d2target . ShapeOval {
c . errorf ( obj . Attributes . Style . DoubleBorder . MapKey , ` key "double-border" can only be applied to squares, rectangles, circles, ovals ` )
}
}
2023-01-22 07:49:07 +00:00
case "shape" :
2023-01-24 11:09:40 +00:00
if obj . Attributes . Shape . Value == d2target . ShapeImage && obj . Attributes . Icon == nil {
c . errorf ( f . LastPrimaryKey ( ) , ` image shape must include an "icon" field ` )
2023-01-22 08:21:03 +00:00
}
2023-01-22 07:49:07 +00:00
in := d2target . IsShape ( obj . Attributes . Shape . Value )
_ , arrowheadIn := d2target . Arrowheads [ obj . Attributes . Shape . Value ]
if ! in && arrowheadIn {
c . errorf ( f . LastPrimaryKey ( ) , fmt . Sprintf ( ` invalid shape, can only set "%s" for arrowheads ` , obj . Attributes . Shape . Value ) )
}
2022-12-29 05:14:25 +00:00
}
2022-11-03 13:54:49 +00:00
return
}
2023-01-22 07:49:07 +00:00
if obj . Attributes . Shape . Value == d2target . ShapeImage {
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
}
2023-01-22 08:59:02 +00:00
obj , ok := obj . HasChild ( [ ] string { f . Name } )
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
}
}
func ( c * compiler ) validateNear ( g * d2graph . Graph ) {
for _ , obj := range g . Objects {
if obj . Attributes . NearKey != nil {
2023-02-19 16:15:59 +00:00
nearObj , isKey := g . Root . HasChild ( d2graph . Key ( obj . Attributes . NearKey ) )
2022-12-25 21:42:11 +00:00
_ , isConst := d2graph . NearConstants [ d2graph . Key ( obj . Attributes . 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 {
c . errorf ( obj . Attributes . NearKey , "near keys cannot be set to an ancestor" )
continue
}
nearIsDescendant := false
for curr := nearObj ; curr != nil ; curr = curr . Parent {
if curr == obj {
nearIsDescendant = true
break
}
}
if nearIsDescendant {
c . errorf ( obj . Attributes . NearKey , "near keys cannot be set to an descendant" )
continue
}
2023-02-25 03:05:23 +00:00
if nearObj . OuterSequenceDiagram ( ) != nil {
c . errorf ( obj . Attributes . NearKey , "near keys cannot be set to an object within sequence diagrams" )
continue
}
2023-02-19 16:15:59 +00:00
} else if isConst {
2022-12-27 05:22:23 +00:00
is := false
for _ , e := range g . Edges {
if e . Src == obj || e . Dst == obj {
is = true
break
}
}
if is {
2023-01-22 07:49:07 +00:00
c . errorf ( obj . Attributes . NearKey , "constant near keys cannot be set on connected shapes" )
2022-12-27 05:22:23 +00:00
continue
}
2023-02-19 16:15:59 +00:00
if obj . Parent != g . Root {
c . errorf ( obj . Attributes . NearKey , "constant near keys can only be set on root level shapes" )
continue
}
if len ( obj . ChildrenArray ) > 0 {
c . errorf ( obj . Attributes . NearKey , "constant near keys cannot be set on shapes with children" )
continue
}
} else {
c . errorf ( obj . Attributes . NearKey , "near key %#v must be the absolute path to a shape or one of the following constants: %s" , d2format . Format ( obj . Attributes . NearKey ) , strings . Join ( d2graph . NearConstantsArray , ", " ) )
continue
2022-12-27 05:22:23 +00:00
}
2022-11-03 13:54:49 +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-02-28 22:30:25 +00:00
if obj . Attributes . Link == nil {
2023-02-05 18:36:38 +00:00
continue
}
linkKey , err := d2parser . ParseKey ( obj . Attributes . Link . Value )
if err != nil {
continue
}
2023-03-01 22:53:58 +00:00
if linkKey . Path [ 0 ] . Unbox ( ) . ScalarString ( ) != "root" {
2023-02-05 18:36:38 +00:00
continue
}
2023-03-03 01:56:32 +00:00
if ! hasBoard ( g . RootBoard ( ) , linkKey . IDA ( ) ) {
2023-03-01 23:38:02 +00:00
c . errorf ( obj . Attributes . Link . MapKey , "linked board not found" )
2023-02-05 18:36:38 +00:00
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
}
2023-03-01 22:53:58 +00:00
func hasBoard ( root * d2graph . Graph , ida [ ] string ) bool {
2023-03-03 03:32:31 +00:00
if len ( ida ) == 0 {
return true
}
if ida [ 0 ] == "root" {
return hasBoard ( root , ida [ 1 : ] )
}
id := ida [ 0 ]
if len ( ida ) == 1 {
return root . Name == id
}
nextID := ida [ 1 ]
switch id {
case "layers" :
for _ , b := range root . Layers {
if b . Name == nextID {
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 {
if b . Name == nextID {
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 {
if b . Name == nextID {
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
func d2graphIDA ( irIDA [ ] string ) ( ida [ ] string ) {
for _ , el := range irIDA {
n := & d2ast . KeyPath {
Path : [ ] * d2ast . StringBox { d2ast . MakeValueBox ( d2ast . RawString ( el , true ) ) . StringBox ( ) } ,
}
ida = append ( ida , d2format . Format ( n ) )
}
return ida
}
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 {
if f . Name == "shape" && f . Primary_ . Value . ScalarString ( ) == d2target . ShapeSequenceDiagram {
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 ++ {
e . ID . SrcPath = append ( [ ] string { "_" } , e . ID . SrcPath ... )
e . ID . DstPath = append ( [ ] string { "_" } , e . ID . DstPath ... )
}
break
}
srcParent = f . Map ( )
}
}
}
func hoistActor ( seqDiagram * d2ir . Map , f * d2ir . Field ) {
f2 := seqDiagram . GetField ( f . Name )
if f2 == nil {
seqDiagram . Fields = append ( seqDiagram . Fields , f . Copy ( seqDiagram ) . ( * d2ir . Field ) )
} else {
d2ir . OverlayField ( f2 , f )
d2ir . ParentMap ( f ) . DeleteField ( f . Name )
}
}
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 {
if f . Name == "shape" && f . Primary_ . Value . ScalarString ( ) == d2target . ShapeSequenceDiagram {
return m
}
}
n = m
}
}