rubrowser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +35 -0
- data/bin/rubrowser +6 -0
- data/lib/d3.rb +33 -0
- data/lib/data.rb +51 -0
- data/lib/parser/directory.rb +44 -0
- data/lib/parser/factory.rb +16 -0
- data/lib/parser/file.rb +101 -0
- data/lib/rubrowser.rb +3 -0
- data/lib/server.rb +25 -0
- data/lib/tree.rb +75 -0
- data/public/css/application.css +36 -0
- data/public/javascript/application.js +121 -0
- data/public/javascript/d3.js +16231 -0
- data/public/javascript/jquery.js +10074 -0
- data/readme.md +29 -0
- data/views/index.haml +12 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c00a8f96b4234e3126fb912dc5e16b91867db034
|
4
|
+
data.tar.gz: fe569abb2aca1ef762f8dae59d02d0517ae0d268
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 57fbbf57070c4d09d6b24ed74d749f065c9bb3e6aaa713793e8f3566dae3d1187d9fec80ed5e6ddb46ae96800747febc9cb62a9168f7a2ad443f03583edd4893
|
7
|
+
data.tar.gz: efdde91ea7fbeeb5d8de14c3f7ec45d36d3bbc41b90e76c039120aa4f57700bc82e7d4e2881134065cb8753bf2dfd39cf752bfe528fe9524fe71ae5717a8962c
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
tmp/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rubrowser (0.1.0)
|
5
|
+
haml (~> 4.0, >= 4.0.0)
|
6
|
+
parallel (~> 1.9, >= 1.9.0)
|
7
|
+
parser (~> 2.3, >= 2.3.0)
|
8
|
+
sinatra (~> 1.4, >= 1.4.0)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ast (2.3.0)
|
14
|
+
haml (4.0.7)
|
15
|
+
tilt
|
16
|
+
parallel (1.9.0)
|
17
|
+
parser (2.3.1.2)
|
18
|
+
ast (~> 2.2)
|
19
|
+
rack (1.6.4)
|
20
|
+
rack-protection (1.5.3)
|
21
|
+
rack
|
22
|
+
sinatra (1.4.7)
|
23
|
+
rack (~> 1.5)
|
24
|
+
rack-protection (~> 1.4)
|
25
|
+
tilt (>= 1.3, < 3)
|
26
|
+
tilt (2.0.5)
|
27
|
+
|
28
|
+
PLATFORMS
|
29
|
+
ruby
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
rubrowser!
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.12.5
|
data/bin/rubrowser
ADDED
data/lib/d3.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rubrowser
|
2
|
+
class D3
|
3
|
+
def initialize(node)
|
4
|
+
@node = node
|
5
|
+
end
|
6
|
+
|
7
|
+
def constants
|
8
|
+
node_id(node)
|
9
|
+
end
|
10
|
+
|
11
|
+
def occurences
|
12
|
+
node_occurences(node)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :node
|
18
|
+
|
19
|
+
def node_id(node)
|
20
|
+
node.children.map do |n|
|
21
|
+
node_id(n)
|
22
|
+
end.reduce(:+).to_a.push({ name: node.name, id: node.id })
|
23
|
+
end
|
24
|
+
|
25
|
+
def node_occurences(node)
|
26
|
+
occurences = []
|
27
|
+
occurences += node.children.map { |n| node_occurences(n) }.reduce(:+).to_a
|
28
|
+
|
29
|
+
occurences += node.occurences.map { |n| {source: node.id, target: n.id } }.to_a
|
30
|
+
occurences
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/data.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'parser/factory'
|
2
|
+
require 'tree'
|
3
|
+
require 'd3'
|
4
|
+
|
5
|
+
module Rubrowser
|
6
|
+
class Data
|
7
|
+
def self.instance
|
8
|
+
@_instance ||= new
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@files = ARGV
|
13
|
+
@parsed = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def constants
|
17
|
+
@_constants ||= d3.constants.to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
def occurences
|
21
|
+
@_occurences ||= d3.occurences.to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse
|
25
|
+
return if parsed?
|
26
|
+
parsers.each(&:parse)
|
27
|
+
@parsed = true
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :files, :parsed
|
33
|
+
alias parsed? parsed
|
34
|
+
|
35
|
+
def parsers
|
36
|
+
@_parsers ||= files.map do |file|
|
37
|
+
Rubrowser::Parser::Factory.build(file)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def d3
|
42
|
+
@_d3 ||= Rubrowser::D3.new(tree)
|
43
|
+
end
|
44
|
+
|
45
|
+
def tree
|
46
|
+
parse
|
47
|
+
@_tree ||= Rubrowser::Tree.from_parsers(parsers)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'parser/factory'
|
2
|
+
require 'parallel'
|
3
|
+
|
4
|
+
module Rubrowser
|
5
|
+
module Parser
|
6
|
+
class Directory
|
7
|
+
attr_reader :directory
|
8
|
+
|
9
|
+
def initialize(directory)
|
10
|
+
@directory = directory
|
11
|
+
@parsers = []
|
12
|
+
read
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse
|
16
|
+
self.parsers = Parallel.map(parsers){ |parser| parser.parse }
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def definitions
|
21
|
+
parsers.map(&:definitions).map(&:to_a).reduce(:+) || []
|
22
|
+
end
|
23
|
+
|
24
|
+
def occurences
|
25
|
+
parsers.map(&:occurences).map(&:to_a).reduce(:+) || []
|
26
|
+
end
|
27
|
+
|
28
|
+
def count
|
29
|
+
parsers.map(&:count).reduce(:+)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_accessor :parsers
|
35
|
+
|
36
|
+
def read
|
37
|
+
files = Dir.glob(::File.join(directory, '**', '*.rb'))
|
38
|
+
self.parsers = Parallel.map(files) do |file|
|
39
|
+
Factory.build(file)
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'parser/file'
|
2
|
+
require 'parser/directory'
|
3
|
+
|
4
|
+
module Rubrowser
|
5
|
+
module Parser
|
6
|
+
class Factory
|
7
|
+
def self.build(file)
|
8
|
+
if ::File.file?(file)
|
9
|
+
File.new(file)
|
10
|
+
elsif ::File.directory?(file)
|
11
|
+
Directory.new(file)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/parser/file.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'parser/current'
|
2
|
+
|
3
|
+
module Rubrowser
|
4
|
+
module Parser
|
5
|
+
class File
|
6
|
+
FILE_SIZE_LIMIT = 2 * 1024 * 1024
|
7
|
+
|
8
|
+
attr_reader :file, :definitions, :occurences
|
9
|
+
|
10
|
+
def initialize(file)
|
11
|
+
@file = file
|
12
|
+
@definitions = Set.new
|
13
|
+
@occurences = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
if file_valid?(file)
|
18
|
+
::File.open(file) do |f|
|
19
|
+
code = f.read
|
20
|
+
ast = ::Parser::CurrentRuby.parse(code)
|
21
|
+
constants = parse_block(ast)
|
22
|
+
self.definitions = constants[:definitions].uniq
|
23
|
+
self.occurences = constants[:occurences].uniq
|
24
|
+
end
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def file_valid?(file)
|
30
|
+
!::File.symlink?(file) && ::File.file?(file) && ::File.size(file) <= FILE_SIZE_LIMIT
|
31
|
+
end
|
32
|
+
|
33
|
+
def count
|
34
|
+
1
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_writer :definitions, :occurences
|
40
|
+
|
41
|
+
def parse_block(node, parents = [])
|
42
|
+
return { definitions: [], occurences: [] } unless node.is_a?(::Parser::AST::Node)
|
43
|
+
|
44
|
+
case node.type
|
45
|
+
when :module
|
46
|
+
parse_module(node, parents)
|
47
|
+
when :class
|
48
|
+
parse_class(node, parents)
|
49
|
+
when :const
|
50
|
+
parse_const(node, parents)
|
51
|
+
else
|
52
|
+
node
|
53
|
+
.children
|
54
|
+
.map { |n| parse_block(n, parents) }
|
55
|
+
.reduce { |a, e| merge_constants(a, e) } || { definitions: [], occurences: [] }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_module(node, parents = [])
|
60
|
+
name = resolve_const_path(node.children.first, parents)
|
61
|
+
node
|
62
|
+
.children[1..-1]
|
63
|
+
.map { |n| parse_block(n, name) }
|
64
|
+
.reduce { |a, e| merge_constants(a, e) }
|
65
|
+
.tap { |constants| constants[:definitions].unshift(name) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_class(node, parents = [])
|
69
|
+
name = resolve_const_path(node.children.first, parents)
|
70
|
+
node
|
71
|
+
.children[1..-1]
|
72
|
+
.map { |n| parse_block(n, name) }
|
73
|
+
.reduce { |a, e| merge_constants(a, e) }
|
74
|
+
.tap { |constants| constants[:definitions].unshift(name) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_const(node, parents = [])
|
78
|
+
constant = resolve_const_path(node)
|
79
|
+
namespace = parents[0...-1]
|
80
|
+
constants = if namespace.empty? || constant.first.nil?
|
81
|
+
[{ parents => [constant.compact] }]
|
82
|
+
else
|
83
|
+
[{ parents => (namespace.size-1).downto(0).map { |i| namespace[0..i] + constant }.push(constant) }]
|
84
|
+
end
|
85
|
+
{ definitions: [], occurences: constants }
|
86
|
+
end
|
87
|
+
|
88
|
+
def merge_constants(constants1, constants2)
|
89
|
+
{
|
90
|
+
definitions: constants1[:definitions] + constants2[:definitions],
|
91
|
+
occurences: constants1[:occurences] + constants2[:occurences]
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def resolve_const_path(node, parents = [])
|
96
|
+
return parents unless node.is_a?(::Parser::AST::Node) && [:const, :cbase].include?(node.type)
|
97
|
+
resolve_const_path(node.children.first, parents) + [node.children.last]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/rubrowser.rb
ADDED
data/lib/server.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'data'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Rubrowser
|
6
|
+
class Server < Sinatra::Base
|
7
|
+
get '/' do
|
8
|
+
data = Rubrowser::Data.instance
|
9
|
+
haml :index,
|
10
|
+
locals: {
|
11
|
+
constants: data.constants,
|
12
|
+
occurences: data.occurences
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.start
|
17
|
+
Rubrowser::Data.instance.parse
|
18
|
+
Thread.new do
|
19
|
+
run! host: 'localhost',
|
20
|
+
port: 9000,
|
21
|
+
root: File.expand_path('../../', __FILE__)
|
22
|
+
end.join
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/tree.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Rubrowser
|
2
|
+
class Tree
|
3
|
+
attr_reader :name, :children, :occurences, :parent
|
4
|
+
|
5
|
+
def self.from_parsers(parsers)
|
6
|
+
return Tree.new if parsers.empty?
|
7
|
+
|
8
|
+
definitions = parsers.map(&:definitions).reduce(:+).uniq
|
9
|
+
occurences = parsers.map(&:occurences).reduce(:+).uniq
|
10
|
+
Tree.new.tap do |tree|
|
11
|
+
definitions.each { |definition| tree.add_child(definition) }
|
12
|
+
occurences.each { |occurence| tree.add_occurence(*occurence.first) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(name = 'Kernel', parent = nil)
|
17
|
+
@name = name
|
18
|
+
@parent = parent
|
19
|
+
@children = Set.new
|
20
|
+
@occurences = Set.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def root?
|
24
|
+
@parent.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
return name if root? || parent.root?
|
29
|
+
"#{parent.id}::#{name}".to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_child(child_name_path = [])
|
33
|
+
return if child_name_path.empty?
|
34
|
+
child = get_or_create_child(child_name_path[0])
|
35
|
+
children.add(child)
|
36
|
+
child.add_child(child_name_path[1..-1])
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_occurence(user, constants)
|
40
|
+
user_node = find_node(user)
|
41
|
+
occured_node = constants.map { |constant| find_node(constant) }.compact.first
|
42
|
+
return unless user_node && occured_node
|
43
|
+
user_node.occurences << occured_node
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_node(path)
|
47
|
+
return self if path.empty?
|
48
|
+
child = children.find { |c| c.name == path.first }
|
49
|
+
return unless child
|
50
|
+
child.find_node(path[1..-1])
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_or_create_child(child_name)
|
54
|
+
children.find { |child| child_name == child.name } || Tree.new(child_name, self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def eq?(other)
|
58
|
+
other.name == name
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_h
|
62
|
+
{
|
63
|
+
id: id,
|
64
|
+
name: name
|
65
|
+
}.tap do |hash|
|
66
|
+
hash[:children] = children.map(&:to_h) unless children.empty?
|
67
|
+
hash[:occurences] = occurences.map(&:id) unless occurences.empty?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
attr_writer :occurences
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
html, body{
|
2
|
+
width: 100%;
|
3
|
+
height: 100%;
|
4
|
+
padding: 0px;
|
5
|
+
margin: 0px;
|
6
|
+
}
|
7
|
+
.dependency_graph, .dependency_graph svg{
|
8
|
+
width: 100%;
|
9
|
+
height: 100%;
|
10
|
+
}
|
11
|
+
|
12
|
+
line{
|
13
|
+
stroke: #888;
|
14
|
+
}
|
15
|
+
|
16
|
+
.link {
|
17
|
+
fill: none;
|
18
|
+
stroke: #666;
|
19
|
+
stroke-width: 1.5px;
|
20
|
+
}
|
21
|
+
|
22
|
+
circle {
|
23
|
+
fill: #ccc;
|
24
|
+
stroke: #333;
|
25
|
+
stroke-width: 1.5px;
|
26
|
+
}
|
27
|
+
|
28
|
+
.fixed {
|
29
|
+
fill: #f00;
|
30
|
+
}
|
31
|
+
|
32
|
+
text {
|
33
|
+
font: 10px sans-serif;
|
34
|
+
pointer-events: none;
|
35
|
+
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
|
36
|
+
}
|
@@ -0,0 +1,121 @@
|
|
1
|
+
var ParseGraph = function(){
|
2
|
+
var svg = d3.select(".dependency_graph svg");
|
3
|
+
var $svg = $('.dependency_graph svg');
|
4
|
+
|
5
|
+
if(!svg.size()){
|
6
|
+
return false;
|
7
|
+
}
|
8
|
+
|
9
|
+
|
10
|
+
var width = $svg.width(),
|
11
|
+
height = $svg.height(),
|
12
|
+
constants = JSON.parse(svg.attr('data-constants')),
|
13
|
+
occurences = JSON.parse(svg.attr('data-occurences'));
|
14
|
+
var color = d3.scaleOrdinal(d3.schemeCategory20);
|
15
|
+
|
16
|
+
var drag = d3.drag()
|
17
|
+
.on("start", dragstarted)
|
18
|
+
.on("drag", dragged)
|
19
|
+
.on("end", dragended);
|
20
|
+
|
21
|
+
|
22
|
+
svg.call(d3.zoom().on("zoom", function () {
|
23
|
+
container.attr("transform", d3.event.transform);
|
24
|
+
}));
|
25
|
+
|
26
|
+
container = svg.append('g');
|
27
|
+
|
28
|
+
|
29
|
+
var simulation = d3.forceSimulation()
|
30
|
+
.force("link", d3.forceLink().id(function(d) { return d.id; }))
|
31
|
+
.force("charge", d3.forceManyBody())
|
32
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
33
|
+
.force("forceCollide", d3.forceCollide(function(){ return 80; }));
|
34
|
+
|
35
|
+
var link = container.append("g")
|
36
|
+
.attr("class", "links")
|
37
|
+
.selectAll("path")
|
38
|
+
.data(occurences)
|
39
|
+
.enter().append("path")
|
40
|
+
.attr("class", 'link')
|
41
|
+
.attr("marker-end", "url(#occurence)");
|
42
|
+
|
43
|
+
var circle = container.append("g").selectAll("circle")
|
44
|
+
.data(constants)
|
45
|
+
.enter().append("circle")
|
46
|
+
.attr("r", 6)
|
47
|
+
.on("dblclick", dblclick)
|
48
|
+
.call(drag);
|
49
|
+
|
50
|
+
var text = container.append("g").selectAll("text")
|
51
|
+
.data(constants)
|
52
|
+
.enter().append("text")
|
53
|
+
.attr("x", 8)
|
54
|
+
.attr("y", ".31em")
|
55
|
+
.text(function(d) { return d.id; });
|
56
|
+
|
57
|
+
container.append("defs").selectAll("marker")
|
58
|
+
.data(['occurence'])
|
59
|
+
.enter().append("marker")
|
60
|
+
.attr("id", function(d) { return d; })
|
61
|
+
.attr("viewBox", "0 -5 10 10")
|
62
|
+
.attr("refX", 15)
|
63
|
+
.attr("refY", -1.5)
|
64
|
+
.attr("markerWidth", 6)
|
65
|
+
.attr("markerHeight", 6)
|
66
|
+
.attr("orient", "auto")
|
67
|
+
.append("path")
|
68
|
+
.attr("d", "M0,-5L10,0L0,5");
|
69
|
+
|
70
|
+
simulation
|
71
|
+
.nodes(constants)
|
72
|
+
.on("tick", ticked);
|
73
|
+
|
74
|
+
simulation.force("link")
|
75
|
+
.links(occurences);
|
76
|
+
|
77
|
+
function ticked() {
|
78
|
+
link.attr("d", linkArc);
|
79
|
+
circle.attr("transform", transform);
|
80
|
+
text.attr("transform", transform);
|
81
|
+
}
|
82
|
+
|
83
|
+
function linkArc(d) {
|
84
|
+
var dx = d.target.x - d.source.x,
|
85
|
+
dy = d.target.y - d.source.y,
|
86
|
+
dr = 0; // Math.sqrt(dx * dx + dy * dy);
|
87
|
+
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
|
88
|
+
}
|
89
|
+
|
90
|
+
function dragstarted(d) {
|
91
|
+
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
|
92
|
+
d3.select(this).classed("fixed", true );
|
93
|
+
d.fx = d.x;
|
94
|
+
d.fy = d.y;
|
95
|
+
}
|
96
|
+
|
97
|
+
function dragged(d) {
|
98
|
+
d.fx = d3.event.x;
|
99
|
+
d.fy = d3.event.y;
|
100
|
+
}
|
101
|
+
|
102
|
+
function dragended(d) {
|
103
|
+
if (!d3.event.active) simulation.alphaTarget(0);
|
104
|
+
}
|
105
|
+
|
106
|
+
function dblclick(d) {
|
107
|
+
var node = d3.select(this);
|
108
|
+
node.classed("fixed", false);
|
109
|
+
d.fx = null;
|
110
|
+
d.fy = null;
|
111
|
+
}
|
112
|
+
|
113
|
+
function transform(d) {
|
114
|
+
return "translate(" + d.x + "," + d.y + ")";
|
115
|
+
}
|
116
|
+
|
117
|
+
return true;
|
118
|
+
|
119
|
+
};
|
120
|
+
|
121
|
+
$(function(){ParseGraph();});
|