ascii_tree 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +75 -0
- data/lib/ascii_tree.rb +15 -0
- data/lib/ascii_tree/base.rb +10 -0
- data/lib/ascii_tree/comment_stripper.rb +9 -0
- data/lib/ascii_tree/coordinate.rb +13 -0
- data/lib/ascii_tree/edge.rb +19 -0
- data/lib/ascii_tree/edge_parser.rb +42 -0
- data/lib/ascii_tree/node.rb +21 -0
- data/lib/ascii_tree/node_builder.rb +50 -0
- data/lib/ascii_tree/parenthesis_toggle.rb +38 -0
- data/lib/ascii_tree/relationship.rb +19 -0
- data/lib/ascii_tree/relationships_builder.rb +68 -0
- data/lib/ascii_tree/scanner.rb +35 -0
- data/lib/ascii_tree/word.rb +35 -0
- data/lib/ascii_tree/word_parser.rb +90 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: edd4a091f0389f3142009d7f90e2f92e28bda572
|
4
|
+
data.tar.gz: afb677ecd9caa5908a140a1a80d359d3c92bd790
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3955dee1d7ae9b8478a464b933520b0a6b665a3c66c45ef3cadd1295c16e09a65108ba9e2bcaf88433fac05df014e99885527bbcfe4c6510929f78e5e1d6f87
|
7
|
+
data.tar.gz: 2570e3503e783e753716ee586b476367963250e93a75e32ac2196807ed5da5eb03b49958ea147eca13ce654401b03149aaec2a39d68e98efef9fff9813139929
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
## Ascii Tree
|
2
|
+
|
3
|
+
Parses a usable tree from ASCII art.
|
4
|
+
|
5
|
+
Ascii Tree turns something humans understand into something computers
|
6
|
+
understand. It is an expressive and efficient way to define trees.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
root = AsciiTree.parse('
|
12
|
+
|
13
|
+
# Christmas themed tree:
|
14
|
+
|
15
|
+
chestnuts
|
16
|
+
/ | \
|
17
|
+
roasting on an
|
18
|
+
/ \ \
|
19
|
+
open fire jack
|
20
|
+
/ \
|
21
|
+
frost nipping
|
22
|
+
/ | \
|
23
|
+
on your nose # Ouch!
|
24
|
+
|
25
|
+
')
|
26
|
+
|
27
|
+
root.id
|
28
|
+
#=> "chestnuts"
|
29
|
+
|
30
|
+
root.parent
|
31
|
+
#=> nil
|
32
|
+
|
33
|
+
root.children
|
34
|
+
#=> [#<AsciiTree::Node @id="roasting">, ...]
|
35
|
+
```
|
36
|
+
|
37
|
+
## Multiple words
|
38
|
+
|
39
|
+
Use parenthesis to group words into a single node:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
root = AsciiTree.parse('
|
43
|
+
|
44
|
+
(this is a single node)
|
45
|
+
/ | | | \
|
46
|
+
(so is this) but these are separate
|
47
|
+
|
48
|
+
')
|
49
|
+
|
50
|
+
root.id
|
51
|
+
#=> "this is a single node"
|
52
|
+
```
|
53
|
+
|
54
|
+
## Values
|
55
|
+
|
56
|
+
You can set arbitrary values on nodes:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
root = AsciiTree.parse('
|
60
|
+
|
61
|
+
root{123}
|
62
|
+
/ \
|
63
|
+
a{"foo"} b
|
64
|
+
/ \
|
65
|
+
c d{[1,2,3].reverse}
|
66
|
+
|
67
|
+
')
|
68
|
+
|
69
|
+
root.value
|
70
|
+
#=> 123
|
71
|
+
```
|
72
|
+
|
73
|
+
## Contribution
|
74
|
+
|
75
|
+
If you'd like to contribute, please open an issue or submit a pull request.
|
data/lib/ascii_tree.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "ascii_tree/base"
|
2
|
+
|
3
|
+
require "ascii_tree/coordinate"
|
4
|
+
require "ascii_tree/parenthesis_toggle"
|
5
|
+
require "ascii_tree/word"
|
6
|
+
require "ascii_tree/edge"
|
7
|
+
require "ascii_tree/relationship"
|
8
|
+
require "ascii_tree/node"
|
9
|
+
|
10
|
+
require "ascii_tree/comment_stripper"
|
11
|
+
require "ascii_tree/edge_parser"
|
12
|
+
require "ascii_tree/word_parser"
|
13
|
+
require "ascii_tree/scanner"
|
14
|
+
require "ascii_tree/relationships_builder"
|
15
|
+
require "ascii_tree/node_builder"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
def self.parse(string)
|
3
|
+
string = CommentStripper.strip(string)
|
4
|
+
words = WordParser.parse(string)
|
5
|
+
edges = EdgeParser.parse(string)
|
6
|
+
relationships = RelationshipsBuilder.build(words, edges)
|
7
|
+
|
8
|
+
NodeBuilder.build(relationships)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class AsciiTree::Edge
|
2
|
+
|
3
|
+
attr_reader :character, :coordinate, :parent_coordinate, :child_coordinate
|
4
|
+
|
5
|
+
def initialize(character:, coordinate:, parent_coordinate:, child_coordinate:)
|
6
|
+
@character = character
|
7
|
+
@coordinate = coordinate
|
8
|
+
@parent_coordinate = parent_coordinate
|
9
|
+
@child_coordinate = child_coordinate
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
character == other.character &&
|
14
|
+
coordinate == other.coordinate &&
|
15
|
+
parent_coordinate == other.parent_coordinate &&
|
16
|
+
child_coordinate == other.child_coordinate
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
module EdgeParser
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def parse(string)
|
6
|
+
edge_chars_with_coordinates(string).map do |char, coordinate|
|
7
|
+
offsets = edge_offsets[char]
|
8
|
+
|
9
|
+
Edge.new(
|
10
|
+
character: char,
|
11
|
+
coordinate: coordinate,
|
12
|
+
parent_coordinate: Coordinate.new(
|
13
|
+
x: coordinate.x + offsets[:parent][:x],
|
14
|
+
y: coordinate.y + offsets[:parent][:y]
|
15
|
+
),
|
16
|
+
child_coordinate: Coordinate.new(
|
17
|
+
x: coordinate.x + offsets[:child][:x],
|
18
|
+
y: coordinate.y + offsets[:child][:y]
|
19
|
+
)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def edge_offsets
|
27
|
+
{
|
28
|
+
"/" => { parent: { x: +1, y: -1 }, child: { x: -1, y: +1 } },
|
29
|
+
"|" => { parent: { x: 0, y: -1 }, child: { x: 0, y: +1 } },
|
30
|
+
"\\" => { parent: { x: -1, y: -1 }, child: { x: +1, y: +1 } }
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def edge_chars_with_coordinates(string)
|
35
|
+
Scanner.scan(string).select do |char, _|
|
36
|
+
edge_offsets.keys.include?(char)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
class Node
|
3
|
+
|
4
|
+
attr_reader :id, :value, :parent, :children
|
5
|
+
|
6
|
+
def initialize(id:, value:, parent:, children:)
|
7
|
+
@id = id
|
8
|
+
@value = value
|
9
|
+
@parent = parent
|
10
|
+
@children = children
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
id == other.id &&
|
15
|
+
value == other.value &&
|
16
|
+
parent == other.parent &&
|
17
|
+
children == other.children
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
class NodeBuilder
|
3
|
+
|
4
|
+
def self.build(*args)
|
5
|
+
new(*args).build
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(relationships)
|
9
|
+
@relationships = relationships
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
build_for(root_word, nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :relationships
|
19
|
+
|
20
|
+
def root_word
|
21
|
+
relationships.first.parent_word
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_for(word, parent)
|
25
|
+
node = Node.new(
|
26
|
+
id: word.id,
|
27
|
+
value: word.value,
|
28
|
+
parent: parent,
|
29
|
+
children: []
|
30
|
+
)
|
31
|
+
|
32
|
+
children_for(word, node).each do |child|
|
33
|
+
node.children << child
|
34
|
+
end
|
35
|
+
|
36
|
+
node
|
37
|
+
end
|
38
|
+
|
39
|
+
def children_for(word, parent)
|
40
|
+
child_relationships = relationships.select do |r|
|
41
|
+
r.parent_word == word
|
42
|
+
end
|
43
|
+
|
44
|
+
child_relationships.map do |r|
|
45
|
+
build_for(r.child_word, parent)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
class ParenthesisToggle
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@on, @count = false, 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def read(char)
|
9
|
+
if char == "("
|
10
|
+
@on = true
|
11
|
+
elsif char == ")"
|
12
|
+
@on = false
|
13
|
+
end
|
14
|
+
|
15
|
+
if char == "("
|
16
|
+
@count += 1
|
17
|
+
elsif char == ")"
|
18
|
+
@count -= 1
|
19
|
+
@count = 0 if @count < 0
|
20
|
+
end
|
21
|
+
|
22
|
+
@on = @count > 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def on?
|
26
|
+
@on
|
27
|
+
end
|
28
|
+
|
29
|
+
def off?
|
30
|
+
!@on
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :on
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
class Relationship
|
3
|
+
|
4
|
+
attr_reader :parent_word, :edge, :child_word
|
5
|
+
|
6
|
+
def initialize(parent_word:, edge:, child_word:)
|
7
|
+
@parent_word = parent_word
|
8
|
+
@edge = edge
|
9
|
+
@child_word = child_word
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
parent_word == other.parent_word &&
|
14
|
+
edge == other.edge &&
|
15
|
+
child_word == other.child_word
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
module RelationshipsBuilder
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def build(words, edges)
|
6
|
+
relationships = edges.map do |edge|
|
7
|
+
parent = words.detect { |w| w.include?(edge.parent_coordinate) }
|
8
|
+
child = words.detect { |w| w.include?(edge.child_coordinate) }
|
9
|
+
|
10
|
+
validate_presence(parent, child, edge)
|
11
|
+
|
12
|
+
Relationship.new(parent_word: parent, edge: edge, child_word: child)
|
13
|
+
end
|
14
|
+
|
15
|
+
validate_one_parent(relationships)
|
16
|
+
relationships
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_presence(parent, child, edge)
|
22
|
+
if parent.nil? && child.nil?
|
23
|
+
error = "No parent or child"
|
24
|
+
elsif parent.nil?
|
25
|
+
error = "No parent for child '#{child.id}'"
|
26
|
+
elsif child.nil?
|
27
|
+
error = "No child for parent '#{parent.id}'"
|
28
|
+
end
|
29
|
+
|
30
|
+
if error
|
31
|
+
c = edge.coordinate
|
32
|
+
error += " for edge '#{edge.character}' at line #{c.y}, column #{c.x}"
|
33
|
+
raise ::AsciiTree::RelationshipError, error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_one_parent(relationships)
|
38
|
+
multiple_parents = relationships.select do |relationship|
|
39
|
+
count = relationships.count do |r|
|
40
|
+
r.child_word == relationship.child_word
|
41
|
+
end
|
42
|
+
|
43
|
+
count > 1
|
44
|
+
end
|
45
|
+
|
46
|
+
groups = multiple_parents.group_by { |r| r.child_word }
|
47
|
+
|
48
|
+
maps = groups.map do |child_word, relationships|
|
49
|
+
parent_words = relationships.map(&:parent_word)
|
50
|
+
[child_word.id, parent_words.map(&:id)]
|
51
|
+
end
|
52
|
+
|
53
|
+
if maps.any?
|
54
|
+
error = ""
|
55
|
+
|
56
|
+
maps.each do |child_word, parent_words|
|
57
|
+
parents = parent_words.join("' and '")
|
58
|
+
error += "'#{child_word}' has more than one parent: '#{parents}'.\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
raise ::AsciiTree::RelationshipError, error if error
|
63
|
+
end
|
64
|
+
|
65
|
+
class ::AsciiTree::RelationshipError < StandardError; end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
module Scanner
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def scan(string)
|
6
|
+
Enumerator.new do |yielder|
|
7
|
+
indexed_lines(string).each do |line, y|
|
8
|
+
indexed_chars(line).each do |char, x|
|
9
|
+
yielder.yield [char, Coordinate.new(x: x, y: y)]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def indexed_lines(string)
|
18
|
+
lines(string).each.with_index.to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
def lines(string)
|
22
|
+
string.split("\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
def indexed_chars(line)
|
26
|
+
chars(line).each_with_index.to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
def chars(line)
|
30
|
+
line.split("")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
class Word
|
3
|
+
|
4
|
+
attr_reader :id, :value, :start_coordinate, :end_coordinate
|
5
|
+
|
6
|
+
def initialize(id:, value:, start_coordinate:, end_coordinate:)
|
7
|
+
@id = id
|
8
|
+
@value = value
|
9
|
+
@start_coordinate = start_coordinate
|
10
|
+
@end_coordinate = end_coordinate
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
id == other.id &&
|
15
|
+
value == other.value &&
|
16
|
+
start_coordinate == other.start_coordinate &&
|
17
|
+
end_coordinate == other.end_coordinate
|
18
|
+
end
|
19
|
+
|
20
|
+
def include?(coordinate)
|
21
|
+
same_line?(coordinate.y) && inside?(coordinate.x)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def same_line?(y)
|
27
|
+
y == start_coordinate.y && y == end_coordinate.y
|
28
|
+
end
|
29
|
+
|
30
|
+
def inside?(x)
|
31
|
+
(start_coordinate.x..end_coordinate.x).include?(x)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module AsciiTree
|
2
|
+
module WordParser
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def parse(string)
|
6
|
+
chars = word_chars_with_coordinates(string)
|
7
|
+
group_contiguous(chars).map do |word_with_coords|
|
8
|
+
id, value = id_value(word_with_coords)
|
9
|
+
|
10
|
+
Word.new(
|
11
|
+
id: id,
|
12
|
+
value: value,
|
13
|
+
start_coordinate: word_with_coords.first.last,
|
14
|
+
end_coordinate: word_with_coords.last.last
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def word_chars_with_coordinates(string)
|
22
|
+
toggle = ParenthesisToggle.new
|
23
|
+
|
24
|
+
Scanner.scan(string).reject do |char, coordinate|
|
25
|
+
toggle.read(char)
|
26
|
+
toggle.off? && (edge?(char) || whitespace?(char))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def edge?(char)
|
31
|
+
["/", "|", "\\"].include?(char)
|
32
|
+
end
|
33
|
+
|
34
|
+
def whitespace?(char)
|
35
|
+
char.match(/\s/)
|
36
|
+
end
|
37
|
+
|
38
|
+
def id_value(word_with_coords)
|
39
|
+
chars = word_with_coords.map(&:first)
|
40
|
+
chars = remove_parentheses(chars)
|
41
|
+
|
42
|
+
word = chars.join
|
43
|
+
|
44
|
+
if word.end_with?("}")
|
45
|
+
id, tail = word.split("{", 2)
|
46
|
+
expression = tail[0..-2]
|
47
|
+
value = eval(expression)
|
48
|
+
[id, value]
|
49
|
+
else
|
50
|
+
[word, nil]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_parentheses(chars)
|
55
|
+
if chars.first == "(" && chars.last == ")"
|
56
|
+
chars[1..-2]
|
57
|
+
else
|
58
|
+
chars
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def group_contiguous(chars)
|
63
|
+
chars.inject([]) do |array, (char, coord)|
|
64
|
+
prev = previous_coordinate(array)
|
65
|
+
|
66
|
+
if contigous?(prev, coord)
|
67
|
+
array.last << [char, coord]
|
68
|
+
else
|
69
|
+
array << [[char, coord]]
|
70
|
+
end
|
71
|
+
|
72
|
+
array
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def previous_coordinate(array)
|
77
|
+
previous_array = array.last
|
78
|
+
previous_tuple = previous_array.last if previous_array
|
79
|
+
previous_tuple.last if previous_tuple
|
80
|
+
end
|
81
|
+
|
82
|
+
def contigous?(previous_coordinate, coordinate)
|
83
|
+
previous_coordinate &&
|
84
|
+
previous_coordinate.y == coordinate.y &&
|
85
|
+
previous_coordinate.x == coordinate.x - 1
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ascii_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Patuzzo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
description: Parses a usable tree from ASCII art.
|
28
|
+
email: chris@patuzzo.co.uk
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- lib/ascii_tree.rb
|
35
|
+
- lib/ascii_tree/base.rb
|
36
|
+
- lib/ascii_tree/comment_stripper.rb
|
37
|
+
- lib/ascii_tree/coordinate.rb
|
38
|
+
- lib/ascii_tree/edge.rb
|
39
|
+
- lib/ascii_tree/edge_parser.rb
|
40
|
+
- lib/ascii_tree/node.rb
|
41
|
+
- lib/ascii_tree/node_builder.rb
|
42
|
+
- lib/ascii_tree/parenthesis_toggle.rb
|
43
|
+
- lib/ascii_tree/relationship.rb
|
44
|
+
- lib/ascii_tree/relationships_builder.rb
|
45
|
+
- lib/ascii_tree/scanner.rb
|
46
|
+
- lib/ascii_tree/word.rb
|
47
|
+
- lib/ascii_tree/word_parser.rb
|
48
|
+
homepage: https://github.com/tuzz/ascii_tree
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.2.2
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Ascii Tree
|
72
|
+
test_files: []
|