PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/ruby/1.9/rdoc/diagram.rb

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