summaryrefslogtreecommitdiffstats
path: root/lib/puppet/external/dot.rb
blob: 77d66e92eed9b5f9a08c4ca7a08087121b8aae84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# rdot.rb
#
#
# This is a modified version of dot.rb from Dave Thomas's rdoc project.  I [Horst Duchene]
# renamed it to rdot.rb to avoid collision with an installed rdoc/dot.
#
# It also supports undirected edges.

module DOT

    # These glogal vars are used to make nice graph source.

    $tab  = '    '
    $tab2 = $tab * 2

    # if we don't like 4 spaces, we can change it any time

    def change_tab (t)
        $tab  = t
        $tab2 = t * 2
    end

    # options for node declaration

    NODE_OPTS = [
        # attributes due to
        # http://www.graphviz.org/Documentation/dotguide.pdf
        # March, 26, 2005
        'bottomlabel', # auxiliary label for nodes of shape M*
        'color',       # default: black; node shape color
        'comment',     # any string (format-dependent)
        'distortion',  # default: 0.0; node distortion for shape=polygon
        'fillcolor',   # default: lightgrey/black; node fill color
        'fixedsize',   # default: false; label text has no affect on node size
        'fontcolor',   # default: black; type face color
        'fontname',    # default: Times-Roman; font family
        'fontsize',    # default: 14; point size of label
        'group',       # name of nodes group
        'height',      # default: .5; height in inches
        'label',       # default: node name; any string
        'layer',       # default: overlay range; all, id or id:id
        'orientation', # dafault: 0.0; node rotation angle
        'peripheries', # shape-dependent number of node boundaries
        'regular',     # default:  false; force polygon to be regular
        'shape',       # default: ellipse; node shape; see Section 2.1 and Appendix E
        'shapefile',   # external EPSF or SVG custom shape file
        'sides',       # default: 4; number of sides for shape=polygon
        'skew' ,       # default: 0.0; skewing of node for shape=polygon
        'style',       # graphics options, e.g. bold, dotted, filled; cf. Section 2.3
        'toplabel',    # auxiliary label for nodes of shape M*
        'URL',         # URL associated with node (format-dependent)
        'width',       # default: .75; width in inches
        'z',           # default: 0.0; z coordinate for VRML output

        # maintained for backward compatibility or rdot internal
        'bgcolor',
        'rank'
    ]

    # options for edge declaration

    EDGE_OPTS = [
        'arrowhead',      # default: normal; style of arrowhead at head end
        'arrowsize',      # default: 1.0; scaling factor for arrowheads
        'arrowtail',      # default: normal; style of arrowhead at tail end
        'color',          # default: black; edge stroke color
        'comment',        # any string (format-dependent)
        'constraint',     # default: true use edge to affect node ranking
        'decorate',       # if set, draws a line connecting labels with their edges
        'dir',            # default: forward; forward, back, both, or none
        'fontcolor',      # default: black type face color
        'fontname',       # default: Times-Roman; font family
        'fontsize',       # default: 14; point size of label
        'headlabel',      # label placed near head of edge
        'headport',       # n,ne,e,se,s,sw,w,nw
        'headURL',        # URL attached to head label if output format is ismap
        'label',          # edge label
        'labelangle',     # default: -25.0; angle in degrees which head or tail label is rotated off edge
        'labeldistance',  # default: 1.0; scaling factor for distance of head or tail label from node
        'labelfloat',     # default: false; lessen constraints on edge label placement
        'labelfontcolor', # default: black; type face color for head and tail labels
        'labelfontname',  # default: Times-Roman; font family for head and tail labels
        'labelfontsize',  # default: 14 point size for head and tail labels
        'layer',          # default: overlay range; all, id or id:id
        'lhead',          # name of cluster to use as head of edge
        'ltail',          # name of cluster to use as tail of edge
        'minlen',         # default: 1 minimum rank distance between head and tail
        'samehead',       # tag for head node; edge heads with the same tag are merged onto the same port
        'sametail',       # tag for tail node; edge tails with the same tag are merged onto the same port
        'style',          # graphics options, e.g. bold, dotted, filled; cf. Section 2.3
        'taillabel',      # label placed near tail of edge
        'tailport',       # n,ne,e,se,s,sw,w,nw
        'tailURL',        # URL attached to tail label if output format is ismap
        'weight',         # default: 1; integer cost of stretching an edge

        # maintained for backward compatibility or rdot internal
        'id'
    ]

    # options for graph declaration

    GRAPH_OPTS = [
        'bgcolor',
        'center', 'clusterrank', 'color', 'concentrate',
        'fontcolor', 'fontname', 'fontsize',
        'label', 'layerseq',
        'margin', 'mclimit',
        'nodesep', 'nslimit',
        'ordering', 'orientation',
        'page',
        'rank', 'rankdir', 'ranksep', 'ratio',
        'size'
    ]

    # a root class for any element in dot notation

    class DOTSimpleElement

        attr_accessor :name

        def initialize (params = {})
            @label = params['name'] ? params['name'] : ''
        end

        def to_s
            @name
        end
    end

    # an element that has options ( node, edge, or graph )

    class DOTElement < DOTSimpleElement

        # attr_reader :parent
        attr_accessor :name, :options

        def initialize (params = {}, option_list = [])
            super(params)
            @name   = params['name']   ? params['name']   : nil
            @parent = params['parent'] ? params['parent'] : nil
            @options = {}
            option_list.each{ |i|
                @options[i] = params[i] if params[i]
            }
            @options['label'] ||= @name if @name != 'node'
        end

        def each_option
            @options.each{ |i| yield i }
        end

        def each_option_pair
            @options.each_pair{ |key, val| yield key, val }
        end

        #def parent=( thing )
        #    @parent.delete( self ) if defined?( @parent ) and @parent
        #    @parent = thing
        #end

    end


    # This is used when we build nodes that have shape=record
    # ports don't have options :)

    class DOTPort < DOTSimpleElement

        attr_accessor :label

        def initialize (params = {})
            super(params)
            @name = params['label'] ? params['label'] : ''
        end

        def to_s
            ( @name && @name != "" ? "<#{@name}>" : "" ) + "#{@label}"
        end
    end

    # node element

    class DOTNode < DOTElement

        @ports

        def initialize (params = {}, option_list = NODE_OPTS)
            super(params, option_list)
            @ports = params['ports'] ? params['ports'] : []
        end

        def each_port
            @ports.each { |i| yield i }
        end

        def << (thing)
            @ports << thing
        end

        def push (thing)
            @ports.push(thing)
        end

        def pop
            @ports.pop
        end

        def to_s (t = '')

            # This code is totally incomprehensible; it needs to be replaced!

            label = @options['shape'] != 'record' && @ports.length == 0 ?
                    @options['label'] ?
                        t + $tab + "label = \"#{@options['label']}\"\n" :
                        '' :
                    t + $tab + 'label = "' + " \\\n" +
                    t + $tab2 + "#{@options['label']}| \\\n" +
                    @ports.collect{ |i|
                        t + $tab2 + i.to_s
                    }.join( "| \\\n" ) + " \\\n" +
                    t + $tab + '"' + "\n"

                t + "#{@name} [\n" +
                @options.to_a.collect{ |i|
                    i[1] && i[0] != 'label' ?
                        t + $tab + "#{i[0]} = #{i[1]}" : nil
                }.compact.join( ",\n" ) + ( label != '' ? ",\n" : "\n" ) +
                label +
                t + "]\n"
        end

    end # class DOTNode

    # A subgraph element is the same to graph, but has another header in dot
    # notation.

    class DOTSubgraph < DOTElement

        @nodes
        @dot_string

        def initialize (params = {}, option_list = GRAPH_OPTS)
            super(params, option_list)
            @nodes      = params['nodes'] ? params['nodes'] : []
            @dot_string = 'graph'
        end

        def each_node
            @nodes.each{ |i| yield i }
        end

        def << (thing)
            @nodes << thing
        end

        def push (thing)
            @nodes.push( thing )
        end

        def pop
            @nodes.pop
        end

        def to_s (t = '')
            hdr = t + "#{@dot_string} #{@name} {\n"

            options = @options.to_a.collect{ |name, val|
                val && name != 'label' ?
                    t + $tab + "#{name} = #{val}" :
                    name ? t + $tab + "#{name} = \"#{val}\"" : nil
            }.compact.join( "\n" ) + "\n"

            nodes = @nodes.collect{ |i|
                i.to_s( t + $tab )
            }.join( "\n" ) + "\n"
            hdr + options + nodes + t + "}\n"
        end

    end # class DOTSubgraph

    # This is a graph.

    class DOTDigraph < DOTSubgraph

    def initialize (params = {}, option_list = GRAPH_OPTS)
        super(params, option_list)
        @dot_string = 'digraph'
    end

    end # class DOTDigraph

    # This is an edge.

    class DOTEdge < DOTElement

        attr_accessor :from, :to

        def initialize (params = {}, option_list = EDGE_OPTS)
            super(params, option_list)
            @from = params['from'] ? params['from'] : nil
            @to   = params['to'] ? params['to'] : nil
        end

        def edge_link
            '--'
        end

        def to_s (t = '')
            t + "#{@from} #{edge_link} #{to} [\n" +
                @options.to_a.collect{ |i|
                    i[1] && i[0] != 'label' ?
                        t + $tab + "#{i[0]} = #{i[1]}" :
                        i[1] ? t + $tab + "#{i[0]} = \"#{i[1]}\"" : nil
                }.compact.join( "\n" ) + "\n#{t}]\n"
        end

    end # class DOTEdge

    class DOTDirectedEdge < DOTEdge

        def edge_link
            '->'
        end

    end # class DOTDirectedEdge
end # module DOT