1 | # A wonderful hack by to draw package diagrams using the dot package.
|
---|
2 | # Originally written by Jah, team Enticla.
|
---|
3 | #
|
---|
4 | # You must have the V1.7 or later in your path
|
---|
5 | # http://www.research.att.com/sw/tools/graphviz/
|
---|
6 |
|
---|
7 | require "rdoc/dot/dot"
|
---|
8 | require 'rdoc/options'
|
---|
9 |
|
---|
10 | module RDoc
|
---|
11 |
|
---|
12 | # Draw a set of diagrams representing the modules and classes in the
|
---|
13 | # system. We draw one diagram for each file, and one for each toplevel
|
---|
14 | # class or module. This means there will be overlap. However, it also
|
---|
15 | # means that you'll get better context for objects.
|
---|
16 | #
|
---|
17 | # To use, simply
|
---|
18 | #
|
---|
19 | # d = Diagram.new(info) # pass in collection of top level infos
|
---|
20 | # d.draw
|
---|
21 | #
|
---|
22 | # The results will be written to the +dot+ subdirectory. The process
|
---|
23 | # also sets the +diagram+ attribute in each object it graphs to
|
---|
24 | # the name of the file containing the image. This can be used
|
---|
25 | # by output generators to insert images.
|
---|
26 |
|
---|
27 | class Diagram
|
---|
28 |
|
---|
29 | FONT = "Arial"
|
---|
30 |
|
---|
31 | DOT_PATH = "dot"
|
---|
32 |
|
---|
33 | # Pass in the set of top level objects. The method also creates
|
---|
34 | # the subdirectory to hold the images
|
---|
35 |
|
---|
36 | def initialize(info, options)
|
---|
37 | @info = info
|
---|
38 | @options = options
|
---|
39 | @counter = 0
|
---|
40 | File.makedirs(DOT_PATH)
|
---|
41 | @diagram_cache = {}
|
---|
42 | end
|
---|
43 |
|
---|
44 | # Draw the diagrams. We traverse the files, drawing a diagram for
|
---|
45 | # each. We also traverse each top-level class and module in that
|
---|
46 | # file drawing a diagram for these too.
|
---|
47 |
|
---|
48 | def draw
|
---|
49 | unless @options.quiet
|
---|
50 | $stderr.print "Diagrams: "
|
---|
51 | $stderr.flush
|
---|
52 | end
|
---|
53 |
|
---|
54 | @info.each_with_index do |i, file_count|
|
---|
55 | @done_modules = {}
|
---|
56 | @local_names = find_names(i)
|
---|
57 | @global_names = []
|
---|
58 | @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
|
---|
59 | 'fontname' => FONT,
|
---|
60 | 'fontsize' => '8',
|
---|
61 | 'bgcolor' => 'lightcyan1',
|
---|
62 | 'compound' => 'true')
|
---|
63 |
|
---|
64 | # it's a little hack %) i'm too lazy to create a separate class
|
---|
65 | # for default node
|
---|
66 | graph << DOT::DOTNode.new('name' => 'node',
|
---|
67 | 'fontname' => FONT,
|
---|
68 | 'color' => 'black',
|
---|
69 | 'fontsize' => 8)
|
---|
70 |
|
---|
71 | i.modules.each do |mod|
|
---|
72 | draw_module(mod, graph, true, i.file_relative_name)
|
---|
73 | end
|
---|
74 | add_classes(i, graph, i.file_relative_name)
|
---|
75 |
|
---|
76 | i.diagram = convert_to_png("f_#{file_count}", graph)
|
---|
77 |
|
---|
78 | # now go through and document each top level class and
|
---|
79 | # module independently
|
---|
80 | i.modules.each_with_index do |mod, count|
|
---|
81 | @done_modules = {}
|
---|
82 | @local_names = find_names(mod)
|
---|
83 | @global_names = []
|
---|
84 |
|
---|
85 | @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
|
---|
86 | 'fontname' => FONT,
|
---|
87 | 'fontsize' => '8',
|
---|
88 | 'bgcolor' => 'lightcyan1',
|
---|
89 | 'compound' => 'true')
|
---|
90 |
|
---|
91 | graph << DOT::DOTNode.new('name' => 'node',
|
---|
92 | 'fontname' => FONT,
|
---|
93 | 'color' => 'black',
|
---|
94 | 'fontsize' => 8)
|
---|
95 | draw_module(mod, graph, true)
|
---|
96 | mod.diagram = convert_to_png("m_#{file_count}_#{count}",
|
---|
97 | graph)
|
---|
98 | end
|
---|
99 | end
|
---|
100 | $stderr.puts unless @options.quiet
|
---|
101 | end
|
---|
102 |
|
---|
103 | #######
|
---|
104 | private
|
---|
105 | #######
|
---|
106 |
|
---|
107 | def find_names(mod)
|
---|
108 | return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
|
---|
109 | mod.modules.collect{|m| find_names(m)}.flatten
|
---|
110 | end
|
---|
111 |
|
---|
112 | def find_full_name(name, mod)
|
---|
113 | full_name = name.dup
|
---|
114 | return full_name if @local_names.include?(full_name)
|
---|
115 | mod_path = mod.full_name.split('::')[0..-2]
|
---|
116 | unless mod_path.nil?
|
---|
117 | until mod_path.empty?
|
---|
118 | full_name = mod_path.pop + '::' + full_name
|
---|
119 | return full_name if @local_names.include?(full_name)
|
---|
120 | end
|
---|
121 | end
|
---|
122 | return name
|
---|
123 | end
|
---|
124 |
|
---|
125 | def draw_module(mod, graph, toplevel = false, file = nil)
|
---|
126 | return if @done_modules[mod.full_name] and not toplevel
|
---|
127 |
|
---|
128 | @counter += 1
|
---|
129 | url = mod.http_url("classes")
|
---|
130 | m = DOT::DOTSubgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}",
|
---|
131 | 'label' => mod.name,
|
---|
132 | 'fontname' => FONT,
|
---|
133 | 'color' => 'blue',
|
---|
134 | 'style' => 'filled',
|
---|
135 | 'URL' => %{"#{url}"},
|
---|
136 | 'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3')
|
---|
137 |
|
---|
138 | @done_modules[mod.full_name] = m
|
---|
139 | add_classes(mod, m, file)
|
---|
140 | graph << m
|
---|
141 |
|
---|
142 | unless mod.includes.empty?
|
---|
143 | mod.includes.each do |m|
|
---|
144 | m_full_name = find_full_name(m.name, mod)
|
---|
145 | if @local_names.include?(m_full_name)
|
---|
146 | @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
147 | 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
|
---|
148 | 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}",
|
---|
149 | 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
|
---|
150 | else
|
---|
151 | unless @global_names.include?(m_full_name)
|
---|
152 | path = m_full_name.split("::")
|
---|
153 | url = File.join('classes', *path) + ".html"
|
---|
154 | @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
155 | 'shape' => 'box',
|
---|
156 | 'label' => "#{m_full_name}",
|
---|
157 | 'URL' => %{"#{url}"})
|
---|
158 | @global_names << m_full_name
|
---|
159 | end
|
---|
160 | @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
161 | 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
|
---|
162 | 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
|
---|
163 | end
|
---|
164 | end
|
---|
165 | end
|
---|
166 | end
|
---|
167 |
|
---|
168 | def add_classes(container, graph, file = nil )
|
---|
169 |
|
---|
170 | use_fileboxes = Options.instance.fileboxes
|
---|
171 |
|
---|
172 | files = {}
|
---|
173 |
|
---|
174 | # create dummy node (needed if empty and for module includes)
|
---|
175 | if container.full_name
|
---|
176 | graph << DOT::DOTNode.new('name' => "#{container.full_name.gsub( /:/,'_' )}",
|
---|
177 | 'label' => "",
|
---|
178 | 'width' => (container.classes.empty? and
|
---|
179 | container.modules.empty?) ?
|
---|
180 | '0.75' : '0.01',
|
---|
181 | 'height' => '0.01',
|
---|
182 | 'shape' => 'plaintext')
|
---|
183 | end
|
---|
184 | container.classes.each_with_index do |cl, cl_index|
|
---|
185 | last_file = cl.in_files[-1].file_relative_name
|
---|
186 |
|
---|
187 | if use_fileboxes && !files.include?(last_file)
|
---|
188 | @counter += 1
|
---|
189 | files[last_file] =
|
---|
190 | DOT::DOTSubgraph.new('name' => "cluster_#{@counter}",
|
---|
191 | 'label' => "#{last_file}",
|
---|
192 | 'fontname' => FONT,
|
---|
193 | 'color'=>
|
---|
194 | last_file == file ? 'red' : 'black')
|
---|
195 | end
|
---|
196 |
|
---|
197 | next if cl.name == 'Object' || cl.name[0,2] == "<<"
|
---|
198 |
|
---|
199 | url = cl.http_url("classes")
|
---|
200 |
|
---|
201 | label = cl.name.dup
|
---|
202 | if use_fileboxes && cl.in_files.length > 1
|
---|
203 | label << '\n[' +
|
---|
204 | cl.in_files.collect {|i|
|
---|
205 | i.file_relative_name
|
---|
206 | }.sort.join( '\n' ) +
|
---|
207 | ']'
|
---|
208 | end
|
---|
209 |
|
---|
210 | attrs = {
|
---|
211 | 'name' => "#{cl.full_name.gsub( /:/, '_' )}",
|
---|
212 | 'fontcolor' => 'black',
|
---|
213 | 'style'=>'filled',
|
---|
214 | 'color'=>'palegoldenrod',
|
---|
215 | 'label' => label,
|
---|
216 | 'shape' => 'ellipse',
|
---|
217 | 'URL' => %{"#{url}"}
|
---|
218 | }
|
---|
219 |
|
---|
220 | c = DOT::DOTNode.new(attrs)
|
---|
221 |
|
---|
222 | if use_fileboxes
|
---|
223 | files[last_file].push c
|
---|
224 | else
|
---|
225 | graph << c
|
---|
226 | end
|
---|
227 | end
|
---|
228 |
|
---|
229 | if use_fileboxes
|
---|
230 | files.each_value do |val|
|
---|
231 | graph << val
|
---|
232 | end
|
---|
233 | end
|
---|
234 |
|
---|
235 | unless container.classes.empty?
|
---|
236 | container.classes.each_with_index do |cl, cl_index|
|
---|
237 | cl.includes.each do |m|
|
---|
238 | m_full_name = find_full_name(m.name, cl)
|
---|
239 | if @local_names.include?(m_full_name)
|
---|
240 | @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
241 | 'to' => "#{cl.full_name.gsub( /:/,'_' )}",
|
---|
242 | 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}")
|
---|
243 | else
|
---|
244 | unless @global_names.include?(m_full_name)
|
---|
245 | path = m_full_name.split("::")
|
---|
246 | url = File.join('classes', *path) + ".html"
|
---|
247 | @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
248 | 'shape' => 'box',
|
---|
249 | 'label' => "#{m_full_name}",
|
---|
250 | 'URL' => %{"#{url}"})
|
---|
251 | @global_names << m_full_name
|
---|
252 | end
|
---|
253 | @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
|
---|
254 | 'to' => "#{cl.full_name.gsub( /:/, '_')}")
|
---|
255 | end
|
---|
256 | end
|
---|
257 |
|
---|
258 | sclass = cl.superclass
|
---|
259 | next if sclass.nil? || sclass == 'Object'
|
---|
260 | sclass_full_name = find_full_name(sclass,cl)
|
---|
261 | unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name)
|
---|
262 | path = sclass_full_name.split("::")
|
---|
263 | url = File.join('classes', *path) + ".html"
|
---|
264 | @global_graph << DOT::DOTNode.new(
|
---|
265 | 'name' => "#{sclass_full_name.gsub( /:/, '_' )}",
|
---|
266 | 'label' => sclass_full_name,
|
---|
267 | 'URL' => %{"#{url}"})
|
---|
268 | @global_names << sclass_full_name
|
---|
269 | end
|
---|
270 | @global_graph << DOT::DOTEdge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}",
|
---|
271 | 'to' => "#{cl.full_name.gsub( /:/, '_')}")
|
---|
272 | end
|
---|
273 | end
|
---|
274 |
|
---|
275 | container.modules.each do |submod|
|
---|
276 | draw_module(submod, graph)
|
---|
277 | end
|
---|
278 |
|
---|
279 | end
|
---|
280 |
|
---|
281 | def convert_to_png(file_base, graph)
|
---|
282 | str = graph.to_s
|
---|
283 | return @diagram_cache[str] if @diagram_cache[str]
|
---|
284 | op_type = Options.instance.image_format
|
---|
285 | dotfile = File.join(DOT_PATH, file_base)
|
---|
286 | src = dotfile + ".dot"
|
---|
287 | dot = dotfile + "." + op_type
|
---|
288 |
|
---|
289 | unless @options.quiet
|
---|
290 | $stderr.print "."
|
---|
291 | $stderr.flush
|
---|
292 | end
|
---|
293 |
|
---|
294 | File.open(src, 'w+' ) do |f|
|
---|
295 | f << str << "\n"
|
---|
296 | end
|
---|
297 |
|
---|
298 | system "dot", "-T#{op_type}", src, "-o", dot
|
---|
299 |
|
---|
300 | # Now construct the imagemap wrapper around
|
---|
301 | # that png
|
---|
302 |
|
---|
303 | ret = wrap_in_image_map(src, dot)
|
---|
304 | @diagram_cache[str] = ret
|
---|
305 | return ret
|
---|
306 | end
|
---|
307 |
|
---|
308 | # Extract the client-side image map from dot, and use it
|
---|
309 | # to generate the imagemap proper. Return the whole
|
---|
310 | # <map>..<img> combination, suitable for inclusion on
|
---|
311 | # the page
|
---|
312 |
|
---|
313 | def wrap_in_image_map(src, dot)
|
---|
314 | res = %{<map id="map" name="map">\n}
|
---|
315 | dot_map = `dot -Tismap #{src}`
|
---|
316 | dot_map.each do |area|
|
---|
317 | unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
|
---|
318 | $stderr.puts "Unexpected output from dot:\n#{area}"
|
---|
319 | return nil
|
---|
320 | end
|
---|
321 |
|
---|
322 | xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
|
---|
323 | url, area_name = $5, $6
|
---|
324 |
|
---|
325 | res << %{ <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
|
---|
326 | res << %{ href="#{url}" alt="#{area_name}" />\n}
|
---|
327 | end
|
---|
328 | res << "</map>\n"
|
---|
329 | # map_file = src.sub(/.dot/, '.map')
|
---|
330 | # system("dot -Timap #{src} -o #{map_file}")
|
---|
331 | res << %{<img src="#{dot}" usemap="#map" border="0" alt="#{dot}">}
|
---|
332 | return res
|
---|
333 | end
|
---|
334 | end
|
---|
335 | end
|
---|