<commit_before>require "open3"
require "ucd/formatter/base"

module UCD
  module Formatter
    class Graphviz < Base

      class Attributes < Hash
        def to_s
          to_a.map { |(a, b)| "#{a}=#{b.inspect}" }.join(" ")
        end
      end

      ACCESS_SYMBOLS = {
        "public"    => "+",
        "protected" => "#",
        "private"   => "-",
      }

      VALID_TYPES = %i[dot xdot ps pdf svg svgz fig png gif jpg jpeg json imap cmapx]

      def initialize(attributes={})
        super

        @graph_attributes = Attributes.new
        @graph_attributes["splines"] = "ortho"
        @graph_attributes["rankdir"] = "BT"

        @edge_attributes = Attributes.new
        @edge_attributes["color"] = "gray50"

        @node_attributes = Attributes.new
        @node_attributes["shape"] = "plain"

        @type = :dot
      end

      attr_reader :graph_attributes
      attr_reader :edge_attributes
      attr_reader :node_attributes

      def type=(value)
        super

        @type = :dot unless VALID_TYPES.include?(@type)
      end

      def format(node)
        dot = super.lines.map(&:rstrip).join("\n")

        generate_from_dot(dot)
      end

      def format_field(node)
        symbol = ACCESS_SYMBOLS[node.access]
        result = "#{symbol} #{node.name}"
        result << " : #{node.type}" if node.type
        result = "<U>#{result}</U>" if node.static

        result
      end

      def format_method(node)
        symbol = ACCESS_SYMBOLS[node.access]
        result = "#{symbol} #{node.name}"
        arguments = node.arguments.map do |argument|
          "#{argument.name}#{" : #{argument.type}" if argument.type}"
        end.join(", ") if node.arguments

        result << "(#{arguments})"
        result << " : #{node.type}" if node.type
        result = "<U>#{result}</U>" if node.static
        result = "<I>#{result}</I>" if node.abstract

        result
      end

      def format_relationship(node)
        arrow = "diamond" if "composition"
        arrow = "odiamond" if "aggregation"
        dir = "back" if %w[aggregation composition].include?(node.type)
        arrow_key = dir == "back" ? "arrowtail" : "arrowhead"

        attributes = Attributes.new

        attributes["style"] = "dashed" if node.type == "dependency"
        attributes["dir"] = dir if dir
        attributes[arrow_key] = arrow
        attributes["headlabel"] = node.from if node.from
        attributes["taillabel"] = node.to if node.to

        graph_parent_name = generate_graph_name(node.parent.name)
        graph_node_name = generate_graph_name(node.name)

        %Q{Class#{graph_parent_name} -> Class#{graph_node_name} [#{attributes}]}
      end

      def format_class_relationship(node)
        attributes = Attributes.new

        attributes["arrowhead"] = "onormal"
        attributes["style"] = "dashed" if node.type == "realizes"

        graph_parent_name = generate_graph_name(node.parent.name)
        graph_node_name = generate_graph_name(node.name)

        %Q{Class#{graph_parent_name} -> Class#{graph_node_name} [#{attributes}]}
      end

      def format_class(node)
        name = "<B>#{node.name}</B>"
        name = "«abstract»<BR/><I>#{name}</I>" if node.modifier == "abstract"

        unless node.fields.empty?
          field_rows  = node.fields.map { |field| %Q{<TR><TD ALIGN="LEFT">#{format_field(field)}</TD></TR>}}
          field_table = <<-HEREDOC.chomp<commit_msg>Formatter: Graphviz to and from positions are based on relationship type<commit_after>require "open3"
require "ucd/formatter/base"

module UCD
  module Formatter
    class Graphviz < Base

      class Attributes < Hash
        def to_s
          to_a.map { |(a, b)| "#{a}=#{b.inspect}" }.join(" ")
        end
      end

      ACCESS_SYMBOLS = {
        "public"    => "+",
        "protected" => "#",
        "private"   => "-",
      }

      VALID_TYPES = %i[dot xdot ps pdf svg svgz fig png gif jpg jpeg json imap cmapx]

      def initialize(attributes={})
        super

        @graph_attributes = Attributes.new
        @graph_attributes["splines"] = "ortho"
        @graph_attributes["rankdir"] = "BT"

        @edge_attributes = Attributes.new
        @edge_attributes["color"] = "gray50"

        @node_attributes = Attributes.new
        @node_attributes["shape"] = "plain"

        @type = :dot
      end

      attr_reader :graph_attributes
      attr_reader :edge_attributes
      attr_reader :node_attributes

      def type=(value)
        super

        @type = :dot unless VALID_TYPES.include?(@type)
      end

      def format(node)
        dot = super.lines.map(&:rstrip).join("\n")

        generate_from_dot(dot)
      end

      def format_field(node)
        symbol = ACCESS_SYMBOLS[node.access]
        result = "#{symbol} #{node.name}"
        result << " : #{node.type}" if node.type
        result = "<U>#{result}</U>" if node.static

        result
      end

      def format_method(node)
        symbol = ACCESS_SYMBOLS[node.access]
        result = "#{symbol} #{node.name}"
        arguments = node.arguments.map do |argument|
          "#{argument.name}#{" : #{argument.type}" if argument.type}"
        end.join(", ") if node.arguments

        result << "(#{arguments})"
        result << " : #{node.type}" if node.type
        result = "<U>#{result}</U>" if node.static
        result = "<I>#{result}</I>" if node.abstract

        result
      end

      def format_relationship(node)
        arrow     = "diamond" if node.type == "composition"
        arrow     = "odiamond" if node.type == "aggregation"
        dir       = "back" if %w[aggregation composition].include?(node.type)
        arrow_key = dir == "back" ? "arrowtail" : "arrowhead"
        from_key  = dir == "back" ? "taillabel" : "headlabel"
        to_key    = dir == "back" ? "headlabel" : "taillabel"

        attributes = Attributes.new

        attributes["style"]   = "dashed" if node.type == "dependency"
        attributes["dir"]     = dir if dir
        attributes[arrow_key] = arrow
        attributes[from_key]  = node.from if node.from
        attributes[to_key]    = node.to if node.to

        graph_parent_name = generate_graph_name(node.parent.name)
        graph_node_name = generate_graph_name(node.name)

        %Q{Class#{graph_parent_name} -> Class#{graph_node_name} [#{attributes}]}
      end

      def format_class_relationship(node)
        attributes = Attributes.new

        attributes["arrowhead"] = "onormal"
        attributes["style"] = "dashed" if node.type == "realizes"

        graph_parent_name = generate_graph_name(node.parent.name)
        graph_node_name = generate_graph_name(node.name)

        %Q{Class#{graph_parent_name} -> Class#{graph_node_name} [#{attributes}]}
      end

      def format_class(node)
        name = "<B>#{node.name}</B>"
        name = "«abstract»<BR/><I>#{name}</I>" if node.modifier == "abstract"

        unless node.fields.empty?
          field_rows  = node.fields.map { |field| %Q{<TR><TD ALIGN="LEFT">#{format_field(field)}</TD></TR>}}
          field_table = <<-HEREDOC.chomp
{'fraction_non_alphanumeric': 0.14043754410726889, 'fraction_numerical': 0.0008468595624558928, 'mean_word_length': 2.3855709507883422, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '335467', 'n_tokens_mistral': 2234, 'n_tokens_neox': 2181, 'n_words': 465}