pax_global_header00006660000000000000000000000064146072646060014525gustar00rootroot0000000000000052 comment=63b45f4bc1c03d491c59fd6cad5c54ce816819ef dot-1.6.2/000077500000000000000000000000001460726460600123215ustar00rootroot00000000000000dot-1.6.2/.gitignore000066400000000000000000000001041460726460600143040ustar00rootroot00000000000000/*.dot /dotx/*.dot /dotx/*.svg /dotx/*.png /*.png .idea coverage.outdot-1.6.2/.travis.yml000066400000000000000000000003211460726460600144260ustar00rootroot00000000000000language: go matrix: include: - go: "1.11.x" script: - go test -race -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) env: - GO111MODULE=on dot-1.6.2/CHANGES.md000066400000000000000000000032011460726460600137070ustar00rootroot00000000000000# Change history of the dot package ## v1.6.2 - fix issue #38 : incorrect handling of strict option (thx kiambogo) ## v1.6.1 - 2024-01-17 - deprecated MermaidShapeCirle => use MermaidShapeCircle ## v1.6.0 - 2023-07-30 - add Record builder ## v1.5.0 - 2023-06-20 - add GetID on Graph ## v1.4.0 - 2023-03-28 - add Composite extension ## v1.3.0 - 2023-02-21 - Escape/quote mermaid - add FindNodeWithLabel ## v1.2.0 - 2022-11-18 - add BidirectionalEdge #28 ## v1.1.0 - 2022-11-07 - add support for Mermaid graph out. ## v1.0.0 - 2022-06-22 - add support for port, see https://github.com/emicklei/dot/pull/25 (thx v-electrolux) ## v0.16.0 - 2021-05-04 - add DeleteNote, see https://github.com/emicklei/dot/pull/24 ## v0.15.0 - 2020-10-30 - add Node initializer, see Issue #15 - add Edge initializer ## v0.14.0 - 2020-08-25 - add Attrs for conveniently adding multiple label=value attribute pairs. ## v0.13.0 - 2020-08-22 - add FindSubgraph ## v0.12.0 - 2020-08-20 - Added style methods to Edge to easily add bold,dotted and dashed lines. (#21) ## v0.11.0 - 2020-05-16 - add functionality to find node by id - add function to find all nodes of a graph ## v0.10.2 - 2020-01-31 - Fix indexing subgraphs by label ; must use id. Issue #16 - Add Label(newLabel) to Graph - Add Delete(key) to AttributesMap - Use internal ids for subgraphs ## v0.10.0 - Allow setting same rank for a group of nodes - Introduce Literal attribute type - Introduce Node.Label(string) function ## v0.9.2 and earlier - Add support for HTML attributes. - fixed undirected transitions - Change how node is printed, so that attributes only affect individual node dot-1.6.2/LICENSE000066400000000000000000000020511460726460600133240ustar00rootroot00000000000000Copyright(c) Ernest Micklei MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dot-1.6.2/README.md000066400000000000000000000057051460726460600136070ustar00rootroot00000000000000## dot - little helper package in Go for the graphviz dot language [![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/dot)](https://goreportcard.com/report/github.com/emicklei/dot) [![GoDoc](https://pkg.go.dev/badge/github.com/emicklei/dot)](https://pkg.go.dev/github.com/emicklei/dot) [![codecov](https://codecov.io/gh/emicklei/dot/branch/master/graph/badge.svg)](https://codecov.io/gh/emicklei/dot) [DOT language](http://www.graphviz.org/doc/info/lang.html) package main import ( "fmt" "github.com/emicklei/dot" ) // go run main.go | dot -Tpng > test.png && open test.png func main() { g := dot.NewGraph(dot.Directed) n1 := g.Node("coding") n2 := g.Node("testing a little").Box() g.Edge(n1, n2) g.Edge(n2, n1, "back").Attr("color", "red") fmt.Println(g.String()) } Output digraph { node [label="coding"]; n1; node [label="testing a little",shape="box"]; n2; n1 -> n2; n2 -> n1 [color="red", label="back"]; } Chaining edges g.Node("A").Edge(g.Node("B")).Edge(g.Node("C")) A -> B -> C g.Node("D").BidirectionalEdge(g.Node("E")) D <-> E Subgraphs s := g.Subgraph("cluster") s.Attr("style", "filled") Initializers g := dot.NewGraph(dot.Directed) g.NodeInitializer(func(n dot.Node) { n.Attr("shape", "rectangle") n.Attr("fontname", "arial") n.Attr("style", "rounded,filled") }) g.EdgeInitializer(func(e dot.Edge) { e.Attr("fontname", "arial") e.Attr("fontsize", "9") e.Attr("arrowsize", "0.8") e.Attr("arrowhead", "open") }) HTML and Literal values node.Attr("label", Literal(`"left-justified text\l"`)) graph.Attr("label", HTML("Hi")) ## cluster example ![](./doc/cluster.png) di := dot.NewGraph(dot.Directed) outside := di.Node("Outside") // A clusterA := di.Subgraph("Cluster A", dot.ClusterOption{}) insideOne := clusterA.Node("one") insideTwo := clusterA.Node("two") // B clusterB := di.Subgraph("Cluster B", dot.ClusterOption{}) insideThree := clusterB.Node("three") insideFour := clusterB.Node("four") // edges outside.Edge(insideFour).Edge(insideOne).Edge(insideTwo).Edge(insideThree).Edge(outside) See also `ext/Subsystem` type for creating composition hierarchies. ## record example See `record_test.go#ExampleNode_NewRecordBuilder`. ## About dot attributes https://graphviz.gitlab.io/doc/info/attrs.html ## display your graph go run main.go | dot -Tpng > test.png && open test.png ## mermaid Output a dot Graph using the [mermaid](https://mermaid-js.github.io/mermaid/#/README) syntax. Only Graph and Flowchart are supported. See MermaidGraph and MermaidFlowchart. ``` g := dot.NewGraph(dot.Directed) ... fmt.Println(dot.MermaidGraph(g, dot.MermaidTopToBottom)) ``` ## extensions See also package `dot/dotx` for types that can help in constructing complex graphs. ![](./doc/TestExampleSubsystemSameGraph.png) ### testing go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out (c) 2015-2023, http://ernestmicklei.com. MIT License. dot-1.6.2/attr.go000066400000000000000000000032231460726460600136220ustar00rootroot00000000000000package dot // HTML renders the provided content as graphviz HTML. Use of this // type is only valid for some attributes, like the 'label' attribute. type HTML string // Literal renders the provided value as is, without adding enclosing // quotes, escaping newlines, quotations marks or any other characters. // For example: // // node.Attr("label", Literal(`"left-justified text\l"`)) // // allows you to left-justify the label (due to the \l at the end). // The caller is responsible for enclosing the value in quotes and for // proper escaping of special characters. type Literal string // AttributesMap holds attribute=value pairs. type AttributesMap struct { attributes map[string]interface{} } // Attrs sets multiple values for attributes (unless empty) taking a label,value list // E.g Attrs("style","filled","fillcolor","red") func (a AttributesMap) Attrs(labelvalues ...interface{}) { if len(labelvalues)%2 != 0 { panic("missing label or value ; must provide pairs") } for i := 0; i < len(labelvalues); i += 2 { label := labelvalues[i].(string) value := labelvalues[i+1] a.Attr(label, value) } } // Attr sets the value for an attribute (unless empty). func (a AttributesMap) Attr(label string, value interface{}) { if len(label) == 0 || value == nil { return } if s, ok := value.(string); ok { if len(s) > 0 { a.attributes[label] = s return } } a.attributes[label] = value } // Value return the value added for this label. func (a AttributesMap) Value(label string) interface{} { return a.attributes[label] } // Delete removes the attribute value at key, if any func (a AttributesMap) Delete(key string) { delete(a.attributes, key) } dot-1.6.2/attr_test.go000066400000000000000000000014661460726460600146700ustar00rootroot00000000000000package dot import "testing" func TestAttributesMap_Attrs(t *testing.T) { g := NewGraph() g.Attrs("l", "v", "l2", "v2") if got, want := g.attributes["l"], "v"; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } if got, want := g.attributes["l2"], "v2"; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } func TestAttributesMap_AttrsMissingValue(t *testing.T) { caught := false defer func() { if r := recover(); r != nil { caught = true } }() NewGraph().Attrs("l", "v", "l2") if !caught { t.Fail() } } func TestAttributesMap_EmptyKey_NilValue(t *testing.T) { g := NewGraph() g.Attr("", "skip") novalue := interface{}(nil) if got, want := g.Value(""), novalue; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } dot-1.6.2/doc.go000066400000000000000000000004111460726460600134110ustar00rootroot00000000000000// Package dot is a helper for producing graphs in DOT format (graphviz). // It offers Graph,Node and Edge objects to construct simple and complex (e.g. nested) graphs. // // Copyright (c) Ernest Micklei. MIT License package dot // import "github.com/emicklei/dot" dot-1.6.2/doc/000077500000000000000000000000001460726460600130665ustar00rootroot00000000000000dot-1.6.2/doc/TestExampleSubsystemExternalGraph.svg000066400000000000000000000050301460726460600224440ustar00rootroot00000000000000 n1 component n2 subsystem n1->n2 in1 n1->n2 in2 n2->n1 out2 dot-1.6.2/doc/TestExampleSubsystemSameGraph.png000066400000000000000000001263211460726460600215430ustar00rootroot00000000000000PNG  IHDR]*RbKGD IDATxwXTg7RUA)b& bWDĂX7lm-YF͚Dqc7XE4",b2 e9:\WS30ys#`1B!&%B!w.B!5E!!BԀB!BP"BQ ]B!j@B!D (tB!.B!5E!!BԀB!BP"BQqCV޴i_XB!Mo߾X`ewXB ƪBit|@HBxxxСCQ !$.E!!BԀB!BP"BQ ]B!j@z*..DZzjK!P">3gb|B!EH=M6 { ٳ!.B@$A /bٲeBH5xDzBZ2.]7oB$^^^8y$=z]]]̜9R{L&D"Apppm]v gΜ#Ə}\p---7ZZZqadd1cƼqH@ `۶m055? 33Ȁ' SZZǏcAxx8H$³gp BA__ `  Bx|rcc111O><{{{֥KuQQg}]2???lmm}0{{{=zH.\ӓHcŋl֬Y,..1]]]6w\cQQQ{ ۸q#={6[d kӦ ?~<۱c2e 8q"߿eBGW9EYJuЁEFFr֮];00P%t1ƘKХݻms̘1 >N8p1233m0333T*eVVV6c ]~1ئMv!nK2ȑ#ܴ+V0---P(riOyg X[[#88Ǐ,Z۱5?pԩشic}!445ާl(--Œ%K0o<̛7YYY֭>|000888pUMAyy9233BBy}G@@oB +5C `HJJBxx8uwB"`˖-ϩSCfmm*444%%%x!.Nի0w\DEE/^h6 ++Zcʔ)ܹ36n܈wb_]"ɐd :B!Bygc޽ZpQX,FYYY"u#22/|P JJJuV ?X!4 ]֭[T#F@СCu^^vލ޽ϟ?GJJ CTr:` 64g //Q" ;;)))x`ffE믿FRR0{lL: J tˊ/G!DVZUߕ:0aBcCr^8::rwHlٲǎ/о}{/_Ƒ#G@OO7oP(|>*iii!-- pssum۶Ş={]t1rHDDDزe i&XXXXv-={R899֭[Xn^zÇ//^W^h߾}Ni*yE ]AAAfBZ\RlWLnn.eeev:/--E^^U+VXX ~aa!B!TA ԸOByE*;L*P]~Sm>nݺ++*6WWo!EObccd888 ** +%BiBRDLL bcccXZZ]!&Dŋ  FbBi(t£A%ҺkB!5E!!BԀB!BP"BQ ]B!j@B!D (tB!.B!5E!!BԀB!BP"BQ ]B!j BeveҢM0 ]ڵk BZ4 ]EH3BZ0BZE!!BԀB!BP"BQ ]B!j@B!D hBHT\\ .͛.GmR)ۇT{ŋ}LJNN4TTT_\N:]!-"!E6mzwسg駟̙3>2220sL*wrrؠBʕ+q =#B!7=ÇP(JMXx18<|||j]ݻH$زe s)<|!!!oY[[4 @III,Z ,BH3A›^z!..sETT\\\ŋmS__>LΝ;cƍ{.U:mCH$Brr2d2Ymoggg=ICi\!(//޽{ǵeeeѣX:o7>>EEEy>@SSGdd$/^>5 ( ny'''`֭*5~Tرc`q@+]tQOi:t1!1lݺ!!!1b:t:F`ݘ0as!??FFF^=HTB(|СCưaPVVV>*͙3k׮E^^k]D"Avv6RRR̰h"wÇk. t/^@neח;<6l؀|^JLLDϞ=1hРՐwRDnn. JQPPBHR/_-m۶&ZWW000>zzzj=N>P"&55'O㑖? l߾ӧO_u%%%8rfΜO>?#++ kJzzz4iJ5k?ƙ3g%K`ɒ%Zˮ_ݻw6mڄ?iii Xz5+bǎuaڵJ@II F֬iiiHIIAjj*?~,ӧENNJ n6m@OOm۶3?uttT&JRruAAcϯ2mt&&&ԩLMMѵkWXYY [v!((o Cpp0phrJ077v\ʪt^ZZ e6[=z@=U㧟~իѮ];"((jIR\BjR\\0۷QQQȑ#{npäС*q19rGFiӦaРA*7&zG!?Ĝ9sйsg̝;FFF_Gb5.]bŊHLLĐ!C`cc999_ ]B)J9r{\UV!##dXp!nܸ7obĈX~=F.B!DMr9݋={b„ ĕ+Wp]|U@ ۷#.. D\\\OBQgӧOG޽Çӓ_۷q!Ȩv)tB!M7nakk{aϞ=4BƍCll,?8`*ϞFB!aoo$={GjNZѣG#!!˗/Z^!FV\\Srf}6.4/_D͍{}m 5BH#Ñp9H#377GTTbƌ͛:.B!<}Æ هzb_~%0i$c֭5>B!?CKK ϟGN.qpQ?"߸,"BYPQQ/Rzضm<(tB! c8q{ 吿سgO# }ɓ'.CBi",] ..ŋl2뫯BǎbŊjS.B!~'d2}_ x{{/J$aĈ*KKKqq=DxN8P sfddĉq%9s;wƌ3SqQ|HLLann)StFDDD222ݨPmDFF" ۶m㎱hkkcʕ W_}D2ZZb̛7044ļyPRRwY(?,--"!!H3ua;j߽{ GGG|_ѭ[7nݺOOOhhh`޼y(((wҥKprr¤IuV|HKKÔ)S;wb…x"f͚n߿ h"̝;{۷cРAdɓpuuyflڴ  ņ EFFbժUpvv-0o޼Zo҂Z&7ntuuqر3Yl4>ihh0 `3f,R >} ŬSNkr4\.g:::lݍ7W^lܴXN<˙ [rz'Ofݻ16mCq,]`G᦭Xiii1BM a%$$p2l֭rϟqqqa1ƤR)b3f0c033iȑlԩUSKW+T*qd2߿Ji98HĽx\cU9z9JKKѵkW/<<7oބ/7R~~~{ᡲ7***k..E:88pX[[i666(//Gff&7m۶Ű-]b/_u "mlleNGii),Yya޼yBnZm-mlVVVHOO2t2J tBRYmW***2>%['Puڶm ccc隚= PWWWeIIIoܶvi.*mڴA.]۠D"޽{[lq5mC@ >[X oooD^4>>>)cR*0B!п"Qv OTo_2022jˑ ++FA$!99YM};tWDPjvڅCB @ `ذaرcebL4 VZtǏC~ɼ t ]Vu\$xuAP^^gaѢE\s> {6$ :uꄶm6PӧO#//Ug6w>ݽؼM6M62H رcȈ24cٳgm۶lÆ M`^^^L 0@222l޼yޞc;wdɓ'1Ʈ]Ɯﳔ\\\޽ˮ]<<<6av}csa"}Gllĉ_޷Ŭ6sLj*&Xbb"ѣw'=6"##X,flM͍U;_X5Ek)((pСn4 &xIXXMG.Zn֮]7nؔ T*>TUXXw]tioÏ? @eԦ!-- k5BofSZv-֭[ٳ| ` ꈙ+!PAA6n܈O>gϞUkz,]nnn7.G Dnn.|||Ǐ&PXXnܸ={… XjnܸW6Q5377Gdd$ݻwk/ٳ`n޼wI<==qLVt777x yWPr !5GTTRRRЧOܺuݻ|[]m6 :ѣGURHpwAT"!!֭޽{Q>T>琦fGܾ}~~~jMZ'@"۶m㻤Fc``CCC1<"cܹXx19WgqaN<3f 771ܾ}ϐe˖]6!FcC1:tWƙ3gx:Қ/0o<8p7o@ ?ɓpqq#G(fPII ̙>pppPi1#D"VZWX` .͛71p@`ȑHLLS(t5 ] ! ;ڶm۷C*oT*Ň~شw;nܸ;w_~>dgg][n!44{˗/qUl۶ ۷(t =9R{5CBǸq_'4@ @hh(d۷ݻwǢEwy/czꅤ$:t111v)tBkЮ]ޅDH}>Ǐzj߿]v/;Lw\lܸ2d^|`ر5Q!5 ʵm ,ǏqA( /ƍ7ʆ|G ХK|6ln޼K.չVM(t51io{reeeM]*!R}ΡR[ ϟ#>>~*': ?xfΜGX`^J52T ((FNNmۆLlٲNNN_ ]Xr%~C"(KKK|x "" &L۷#55R[BXlذ^^^066FHH~z#** ӦMC6mk@eS?4̄ z iLxTPPMMjy%^ݽ{8<._/_{4h wwwB(vJ%%%Ett4]K.;w#GĆQ !4CCCK F… Q^^k׮ܹsr ۇ/_B__nnnpwwz=z@,nEEEHLLĝ;wׯ#!!rXz5`kkkA!ZZZ2d }6g޽999P(Ԅ-z {{{XYY ]v~P(HHH@BB?~ Ӄ#e˖!J,W^طonܸ/| Prl߾P(^+++XXXK.011Aǎajj cccа7r"''999DNNLɓ'pmڴ{;;;̞=={DϞ=aii!.B!*--Ehh(N<{bʔ)ggg8;;,+(55iiiHHH@nn.PPPPeӃ>@ri^KKK_dAAN&J(,,DQQR)JKKU%`bb ZvN:5n!GƝ;wpY 8544Э[7t˔!77O>EQQ QPPTbrHRn$''ɓ'pppןLKK &&&\P\+͛GA"4ΛP"BZ|5 clW[[fff033w6dآ/_bذat뽝/%BZ, 8ٸrJIII4h?w9B!B<{ C\.{)qqX[[]ZT7L&C\\Fj!foŖ-[/ӓrM7d2eeejqP"BH,YׯǸq.G,,, W9T?\YQ"BtL8~~~Xd 娝)r?j(tB!Dyy9ƏcccݻԔ{X_U1///,Xpj !VlʕHJJBll,tuu.d*b1 FUV͍B! \r7nѣGMΝ.X @ kN⹺ˋB ???L>rxU9*./^t|7?su CBŋQVV;v] $ 6l؀Obر#|||`hh0k ]BOڵ allw9’%KMԄ?=ce GBRļyп]NhxRB!“m۶͛غu;9 <ݻwGJJ Oޠdgg#** lT*Ŷm۰tRܹ/_L=6E!L&Ù3g0n8@\\vލ;wy[Xhpر.{Lѣ6n܈of͂cNx1zRW!5FAAF Dnn.|||꼭Ǐ#44]f>S9sGFFfΜGaŊ*Y[[CPѣGV={UK]Q"B$::VVVܴ:k[nnniU])66SL#k֬P(ĵkT555<{Qjx"-[VZJܠ !Rkݻ7ZTҥKՅ ==GD?~2e VRcv$ \]]!FvA("//RHJJF333ɓ'#b̙Jسgd2$ l۶ ׯ_k#BZ{DcС*(bؼy36mڄhbÆ |^k9W*)E" p-xzzBCCCAAKعs'V^ Chh(>s|w###8::BKK 033S-uEBQ %+WT3f?ɓpqq#G^o}41]|b~iyFFFؾ};Ǝq .ѣ1k,$&&@gZqx511x`իε.B!DM```ҪײÓ'OFǤP(rJ8qU旕!77*ӽQQQ]vթZj[K]P"BԤP6"&?5ӢE`8;;W;rTKMm.B!DM455QQQwѣGqʐYXX@CCFFFuBWmj ]B루2cǎ1P.]Ry]y˗U'$$@&o߾reee5S @PԻ!#!5H$^W^&U[P^^ƘJL~~>5XS}ذaBBBx՟*11={ĠA1c|2|?7<gggrY~= DEET*Ś5kb b>}?_ЩS'~[f ӨQ*ӧOϟ?Gyy9/_~ Fŋ! .@SS۷c믱n:G̙3e\]]fxzzֺE!= pwwǡC,L8'N|||p){Lŵ~eK{ݻ077熦ׯ#77^o2F^^B!TK]Q.B!DMݑrǏ!ХKǽNe*p)Q"BԤo߾ӧ.ŊE.B!DM 0|3|sWWW˨ ]B?4tD=5@|E!ј1cT*q)KiqݻLxzz]JP"BԨ]v5j7Ǐ|R/!5[l]VeDuR'O"RB!f8p KKi1rss K7 ]B/_]JoA,c|Ro!x{{~)c|o:th.B!'?]={]Jp?RH!~!.\___tЁj%33WV3gN:UY~gD"L0AVb۶mXd J%7TbB!*'!ﺬ,C&q Cp@ L ]˱}vy%%%OUSN8qrrr; c ؽ{7L&'NlїjA*@EE4TN^BDpKii)Jeshȑ|KZ?Ν;qQnzqq11m4XaUB!!!ŐddP*/0'OnоbbbpM̘11JVs߿?7JB!Ν;2BZSxΣ`5UD5kf̘Taq ?]b&M⮨zݠڵ 666۷oPjENZ|@P5UCHT|HcǎjK;XXX ((CE\\d2 A.] {o>())0k֬zo9Պ8J@ رcX!-K0lذϣ}!}vܾ}nR_X]N jd ޽24PjEUH$7ڷoCe!!!o3I(d1{lUkڵn<)Svvv޶BoL>6Z;+1CE,ƍfb000*HLLTi᪤T*q=>}ެ[npttr QCC~}A<~Q~l.(t2cƌV隚-yUK۶mW咉B/.IdggHNN6pUDXv+*WXryڸq#Э[m9ʴicǎUرcѶm[+#FSE5{,--P(? x_k׮4iPXXX{gΜA\\/^%6Zɓ'|[d2e Ҳ5 k BGGǪHk䄳g">>~~~o _bׯWs5H$[ W_apqqi ]7"BZMMMq} Q^zѣѣ _rHHH~W/c ƍ"##qEX1JkV(tB4i455UMɓ's?bС&&&֊+0b < l&(tR#d2L4riqݦ^BF||<|}}%ɰ~]"]v1bc ĉƚ5kBW+5`t;vĀ.G$q'Ns5]cǎ|) lܸT@GGGJ9ƍwwFyJ BL:Vϒ#ToҤID~.Ν;0avލ<3'O^?pܹj[˛|磬 (**L&Caa!7M*B.C.C*VFQQ w2Ι3"Hs}%===bbAGG000iFFF022j())A~~>wt.@aaaKKKQVV7nJxDZZZhӦ oRRRxٷRĝ;wѣGSjN 4"-- ?FVV23332 :vݺu{oPG !TZ !TANN6:vSSSڵ+,--aaaQm+i***ܩ'OLӡCt:t:u[[j@__ZuեV~rrr\dff|I ߏΝ;cii Dc#M:t($ Ν;GW@&a„ yT_r(t@*"99IIIHJJBrr2RRR*ebb>}@"@"pAeKRQ*hVVS矑ícddKKKXYYud2"11ý{C Je6mŐ!CЩS'ٙ a...ܹ:<@~`dd+V?令VOPիWqYܥC׳gp \zW\ALL ***```={bXp!\]]akkK+4bݻwG2/33Ϻu됓Hkkk2dx:C&8<\W"??޽;\]]1n8ŅZ~^.)((@BBwD"1`.SeXZZqUصk z ^^^t9d2^3g?QQQ.]`Xv-<==aggƇG{wWcuNV!Se)Kfk(T٢i XL6f쌈Dv}$,R#-F!÷slxx}^}E #** QQQ駟P\\͛>t'r19\Gi4N:SJt>/B!o;waaa}6|>XYY_~tw\Wya2_h\r`W^=@XX"t7SXaԄ5=(\v Vœ9s;;;ښN!EEEKve r pIBOOvvv?>lllj444D;paX|91zh{>`ܽ{4h`gg#EEEn޼)*w X[[cܸqprr; L]111Dff&LMM1o< >ݻw:b3f ƌ{.Ξ=ÇcΝhݺ5\\\KKK֟ ۷W_7nDPIrrrV^lرc􄗗777 6 \G&DHGbڵ ӧN:'"99qqqXp!\ƌh"#==fBhh(zCCCl޼AoVΜ9'O? \FZl 9sϟ?ǎ;k⫯ITڢA6m0gx!`dduD@c…HII۷7`Ţm\G"EEEزe пxH“:ѴiSxxx $$O~-_xyyA^^n$L>pvvF^^cشiz ܻw[n QQQӧajjc"1]7oބ o\t k׮LJXr%QgϞoWUUׯŋN .ҥKN:q&q6l.\ ?vXܽ{;v_ 6H}B+]/^ ̐sUx4AAA\Girrr0o<ɓ>>x";$$$PQ^ 臟jҥKx @II Gnݺ$hiiaȑ\sBmT`0vX6vj[VVƴ?\&kdwf1vm֫W/kݺucmڴK+6|pY.]6a>Ǐl~zcˬSN ۰a1c[`SQQacƌawflܸq1GGGQdZZZI&oeSNeÆ c%pM6)S0/"""Y\\ fjjjᆱ:ܹì6dwܩoXVVٳgWO.1+Wd͚5ȑ#&pʔ)GL(~I:CHP(dGa]veO<Ο%&&2Ν;Wkj₳3svv2臟ju֍JFFF,55j\_MOm?ؑ#G>엒XGզZ E,22R4"pرb)c}***:GPֳgOk.2LQQ2fddĖ-[&㙢"wc72ѣy-ZǏ-Y))) Ѵ &0EӖ.];vT;Ce{bssscEEE^6mnܸQu0Ƙc%EWzz:._\ejZtikk~KGߏә 6mnݺUe뛅5oc.$j̙3 bL﹪fjӧE%k߿---NAx044+N> 7o^ӭ[7ѩoooٳgx >\40O>b:t(/Bye=zM322BYY233ETUU!//nݺ-Zzj3TBy]"##R,X3g̙3:WX:*5S eAϞ=eEH*vڅ"lڴ EEE?zYo}!D? t7n=[СCm:}6}ZH.P(#oٲprr5k=d>}󑙙B[[[lEEE@RR@MMM?َ*+))lFi999 '''\݃.nٶ?J ޾H(я|>|}}qu?~eee?SNNo޼4~XU<ԩSÇ#<<>>>6}6}Z$V#//Ϟ=TճgO˗affVQ544B 22V5t ڵjS3Y999Bѕ|5k ΞKԏV?Ef8/;C M}֭[cÆ wu&62AU˿e_V+4ijeee8pձuV={YYY8q~s^| {{{!C "v}8|0TTTDG:СCqzYuQ?jx(11Q*~1n޼ ;;;H=Iê}Edd$ϟ)ST;l]i&KEEPXX(f0ưc!!CEhѢ\xӑ/ZOqqT=zƈ#`jjoťKi&L:Æ C=0i$\zUz(t 3f6tFn>%T:}w>xcǎapppv/_x;O\1WWWa޼yXn3f`ĉZٳgHOOGZZZ+_`ݺu1cpu 6BHvQii)֬YDѴ/^Ν;شiKҥK#FpEIVF%///hjj"77WȪupp/ԶOs6Wnlƍ-便LWW7=z_^Σ"֧Ou҅8q= :Ttwƅ )a+V`^^^짟~b<}2x<8p {X3gnݺ}={Ç cׯ_g=z`ؤIXzz:dfff >|8w~( >c1///&''Ǿ{6|6n8^|Y /_f dYYY,((ihh0lŊ%%%Ν;3 ֭:"##7 e kٲ%۽{7ʪfQkB0]]][ekz"cVZ-WWn?*..f1KKKtRyfVTTTFTSNxhhw/JVƻ[ue6P>=dD/2`4YYY{'}_ZZy^z%)ŋ|EGG9^^^LAA1XFF+,, =*b;3Y! ܹs%EW^^344d,//mGAR(??|q;u-"")++Yfh\t1&~X6c֖xOm E%'OFaa!IPy_۶m?9ϻwL}-:1U=WRSS4[UƺPۡ@e\V̜9ÁFMOZZZ8wڷo/+Q? GۣGFu"~X6```?#Ϊ8vѷo_pz޼y#:M^RRz#Gɓ7nDGtt4ѳgOJƀQ*))\\\0c >diӦ n޼ɓ'cĉ?~Q?_n݂9?ǏcTpP۷oc߾}Xdď*N;"** ˖-/LLLpQ.#< ??k֬ Mxx8,--ٳgի'.\իWѡC]uԏGFFЯ_?n =z4ױD^^uG yyy,\qqq000 q-$MMM4mTU41114hlmmF<666w;^;w޽{id/Dneggcԩ._ u\G# //OG(kHj~[ݻwǙ3gp TTTwް@@@@H'PڢO>x5\SNC#M455?ѣGpwww}mbŊ:iܹ///Xj޽ gg BjJjJ}˗===L:_u<=k֬A۶m1j(#""7n7|ujiѢ wwwh۶-NXN ȑ#aff7n`ӦMx.\(ѻ 6RWtUɓ'iӦaӦMhժkl޼\G$ DAA===[cƌp 4_D__֭CVVv܉XXXX-I ڴiCKK /^DBBf̘c9-*j +V@FF M65j>LwlGPPF xyyAQQ͛8JJJ@BB_GGGK.077rȘ7oʕ+5kZjƍXh222:H;$>8jҤ \]]BLMM11!,, Ν͛7`cc;wI&h}o߾ظq#\ 믘?>:t{{{ctd|pk׮1n8tؑ누H5)ޥɓ'cŋ-[`ɒ%!C0`XYYލTJJ qU?ϟ?.[[[[0x` <۷o0lݺJJJ0` PRR:6\~׮]Cxx8ФI 8W=ZԀL]҂ \\\C||| |oFFFҥ Ю];}tq-z >ģG##99xB!塯###899fff?ֆ3'8$$$ ""۷oGII cU~ԩڷo/4o޼ӧO#<~+GBK.ٳ'LSSSӸYm5h |Wx"5kV/4}<:t@Ğ%p}>%^rײeKkNiӦ +j -[D֭[\QQ233L<\szw͛iӦPtk֬lll>2Ɛ!:RYO{чCOObe˖Z2dggCvv6%??ӧOE{RVV> 1tP̞=]vaiT\:::pB\]ݻ{K<~=;Js%QQCTtuu-[y엲\cV\\2}}sٳgxh|>-[DVо}{&L٪s.I]xh׮ڵk!C&ѣGHHH?<_///-[BGGCr@ @II ^|A?z+;;Ϟ= hjjUVh׮`gg'vPGGwH,.^X3066G_@vv6?lddd !!AEڻTUUMBAAhҤ dDJJJ|Ħvl߿|(,,DYYX.>NYf077+ -[\տ\((**0J~~G}^^лEڻ())ASS v[~67oޠ***K~(**Byy9 >544>G;vDEG[l):Gw 6l:FhӦ Ο?/rrrՅ.zYe v eee(..K(//GQQXU\\+wV~:X!V:дiSܹs󃚚؎1 $JUuҥZ󗖖~P@ |/(Իrbk +iYAT266F~~?15 ۧQ%%455Yqqq>ݻquY.BC&MФI BdҥK@$@ꟽH7bݺu|2q!FE,OOO5 SL˗/C!,*HoߎR̙3(B̢TI[[v޽{q1B!2.R-#FigϞqB9Ttjۼy30yd0ƸC!*Hb߾}Ǟ={C!*Hǜ9srB48*UV!<<&MµkhT: -$D4B]~XXX`ڵ$,,,ccB*ڵ+V^E\GI[Bi$.̙o&MBii)q!FEb|>dzgϰdB!R.R+mڴaaa\!B]<<< OOOqBJTt:cx<̞=(BT ---ݻ‘#GC!H*Hŷ~oO<:!"U"ujꫯ0m4z(6!*HRQQA`` ._m۶qBTt:gnnŋcHMM:!""bҥ066顴B"D^^CJJ ֬YuB!sTtzcddkb͚5:!)*H9s&lmmj/Sӧ5n6l.\BkTtzg`…^...{n˃{{{ׯ !:'uj vرc1|p 6eƎhѢFm֭[h֬`XlaeeE !@GD=nnnċ/LM .@C .ø@CCF"B]DbmL>yB!"##qmѴ'O` HLLĚ5kpB"?uf!H&݋SN/)) ur58;;=ZgHqqq8CBBk(**BEEBd4#]D*ݻ^ܹskMRE!DPE6v܉={|0)!PEȑ#1ydx{{\!B]DČ3B!)*TQSSþ}pYݻ8BHH+++̝;gƃC! *TZz5:vɓ'8BHQEsB5*֭VZK:!R+Tt? &MBii)q!/FEj|>@VV.]uB!QE^6mqFlڴ bz = J!DQEdɓ1fxxx ??p-c֭p !ϣȌm۶͛7+Я_?ddd@NNϟ:!Y\ ZhիWc Ν;x< !O#]D&0ưk.̚5 rrrb!>>tBHը"R/33C 7^~#!FEj^5?87oB!5CEj***Azse!&&LG!T]DҥKظq#!/?***peɆ#B."x<|||p i #//OuBZTtbaa 6DDyy9BBBF!|]D樫Ǐ#==tBQEd3޽ SSSx{ D!DQEdZs͛7 :!*SPPoϣy戌Dyy9ױ!1Tt8p bbbC!^:'b x[j;w/acc3fpB%*HpqqADDzu233/^PE!ԙr"<<gϞŀDݻӧO!7ouBi"uB /^KT\ÇC!]HOj .nvZBHGGH!pEc5z7nʕ+dZ8B@E3g ""0o<ӐJ:::1c\"E"" yyy'!j֬ u B! ---#B!R.'B*!B$.B! B!D"B*!B$.B! B!DhpT"Sq%c\Ǒ":t>Dǎ1~xpBH PEdʱc0|4o޼]8p c!** :::\#RMtzȔɓ'‚X[sq}<}HKKÒ%K$BHQEdx<gGDD`i+66011hkkcժU~D2BtzHc W\A||<`dd[[[ -- jjjDQQP^^]]]~ׯ0111clҥKx @II Gnݺ$hiiaȑ\>22NNNxعs'ZjGGG@ff&SXYYZOƈ#PѲrrrx9Μ9>ggghhhڷo333 sssS%YBG$3gDӦMѴiS̜9%%%\O?}O?pttĞ={rJ:<<<|rl޼Yleeeptt/Gbر8qbmׯǔ)Sлwo())zꅵkעK.]^KK &&&PRR!XbLMMѥK899a̙+WGpssÎ;믿pww+ك~>}:&L z͛7Q'O޾ B!C%`8x wFYYq2ca׮]8z(#FޥKܼyS::vz?;w`aԨQ8x Ə;;O_#F@DD YYY޽;:wٌ={62220p@ojOOO?PUU)Ο?m۶aĉ0`1w\ms|a8x CX~=B!&z*1gΜ/#BgB\P^^ Bq8}4`޼y5^On`hh(Z7ٳU.]`ƍ`:jg|SPPJKK`̜93gDVV:t455Ƣ*C4eeeepwF![Tt3PWTT4ʢ l 899^g>}Ex?> ;]WUw{AWW[n}={<;U>ee)(('O?ϛ7s΅iUB!Rz&//Cv}gϞw}˗/ yyyZD j[Ɔ p=tMQ]rrrHMM;YvSSSӱBd]_axxݻʼn28p#BYYY8qEׯk;w˗ PTT/"##1|L2y<=z;vTPPm۶|ɓ'tʕ:Y?!GEhkk#,, (,,ĹsТE cq1;v2dZh!} 2GII #??_bӳG+l455nݺU;.={tzzz7o֭[dcƌ;*-}ĎUV|wp]زe lق͛7 OB88oqdFpp0Q8C?cƌǏ '''3vڅSbݺuXf QRRǏ>>>Xp!YYYXU&vq{M2cժU5kΟ?''',X ,@nDGnܸƍ|r<~۷o\ EG@׬YEEEprrBII bbb2*++ !FEihh ::RVVFFFB!={cNJ7n ''{{{mmmakkRʪnJKKïZ\|x;Ejj*?~ mۊ۷/ܸ֡qiG!6::u*>}ZesN,Z{WDuY"\K;yyy(**&,M4I 000@ӦMkzWv>&!Ƌt}D\\6mT{T 8x1XZZt?Gbcc`8uב!4t#Ǝ?feΜ98<߿OiiiXd 444A }6ۇ%K}\G"HP 5066011تUEEǠAp…:Kyyy˃3q!4"Tt}P(Ddd$n߾-l޼BXf 8 }?~ztuuann---4;;;\tIlH"Y|!!R_h󞤤$bBBB`nn___ظq#n޼ ]мysQ+=yD4JJJ]BHcCE{ve˖MsttĴi}`޽{333?~z*1gѴaaa!"%%5i`dd$ֵkWddd|tXlΜ9555qܹ:LL!iGEW-ɉyaܹbСHLLdF!zk.bĈ}}PUU]BH#BEW;y$c~ʰ`BiXhD!77W4˗@ 20kb„ زe v%%%{0`h1cCBizOLL ֯_8rLMM'O~^/_ƵkPTTUVaذaprrBII bbb֩Ol#pѝ@G!*ӻwo=ziiib?7ƍV\\\v0d?~.Rꊤ$[8h5]3f Ñu@/^ٳgNqB,*84b|p4@TpBCIشi\"E.3x%444ż@ޓ/ʊ(B@EF ///ݝ85W_\Gy%RRRгgO(**r ʕ+")BEǚ5k[[[d5`:ukXZZGOg !H#I777\pAl0Ve.\'OTpB*WF"]P_K.]Ÿ[m۶\!BP%TUU1|pqEf`ԩprrӴB>*]OrE&}wx vuB!䣨Æ CӦMqȜǏؽ{77ouB!䣨prrS53f`̙6lq!OK!66:L`ZZZ_C!|]Rdhժ>uŋ q!ϢK|;N1VCJJ -Z~ {:!R%*RRRuL4 ]vŏ?uB!Z2}AΝuj*$&&"00 \!B.)@y(R'66~~~Xn C!T]RhĉK"U^zc:!R#TtI:wt=?_qB*;N8bH .`ΝرcC!]RgΜ: rss1ydL0\!B]RJ[[C SfΜ >͛7sBbTtI1www\pϟ?: gߏcǎ! ZZZ\!B]R 8rQ8S̙3<<w IDATx0q!ZK5iNNNP(ttt?sB5*;nݺTHƍI&\!Bj.)gmm֭[СC\G$,]+W%q!:AE7n:N?~<̰`B!u.4ܼy(nٲex999B!u.`jjݻ7 ꣣~zԩSUPP 6.\@EEEG!P%#@uzQRRɓ'c׶`aa$&&6 !*dĄ PPPs^̞=عsg; @`ܺu tVX[n!::&B*Q%#ڶmAb<}4݋۷CGGNŋ~:t(5k&ШӶ !wsT;|||PXXMMMԉxyyaڴi;vkEEE Err20dBBB555xzz(//.\]] '''x<ܹZ#cccoB!!`ĉ\G3PQQƍŦ'$$ 9s& еkWѩBGGGٳ+W˗/=[KK &&&PRR`Cpp0-Z۷KBį!ZZZ>|x9Ÿ{n}Ħ:WxLSUUŮ]PTTM65#BH5Q%c  >|(_L(bʔ)h۶-/_GĦ\>VtUѣqh݄BHuQ%c1zhq͛8t?x7nMo׮U>WtUEf͠TuB!EE ?~:u*,--ѴiSgϞ8~h&M011A^l2"44}F!D:X-GY R=7,--^z}to{õk׮a_wڶm6m|thkk^~ub_{wՕ ) 1 "(u Q;ִcsMd%1}sM:1D\!D80` c?TA֪sN`z< `nnVUUCCCtlG9}b1113gNXZZbܸq \ԁ =o̷\`eeEDDZՇ-X{Ru)j*455bDDD}CW 꺔JHHݻV CW6x`5 {u)TRRKbŊ5k!"")>.** ~mI]K!""9>.<<5558r䈮K'ȑ#ؽ{w|G ]}Ǝ+hs5_ZEW ]z 22DccKRDtt4_] ]z << HNNu)_l|.`?~·;߇Nk!""mDdd$Q__'bժU:7z4Ԅ0 'N@cc#N wwwgFhhh9{,FnW_}eee8z($I΅H1t T*@"uSSSJV,^DGG{쁛ѣOg8;;wTCWU__Tȑ#tz݁ Z[[ ]!??(**°aðuV̛7K, """s ]}T]]-Z*T}-Ç!ɠT*؈XkH$֥6֭[;[W{hBTTT?c%""z0ta ,3<〡K 44TcQ"੧]MNNXG.{Ů]`jjڭ ]z^Ì111]nO$( T*q@ODD ]zAcvxT.wܼyظq#Ξ=^w:K-\3f?]nȑ#4lܸQc:KOܹfffV;qE?UGDD׽Lee%*++ԄF@PZJJԠ8w JaaaXsssd2d2_~066% ObX[[πH1tJ+QVV۷oUee%~###LB `hh_܌{ uuuhhh@ss17nxomm񲲲5;;;'`eebj(**Baa!JKKQRR2455尵 +++ <]i{]q\.}Y]=?Bo_Q^^rӎ`ll ;;;8::...pqq\]]rϫpDDDjkk //(**Ҹ*ekkGA-l%m!oBhii)JKK믿?O?cmm WWW򂧧'y C&\pprssׯ3///L>]\]] A"H`gg;;;P}0??k=:;; ^^^C@@`ddS""".t"33Sue(J>>>6m 777NLLLvJ% xyšr>>>xLNCWYY~':u 'OKKKbҤIxooop@& WRRtFyy9R)<==1~x੧΀Hޅ[nѣ8z(N<+W@*&LGcРA.XQQΞ=SNg}V :ǏԩS1uT 0@χ.J=qy 88 qtk.22N>SN!-- B`ј1c~i&iM ]* iiiػw/pM8;;cƌxW0eǜf̘^)))HNNƧ~7x6667o,X &0QSܹsطoߏ`ڵ5k|}}u]b?>ϟB}ۇ۷'@DD,XQFZ""Guuuرco=rrru1pQaB~~>^|E$%%aĖ-[PWW2Hu^NNNXf Ǝtw߅K$=u!77ϟǓO> 6^HU\\˗† P\\;v 00P呞9r$>SbӦM8x \vMQkBWEEV^ MW^źu?i%V^_~{EVV|||K/BQ+B!C믿G}/"<=zΝ?~'@DD}NBѣG1e";;&ME_#66{u)< :_bҥǍ7^!;;Çɓq1@DD}CWFF͛0$$$J%tŋ1rH]z̚5kpa\rׯ_Gll,]?Z=kkkruum7Ot k.bx7tZR}Yvfamm-tn{ħ~*b}Æ NNN555B;Vm֬YM̞=[̚5Kx{{ "&&F}LNN9s B,X@ 0@\vM!DVV|(//|033_|BǏ @˖-011~QQQB"Pu?ׯXbx̙31j(ҡ >C^̞=[󎺿cǎK ~aff&?tL"lllDjj^[LOٴi߿gLDGgzBױcQXX؝.ERTC>"00]2446Ν+$T*ň#Ď;ԟIOOСCYxyy7jNK. !_@|Wc֯_/oF022m111B"/ mKIIy !Nɒ%8sLB0,:+--M899N' .~zuX[[E[]#HHVlܸ033g{,--q]BDDnQTb["<<aaaӤR)Z[[u]BZKAnnn}NTۈ#?8~8=JzΜ9rݪ~q ?R)P(]_gB׎;9stGpwwu)DD i-t=ׯ߯.innƮ]`nn?}JKKqwojjtO?ܳg1nBBBƌ8q/BP`ر]9:{,0{GVQ__m۶il'|$I%$$@ jlOKKp=e߾}011Q_1$""ֆMLL-Ҙ>A[ضmbbb H0m4 8L6 C\\"""~ܺu MMMT_.ꫯɓ'/`llp_DZ~aѢE8p1h ɓ'e˖3p'(ܾ}aŻRx{{3ĉ1{lP 555u7oDss3믿kת݅ _ {Xq!=LMMRRR{!&&[npޮ˗/'N|؟@͛l2i9Ժs~g)//"**JTt%ADEEJ|O֊`@x{{yӧ8rSLoX|xׅBPsu1uT!HD"&Mׯ_רcʕbذaعs5k(..Bqi1|p@,ZHT(YfK.ӧO덈W\B|r!JŪU+"Dhhp Ǐb޽BoP(bС &222:FjjdJ|Gݥ SSSu?wŭ[şHT*!͛7uV^@"ohpwtTJJ fΜ5kj]T*Rp ߪPtqMD *J}oUWWҥK4hp6b |hiioXZZjL5A"g0000yk!֮]["))IcbX"=}Ei}*)S ../Fuu5>#}6{O+pwBxPu%---1nܸ]Qݩ@t1]---Xr%`׮] \DD@:!::_5vލc"77We譆(J=`]|cƌ_~DEE$""t6qܹs \ |'PT*Go޽G֭CVVK+* 1F cccdeeN ""zV:d򈈨uɓb,Y~!1~xlٲ7ou'P8;;c͘?>^੧uDDzmj7|ؽ{7lllnkB JIDAT:899g}PYY2޽{1w\c044ݻQZZ-[Ue]կ_?DFF"22HHH}s3,OcƌD"qԛ!d|8{,$ L۷#,,ONJDD}֗z*++qQ$''#99Ǵi0qDpڄTnn.N:'N(++:O:~ WD}JX[[#""B ++ 8z(^|E444ƍÄ @>gZZZSNӧQQQ㥗^Œ30|p^%""n T*'OԩSxwQQQ\_055)P_~AzzFXXX`xQFH%W-LUWpgDff&Q]] T ///^^^􄧧'u yyyCnn.rrr yyyhmm0fX#G&""']/// B###pQPPV^^^;\\\WWWGNNPXXJL777xyy!,,  ҥg"""dmQC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiC0tiLPbϞ=(((!C&&&.~äI`nn"wɓ'aoo򈈈. ]}H||<.\~f>|سg4i&Nt5ڙ3gf͚ډw ]}ıcǰauJOOGtt4C Ann.ʕ+χPThmmŒ%KtpDDD7.-ERRrrriӦp!\v fffEmm-P(H",, ۷o#ƍ@uG Bg-5jVΛO/l@.cʕعs'6m077… o`˖-kkkpvvƀ H4zX\.ŋ}zCWjiiATTy̛7666x1g,]/_x{{k|C Q1blll`llI&aĈ( DFFv󬈈+zPrr2rss}hiig}֩t;w{ܰa0lذNIDDDCWjeff}„ NеvZO²e JXhQ#""Gp...尶T{ ];v@@@̙ 1Jꏈ4f'4_x cǎd2455=-Dr0!z* ggg3`̘1puuQ70tÇcѢE8qO< ,[ 0m4ܼyqqqG\\nݺ|TVVp ڵkGJJ {=( lݺ[nŖ-[ ib``1*ia۶mfΜW^yJIII`hhǎ;cxz|7Uz !!! C}}=Ν;ѧPy&h ꫯYA.] Atc***`cchjjq6 `nnO~0e$''w .~_QoE-ĸqv.BDGGw`"""z8 !ܹsu]cKO#44`aajKO)J7JDDD]&CW^̙3/#)) qeDDD/H/^aEDDDХg Ξ= R̙3P(:Хgr9LMMm755\.AEDDD0tUVA*KR:x#ڴi]vA"`…Xj"""z1t!L5k`͚5.?/iC0tiC0tiC0tiCt{rԔ=ZzD~~>L21׭wwGU Qu ! """wH H H H H H H du]&TIENDB`dot-1.6.2/doc/cluster.dot000077500000000000000000000004071460726460600152630ustar00rootroot00000000000000digraph { subgraph cluster_s2 { label="Cluster A"; n3[label="one"]; n4[label="two"]; } subgraph cluster_s5 { label="Cluster B"; n7[label="four"]; n6[label="three"]; } n1[label="Outside"]; n1->n7; n7->n3; n3->n4; n6->n1; n4->n6; } dot-1.6.2/doc/cluster.png000066400000000000000000000531331460726460600152620ustar00rootroot00000000000000PNG  IHDRPdasRGB@IDATxdű{B\,%..n.8 pಸܠBկpٙyGzyg#}ϩS]^Ɓ 8 ڴ&r@f0kaq@f0kaq@f0kaq@f0kaq@f0kaq@f0kaq@fI'-9=#4aɁ믿34@~HAo魷rz3Ɂ4\r%M~!\6p6δSs# &Z@8`Lec5&`J6a6\@8`Lec5&`J6a6\@8`Lec5;^b:|I3ϸGn9p-0`{K/|Mw뭷'x]tEtW_W+:8' `[~9=CnvK-{",{1_c9~y%.Bޮ5hk0|4h{W]wݥZJ`r-6`W_}&tR&h*'mݖ[nv۝{{}wm0t!r`FX_Cu_~eeO~]6U6JN;O#]\5ߝx^[m3fLVZ;aO뮻eY7e~}?vկ_k?݌3XcJw}{GݔSN6h#7SW=Î%sF _|Z?;<?W^q/kjی]Ł\5zJ6gu9_fM;nrTn{*r!0瞚3cwM4ksݎ~W_]Bso[mtyw7wynĈzn?릚j*7ӻ]w}3Ψwm7tr0tc?.^\z>sm6tr0R Yn8@j/$SOuhYÆ m|zK)M%Bjim|駺<[veu7wYcev׺VfŁ\m0K_~XRvYgWjv+ꪫtMW')`nc*pi.o`Fal[|6j0ĸz&7uKcjMF;)`xgS o04_zM[q9a%zZԣ+Pcg9Rm&S*_ufJ(X]BVS˺3q Wx@dՌǦa[;^cei"xfs^{jx$6=o`3^<9~'F;K,#b)h^},A<^_+BċGȋ't*0^4!mCƺY?u;h7Ë[;4Wf|'^#/9}JϝdIqn1xR=18 0vgO>q'`LiX"pGL L-6;fTB]m4#ABNP`L0y;.ՋT-2v8>p'_4&6Ms wL#ƁsL.8/L;762@9`Sh`&`q0S) 00΍8Pz) xFf(=L~ rLsc#3&`J?vƁx9`&޹J0B@0Ȍ O]q ^8k+-;h9)M u|RUsK#>Ȧq8>B ,(#h 8_u˄5T @xPaPl,zќOY_(jK/6fgJ+`>@eʃ \(SBY7nR@zN v3 /楗^RoRq W]uUmKt'vqG8P*A"j;SL hmV'me.E(d ʨPbD8(3RJ*w1P6ÁRy_y}y]IJ;p~Wb-jV Hs<+N8IYnt 7hW\Q٧e(;0CaĈjEZJyMu)Fy?jӑ:HZ[ok֏q jD)`I37<}.s?u])U&Py'tلYiԎSԸ_@ N~矯voUaư }[ eo>r0z*3>\ZJh,Tk,1~'xj~7v6^@B|GnȐ!sq\rkJo`c=?16 .;vlf ā̸qܠA<u0?c Emi#@pPC'XK7]wio쮺nL&x(,Ўe~; ߿{5hnuMv#o뭷v!j72t3 0ܯoX"r{0aZoԭ=sʥu  Y"wqG\"{ .tM3#@r w׿uQųҋ4c9Y`#]B vۭGX\2I쳧ݼg(i0<̙]O?I 6LKXuk|Z"oZg2?c=b@l34 ]mO3$ ^9c!;>J6/U]ʔxzߦz=@mT^pK,DnH_N9D EH:rEE]T !kMó>[Ufmā EȠ΂Ix [b{]VJ_c5Ꝓvc`zv!lIJb@nfwov&utyל38C8V7}L2ot|p#t,S`㱑q K*`"i?S uL a7e   pb4ョt=ozg}8ɂLGްj8æ~饗}܈Edډ.$Zw^3G\^>x"eh(8r`nG};cm݋x 4V*Db"qUZY;syS=&o<>^Cs_G+g=k0# n#%OʋK)zp %R=sgMU%T8(>4|p/.$>Dsw+ʋU$'i ^\ /zǁBRPDh47߬9M@WC~6釈S C$"44'MF`{wFx2U >YԵ0.].M"Xo|> Ā/dڞp@ĩ)T.U ApOUHB6O< K/8N;mI2LlpX0quN8Kg+9OA`t%OG8p#,6UY _Ptj̠8w'޹2 {F٨l0 _ dRDORc1Q#69ƹn|@0)FqpL`hcjN7Xټ!Lt@;= 4I(yQGumFpL1|^3BoO?&"j|9`&_~[o9r,!D/(ɇK|1l4gM7X]ih2pl)]m7͖ nM8 5 SOT.b85Ž2.O~7l0/H91J&`㥵TBP.hSY\: yh-tHj%5n8LQg0.os ǍeW嘀ɏSI8 @fZ PɨuigvFp{ B``^iܛo#WeIJr`?IVʞQs0ѿh3mV 8 1N g%@>$8Iq=-{`# H&†QPgq;\Q[ps9L̶ƒ3s '>ؑ~Q_8Hd+F?p_Ɓ8@I 2flSs Ɓ8@;m6?2ן[k8`M&" Σ5vROgcLN]xV vR-Z-4boL3d+%]vY1)j)A锃vq@#ŀ%\R=Mu&`vjb߱cǪ {Vq1+1ɋOr`I&q7|#|;gxag.HL0kul[s5M7TprԹ:u?v]vCp 7ܠ¦bg׮-: 0}j3,X6u+!!?o.S~駟~cmq - d;~zB᭶ʱ,G:CoۍYqҭު'W_}UW3eZJ-B :.fȅ}7ܖ[n5P=#`Okj,<ۘ3Fƌ8∜{Ϧ0-5o5Q릞>7dɁkG,ʣ0$#@,~$x/B,jk=%`fm6G18#@L_Fߞ0@h+!AN3ӽec1Gwwyr ٬NƖGw~T)ӗOYavi'5ܝFƁX9*=nŽusFlʏW\M=xcۍQpw?5>&hQ Am2B4$$:Mio}gj/5tI 3fqFph;Fs>H?~/7$h3S TSMɫyaܟW^yo~N9GY+ B؅x@+Q_Z&l2xLO]nqƩF~D(dNabd~G>Lk#D}Y<{"W|M D!`v$A_9!Y{ć sHA*^Dr~Sj!L j17 z2@A傰vMܘ M3sjY&MYkyAK/iᬿo*9̛W[uUu@%]y81LyC)3-q\G>뮻~ ^o|AGy/ڪ 4hRz=Æ RsL zyzY&0cNZ X$N˚ۋCm {,,HLMF^~fm^^\^pfؽff1R4z^.[^&x%Vf6Ӱ8./S% KƎx /z3 ܿ⋩(#/pMt*R5kA!=ry"'k/"o oG&桱ATs[ ^^b>0-wuW]^Om'LEkci,6&y/ū5K0Lu$TZ3`<ӼĽ?ɨuX0Çwǟq hwe}GL5<5lΏz^0ԇ#^ v/ホ(k{ ȑ#Ęt¼""s$/2*ʒ$K``]v!uNz<1&=\ٖ;M#I`o[zirV,apk nf}Jʌ zhBQGjwܥ>O򦼄UfZA,Q`5ʎ.]ɂp"hA0Ne%['xcEtkBXH|t63$5s-{~8ꫯND*J+3Ns &`f) f=9vkǥ;N=t5M{.Ųdj0du]O(cIhtywN?t'j~r#`giXE`HmvVsqbYW঎G ~*3pY Yg'L{'UxDCrI0L:jk+^t .@^µ++\Fo_?,H#yqԨQtu)FaNHDI37M~.5'}eStJc\rI2.P,M_Pn^$bH*" $_c5z'ϕ7O+KF`VhiEiWȴR"FD8&Zc`lx}҆[r[" Q_$Jm馊1ϲ,I'Qԁ:Cĩ$ߩ:$\zkҭrtPC*ˍ( ߍM{cT^6 R ?j4?=`JDOZbs~j QAC$ѣG+N+gvmu:@r ^-B,hر!$H$D*r l>.r]wQ0Wo#H$EAXO&::(M^'7;$ =U_Deoݽ M)Hݩ<ǀAa:c4O̤D+h s~hHp`iѪ௲ +5syJY Jt:UTivgBGYtb&$@%@B#m@Y[)@5Bw"}<J/9#ijLT ER7dxHaIR[ȂU),`O lHtws5:z_n` C_e/RT&o|nUp|͓;E^SiTJQ9S1Pnf&!7M0 RcÀی=Z CDQIRc,PP M,M6$3RWQmz^=,X \mY{]QF8TNDzm,mBg2lu Yep`䫦vI=H'] \vykq*$Tmà!xeAb0X¤ "Kp{kltW 􍈹M+^C4RN ڥ'^ZD?W:h C>'4$mMaاYuJ$ov!HDZK"Rʃ*o(7$ՊEn> `JZ|gY\Vob>HDՊmO25F2'GQTW_}Ce9s߲Boc1ktt ,G6*&&)'.u='m3n2_C?|ɛuWS ;f̘>~QhOyk%?R%vK Q/K>O *[wȋϓ9,%uP5[ߢ"T0 ;dX39̏J4j5Qb>9&Te*h-\'-Ci/yHzUe TsNkd*L'㏡Ln"Tyfܔ%\> I}Q¨č[TYƔ%q- W Z}27E51M6~- I_4qGj~F&MPH_*`$0.n7W#ѶzYa4}E9R'>${X;ovvϗwV2޳vs3ʦ>B[ (.jru"ܺEUB뛨P4ywHiES* Y}/&Aŝ'=1[uUOg"tʚy 4h]Md,@yO.]@<<ǀng5qXRQ<حa FC=oP}bPr(, # f H@&wXO ?rX[\^{=-`$0^2 2n'LD nB # hSm+tL9/N)#\T{ZpwC4#ILeLB (gr$;`$w,L̈́QVrO)iQC5qƢ$rLY/|ACNGAr78*]>\" bbrA-'u٣J7s @Ѣ]L[&D%-9pĸ0b3F>i'U2gNbEپ7AERxcpY!Tj9Hnf Q\܄Va•;ʢ:m[cO H#K!nN".] B3UWZ>"h:!aA^1wU9vF\mfoD,JcÁ0@(p򦂸1VM$;܈8 KnʠEPv T,b` h |0 fs3WI`=ob<38XОC q(haiMAƜM!Zy$o&=ER4z q&x[T@(ަ>Ać7[e `*K e^x RB,CbY4}E9A=%F9@"sp,޴ix!^2Ͳy6]MF9…xZ…iR/zD LVN i[c)l 'a]S8MN`beQA,"{؋HX$!mp r)mOZ'].ozQ^!9kBY_#Nv؜>:E"GG!(ՄuY8b!~֒$² "-e 9nM (Ġt$`eR:rlZ{L3Ixո]pز GqD'ѭh>h,bZ`ZX;{fK~vhC$ގ@kznُ˜Vbg3c#Bؖ{&`6!f66+)T &`MȺ؋Yn ^0v tIZA ;.R#r-b g!T$vhO˼zu2 ^2fld&!u2ݠǪSO=UK@&$̘{c\Iy5/1J}n֚eNTqx=nip64Gjj%ɶVX93k  yVr0-!k܉'9tq،@.n^{M{}9 H?OjwtI;;HH÷馛j)x!,nj~߹[-8;S8F2Ӭ>G%v"P`7}eǒBRv.oLX\fm榘b mTSM4B3R!L43M6ƆpL!H Hj"vt:m9RNiW %P"z`zEƚ} $jɲ9 ۤzM &o布:RLK -s]n}/X/ËRo_s5^Ľ|) y@pTRX^"zALҏhL^/ҋf}V@ pfCI7 &C]i"\_ Lmcl4T0&HV<-j48J!CVQX1XQ=8fE\  Qw?o֟}i5ʝŖ5yQ/ # p!<=FD5COq"`v\sͥK!DM*& D0K|Z 'Ќ3Ψ?Pda(cker+K^K+0(oPƣ!2vХ^N(TA!@j }%sムe( a{3߳2 _]8xthil 1877>b0b$W_9_nĈnm'PTm&g}ڈVƘweV]Z}7-i4_'ǘ{O=J5; ;M;E#bPXf2XxN,Տm*_ufJ 볱 ˀiGTL8CbsA.l I-Qlnƥv+c;9O>hfGyD!]%Y&@sᇫq kY`^\=أb ˣbGO4tAԼ1;]xyh-^D^rvh^<:&>^<9~'VWd{y`uľxbs" IY_؊ԓa&ɍ~KXp.xL Jn0=keqbȝˆ]4]L IDATuU {p]O&`(7a$Q裏֥|W.C˴Yݫ^fv0)!hIg\7'x@ yOeKRTc#Oq#A,M c(^V]uUg(j E2$P|ͮFg l0BR(Ae"h+! !9s=@7>H QMY=JKf#YZ62 &#B…~-^HBģKP8&& DVWI5Ib]5/LHA.@lD0,@{i4Ze HfOnXm=/b.z;1"€D'xnCע,m0D2tYRl34[R=s\}9,E3K M6DC f H\ܼj1eMӺ ؍7سZ|$J(:ǡd%`Hj|4֘ʲL3(8(e0 6&KB{R,ڦ,E%^Y  amJc&`b@'h P$6xHٔfuVKLO681rzB,Ю#Ol eS) ʍ! R\&~78|wkF0.Q"XjsXż0&5G4 |hAj 600f8m4dy)vN>k gbI.K.ϲ,"VD3. %^ȲN%h4h@ L >y;«FmľoBljpOBE"S& ZpI#@P7e oSH9Fc |(6EQ\eSxD O/XxBT'ѸR[RRЎ$xχ<0̠s\sR`js T-U4/AQVD7/o Rl;Iߌ֤cc[Il*^J)%T` hAXҸlIJ ]f 5 ܼs3&+cfBN8Sk5roy0W#ucme\<.iԺǦq.{O4V Jݖv5rzԱ\yUFʒSo_-* g6?f#՟~npkZf="Җ1P@9GH쳏P8V" ,6#2hn'k嚹vxP4Od,QQD`$ɒD~^ OX*om{V'<E}S4 H4xY#ZE 8ƮrMg.ffޢ?oY!\zI"gUAA&`2:+_tE.XµxO&9w}N-҉-1IdRbʌ믿~ʭ9x OM^D }R>$wl `If'o22 裏rm^xI{뭷ĺeL9Rޞ=0)5͙^Ԣ xoQք+&s&I6.Xzl2ngpMnySscMOqsP_Oۃ'eM`ѳmFזQ9t̘1[̜@ smym){ւ&<#x3NXS*6h#m?rP? yi0)qzܸqRVx;5\-(R .; xR\Gq#CqaÆU8ʁm҆ ӅjӀt]7!Tc瀟~.AD<sq{"l) 7wzP KQ D7fuMd!)7XZ]{y'BLd-]F̲QRG!-jMް^<Аv[=Vht UV$) zMi/1^@wJPRvċ8Ya+D0yYVvH0+]~HLATK%R2#@dI:كY`*MPKBʎ&&_xᅚ4|p=?F6ܿKTUVN{.ꍗ&!c_N9z^o?嫯강O7<ɲgic @B1v0BBc@zBNǷ?c$^eEM?fM]jAWlqk]7s,_=Ts(6fx'ـ]/umi3Fpx{)KìnhՆS0Xܦ7zh{iJ%R&jF^`!p Iψ{+ckc'(P},>KD<_oD?ktsݟiHZCUV/}/^ֱ1n#q13vk)#z'zAyRG_y啩%HfxӁxoKwB nм<>&|AS #1cN.H-6Ww"b<$^*JEByp40 f& 7hKpA<~Dc*10`7cdۈ,l NIhw׊1ZJx)eBPN8{K7`f`p[Y۪i}'}0b}6З?clͦ׎T*8zoxAOe*u+c %"O-~d54Rޠ@\?5̛ZII`/FiɈ` N?T$܎0 |FpݸSle'P,6[}U'sE+ωg;s-Tjĝ};sӸM4`{ n|ӻh#T̓OIPnr<9K~~ӟjd1C[`{ ȸo!AJ4=.?_?@bCHSZa.(<099CM*Q"O EQ 4. 7%Ze17,F&7;8AXIцǃ`<5BغaR,9`0g …cb4Kve6[Ǩ8 .Fwh RO 87L=?6:@9`g70qϏ8Pj)qsLc3&`J=}6x@@~Gq!Cmq8KvtM&`S6hvπ"E?6@@y9`6Ν8=LD?E6@@y9`sg#7D0O 8P^)ȍsLSd4&`;w6r@0 Ν8=LD?E6@@y9`sg#7D0O 8P^)ȍsLSd4[IENDB`dot-1.6.2/doc/subsystem.svg000066400000000000000000000105341460726460600156500ustar00rootroot00000000000000 n3 n1 subcomponent 1 n3:s->n1:n in1 n4 n2 subcomponent 2 n4:s->n2:n in2 n5 n1->n2 n6 subsystem2 n1->n6 in3 n2:s->n5:n out2 n6->n2 out3 dot-1.6.2/doc/subsystem2.svg000066400000000000000000000024711460726460600157330ustar00rootroot00000000000000 n2 n1 subcomponent 3 n2:s->n1:n in3 dot-1.6.2/dotx/000077500000000000000000000000001460726460600132775ustar00rootroot00000000000000dot-1.6.2/dotx/Makefile000066400000000000000000000010451460726460600147370ustar00rootroot00000000000000test: go test dot -Tpng < TestCompositeWithUnusedIOSameGraph.dot > TestCompositeWithUnusedIOSameGraph.png && open TestCompositeWithUnusedIOSameGraph.png dot -Tpng < TestExampleSubsystemSameGraph.dot > TestExampleSubsystemSameGraph.png && open TestExampleSubsystemSameGraph.png dot -Tsvg < TestExampleSubsystemExternalGraph.dot > TestExampleSubsystemExternalGraph.svg && open TestExampleSubsystemExternalGraph.svg dot -Tsvg < subsystem.dot > subsystem.svg && open subsystem.svg dot -Tsvg < subsystem2.dot > subsystem2.svg && open subsystem2.svgdot-1.6.2/dotx/README.md000066400000000000000000000036141460726460600145620ustar00rootroot00000000000000## dotx package (dot extensions) This package contains utilities to create graphs on top of the `emicklei/dot package`. ### Composite The `Composite` type can be used to create composition hierarchies like clustering. Let's examine this diagram. ![](../doc/TestExampleSubsystemSameGraph.png) On the most right, you find a node called `subsystem` which is a Composite with 2 inputs and 1 output edge. On the most left, you see the contents of the same `subsystem` with both inputs and an output (point shaped with label on edge). The `subsystem` contains other nodes, 2 regular nodes (`subcomponent 1` and `subcomponent 2`) and another Composite labeled `subsystem2`. So, `subsystem` is a composition of 3 components and 1 of these components is itself a composition of a component (`subcomponent 3`). ### external option If you create a Composite using the `ExternalGraph` kind then its graph is exported separately from the containing graph. If you visualize such a graph using `SVG` then you can **nagivate into** the subsystems. ![](../doc/TestExampleSubsystemExternalGraph.svg) And clicking on `subsystem`, your browse will show: ![](../doc/subsystem.svg) And clicking on `subsystem2`, your browse will show: ![](../doc/subsystem2.svg) See `subsystem_test.go` for the code of these examples. ### usage pattern import ( "github.com/emicklei/dot" "github.com/emicklei/dot/dotx" ) func YourService(parent *dot.Graph) *dotx.Composite { // external means it exports its own DOT file sub := dotx.NewComposite("Your Service", parent, dotx.ExternalGraph) // export right after building the inner graph return sub.Export(func(g *dot.Graph) { // build the inner graph of the Composite myComp := g.Node("myComp") // connect any inputs,outputs sub.Input("in", myComp) }) }dot-1.6.2/dotx/composite.go000066400000000000000000000107121460726460600156310ustar00rootroot00000000000000package dotx import ( "errors" "log" "os" "strings" "github.com/emicklei/dot" ) type compositeGraphKind int // Connectable is a dot.Node or a *dotx.Composite type Connectable interface { Attr(label string, value interface{}) dot.Node } const ( // SameGraph means that the composite graph will be a cluster within the graph. SameGraph compositeGraphKind = iota // ExternalGraph means the composite graph will be exported on its own, linked by the node within the graph ExternalGraph ) // Composite is a graph and node to create abstractions in graphs. type Composite struct { *dot.Graph outerNode dot.Node outerGraph *dot.Graph dotFilename string kind compositeGraphKind } // NewComposite creates a Composite abstraction that is represented as a Node (box3d shape) in the graph. // The kind determines whether the graph of the composite is embedded (same graph) or external. func NewComposite(id string, g *dot.Graph, kind compositeGraphKind) *Composite { var innerGraph *dot.Graph if kind == SameGraph { innerGraph = g.Subgraph(id, dot.ClusterOption{}) } else { innerGraph = dot.NewGraph(dot.Directed) } sub := &Composite{ Graph: innerGraph, outerNode: g.Node(id).Attr("shape", "box3d"), outerGraph: g, kind: kind, } sub.ExportName(id) return sub } // ExportFilename returns the name of the file used by ExportFile. Override it using ExportName. func (s *Composite) ExportFilename() string { return s.dotFilename } // Attr sets label=value and returns the Node in the graph func (s *Composite) Attr(label string, value interface{}) dot.Node { return s.outerNode.Attr(label, value) } // ExportName argument name will be used for the .dot export and the HREF link using svg // So if name = "my example" then export will create "my_example.dot" and the link will be "my_example.svg" func (s *Composite) ExportName(name string) { hrefFile := strings.ReplaceAll(name, " ", "_") + ".svg" dotFile := strings.ReplaceAll(name, " ", "_") + ".dot" s.outerNode.Attr("href", hrefFile) s.dotFilename = dotFile } // Input creates an edge. // If the from Connectable is part of the parent graph then the edge is added to the parent graph. // If the from Connectable is part of the composite then the edge is added to the inner graph. func (s *Composite) Input(id string, from Connectable) dot.Edge { var fromNode dot.Node if n, ok := from.(dot.Node); ok { fromNode = n } else { if c, ok := from.(*Composite); ok { fromNode = c.outerNode } } if s.Graph.HasNode(fromNode) { // edge on innergraph return s.connect(id, true, fromNode) } // ensure input node in innergraph s.Node(id).Attr("shape", "point") // edge on outergraph return fromNode.Edge(s.outerNode).Label(id) } // Output creates an edge. // If the to Connectable is part of the parent graph then the edge is added to the parent graph. // If the to Connectable is part of the composite then the edge is added to the inner graph. func (s *Composite) Output(id string, to Connectable) dot.Edge { var toNode dot.Node if n, ok := to.(dot.Node); ok { toNode = n } else { if c, ok := to.(*Composite); ok { toNode = c.outerNode } } if s.Graph.HasNode(toNode) { // edge on innergraph return s.connect(id, false, toNode) } // ensure output node in innergraph s.Node(id).Attr("shape", "point") // edge on outergraph return s.outerNode.Edge(toNode).Label(id) } func (s *Composite) connect(portName string, isInput bool, inner dot.Node) dot.Edge { // node creation is idempotent port := s.Node(portName).Attr("shape", "point") if isInput { return s.EdgeWithPorts(port, inner, "s", "n").Attr("taillabel", portName) } else { // is output return s.EdgeWithPorts(inner, port, "s", "n").Attr("headlabel", portName) } } // ExportFile creates a DOT file using the default name (based on name) or overridden using ExportName(). func (s *Composite) ExportFile() error { if s.kind != ExternalGraph { return errors.New("ExportFile is only applicable to a ExternalGraph Composite") } return os.WriteFile(s.ExportFilename(), []byte(s.Graph.String()), os.ModePerm) } // Export writes the DOT file for a Composite after building the content (child) graph using the build function. // Use ExportName() on the Composite to modify the filename used. // If writing of the file fails then a warning is logged. func (s *Composite) Export(build func(g *dot.Graph)) *Composite { build(s.Graph) if err := s.ExportFile(); err != nil { log.Println("WARN: dotx.Composite.Export failed", err) } return s } dot-1.6.2/dotx/composite_test.go000066400000000000000000000046331460726460600166750ustar00rootroot00000000000000package dotx import ( "os" "strings" "testing" "github.com/emicklei/dot" ) func TestExampleSubsystemSameGraph(t *testing.T) { g := dot.NewGraph(dot.Directed) c1 := g.Node("component") sub := NewComposite("subsystem", g, SameGraph) sub.Input("in1", c1) sub.Input("in2", c1) sub.Output("out2", c1) sc1 := sub.Node("subcomponent 1") sc2 := sub.Node("subcomponent 2") sub.Input("in1", sc1) sub.Input("in2", sc2) sub.Output("out2", sc2) sc1.Edge(sc2) sub2 := NewComposite("subsystem2", sub.Graph, SameGraph) sub2.Input("in3", sc1) sub2.Output("out3", sc2) sub3 := sub2.Node("subcomponent 3") sub2.Input("in3", sub3) os.WriteFile("TestExampleSubsystemSameGraph.dot", []byte(g.String()), os.ModePerm) } func TestExampleSubsystemExternalGraph(t *testing.T) { g := dot.NewGraph(dot.Directed) c1 := g.Node("component") sub := NewComposite("subsystem", g, ExternalGraph) sub.Input("in1", c1) sub.Input("in2", c1) sub.Output("out2", c1) sub.Export(func(g *dot.Graph) { sc1 := sub.Node("subcomponent 1") sc2 := sub.Node("subcomponent 2") sub.Input("in1", sc1) sub.Input("in2", sc2) sub.Output("out2", sc2) sc1.Edge(sc2) sub2 := NewComposite("subsystem2", sub.Graph, ExternalGraph) sub2.Export(func(g *dot.Graph) { sub2.Input("in3", sc1) sub2.Output("out3", sc2) sub3 := sub2.Node("subcomponent 3") sub2.Input("in3", sub3) }) }) os.WriteFile("TestExampleSubsystemExternalGraph.dot", []byte(g.String()), os.ModePerm) } func TestAttrOnSubsystem(t *testing.T) { s := NewComposite("test", dot.NewGraph(), SameGraph) s.Attr("shape", "box3d") if !strings.Contains(s.String(), "test") { // dont care about structure, dot has tested that t.Fail() } } func TestWarninOnExport(t *testing.T) { s := NewComposite("/////fail", dot.NewGraph(), SameGraph) s.Export(func(g *dot.Graph) {}) } func TestCompositeWithUnusedIOSameGraph(t *testing.T) { g := dot.NewGraph(dot.Directed) c1 := g.Node("component") sub := NewComposite("subsystem", g, SameGraph) sub.Input("in", c1) sub.Output("out", c1) os.WriteFile("TestCompositeWithUnusedIOSameGraph.dot", []byte(g.String()), os.ModePerm) } func TestConnectToComposites(t *testing.T) { g := dot.NewGraph() c1 := NewComposite("c1", g, SameGraph) c2 := NewComposite("c2", g, SameGraph) e := c1.Input("in", c2) if e.From().ID() != c2.outerNode.ID() { t.Fail() } f := c1.Output("out", c2) if f.To().ID() != c2.outerNode.ID() { t.Fail() } } dot-1.6.2/edge.go000066400000000000000000000035531460726460600135620ustar00rootroot00000000000000package dot // Edge represents a graph edge between two Nodes. type Edge struct { AttributesMap graph *Graph from, to Node fromPort, toPort string } // Attr sets key=value and returns the Edge. func (e Edge) Attr(key string, value interface{}) Edge { e.AttributesMap.Attr(key, value) return e } // Label sets "label"=value and returns the Edge. // Same as Attr("label",value) func (e Edge) Label(value interface{}) Edge { e.AttributesMap.Attr("label", value) return e } // Solid sets the edge attribute "style" to "solid" // Default style func (e Edge) Solid() Edge { return e.Attr("style", "solid") } // Bold sets the edge attribute "style" to "bold" func (e Edge) Bold() Edge { return e.Attr("style", "bold") } // Dashed sets the edge attribute "style" to "dashed" func (e Edge) Dashed() Edge { return e.Attr("style", "dashed") } // Dotted sets the edge attribute "style" to "dotted" func (e Edge) Dotted() Edge { return e.Attr("style", "dotted") } // Edge returns a new Edge between the "to" node of this Edge and the argument Node. func (e Edge) Edge(to Node, labels ...string) Edge { return e.graph.Edge(e.to, to, labels...) } // ReverseEdge returns a new Edge between the "from" node of this Edge and the argument Node. func (e Edge) ReverseEdge(from Node, labels ...string) Edge { return e.graph.Edge(from, e.to, labels...) } // EdgesTo returns all existing edges between the "to" Node of the Edge and the argument Node. func (e Edge) EdgesTo(to Node) []Edge { return e.graph.FindEdges(e.to, to) } // GetAttr returns the value stored by a name. Returns nil if missing. func (e Edge) GetAttr(name string) interface{} { return e.attributes[name] } // From returns the Node that this edge is pointing from. func (e Edge) From() Node { return e.from } // To returns the Node that this edge is pointing to. func (e Edge) To() Node { return e.to } dot-1.6.2/edge_test.go000066400000000000000000000070211460726460600146130ustar00rootroot00000000000000package dot import ( "testing" ) func TestEdgeStyleHelpers(t *testing.T) { type test struct { input string want string } tests := []test{ {input: "solid", want: `digraph {n1[label="A"];n2[label="B"];n1->n2[style="solid"];}`}, {input: "bold", want: `digraph {n1[label="A"];n2[label="B"];n1->n2[style="bold"];}`}, {input: "dashed", want: `digraph {n1[label="A"];n2[label="B"];n1->n2[style="dashed"];}`}, {input: "dotted", want: `digraph {n1[label="A"];n2[label="B"];n1->n2[style="dotted"];}`}, } for _, tc := range tests { di := NewGraph(Directed) n1 := di.Node("A") n2 := di.Node("B") switch tc.input { case "solid": di.Edge(n1, n2).Solid() case "bold": di.Edge(n1, n2).Bold() case "dashed": di.Edge(n1, n2).Dashed() case "dotted": di.Edge(n1, n2).Dotted() } if got, want := flatten(di.String()), tc.want; got != want { t.Errorf("got [%v] want [%v]", got, want) } } } func TestEdgeWithTwoPorts(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n1.Attr("label", HTML("
A
")) n2 := di.Node("B") n2.Attr("label", HTML("
B
")) di.EdgeWithPorts(n1, n2, "port_a", "port_b") want := "digraph {n1[label=<
A
>];n2[label=<
B
>];n1:port_a->n2:port_b;}" if got, want := flatten(di.String()), want; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEdgeWithNoPorts(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n1.Attr("label", HTML("
A
")) n2 := di.Node("B") n2.Attr("label", HTML("
B
")) di.EdgeWithPorts(n1, n2, "", "") want := "digraph {n1[label=<
A
>];n2[label=<
B
>];n1->n2;}" if got, want := flatten(di.String()), want; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEdgeWithFirstPort(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n1.Attr("label", HTML("
A
")) n2 := di.Node("B") n2.Attr("label", HTML("
B
")) di.EdgeWithPorts(n1, n2, "port_a", "") want := "digraph {n1[label=<
A
>];n2[label=<
B
>];n1:port_a->n2;}" if got, want := flatten(di.String()), want; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEdgeWithSecondPort(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n1.Attr("label", HTML("
A
")) n2 := di.Node("B") n2.Attr("label", HTML("
B
")) di.EdgeWithPorts(n1, n2, "", "port_b") want := "digraph {n1[label=<
A
>];n2[label=<
B
>];n1->n2:port_b;}" if got, want := flatten(di.String()), want; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEdgeSetLabel(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n2 := di.Node("B") e := n1.Edge(n2).Label("ab") v := e.Value("label") if s, ok := v.(string); !ok { t.Fail() } else { if s != "ab" { t.Fail() } } } func TestNonStringAttribute(t *testing.T) { di := NewGraph(Directed) di.Node("A").Attr("shoesize", 42) if got, want := flatten(di.String()), `digraph {n1[label="A",shoesize="42"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } dot-1.6.2/go.mod000066400000000000000000000000501460726460600134220ustar00rootroot00000000000000module github.com/emicklei/dot go 1.13 dot-1.6.2/graph.go000066400000000000000000000242601460726460600137550ustar00rootroot00000000000000package dot import ( "bytes" "fmt" "io" "sort" "strings" ) // Graph represents a dot graph with nodes and edges. type Graph struct { AttributesMap id string isStrict bool graphType string seq int nodes map[string]Node edgesFrom map[string][]Edge subgraphs map[string]*Graph parent *Graph sameRank map[string][]Node // nodeInitializer func(Node) edgeInitializer func(Edge) } // NewGraph return a new initialized Graph. func NewGraph(options ...GraphOption) *Graph { graph := &Graph{ AttributesMap: AttributesMap{attributes: map[string]interface{}{}}, isStrict: false, graphType: Directed.Name, nodes: map[string]Node{}, edgesFrom: map[string][]Edge{}, subgraphs: map[string]*Graph{}, sameRank: map[string][]Node{}, } for _, each := range options { each.Apply(graph) } return graph } // GetID returns the identifier of the graph. func (g *Graph) GetID() string { return g.id } // ID sets the identifier of the graph. func (g *Graph) ID(newID string) *Graph { if len(g.id) > 0 { panic("cannot overwrite non-empty id ; both the old and the new could be in use and we cannot tell") } g.id = newID return g } // Label sets the "label" attribute value. func (g *Graph) Label(label string) *Graph { g.AttributesMap.Attr("label", label) return g } func (g *Graph) beCluster() { g.id = "cluster_" + g.id } // Root returns the top-level graph if this was a subgraph. func (g *Graph) Root() *Graph { if g.parent == nil { return g } return g.parent.Root() } func (g *Graph) FindNodeWithLabel(label string) (Node, bool) { for _, each := range g.nodes { if eachLabel, ok := each.attributes["label"]; ok { if eachLabel == label { return each, true } } } // TODO search subgraphs too? if g.parent == nil { return Node{id: "void"}, false } return g.parent.FindNodeWithLabel(label) } // FindSubgraph returns the subgraph of the graph or one from its parents. func (g *Graph) FindSubgraph(id string) (*Graph, bool) { sub, ok := g.subgraphs[id] if !ok { if g.parent != nil { return g.parent.FindSubgraph(id) } } return sub, ok } // Subgraph returns the Graph with the given id ; creates one if absent. // The label attribute is also set to the id ; use Label() to overwrite it. func (g *Graph) Subgraph(id string, options ...GraphOption) *Graph { sub, ok := g.subgraphs[id] if ok { return sub } sub = NewGraph(Sub) sub.Attr("label", id) // for consistency with Node creation behavior. sub.id = fmt.Sprintf("s%d", g.nextSeq()) for _, each := range options { each.Apply(sub) } sub.parent = g sub.edgeInitializer = g.edgeInitializer sub.nodeInitializer = g.nodeInitializer g.subgraphs[id] = sub return sub } func (g *Graph) findNode(id string) (Node, bool) { if n, ok := g.nodes[id]; ok { return n, ok } if g.parent == nil { return Node{id: "void"}, false } return g.parent.findNode(id) } // nextSeq takes the next sequence number from the root graph func (g *Graph) nextSeq() int { root := g.Root() root.seq++ return root.seq } // NodeInitializer sets a function that is called (if not nil) when a Node is implicitly created. func (g *Graph) NodeInitializer(callback func(n Node)) { g.nodeInitializer = callback } // EdgeInitializer sets a function that is called (if not nil) when an Edge is implicitly created. func (g *Graph) EdgeInitializer(callback func(e Edge)) { g.edgeInitializer = callback } // Node returns the node created with this id or creates a new node if absent. // The node will have a label attribute with the id as its value. Use Label() to overwrite this. // This method can be used as both a constructor and accessor. // not thread safe! func (g *Graph) Node(id string) Node { if n, ok := g.findNode(id); ok { return n } n := Node{ id: id, seq: g.nextSeq(), // create a new, use root sequence AttributesMap: AttributesMap{attributes: map[string]interface{}{ "label": id}}, graph: g, } if g.nodeInitializer != nil { g.nodeInitializer(n) } // store local g.nodes[id] = n return n } // DeleteNode deletes a node and all the edges associated to the node // Returns false if the node wasn't found, true otherwise func (g *Graph) DeleteNode(id string) bool { if _, ok := g.findNode(id); ok { // Remove Node delete(g.nodes, id) // Remove all the edges from the Node delete(g.edgesFrom, id) // Remove all the edges to the Node for parent, edgeList := range g.edgesFrom { for i, edge := range edgeList { if edge.to.id == id { g.edgesFrom[parent] = append(g.edgesFrom[parent][:i], g.edgesFrom[parent][i+1:]...) break } } } return true } return false } // Edge creates a new edge between two nodes. // Nodes can have multiple edges to the same other node (or itself). // If one or more labels are given then the "label" attribute is set to the edge. func (g *Graph) Edge(fromNode, toNode Node, labels ...string) Edge { return g.EdgeWithPorts(fromNode, toNode, "", "", labels...) } // EdgeWithPorts creates a new edge between two nodes with ports. // Other functionality are the same func (g *Graph) EdgeWithPorts(fromNode, toNode Node, fromNodePort, toNodePort string, labels ...string) Edge { // assume fromNode owner == toNode owner edgeOwner := g if fromNode.graph != toNode.graph { // 1 or 2 are subgraphs edgeOwner = commonParentOf(fromNode.graph, toNode.graph) } e := Edge{ from: fromNode, to: toNode, AttributesMap: AttributesMap{attributes: map[string]interface{}{}}, graph: edgeOwner} if fromNodePort != "" { e.fromPort = fromNodePort } if toNodePort != "" { e.toPort = toNodePort } if len(labels) > 0 { e.Attr("label", strings.Join(labels, ",")) } if g.edgeInitializer != nil { g.edgeInitializer(e) } edgeOwner.edgesFrom[fromNode.id] = append(edgeOwner.edgesFrom[fromNode.id], e) return e } // FindEdges finds all edges in the graph that go from the fromNode to the toNode. // Otherwise, returns an empty slice. func (g *Graph) FindEdges(fromNode, toNode Node) (found []Edge) { found = make([]Edge, 0) edgeOwner := g if fromNode.graph != toNode.graph { edgeOwner = commonParentOf(fromNode.graph, toNode.graph) } if edges, ok := edgeOwner.edgesFrom[fromNode.id]; ok { for _, e := range edges { if e.to.id == toNode.id { found = append(found, e) } } } return found } func commonParentOf(one *Graph, two *Graph) *Graph { // TODO return one.Root() } // AddToSameRank adds the given nodes to the specified rank group, forcing them to be rendered in the same row func (g *Graph) AddToSameRank(group string, nodes ...Node) { g.sameRank[group] = append(g.sameRank[group], nodes...) } // String returns the source in dot notation. func (g *Graph) String() string { b := new(bytes.Buffer) g.Write(b) return b.String() } func (g *Graph) Write(w io.Writer) { g.IndentedWrite(NewIndentWriter(w)) } // IndentedWrite write the graph to a writer using simple TAB indentation. func (g *Graph) IndentedWrite(w *IndentWriter) { if g.isStrict && g.graphType != Sub.Name { fmt.Fprintf(w, "strict ") } fmt.Fprintf(w, "%s %s {", g.graphType, g.id) w.NewLineIndentWhile(func() { // subgraphs for _, key := range g.sortedSubgraphsKeys() { each := g.subgraphs[key] each.IndentedWrite(w) } // graph attributes appendSortedMap(g.AttributesMap.attributes, false, w) w.NewLine() // graph nodes for _, key := range g.sortedNodesKeys() { each := g.nodes[key] fmt.Fprintf(w, "n%d", each.seq) appendSortedMap(each.attributes, true, w) fmt.Fprintf(w, ";") w.NewLine() } // graph edges denoteEdge := "->" if g.graphType == "graph" { denoteEdge = "--" } for _, each := range g.sortedEdgesFromKeys() { all := g.edgesFrom[each] for _, each := range all { fromPort := "" if each.fromPort != "" { fromPort = ":" + each.fromPort } toPort := "" if each.toPort != "" { toPort = ":" + each.toPort } fmt.Fprintf(w, "n%d%s%sn%d%s", each.from.seq, fromPort, denoteEdge, each.to.seq, toPort) appendSortedMap(each.attributes, true, w) fmt.Fprint(w, ";") w.NewLine() } } for _, nodes := range g.sameRank { str := "" for _, n := range nodes { str += fmt.Sprintf("n%d;", n.seq) } fmt.Fprintf(w, "{rank=same; %s};", str) w.NewLine() } }) fmt.Fprintf(w, "}") w.NewLine() } func appendSortedMap(m map[string]interface{}, mustBracket bool, b io.Writer) { if len(m) == 0 { return } if mustBracket { fmt.Fprint(b, "[") } first := true // first collect keys keys := []string{} for k := range m { keys = append(keys, k) } sort.StringSlice(keys).Sort() for _, k := range keys { if !first { if mustBracket { fmt.Fprint(b, ",") } else { fmt.Fprintf(b, ";") } } if html, isHTML := m[k].(HTML); isHTML { fmt.Fprintf(b, "%s=<%s>", k, html) } else if literal, isLiteral := m[k].(Literal); isLiteral { fmt.Fprintf(b, "%s=%s", k, literal) } else if str, ok := m[k].(string); ok { fmt.Fprintf(b, "%s=%q", k, str) } else { fmt.Fprintf(b, "%s=\"%v\"", k, m[k]) } first = false } if mustBracket { fmt.Fprint(b, "]") } else { fmt.Fprint(b, ";") } } // VisitNodes visits all nodes recursively func (g *Graph) VisitNodes(callback func(node Node) (done bool)) { for _, node := range g.nodes { done := callback(node) if done { return } } for _, subGraph := range g.subgraphs { subGraph.VisitNodes(callback) } } // FindNodeById return node by id func (g *Graph) FindNodeById(id string) (foundNode Node, found bool) { g.VisitNodes(func(node Node) (done bool) { if node.id == id { found = true foundNode = node return true } return false }) return } // FindNodes returns all nodes recursively func (g *Graph) FindNodes() (nodes []Node) { var foundNodes []Node g.VisitNodes(func(node Node) (done bool) { foundNodes = append(foundNodes, node) return false }) return foundNodes } // IsDirected returns info about the graph type func (g *Graph) IsDirected() bool { return g.graphType == Directed.Name } // EdgesMap returns a map with Node.id -> []Edge func (g *Graph) EdgesMap() map[string][]Edge { return g.edgesFrom } // HasNode returns whether the node was created in this graph (does not look for it in subgraphs). func (g *Graph) HasNode(n Node) bool { return g == n.graph } dot-1.6.2/graph_options.go000066400000000000000000000010341460726460600155220ustar00rootroot00000000000000package dot type GraphOption interface { Apply(*Graph) } type ClusterOption struct{} func (o ClusterOption) Apply(g *Graph) { g.beCluster() } var ( Strict = GraphTypeOption{"strict"} // only for graph and digraph, not for subgraph Undirected = GraphTypeOption{"graph"} Directed = GraphTypeOption{"digraph"} Sub = GraphTypeOption{"subgraph"} ) type GraphTypeOption struct { Name string } func (o GraphTypeOption) Apply(g *Graph) { if o.Name == Strict.Name { g.isStrict = true return } g.graphType = o.Name } dot-1.6.2/graph_test.go000066400000000000000000000255721460726460600150230ustar00rootroot00000000000000package dot import ( "os" "reflect" "strings" "testing" ) func TestEmpty(t *testing.T) { di := NewGraph(Directed) if got, want := flatten(di.String()), `digraph {}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEmptyStrictDirected(t *testing.T) { di := NewGraph(Directed, Strict) if got, want := flatten(di.String()), `strict digraph {}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } di2 := NewGraph(Strict, Directed) if got, want := flatten(di2.String()), `strict digraph {}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestOverrideID(t *testing.T) { caught := false defer func() { if r := recover(); r != nil { caught = true } }() di := NewGraph(Directed) di.ID("one") if got, want := di.GetID(), "one"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } di.ID("two") if !caught { t.Fail() } } func TestEmptyWithIDAndAttributes(t *testing.T) { di := NewGraph(Directed) di.ID("test") di.Attr("style", "filled") di.Attr("color", "lightgrey") if got, want := flatten(di.String()), `digraph test {color="lightgrey";style="filled";}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEmptyWithHTMLLabel(t *testing.T) { di := NewGraph(Directed) di.ID("test") di.Attr("label", HTML("Hi")) if got, want := flatten(di.String()), `digraph test {label=<Hi>;}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestDeleteNode(t *testing.T) { di := NewGraph(Undirected) n1 := di.Node("A") n2 := di.Node("B") n3 := di.Node("C") di.Edge(n1, n2) // Will be deleted di.Edge(n2, n3) // Will also be deleted di.Edge(n1, n3) // Must not be deleted wasDeleted := di.DeleteNode("B") if got, want := flatten(di.String()), `graph {n1[label="A"];n3[label="C"];n1--n3;}`; wasDeleted && got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestDeleteNodeWhenNodeDoesNotExist(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n2 := di.Node("B") n3 := di.Node("C") di.Edge(n1, n2) di.Edge(n2, n3) wasDeleted := di.DeleteNode("D") if got, want := flatten(di.String()), `digraph {n1[label="A"];n2[label="B"];n3[label="C"];n1->n2;n2->n3;}`; !wasDeleted && got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEmptyWithLiteralValueLabel(t *testing.T) { di := NewGraph(Directed) di.ID("test") di.Attr("label", Literal(`"left-justified text\l"`)) if got, want := flatten(di.String()), `digraph test {label="left-justified text\l";}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestTwoConnectedNodes(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n2 := di.Node("B") if !di.HasNode(n1) { t.Fail() } di.Edge(n1, n2) if got, want := flatten(di.String()), `digraph {n1[label="A"];n2[label="B"];n1->n2;}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraph_FindEdges(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("A") n2 := di.Node("B") want := []Edge{di.Edge(n1, n2)} got := di.FindEdges(n1, n2) if !reflect.DeepEqual(got, want) { t.Errorf("TestGraph.FindEdges() = %v, want %v", got, want) } n3 := di.Node("C") n2.Edge(n3) list := want[0].EdgesTo(n3) if len(list) != 1 { t.Fail() } } func TestSubgraph(t *testing.T) { di := NewGraph(Directed) sub := di.Subgraph("test-id") if second := di.Subgraph("test-id"); second != sub { t.Fatal() } sub.Attr("style", "filled") if got, want := flatten(di.String()), `digraph {subgraph s1 {label="test-id";style="filled";}}`; got != want { t.Errorf("got\n[%v] want\n[%v]", got, want) } sub.Label("new-label") if got, want := flatten(di.String()), `digraph {subgraph s1 {label="new-label";style="filled";}}`; got != want { t.Errorf("got\n[%v] want\n[%v]", got, want) } found, _ := di.FindSubgraph("test-id") if got, want := found, sub; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } subsub := sub.Subgraph("sub-test-id") found, _ = subsub.FindSubgraph("test-id") if got, want := found, sub; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } func TestSubgraphIgnoreStrict(t *testing.T) { di := NewGraph() _ = di.Subgraph("test", ClusterOption{}, Strict) if got, want := flatten(di.String()), `digraph {subgraph cluster_s1 {label="test";}}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } di2 := NewGraph() _ = di2.Subgraph("test", Strict, ClusterOption{}) if got, want := flatten(di2.String()), `digraph {subgraph cluster_s1 {label="test";}}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestSubgraphClusterOption(t *testing.T) { di := NewGraph(Directed) sub := di.Subgraph("test", ClusterOption{}) if got, want := sub.id, "cluster_s1"; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestEdgeLabel(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("e1") n2 := di.Node("e2") n1.Edge(n2, "what").Attr("x", "y") if got, want := flatten(di.String()), `digraph {n1[label="e1"];n2[label="e2"];n1->n2[label="what",x="y"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestSameRank(t *testing.T) { di := NewGraph(Directed) foo1 := di.Node("foo1") foo2 := di.Node("foo2") bar := di.Node("bar") foo1.Edge(foo2) foo1.Edge(bar) di.AddToSameRank("top-row", foo1, foo2) if got, want := flatten(di.String()), `digraph {n3[label="bar"];n1[label="foo1"];n2[label="foo2"];n1->n2;n1->n3;{rank=same; n1;n2;};}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } // dot -Tpng cluster.dot > cluster.png && open cluster.png func TestCluster(t *testing.T) { di := NewGraph(Directed) outside := di.Node("Outside") clusterA := di.Subgraph("Cluster A", ClusterOption{}) insideOne := clusterA.Node("one") insideTwo := clusterA.Node("two") clusterB := di.Subgraph("Cluster B", ClusterOption{}) insideThree := clusterB.Node("three") insideFour := clusterB.Node("four") outside.Edge(insideFour).Edge(insideOne).Edge(insideTwo).Edge(insideThree).Edge(outside) os.WriteFile("doc/cluster.dot", []byte(di.String()), os.ModePerm) } // remove tabs and newlines and spaces func flatten(s string) string { return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) } func TestDeleteLabel(t *testing.T) { g := NewGraph() n := g.Node("my-id") n.AttributesMap.Delete("label") if got, want := flatten(g.String()), `digraph {n1;}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraph_FindNodeById_emptyGraph(t *testing.T) { di := NewGraph(Directed) _, found := di.FindNodeById("F") if got, want := found, false; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraph_FindNodeById_multiNodeGraph(t *testing.T) { di := NewGraph(Directed) di.Node("A") di.Node("B") node, found := di.FindNodeById("A") if got, want := node.id, "A"; got != want { t.Errorf("got [%v] want [%v]", got, want) } if got, want := found, true; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraph_FindNodeById_multiNodesInSubGraphs(t *testing.T) { di := NewGraph(Directed) di.Node("A") di.Node("B") sub := di.Subgraph("new subgraph") sub.Node("C") node, found := di.FindNodeById("C") if got, want := node.id, "C"; got != want { t.Errorf("got [%v] want [%v]", got, want) } if got, want := found, true; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraph_FindNodes_multiNodesInSubGraphs(t *testing.T) { di := NewGraph(Directed) di.Node("A") di.Node("B") sub := di.Subgraph("new subgraph") sub.Node("C") nodes := di.FindNodes() if got, want := len(nodes), 3; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestLabelWithEscaping(t *testing.T) { di := NewGraph(Directed) n := di.Node("without linefeed") n.Attr("label", Literal(`"with \l linefeed"`)) if got, want := flatten(di.String()), `digraph {n1[label="with \l linefeed"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestGraphNodeInitializer(t *testing.T) { di := NewGraph(Directed) di.NodeInitializer(func(n Node) { n.Attr("test", "test") }) n := di.Node("A") if got, want := n.attributes["test"], "test"; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } func TestGraphEdgeInitializer(t *testing.T) { di := NewGraph(Directed) di.EdgeInitializer(func(e Edge) { e.Attr("test", "test") }) e := di.Node("A").Edge(di.Node("B")) if got, want := e.attributes["test"], "test"; got != want { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } func TestGraphCreateNodeOnce(t *testing.T) { di := NewGraph(Undirected) n1 := di.Node("A") n2 := di.Node("A") if got, want := n1, n2; &n1 == &n2 { t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) } } func TestGraphCommonParent(t *testing.T) { di := NewGraph(Directed) a := di.Node("a") b := di.Node("b") s1 := di.Subgraph("s1") a1 := s1.Node("a1") b1 := s1.Node("b1") s2 := di.Subgraph("s2") a2 := s2.Node("a2") b2 := s2.Node("b2") a.Edge(a1) b.Edge(b2) e := a2.Edge(b1) if got, want := flatten(di.String()), `digraph {subgraph s3 {label="s1";n4[label="a1"];n5[label="b1"];}subgraph s6 {label="s2";n7[label="a2"];n8[label="b2"];}n1[label="a"];n2[label="b"];n1->n4;n7->n5;n2->n8;}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } list := di.FindEdges(a2, b1) if len(list) != 1 { t.Fail() } if !reflect.DeepEqual(list[0], e) { t.Fail() } same := a2.EdgesTo(b1) if !reflect.DeepEqual(list, same) { t.Fail() } } func TestReverseEdge(t *testing.T) { di := NewGraph(Directed) if !di.IsDirected() { t.Fail() } a := di.Node("a") b := di.Node("b") e := a.ReverseEdge(b) if got, want := flatten(di.String()), `digraph {n1[label="a"];n2[label="b"];n2->n1;}`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e.From().ID(), b.ID(); got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e.To().id, a.id; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } m := di.EdgesMap() if got, want := len(m), 1; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := len(m["b"]), 1; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } c := di.Node("c") e.ReverseEdge(c) if got, want := flatten(di.String()), `digraph {n1[label="a"];n2[label="b"];n3[label="c"];n2->n1;n3->n1;}`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } func TestFindNodeWithLabel(t *testing.T) { di := NewGraph(Directed) di.Node("A") di.Node("B") sub := di.Subgraph("new subgraph") sub.Node("C") n1, ok := di.FindNodeWithLabel("B") if !ok { t.Fail() } if l := n1.GetAttr("label"); l != "B" { t.Fail() } n2, ok := sub.FindNodeWithLabel("A") if !ok { t.Fail() } if l := n2.GetAttr("label"); l != "A" { t.Fail() } _, ok = sub.FindNodeWithLabel("D") if ok { t.Fail() } } dot-1.6.2/indent.go000066400000000000000000000026721460726460600141400ustar00rootroot00000000000000package dot import ( "fmt" "io" ) // IndentWriter decorates an io.Writer to insert leading TAB \t character per line type IndentWriter struct { level int writer io.Writer } // NewIndentWriter returns a new IndentWriter with indent level 0. func NewIndentWriter(w io.Writer) *IndentWriter { return &IndentWriter{level: 0, writer: w} } // Indent raises the level and writes the extra \t (TAB) character. func (i *IndentWriter) Indent() { i.level++ fmt.Fprint(i.writer, "\t") } // BackIndent drops the level with one. func (i *IndentWriter) BackIndent() { i.level-- } // IndentWhile call the blocks after an indent and will restore that indent afterward. func (i *IndentWriter) IndentWhile(block func()) { i.Indent() block() i.BackIndent() } // NewLineIndentWhile is a variation of IndentWhile that produces extra newlines. func (i *IndentWriter) NewLineIndentWhile(block func()) { i.NewLine() i.Indent() block() i.BackIndent() i.NewLine() } // NewLine writes the new line and a number of tab \t characters that matches the level count. func (i *IndentWriter) NewLine() { fmt.Fprint(i.writer, "\n") for j := 0; j < i.level; j++ { fmt.Fprint(i.writer, "\t") } } // Write makes it an io.Writer func (i *IndentWriter) Write(data []byte) (n int, err error) { return i.writer.Write(data) } // WriteString is a convenient Write. func (i *IndentWriter) WriteString(s string) (n int, err error) { fmt.Fprint(i.writer, s) return len(s), nil } dot-1.6.2/indent_test.go000066400000000000000000000013561460726460600151750ustar00rootroot00000000000000package dot import ( "bytes" "fmt" "testing" ) func TestIndentWriter(t *testing.T) { b := new(bytes.Buffer) i := NewIndentWriter(b) i.WriteString("doc {") i.NewLineIndentWhile(func() { fmt.Fprint(i, "chapter {") i.NewLineIndentWhile(func() { fmt.Fprint(i, "chapter text") }) i.WriteString("}") }) i.WriteString("}") got := b.String() want := `doc { chapter { chapter text } }` if got != want { t.Fail() } } func TestIndentWriter_IndentWhile(t *testing.T) { b := new(bytes.Buffer) i := NewIndentWriter(b) i.IndentWhile(func() { i.WriteString("[") i.IndentWhile(func() { i.WriteString("test") }) i.WriteString("]") }) got := b.String() want := ` [ test]` if got != want { t.Log(got) t.Fail() } } dot-1.6.2/mermaid.go000066400000000000000000000046751460726460600143020ustar00rootroot00000000000000package dot import ( "fmt" "html" "strings" ) const ( MermaidTopToBottom = iota MermaidTopDown MermaidBottomToTop MermaidRightToLeft MermaidLeftToRight ) var ( MermaidShapeRound = shape{"(", ")"} MermaidShapeStadium = shape{"([", "])"} MermaidShapeSubroutine = shape{"[[", "]]"} MermaidShapeCylinder = shape{"[(", ")]"} MermaidShapeCirle = shape{"((", "))"} // Deprecated: use MermaidShapeCircle instead MermaidShapeCircle = shape{"((", "))"} MermaidShapeAsymmetric = shape{">", "]"} MermaidShapeRhombus = shape{"{", "}"} MermaidShapeTrapezoid = shape{"[/", "\\]"} MermaidShapeTrapezoidAlt = shape{"[\\", "/]"} ) type shape struct { open, close string } func MermaidGraph(g *Graph, orientation int) string { return diagram(g, "graph", orientation) } func MermaidFlowchart(g *Graph, orientation int) string { return diagram(g, "flowchart", orientation) } func escape(value string) string { return fmt.Sprintf(`"%s"`, html.EscapeString(value)) } func diagram(g *Graph, diagramType string, orientation int) string { sb := new(strings.Builder) sb.WriteString(diagramType) sb.WriteRune(' ') switch orientation { case MermaidTopDown, MermaidTopToBottom: sb.WriteString("TD") case MermaidBottomToTop: sb.WriteString("BT") case MermaidRightToLeft: sb.WriteString("RL") case MermaidLeftToRight: sb.WriteString("LR") default: sb.WriteString("TD") } writeEnd(sb) // graph nodes for _, key := range g.sortedNodesKeys() { nodeShape := MermaidShapeRound each := g.nodes[key] if s := each.GetAttr("shape"); s != nil { nodeShape = s.(shape) } txt := "?" if label := each.GetAttr("label"); label != nil { txt = label.(string) } fmt.Fprintf(sb, "\tn%d%s%s%s;\n", each.seq, nodeShape.open, escape(txt), nodeShape.close) if style := each.GetAttr("style"); style != nil { fmt.Fprintf(sb, "\tstyle n%d %s\n", each.seq, style.(string)) } } // all edges // graph edges denoteEdge := "-->" if g.graphType == "graph" { denoteEdge = "---" } for _, each := range g.sortedEdgesFromKeys() { all := g.edgesFrom[each] for _, each := range all { if label := each.GetAttr("label"); label != nil { fmt.Fprintf(sb, "\tn%d%s|%s|n%d;\n", each.from.seq, denoteEdge, escape(label.(string)), each.to.seq) } else { fmt.Fprintf(sb, "\tn%d%sn%d;\n", each.from.seq, denoteEdge, each.to.seq) } } } return sb.String() } func writeEnd(sb *strings.Builder) { sb.WriteString(";\n") } dot-1.6.2/mermaid_test.go000066400000000000000000000055461460726460600153370ustar00rootroot00000000000000package dot import ( "testing" ) func TestMermaidSimple(t *testing.T) { di := NewGraph(Directed) n1 := di.Node("e1").Label("E1") n2 := di.Node("e2").Attr("shape", MermaidShapeRound).Attr("style", "fill:#90EE90") n1.Edge(n2, "what").Attr("x", "y") out := flatten(MermaidGraph(di, MermaidTopDown)) if got, want := out, `graph TD;n1("E1");n2("e2");style n2 fill:#90EE90n1-->|"what"|n2;`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } func TestEmptyFlow(t *testing.T) { di := NewGraph(Directed) s := MermaidFlowchart(di, MermaidTopDown) if got, want := s, "flowchart TD;\n"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } func TestEmptyGraphLR(t *testing.T) { di := NewGraph(Directed) s := MermaidGraph(di, MermaidLeftToRight) if got, want := s, "graph LR;\n"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } s = MermaidGraph(di, MermaidRightToLeft) if got, want := s, "graph RL;\n"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } s = MermaidGraph(di, MermaidBottomToTop) if got, want := s, "graph BT;\n"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } s = MermaidGraph(di, 42) if got, want := s, "graph TD;\n"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } func TestMermaidShapes(t *testing.T) { di := NewGraph(Directed) di.Node("round").Attr("shape", MermaidShapeRound) di.Node("asym").Attr("shape", MermaidShapeAsymmetric) di.Node("circ").Attr("shape", MermaidShapeCircle) di.Node("cyl").Attr("shape", MermaidShapeCylinder) di.Node("rhom").Attr("shape", MermaidShapeRhombus) di.Node("stad").Attr("shape", MermaidShapeStadium) di.Node("sub").Attr("shape", MermaidShapeSubroutine) di.Node("trap").Attr("shape", MermaidShapeTrapezoid) di.Node("trapalt").Attr("shape", MermaidShapeTrapezoidAlt) s := MermaidGraph(di, MermaidLeftToRight) // t.Log(s) if got, want := flatten(s), `graph LR;n2>"asym"];n3(("circ"));n4[("cyl")];n5{"rhom"};n1("round");n6(["stad"]);n7[["sub"]];n8[/"trap"\];n9[\"trapalt"/];`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } // Deprecated: Use MermaidShapeCircle instead of MermaidShapeCirle func TestMermaidShapeCirle(t *testing.T) { di := NewGraph(Directed) di.Node("circ").Attr("shape", MermaidShapeCirle) s := MermaidGraph(di, MermaidLeftToRight) // t.Log(s) if got, want := flatten(s), `graph LR;n1(("circ"));`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } func TestUndirectedMermaid(t *testing.T) { un := NewGraph(Undirected) un.Node("love").Edge(un.Node("happinez")) s := MermaidFlowchart(un, MermaidLeftToRight) //t.Log(s) if got, want := flatten(s), `flowchart LR;n2("happinez");n1("love");n1---n2;`; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } dot-1.6.2/node.go000066400000000000000000000034131460726460600135760ustar00rootroot00000000000000package dot // Node represents a dot Node. type Node struct { AttributesMap graph *Graph id string seq int } // ID returns the assigned id to this node. func (n Node) ID() string { return n.id } // Attr sets label=value and return the Node func (n Node) Attr(label string, value interface{}) Node { n.AttributesMap.Attr(label, value) return n } // Label sets the attribute "label" to the given label func (n Node) Label(label string) Node { return n.Attr("label", label) } // Box sets the attribute "shape" to "box" func (n Node) Box() Node { return n.Attr("shape", "box") } // Edge sets label=value and returns the Edge for chaining. func (n Node) Edge(toNode Node, labels ...string) Edge { return n.graph.Edge(n, toNode, labels...) } // EdgesTo returns all existing edges between this Node and the argument Node. func (n Node) EdgesTo(toNode Node) []Edge { return n.graph.FindEdges(n, toNode) } // GetAttr returns the value stored by a name. Returns nil if missing. func (n Node) GetAttr(name string) interface{} { return n.attributes[name] } // ReverseEdge sets label=value and returns the Edge for chaining. func (n Node) ReverseEdge(fromNode Node, labels ...string) Edge { return n.graph.Edge(fromNode, n, labels...) } // BidirectionalEdge adds two edges, marks the first as invisible and the second with direction "both". Returns both edges. func (n Node) BidirectionalEdge(toAndFromNode Node) []Edge { e1 := n.Edge(toAndFromNode).Attr("style", "invis") e2 := toAndFromNode.Edge(n).Attr("dir", "both") return []Edge{e1, e2} } // NewRecordBuilder returns a new recordBuilder for setting the attributes for a record-shaped node. // Call Build() on the builder to set the label and shape. func (n Node) NewRecordBuilder() *recordBuilder { return newRecordBuilder(n) } dot-1.6.2/node_test.go000066400000000000000000000023741460726460600146420ustar00rootroot00000000000000package dot import ( "testing" ) func TestNode_Box(t *testing.T) { g := NewGraph(Directed) n := g.Node("A") n.Box() if n.Value("shape") != "box" { t.Fail() } } func TestNode_Label(t *testing.T) { g := NewGraph(Directed) n := g.Node("A") n.Label("42") if n.Value("label") != "42" { t.Fail() } } func TestNodesWithBidirectionalEdge(t *testing.T) { g := NewGraph(Directed) a := g.Node("A") b := g.Node("B") e := a.BidirectionalEdge(b) if got, want := len(e), 2; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[0].from, a; got.id != want.id { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[0].to, b; got.id != want.id { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[1].from, b; got.id != want.id { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[1].to, a; got.id != want.id { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[0].attributes["style"], "invis"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := e[1].attributes["dir"], "both"; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } dot-1.6.2/record.go000066400000000000000000000053641460726460600141360ustar00rootroot00000000000000package dot import ( "fmt" "strings" ) // recordBuilder can build the label of a node with shape "record" or "mrecord". type recordBuilder struct { target Node shape string nesting *stack currentLabel recordLabel } // newRecordBuilder returns a new recordBuilder for constructing the label of the node. func newRecordBuilder(n Node) *recordBuilder { return &recordBuilder{ target: n, shape: "record", nesting: new(stack), } } type recordLabel []recordField func (r recordLabel) writeOn(buf *strings.Builder) { for i, each := range r { if i > 0 { buf.WriteRune('|') } each.writeOn(buf) } } type recordField struct { id recordFieldId // or nestedLabel *recordLabel } func (r recordField) writeOn(buf *strings.Builder) { if r.nestedLabel != nil { buf.WriteRune('{') r.nestedLabel.writeOn(buf) buf.WriteRune('}') return } r.id.writeOn(buf) } type recordFieldId struct { id string content string } func (r recordFieldId) writeOn(buf *strings.Builder) { if r.id != "" { fmt.Fprintf(buf, "<%s> ", r.id) } buf.WriteString(r.content) } // MRecord sets the shape of the node to "mrecord" func (r *recordBuilder) MRecord() *recordBuilder { r.shape = "mrecord" return r } // Field adds a record field func (r *recordBuilder) Field(content string) *recordBuilder { rf := recordField{ id: recordFieldId{ content: content, }, } r.currentLabel = append(r.currentLabel, rf) return r } // FieldWithId adds a record field with an identifier for connecting edges. func (r *recordBuilder) FieldWithId(content, id string) *recordBuilder { rf := recordField{ id: recordFieldId{ id: id, content: content, }, } r.currentLabel = append(r.currentLabel, rf) return r } // Nesting will create a nested (layout flipped) list of rlabel. func (r *recordBuilder) Nesting(block func()) { r.nesting.push(r.currentLabel) r.currentLabel = recordLabel{} block() // currentLabel has fields added by block // top of stack has label before block top := r.nesting.pop() cpy := r.currentLabel[:] top = append(top, recordField{ nestedLabel: &cpy, }) r.currentLabel = top } // Build sets the computed label and shape func (r *recordBuilder) Build() error { r.target.Attr("shape", r.shape) r.target.Attr("label", r.Label()) return nil } // Label returns the computed label func (r *recordBuilder) Label() string { buf := new(strings.Builder) for i, each := range r.currentLabel { if i > 0 { buf.WriteString("|") } each.writeOn(buf) } return buf.String() } // stack implements a lifo queue for recordLabel instances. type stack []recordLabel func (s *stack) push(r recordLabel) { *s = append(*s, r) } func (s *stack) pop() recordLabel { top := (*s)[len(*s)-1] *s = (*s)[0 : len(*s)-1] return top } dot-1.6.2/record_test.go000066400000000000000000000057571460726460600152030ustar00rootroot00000000000000package dot import ( "fmt" "testing" ) func TestSimpleRecord(t *testing.T) { g := NewGraph(Directed) rb := newRecordBuilder(g.Node("r")) rb.Field("a") rb.Build() if got, want := flatten(g.String()), `digraph {n1[label="a",shape="record"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestSimpleMRecordWithFieldID(t *testing.T) { g := NewGraph(Directed) rb := newRecordBuilder(g.Node("r")) rb.MRecord() rb.FieldWithId("a", "a1") rb.Build() if got, want := flatten(g.String()), `digraph {n1[label=" a",shape="mrecord"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestTwoColumnsRecord(t *testing.T) { g := NewGraph(Directed) rb := newRecordBuilder(g.Node("r")) rb.Field("a").Field("b") rb.Build() if got, want := flatten(g.String()), `digraph {n1[label="a|b",shape="record"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestTwoColumnsNestedRecord(t *testing.T) { g := NewGraph(Directed) rb := newRecordBuilder(g.Node("r")) rb.Field("a") rb.Nesting(func() { rb.Field("b") rb.Field("c") }) rb.Field("d") rb.Build() if got, want := flatten(g.String()), `digraph {n1[label="a|{b|c}|d",shape="record"];}`; got != want { t.Errorf("got [%v] want [%v]", got, want) } } func TestStack(t *testing.T) { one := recordLabel{} two := recordLabel{} s := new(stack) s.push(one) s.push(two) if got, want := s.pop(), two; &got == &want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := s.pop(), one; &got == &want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } if got, want := len(*s), 0; got != want { t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want) } } // https://graphviz.org/doc/info/shapes.html#record /* digraph structs { node [shape=record]; struct1 [label=" left| mid\ dle| right"]; struct2 [label=" one| two"]; struct3 [label="hello\nworld |{ b |{c| d|e}| f}| g | h"]; struct1:f1 -> struct2:f0; struct1:f2 -> struct3:here; } */ func ExampleNode_NewRecordBuilder() { g := NewGraph(Directed) r1 := g.Node("struct1").NewRecordBuilder() r1.FieldWithId("left", "f0") r1.FieldWithId("mid\dle", "f1") r1.FieldWithId("right", "f2") r1.Build() r2 := g.Node("struct2").NewRecordBuilder() r2.FieldWithId("one", "f0") r2.Build() r3 := g.Node("struct3").NewRecordBuilder() r3.Field("hello\world") r3.Nesting(func() { r3.Field("b") r3.Nesting(func() { r3.Field("c") r3.FieldWithId("d", "here") r3.Field("e") }) r3.Field("f") }) r3.Field("g") r3.Field("h") r3.Build() g.EdgeWithPorts(g.Node("struct1"), g.Node("struct2"), "f1", "f0") g.EdgeWithPorts(g.Node("struct1"), g.Node("struct3"), "f2", "here") fmt.Println(flatten(g.String())) // Output:digraph {n1[label=" left| mid\dle| right",shape="record"];n2[label=" one",shape="record"];n3[label="hello\world|{b|{c| d|e}|f}|g|h",shape="record"];n1:f1->n2:f0;n1:f2->n3:here;} } dot-1.6.2/sort.go000066400000000000000000000007671460726460600136510ustar00rootroot00000000000000package dot import "sort" func (g *Graph) sortedNodesKeys() (keys []string) { for each := range g.nodes { keys = append(keys, each) } sort.StringSlice(keys).Sort() return } func (g *Graph) sortedEdgesFromKeys() (keys []string) { for each := range g.edgesFrom { keys = append(keys, each) } sort.StringSlice(keys).Sort() return } func (g *Graph) sortedSubgraphsKeys() (keys []string) { for each := range g.subgraphs { keys = append(keys, each) } sort.StringSlice(keys).Sort() return }