ascii_tree 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []