# rdot.rb
# 
# $Id: rdot.rb 1892 2006-11-17 20:15:23Z luke $
#
# 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 node’s 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


syntax highlighted by Code2HTML, v. 0.9.1