richtext 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 16ef90a0b51b4d329a49909ba65958daf6c47de2
4
- data.tar.gz: 5deebc04e53b025b8da28ddfc706bf50631ab990
3
+ metadata.gz: af2b68e549093d993d27e2a6d32f7f1f5388636e
4
+ data.tar.gz: 6c92d9931c6252d9e56bf7594f09e969e903fde2
5
5
  SHA512:
6
- metadata.gz: 5de128f8a275745aafd51273de5eb9e75d2fb32973e414afae0b0bddb9e7ccba96cc743611100de61ce98c0b502e6b38e2f48b83e2de9e5cde38745a361627e5
7
- data.tar.gz: 52118715ab8bee14685d83db86bc1f4d552680b80f93e9776b3918ca678c418e5da424ebc62e74344a159da63c6c85cffae09ae79380620394bffa6ec31c66a2
6
+ metadata.gz: 9181421572a02691f179593a544414df3e25b6aae59c134915962e83afe9aa006ffa67dc05925650938101e8c7825feb4cf57fbc443096cdc5eab295af9d1dd3
7
+ data.tar.gz: 513bc38940ae2c975aa6656e6c0ae9edbd9a92dcfb58efee10383e5288d01fdacc2ecd4d41a92491a745e49d0535f281a5cdea6db53c77e268bffb512202a892
data/README.md CHANGED
@@ -1,4 +1,9 @@
1
- # Richtext
1
+ # RichText
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/richtext.png)](http://badge.fury.io/rb/richtext)
4
+ [![Build Status](https://travis-ci.org/seblindberg/ruby-richtext.svg?branch=master)](https://travis-ci.org/seblindberg/ruby-richtext)
5
+ [![Coverage Status](https://coveralls.io/repos/github/seblindberg/ruby-richtext/badge.svg?branch=master)](https://coveralls.io/github/seblindberg/ruby-richtext?branch=master)
6
+ [![Inline docs](http://inch-ci.org/github/seblindberg/ruby-richtext.svg?branch=master)](http://inch-ci.org/github/seblindberg/ruby-richtext)
2
7
 
3
8
  This gem is intended to simplify the handling of formatted text. Out of the box there is no support for any actual format, but that is intentional. The RichText::Document class is primarily ment to be subclassed and extended, and only includes functionality that is (potentially) useful to any format.
4
9
 
@@ -69,7 +74,7 @@ class MyFormat < RichText::Document
69
74
  # each word is represented by its own entry. Entries are
70
75
  # given a random visibility attribute.
71
76
  string.split(' ').each do |word|
72
- base.create_child word, visible: (word.length > 6)
77
+ base.append_child word, visible: (word.length > 6)
73
78
  end
74
79
  end
75
80
 
data/lib/richtext.rb CHANGED
@@ -1,15 +1,16 @@
1
+ require 'rooted_tree'
2
+
1
3
  require 'richtext/version'
2
- require 'richtext/node'
4
+ require 'richtext/styleable'
3
5
  require 'richtext/document/entry'
4
6
  require 'richtext/document'
5
7
 
6
8
  module RichText
7
9
  end
8
10
 
9
- # RichText
10
- #
11
11
  # Convenience method for creating RichText objects. Calling RichText(obj) is
12
12
  # equivalent to RichText::Document.new(obj).
13
+
13
14
  def RichText(string)
14
15
  RichText::Document.new string
15
16
  end
@@ -7,68 +7,69 @@ module RichText
7
7
  attr_reader :raw
8
8
  protected :raw
9
9
 
10
- # Initialize
11
- #
12
10
  # Create a new RichText Document, either from a string or from an existing
13
- # ducument. That feature is particularly useful when converting between
11
+ # document. That feature is particularly useful when converting between
14
12
  # formats.
15
13
  #
16
14
  # When given a string or a RichText Document of the same class no parsing is
17
15
  # performed. Only when given a document of a different subclass will the
18
16
  # parser need to be run parsed. Note that the document(s) may already be in
19
- # parsed form, in which case no further parsing is performed. See #base for
17
+ # parsed form, in which case no further parsing is performed. See #root for
20
18
  # more details.
19
+
21
20
  def initialize(arg = '')
22
- @base, @raw =
23
- if arg.class == self.class
24
- arg.parsed? ? [arg.base, nil] : [nil, arg.raw]
21
+ @root, @raw =
22
+ if arg.instance_of? self.class
23
+ arg.parsed? ? [arg.root, nil] : [nil, arg.raw]
25
24
  elsif arg.is_a? Document
26
- # For any other RichText object we take the base node
27
- [arg.base, nil]
25
+ # For any other RichText object we take the root node
26
+ [arg.root, nil]
28
27
  elsif arg.is_a? Entry
29
28
  # Also accept an Entry which will be used as the
30
- # document base
31
- [arg, nil]
29
+ # document root
30
+ [arg.root, nil]
32
31
  else
33
32
  [nil, arg.to_s]
34
33
  end
35
34
  end
36
35
 
37
- # To String
38
- #
39
- # Use the static implementation of .render to convert the document back into
40
- # a string. If the document was never parsed (and is unchanged) the
36
+ # Uses the static implementation of .render to convert the document back
37
+ # into a string. If the document was never parsed (and is unchanged) the
41
38
  # origninal string is just returned.
42
39
  #
43
40
  # If a block is given it will be used in place of .render to format the node
44
41
  # tree.
42
+ #
43
+ # Returns a string formatted according to the rules outlined by the Document
44
+ # format.
45
+
45
46
  def to_s(&block)
46
47
  if block_given?
47
- base.to_s(&block)
48
+ root.to_s(&block)
48
49
  elsif parsed? || should_parse?
49
- self.class.render base
50
+ self.class.render root
50
51
  else
51
52
  @raw
52
53
  end
53
54
  end
54
55
 
55
- # To Plain
56
+ # Uses Entry#to_s to reduce the node structure down to a string.
56
57
  #
57
58
  # Returns the strings from all of the leaf nodes without any formatting
58
59
  # applied.
60
+
59
61
  def to_plain
60
- base.to_s
62
+ root.to_s
61
63
  end
62
64
 
63
- # Add (+)
64
- #
65
65
  # Add another Document to this one. If the two are of (exactly) the same
66
66
  # class and neither one has been parsed, the two raw strings will be
67
- # concatenated. If the other is a Document the two base nodes will be merged
67
+ # concatenated. If the other is a Document the two root nodes will be merged
68
68
  # and the new root added to a new Document.
69
69
  #
70
70
  # Lastly, if other is a string it will first be wraped in a new Document and
71
71
  # then added to this one.
72
+
72
73
  def +(other)
73
74
  # If the other object is of the same class, and neither
74
75
  # one of the texts have been parsed, we can concatenate
@@ -78,7 +79,7 @@ module RichText
78
79
  end
79
80
 
80
81
  # Same root class
81
- return self.class.new(base + other.base) if other.is_a? Document
82
+ return self.class.new(root + other.root) if other.is_a? Document
82
83
 
83
84
  unless other.respond_to? :to_s
84
85
  raise TypeError,
@@ -91,33 +92,39 @@ module RichText
91
92
  self + self.class.new(other)
92
93
  end
93
94
 
94
- # Append
95
+ # Append a string to the document. The string will not be parsed but
96
+ # inserted into a new entry, directly under the document root.
95
97
  #
98
+ # string - the string that will be wrapped in an Entry object.
99
+ # attributes - a hash of attributes that will be applied to the Entry.
96
100
  #
101
+ # Returns the newly created child.
102
+
97
103
  def append(string, **attributes)
98
- base.create_child string, **attributes
104
+ root.append_child string, **attributes
105
+ root.child(-1)
99
106
  end
100
107
 
101
- # Base
108
+ # Getter for the root node. If the raw input has not yet been
109
+ # parsed that will happen first, before the root node is returned.
102
110
  #
103
- # Getter for the base node. If the raw input has not yet been
104
- # parsed that will happen first, before the base node is returned.
105
- def base
106
- unless @base
107
- @base = Entry.new
108
- self.class.parse @base, @raw
111
+ # Returns the root Entry.
112
+
113
+ def root
114
+ unless @root
115
+ @root = Entry.new
116
+ self.class.parse @root, @raw
109
117
  @raw = nil
110
118
  end
111
119
 
112
- @base
120
+ @root
113
121
  end
114
122
 
115
- alias root base
123
+ alias base root
116
124
 
117
- # Parsed?
118
- #
119
125
  # Returns true if the raw input has been parsed and the internal
120
126
  # representation is now a tree of nodes.
127
+
121
128
  def parsed?
122
129
  @raw.nil?
123
130
  end
@@ -126,39 +133,35 @@ module RichText
126
133
  false
127
134
  end
128
135
 
129
- # Each Node
130
- #
131
136
  # Iterate over all Entry nodes in the document tree.
137
+
132
138
  def each_node(&block)
133
- base.each(&block)
139
+ root.each(&block)
134
140
  end
135
141
 
136
142
  alias each_entry each_node
137
143
 
138
- # Parse
139
- #
140
144
  # Document type specific method for parsing a string and turning it into a
141
145
  # tree of entry nodes. This method is intended to be overridden when the
142
146
  # Document is subclassed. The default implementation just creates a top
143
147
  # level Entry containing the given string.
144
- def self.parse(base, string)
145
- base[:text] = string
148
+
149
+ def self.parse(root, string)
150
+ root.text = string
146
151
  end
147
152
 
148
- # Render
149
- #
150
153
  # Document type specific method for rendering a tree of entry nodes. This
151
154
  # method is intended to be overridden when the Document is subclassed. The
152
155
  # default implementation just concatenates the text entries into.
153
- def self.render(base)
154
- base.to_s
156
+
157
+ def self.render(root)
158
+ root.to_s
155
159
  end
156
160
 
157
- # From
158
- #
159
161
  # Convenience method for instansiating one RichText object from another. The
160
162
  # methods only purpose is to make that intent more clear, and to make the
161
163
  # creation from another RichText object explicit.
164
+
162
165
  def self.from(doc)
163
166
  unless doc.is_a? Document
164
167
  raise TypeError,
@@ -2,8 +2,7 @@
2
2
 
3
3
  module RichText
4
4
  class Document
5
- # Entry
6
- #
5
+
7
6
  # The Entry class extends the basic Node class and adds methods that make
8
7
  # handling text a little nicer. Essentially the :text attribute is given
9
8
  # special status by allowing it to a) be set during initialization, b) only
@@ -13,129 +12,170 @@ module RichText
13
12
  # Some attributes are also supported explicitly by the inclusion of special
14
13
  # accesser methods. The attributes are are bold, italic, underline, color
15
14
  # and font.
16
- #
17
- class Entry < Node
18
- # Initialize
19
- #
15
+
16
+ class Entry < RootedTree::Node
17
+ include Styleable
18
+
19
+ attr_reader :attributes
20
+ protected :prepend_child, :prepend_sibling, :value, :value=
21
+
20
22
  # Extend the default Node initializer by also accepting a string. It will,
21
23
  # if given, be stored as a text attribute.
22
- def initialize(text = nil, **attributes)
23
- attributes[:text] = text if text
24
- super attributes
25
- end
26
24
 
27
- # Text
28
- #
29
- # Read the text of the node. This will return nil unless the node is a
30
- # leaf node. Note that nodes that are not leafs can have the text entry,
31
- # but it is discouraged by dissalowing access using this method.
32
- def text
33
- self[:text] || '' if leaf?
25
+ def initialize(text = nil, **attributes)
26
+ @attributes = attributes
27
+ super text
34
28
  end
35
29
 
36
- # Append
30
+ # Freeze the attributes hash, as well as the node structure.
37
31
  #
38
- # Since the text attribute is treated differently, and only leaf nodes can
39
- # expose it, it must be pushed to a new child if a) this node was a leaf
40
- # prior to this method call and b) its text attribute is not empty.
41
- def <<(child)
42
- if leaf?
43
- # Remove the text entry from the node and put it in a new leaf node
44
- # among the children, unless it is empty
45
- if (t = @attributes.delete :text)
46
- create_child(t) unless t.empty?
47
- end
48
- end
32
+ # Returns self.
49
33
 
34
+ def freeze
35
+ @attributes.freeze
50
36
  super
51
37
  end
52
38
 
53
- # Create Child
39
+ # Accessor for single attributes.
54
40
  #
55
- # Create and append a new child, initialized with the given text and
56
- # attributes.
57
- def create_child(text = nil, **attributes)
58
- attributes = attributes.merge(text: text) if text
59
- super(attributes)
41
+ # key - the attribute key
42
+ #
43
+ # Returns the attribute value if it is set and nil otherwise.
44
+
45
+ def [](key)
46
+ attributes[key]
60
47
  end
61
48
 
62
- # Optimize!
49
+ # Write a single attribute.
63
50
  #
64
- # See RichText::Node#optimize! for a description of the fundemental
65
- # behavior. Entries differ from regular Nodes in that leaf children with
66
- # no text in them will be removed.
67
- def optimize!
68
- super { |child| !child.leaf? || !child.text.empty? }
51
+ # key - the attribute key
52
+ # v - the new value
53
+
54
+ def []=(key, v)
55
+ attributes[key] = v
69
56
  end
70
57
 
71
- # To String
58
+ # Read the text of the node.
72
59
  #
73
- # Combine the text from all the leaf nodes in the tree, from left to
74
- # right. If a block is given the node, along with its text will be passed
75
- # as arguments. The block will be called recursivly, starting at the leaf
76
- # nodes and propagating up until the entire tree has been "rendered" in
77
- # this way.
78
- def to_s(&block)
79
- string =
80
- if leaf?
81
- text
82
- else
83
- @children.reduce('') { |a, e| a + e.to_s(&block) }
84
- end
60
+ # Returns the string stored in the node, if it is a leaf. Otherwise nil.
85
61
 
86
- block_given? ? yield(self, string) : string
62
+ def text
63
+ value || '' if leaf?
87
64
  end
88
65
 
89
- # Supported Text Attributes
66
+ # Write the text of the node. The method will raise a RuntimeException if
67
+ # the node is not a leaf.
90
68
 
91
- # Bold
92
- #
93
- def bold?
94
- self[:bold]
69
+ def text=(new_text)
70
+ raise 'Only leafs can have a text entry' unless leaf?
71
+ self.value = new_text
95
72
  end
96
73
 
97
- def bold=(b)
98
- self[:bold] = b ? true : false
74
+ # Create and append a new child, initialized with the given text and
75
+ # attributes.
76
+ #
77
+ # child_text - the text of the child or an Entry object.
78
+ # attributes - a hash of attributes to apply to the child if child_text is
79
+ # not an Entry object.
80
+ #
81
+ # Returns self to allow chaining.
82
+
83
+ def append_child(child_text = nil, **attributes)
84
+ if leaf? && !text.empty?
85
+ super self.class.new(value)
86
+ end
87
+
88
+ if child_text.is_a? self.class
89
+ super child_text
90
+ else
91
+ super self.class.new(child_text, attributes)
92
+ end
99
93
  end
100
94
 
101
- # Italic
95
+ alias << append_child
96
+
97
+ # Go through each child and merge any node that a) is not a lead node and
98
+ # b) only has one child, with its child. The attributes of the child will
99
+ # override those of the parent.
102
100
  #
103
- def italic?
104
- self[:italic]
105
- end
101
+ # Returns self.
102
+
103
+ def optimize!(&block)
104
+ # If the node is a leaf it cannot be optimized further
105
+ return self if leaf?
106
106
 
107
- def italic=(i)
108
- self[:italic] = i ? true : false
107
+ block = proc { |e| e.leaf? && e.text.empty? } unless block_given?
108
+
109
+ children.each do |child|
110
+ child.delete if block.call child.optimize!(&block)
111
+ end
112
+
113
+ # If we only have one child it is superfluous and
114
+ # should be merged. That means this node will inherrit
115
+ # the children of the single child as well as its
116
+ # attributes
117
+ if degree == 1
118
+ # Move the attributes over
119
+ attributes.merge! child.attributes
120
+ self.value = child.text
121
+ # Get the children of the child and add them to self
122
+ first_child.delete.each { |child| append_child child }
123
+ end
124
+
125
+ self
109
126
  end
110
127
 
111
- # Underline
128
+ # Optimize a copy of the node tree based on the rules outlined for
129
+ # #optimize!.
112
130
  #
113
- def underline?
114
- self[:underline]
115
- end
131
+ # Returns the root of the new optimized node structure.
116
132
 
117
- def underline=(u)
118
- self[:underline] = u ? true : false
133
+ def optimize(&block)
134
+ dup.optimize!(&block)
119
135
  end
120
136
 
121
- # Color
137
+ # Combine the text from all the leaf nodes in the tree, from left to
138
+ # right. If a block is given the node, along with its text will be passed
139
+ # as arguments. The block will be called recursivly, starting at the leaf
140
+ # nodes and propagating up until the entire tree has been "rendered" in
141
+ # this way.
122
142
  #
123
- def color
124
- self[:color]
125
- end
143
+ # block - a block that will be used to generate strings for each node.
144
+ #
145
+ # Returns a string representation of the node structure.
126
146
 
127
- def color=(c)
128
- self[:color] = c
147
+ def to_s(&block)
148
+ string =
149
+ if leaf?
150
+ text
151
+ else
152
+ children.reduce('') { |a, e| a + e.to_s(&block) }
153
+ end
154
+
155
+ block_given? ? yield(self, string) : string
129
156
  end
130
157
 
131
- # Font
158
+ # Represents the Entry structure as a hierarchy, showing the attributes of
159
+ # each node as well as the text entries in the leafs.
132
160
  #
133
- def font
134
- self[:font]
135
- end
161
+ # If a block is given, it will be called once for each entry, and the
162
+ # returned string will be used to represent the object in the output
163
+ # graph.
164
+ #
165
+ # Returns a string. Note that it will contain newline characters if the
166
+ # node has children.
167
+
168
+ def inspect *args, &block
169
+ unless block_given?
170
+ block = proc do |entry|
171
+ base_name = entry.leaf? ? %Q{"#{entry.text}"} : '◯'
172
+ base_name + entry.attributes.reduce('') do |a, (k, v)|
173
+ a + " #{k}=#{v.inspect}"
174
+ end
175
+ end
176
+ end
136
177
 
137
- def font=(f)
138
- self[:font] = f
178
+ super(*args, &block)
139
179
  end
140
180
  end
141
181
  end
@@ -0,0 +1,68 @@
1
+ module RichText
2
+ module Styleable
3
+
4
+ # Returns true if bold formatting is applied.
5
+
6
+ def bold?
7
+ self[:bold]
8
+ end
9
+
10
+ # Sets bold to either true or false, depending on the given argument.
11
+
12
+ def bold=(b)
13
+ self[:bold] = b ? true : false
14
+ end
15
+
16
+ # Returns true if italic formatting is applied.
17
+
18
+ def italic?
19
+ self[:italic]
20
+ end
21
+
22
+ # Sets italic to either true or false, depending on the given argument.
23
+
24
+ def italic=(i)
25
+ self[:italic] = i ? true : false
26
+ end
27
+
28
+ # Returns true if underlined formatting is applied.
29
+
30
+ def underlined?
31
+ self[:underlined]
32
+ end
33
+
34
+ alias underline? underlined?
35
+
36
+ # Sets underlined to either true or false, depending on the given argument.
37
+
38
+ def underlined=(u)
39
+ self[:underlined] = u ? true : false
40
+ end
41
+
42
+ alias underline= underlined=
43
+
44
+ # Returns the color value if it is set, otherwise nil.
45
+
46
+ def color
47
+ self[:color]
48
+ end
49
+
50
+ # Sets the color value.
51
+
52
+ def color=(c)
53
+ self[:color] = c
54
+ end
55
+
56
+ # Returns the font value if it is set, otherwise nil.
57
+
58
+ def font
59
+ self[:font]
60
+ end
61
+
62
+ # Sets the font value.
63
+
64
+ def font=(f)
65
+ self[:font] = f
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RichText
4
- VERSION = '0.2.4'
4
+ VERSION = '0.3.0'
5
5
  end
data/richtext.gemspec CHANGED
@@ -19,7 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
+ spec.add_dependency "rooted_tree", "~> 0.2.3"
23
+
22
24
  spec.add_development_dependency "bundler", "~> 1.12"
23
25
  spec.add_development_dependency "rake", "~> 10.0"
24
26
  spec.add_development_dependency "minitest", "~> 5.0"
27
+ spec.add_development_dependency "coveralls", "~> 0.8"
25
28
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: richtext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Lindberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-19 00:00:00.000000000 Z
11
+ date: 2016-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rooted_tree
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.3
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
55
83
  description:
56
84
  email:
57
85
  - seb.lindberg@gmail.com
@@ -70,7 +98,7 @@ files:
70
98
  - lib/richtext.rb
71
99
  - lib/richtext/document.rb
72
100
  - lib/richtext/document/entry.rb
73
- - lib/richtext/node.rb
101
+ - lib/richtext/styleable.rb
74
102
  - lib/richtext/version.rb
75
103
  - richtext.gemspec
76
104
  homepage: https://github.com/seblindberg/ruby-richtext
data/lib/richtext/node.rb DELETED
@@ -1,236 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RichText
4
- # Node
5
- #
6
- # A Node can have children, which themselvs can have children. A tree like
7
- # structure can thus be formed by composing multiple Nodes. An example of such
8
- # a tree structure can be seen below.
9
- #
10
- # The Node class implements some convenience methods for iterating, left to
11
- # right, over either all
12
- # - nodes in the tree
13
- # - leafs in the tree
14
- # - direct decendant of a node
15
- #
16
- # In addition to having children a Node can also have attributes, represented
17
- # by simple key => value pairs.
18
- #
19
- # Example Tree
20
- # +--------------------------+
21
- # A <- Root Node | Left to right order: ABC |
22
- # / \ +--------------------------+
23
- # Leaf Node -> B C <- Child to A
24
- # (no children) /|\
25
- # ...
26
- #
27
- class Node
28
- include Enumerable
29
-
30
- attr_reader :attributes, :children
31
- protected :children
32
-
33
- def initialize(**attributes)
34
- @children = []
35
- @attributes = attributes
36
- end
37
-
38
- def initialize_copy(original)
39
- @children = original.children.map(&:dup)
40
- @attributes = original.attributes.dup
41
- end
42
-
43
- # Leaf?
44
- #
45
- # Returns true if this node a leaf (childless) node.
46
- def leaf?
47
- @children.empty?
48
- end
49
-
50
- # Child
51
- #
52
- # Access the individual children of the node. If the method is called
53
- # without argument and the node has only one child it will be returned.
54
- # Otherwise an exception will be raised.
55
- def child n = nil
56
- if n
57
- @children[n]
58
- else
59
- raise 'Node does not have one child' unless count == 1
60
- @children[0]
61
- end
62
- end
63
-
64
- # Append
65
- #
66
- # Add a child to the end of the node child list. The child must be of this
67
- # class to be accepted. Note that subclasses of Node will not accept regular
68
- # Nodes. The method returns self so that multiple children can be added via
69
- # chaining:
70
- # root << child_a << child_b
71
- def <<(child)
72
- unless child.is_a? self.class
73
- raise TypeError,
74
- "Only objects of class #{self.class.name} can be appended"
75
- end
76
-
77
- @children << child
78
- self
79
- end
80
-
81
- # Create Child
82
- #
83
- # Create and append a new child, initialized with the given attributes.
84
- def create_child(**attributes)
85
- child = self.class.new(**attributes)
86
- self << child
87
- child
88
- end
89
-
90
- # Add (+)
91
- #
92
- # Combines two nodes by creating a new root and adding the two as children.
93
- def +(other)
94
- self.class.new.tap { |root| root << self << other }
95
- end
96
-
97
- # Each
98
- #
99
- # Iterate over each node in the tree, including self.
100
- def each(&block)
101
- return to_enum(__callee__) unless block_given?
102
-
103
- yield self
104
-
105
- @children.each do |child|
106
- yield child
107
- child.each(&block) unless child.leaf?
108
- end
109
- end
110
-
111
- # Each Leaf
112
- #
113
- # Iterate over each leaf in the tree. This method will yield the leaf nodes
114
- # of the tree from left to right.
115
- def each_leaf(&block)
116
- return to_enum(__callee__) unless block_given?
117
- return yield self if leaf?
118
-
119
- @children.each do |child|
120
- if child.leaf?
121
- yield child
122
- else
123
- child.each_leaf(&block)
124
- end
125
- end
126
- end
127
-
128
- # Each child
129
- #
130
- # Iterate over the children of this node.
131
- def each_child(&block)
132
- @children.each(&block)
133
- end
134
-
135
- # Attribute accessor
136
- #
137
- # Read and write an attribute of the node. Attributes are simply key-value
138
- # pairs stored internally in a hash.
139
- def [](attribute)
140
- @attributes[attribute]
141
- end
142
-
143
- def []=(attribute, value)
144
- @attributes[attribute] = value
145
- end
146
-
147
- # Count
148
- #
149
- # Returns the child count of this node.
150
- def count
151
- @children.size
152
- end
153
-
154
- # Size
155
- #
156
- # Returns the size of the tree where this node is the root.
157
- def size
158
- @children.reduce(1) { |a, e| a + e.size }
159
- end
160
-
161
- # Minimal?
162
- #
163
- # Test if the tree under this node is minimal or not. A non minimal tree
164
- # contains children which themselvs only have one child.
165
- def minimal?
166
- all? { |node| node.count != 1 }
167
- end
168
-
169
- # Optimize!
170
- #
171
- # Go through each child and merge any node that a) is not a lead node and b)
172
- # only has one child, with its child. The attributes of the child will
173
- # override those of the parent.
174
- def optimize!
175
- # If the node is a leaf it cannot be optimized further
176
- return self if leaf?
177
-
178
- # First optimize each of the children. If a block was
179
- # given each child will be yielded to it, and children
180
- # for which the block returns false will be removed
181
- if block_given?
182
- @children.select! { |child| yield child.optimize! }
183
- else
184
- @children.map(&:optimize!)
185
- end
186
-
187
- # If we only have one child it is superfluous and
188
- # should be merged. That means this node will inherrit
189
- # the children of the single child as well as its
190
- # attributes
191
- if count == 1
192
- child = @children[0]
193
- # Move the children over
194
- @children = child.children
195
- @attributes.merge! child.attributes
196
- end
197
-
198
- self
199
- end
200
-
201
- def optimize
202
- dup.optimize!
203
- end
204
-
205
- # Shallow equality (exclude children)
206
- #
207
- # Returns true if the other node has the exact same attributes.
208
- def equal?(other)
209
- count == other.count && @attributes == other.attributes
210
- end
211
-
212
- # Deep equality (include children)
213
- #
214
- # Returns true if the other node has the same attributes and its children
215
- # are also identical.
216
- def ==(other)
217
- # First make sure the nodes child count matches
218
- return false unless equal? other
219
-
220
- # Lastly make sure all of the children are equal
221
- each_child.zip(other.each_child).all? { |c| c[0] == c[1] }
222
- end
223
-
224
- def inspect
225
- children = @children.reduce('') do |s, c|
226
- s + "\n" + c.inspect.gsub(/(^)/) { |m| m + ' ' }
227
- end
228
-
229
- format '#<%{name} %<attrs>p:%<id>#x>%{children}',
230
- name: self.class.name,
231
- id: object_id,
232
- attrs: @attributes,
233
- children: children
234
- end
235
- end
236
- end