ascii_tree 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ module AsciiTree
2
+ module CommentStripper
3
+
4
+ def self.strip(string)
5
+ string.gsub(/#[^\n]*/, "").gsub(/\s+\n/, "\n")
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ class AsciiTree::Coordinate
2
+
3
+ attr_reader :x, :y
4
+
5
+ def initialize(x:, y:)
6
+ @x, @y = x, y
7
+ end
8
+
9
+ def ==(other)
10
+ x == other.x && y == other.y
11
+ end
12
+
13
+ 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: []