richtext 0.2.4 → 0.3.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 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