richtext 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/richtext/document/entry.rb +142 -0
- data/lib/richtext/document.rb +180 -0
- data/lib/richtext/node.rb +234 -0
- data/lib/richtext/version.rb +3 -0
- data/lib/richtext.rb +18 -0
- data/richtext.gemspec +25 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a41039f175ce7ee67d507040de2a1c84f58c9069
|
4
|
+
data.tar.gz: 60ae51774de4953ea1b5e59ef3ffb8afd2f520ac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86924ff26a091d4087245467c47e43aabd5499eb8fc785ac2ba8b2dbca596ab49d9ba79399ad6c51e65c6f629fb9ba38e6d02cd8725845f9a6c1d0ffe84f70a6
|
7
|
+
data.tar.gz: 8e08c49fcb57d006235eaa58258b9e0dc0b0e0e5a753594b15becda8b7715413ce6a9b0ef5f5bea8be0a1075e05902d6dad5194e87635b9d34a5dd51432dac42
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Sebastian Lindberg
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Richtext
|
2
|
+
|
3
|
+
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
|
+
|
5
|
+
See _Usage_ below for more details on how to work with and extend the gem.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'richtext'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install richtext
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Create a new RichText document
|
27
|
+
rt = RichText::Document.new 'hello '
|
28
|
+
|
29
|
+
# Or use the more convenient method
|
30
|
+
rt = RichText 'hello '
|
31
|
+
|
32
|
+
# Format the text using attributes
|
33
|
+
entry = rt.append('world', bold: true, my_attribute: '.')
|
34
|
+
|
35
|
+
# Some common styling attributes are supported directly
|
36
|
+
# This line is equivalent to entry[:italic] = true
|
37
|
+
entry.italic = true
|
38
|
+
# Under the covers the attributes are stored as
|
39
|
+
# key-value pairs, so any attribute is valid
|
40
|
+
entry[:my_attribute] = '!'
|
41
|
+
|
42
|
+
# Render the text without any formatting
|
43
|
+
puts rt.to_s # => 'hello world'
|
44
|
+
|
45
|
+
# Or style the text yourself
|
46
|
+
html = rt.to_s do |entry, string|
|
47
|
+
# Access the attributes from the entry and format the
|
48
|
+
# string accordingly
|
49
|
+
string += entry[:my_attribute] if entry[:my_attribute]
|
50
|
+
string = "<b>#{string}</b>" if entry.bold?
|
51
|
+
|
52
|
+
# Return the formatted string at the end of the block
|
53
|
+
string
|
54
|
+
end
|
55
|
+
|
56
|
+
puts html # => 'hello <b>world!</b>'
|
57
|
+
```
|
58
|
+
|
59
|
+
Implementing new formats is easy. Just extend the `RichText::Document` class and implement the class methods `.parse` and `.render`. The following snippet describes a document type that only renders words with more than 6 letters.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class MyFormat < RichText::Document
|
63
|
+
# Use this method to signal if the document needs to be
|
64
|
+
# parsed, or if its raw form will work.
|
65
|
+
def should_parse?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.parse string
|
70
|
+
base = RichText::Document::Entry.new
|
71
|
+
# Format specific implementation to parse a string. Here
|
72
|
+
# each word is represented by its own entry. Entries are
|
73
|
+
# given a random visibility attribute.
|
74
|
+
string.split(' ').each do |word|
|
75
|
+
entry = RichText::Document::Entry.new word, visible: (word.length > 6)
|
76
|
+
base.add entry
|
77
|
+
end
|
78
|
+
base
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.render base
|
82
|
+
# Format specific implementation to render the document
|
83
|
+
base.to_s do |entry, string|
|
84
|
+
next string unless entry.leaf?
|
85
|
+
entry[:visible] ? string + ' ' : ''
|
86
|
+
end.rstrip!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
doc = MyFormat.new 'Format specific implementation to parse a string'
|
91
|
+
puts doc.to_s # => 'specific implementation'
|
92
|
+
|
93
|
+
```
|
94
|
+
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
99
|
+
|
100
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
|
104
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/seblindberg/richtext.
|
105
|
+
|
106
|
+
|
107
|
+
## License
|
108
|
+
|
109
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
110
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "richtext"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# Entry
|
2
|
+
#
|
3
|
+
# The Entry class extends the basic Node class and adds methods that make
|
4
|
+
# handling text a little nicer. Essentially the :text attribute is given special
|
5
|
+
# status by allowing it to a) be set during initialization, b) only visible in
|
6
|
+
# leaf nodes and c) copied over when adding children to leaf nodes.
|
7
|
+
#
|
8
|
+
# Some attributes are also supported explicitly by the inclusion of special
|
9
|
+
# accesser methods. The attributes are are bold, italic, underline, color and
|
10
|
+
# font.
|
11
|
+
|
12
|
+
module RichText
|
13
|
+
class Document
|
14
|
+
class Entry < Node
|
15
|
+
|
16
|
+
# Initialize
|
17
|
+
#
|
18
|
+
# Extend the default Node initializer by also accepting a string. It will,
|
19
|
+
# if given, be stored as a text attribute.
|
20
|
+
|
21
|
+
def initialize text = nil, **attributes
|
22
|
+
super attributes
|
23
|
+
self[:text] = text if text
|
24
|
+
end
|
25
|
+
|
26
|
+
|
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
|
+
|
33
|
+
def text
|
34
|
+
if leaf?
|
35
|
+
self[:text] || ''
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Add child
|
43
|
+
#
|
44
|
+
# A child is either another node or any object that respond to #to_s.
|
45
|
+
|
46
|
+
def add *new_children
|
47
|
+
if leaf?
|
48
|
+
# Remove the text entry from the node and put it in a new leaf node
|
49
|
+
# among the children, unless it is empty
|
50
|
+
if t = @attributes.delete(:text)
|
51
|
+
new_children.unshift self.class.new(t) unless t.empty?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :<<, :add
|
59
|
+
|
60
|
+
|
61
|
+
# To String
|
62
|
+
#
|
63
|
+
# Combine the text from all the leaf nodes in the tree, from left to
|
64
|
+
# right. If a block is given the node, along with its text will be passed
|
65
|
+
# as arguments. The block will be called recursivly, starting at the leaf
|
66
|
+
# nodes and propagating up until the entire tree has been "rendered" in
|
67
|
+
# this way.
|
68
|
+
|
69
|
+
def to_s &block
|
70
|
+
string = leaf? ?
|
71
|
+
text :
|
72
|
+
@children.reduce('') {|str, child| str + child.to_s(&block) }
|
73
|
+
|
74
|
+
block_given? ? yield(self, string) : string
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Supported Text Attributes
|
79
|
+
#
|
80
|
+
|
81
|
+
|
82
|
+
# Bold
|
83
|
+
#
|
84
|
+
|
85
|
+
def bold?
|
86
|
+
self[:bold]
|
87
|
+
end
|
88
|
+
|
89
|
+
def bold= b
|
90
|
+
self[:bold] = b ? true : false
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Italic
|
95
|
+
#
|
96
|
+
|
97
|
+
def italic?
|
98
|
+
self[:italic]
|
99
|
+
end
|
100
|
+
|
101
|
+
def italic= i
|
102
|
+
self[:italic] = i ? true : false
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Underline
|
107
|
+
#
|
108
|
+
|
109
|
+
def underline?
|
110
|
+
self[:underline]
|
111
|
+
end
|
112
|
+
|
113
|
+
def underline= u
|
114
|
+
self[:underline] = u ? true : false
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# Color
|
119
|
+
#
|
120
|
+
|
121
|
+
def color
|
122
|
+
self[:color]
|
123
|
+
end
|
124
|
+
|
125
|
+
def color= c
|
126
|
+
self[:color] = c
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# Font
|
131
|
+
#
|
132
|
+
|
133
|
+
def font
|
134
|
+
self[:font]
|
135
|
+
end
|
136
|
+
|
137
|
+
def font= f
|
138
|
+
self[:font] = f
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module RichText
|
2
|
+
class Document
|
3
|
+
# Initialize
|
4
|
+
#
|
5
|
+
# Create a new RichText Document, either from a string or from an existing
|
6
|
+
# ducument. That feature is particularly useful when converting between
|
7
|
+
# formats.
|
8
|
+
#
|
9
|
+
# When given a string or a RichText Document of the same class no parsing is
|
10
|
+
# performed. Only when given a document of a different subclass will the
|
11
|
+
# parser need to be run parsed. Note that the document(s) may already be in
|
12
|
+
# parsed form, in which case no further parsing is performed. See #base for
|
13
|
+
# more details.
|
14
|
+
|
15
|
+
def initialize arg = ''
|
16
|
+
@base, @raw = if self.class == arg.class
|
17
|
+
arg.parsed? ?
|
18
|
+
[arg.base, nil] :
|
19
|
+
[nil, arg.raw]
|
20
|
+
elsif Document === arg
|
21
|
+
# For any other RichText object we take the base node
|
22
|
+
[arg.base, nil]
|
23
|
+
elsif Entry === arg
|
24
|
+
# Also accept an Entry which will be used as the
|
25
|
+
# document base
|
26
|
+
[arg, nil]
|
27
|
+
else
|
28
|
+
[nil, arg.to_s]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# To String
|
34
|
+
#
|
35
|
+
# Use the static implementation of .render to convert the document back into
|
36
|
+
# a string. If the document was never parsed (and is unchanged) the
|
37
|
+
# origninal string is just returned.
|
38
|
+
#
|
39
|
+
# If a block is given it will be used in place of .render to format the node
|
40
|
+
# tree.
|
41
|
+
|
42
|
+
def to_s &block
|
43
|
+
if block_given?
|
44
|
+
base.to_s(&block)
|
45
|
+
elsif parsed? || should_parse?
|
46
|
+
self.class.render base
|
47
|
+
else
|
48
|
+
@raw
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Add (+)
|
54
|
+
#
|
55
|
+
# Add this RichText to another.
|
56
|
+
|
57
|
+
def + other
|
58
|
+
# If the other object is of the same class, and neither
|
59
|
+
# one of the texts have been parsed, we can concatenate
|
60
|
+
# the raw inputs together
|
61
|
+
if other.class == self.class && !parsed? && !other.parsed?
|
62
|
+
return self.class.new (@raw + other.raw)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Same root class
|
66
|
+
if Document === other
|
67
|
+
return self.class.new (base + other.base)
|
68
|
+
end
|
69
|
+
|
70
|
+
unless other.respond_to?(:to_s)
|
71
|
+
raise TypeError,
|
72
|
+
"cannot add #{other.class.name} to #{self.class.name}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Assume that the input is a raw string of the same
|
76
|
+
# class as the current RichText object and wrap it
|
77
|
+
# before adding it
|
78
|
+
self + self.class.new(other)
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def append string, **attributes
|
83
|
+
node = Entry.new(string, **attributes)
|
84
|
+
base.add node
|
85
|
+
node
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Base
|
90
|
+
#
|
91
|
+
# Getter for the base node. If the raw input has not yet been
|
92
|
+
# parsed that will happen first, before the base node is returned.
|
93
|
+
|
94
|
+
def base
|
95
|
+
unless @base
|
96
|
+
@base = Entry.new
|
97
|
+
self.class.parse @base, @raw
|
98
|
+
@raw = nil # Free the cached string
|
99
|
+
end
|
100
|
+
|
101
|
+
@base
|
102
|
+
end
|
103
|
+
|
104
|
+
alias_method :root, :base
|
105
|
+
|
106
|
+
|
107
|
+
# Raw
|
108
|
+
#
|
109
|
+
# Protected getter for the raw input.
|
110
|
+
|
111
|
+
protected def raw
|
112
|
+
@raw
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# Parsed?
|
117
|
+
#
|
118
|
+
# Returns true if the raw input has been parsed and the internal
|
119
|
+
# representation is now a tree of nodes.
|
120
|
+
|
121
|
+
def parsed?
|
122
|
+
@raw.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
protected def should_parse?
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# Each Node
|
132
|
+
#
|
133
|
+
# Iterate over all Entry nodes in the document tree.
|
134
|
+
|
135
|
+
def each_node &block
|
136
|
+
base.each(&block)
|
137
|
+
end
|
138
|
+
|
139
|
+
alias_method :each_entry, :each_node
|
140
|
+
|
141
|
+
|
142
|
+
# Parse
|
143
|
+
#
|
144
|
+
# Document type specific method for parsing a string and turning it into a
|
145
|
+
# tree of entry nodes. This method is intended to be overridden when the
|
146
|
+
# Document is subclassed. The default implementation just creates a top
|
147
|
+
# level Entry containing the given string.
|
148
|
+
|
149
|
+
def self.parse base, string
|
150
|
+
base[:text] = string
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Render
|
155
|
+
#
|
156
|
+
# Document type specific method for rendering a tree of entry nodes. This
|
157
|
+
# method is intended to be overridden when the Document is subclassed. The
|
158
|
+
# default implementation just concatenates the text entries into.
|
159
|
+
|
160
|
+
def self.render base
|
161
|
+
base.to_s
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
# From
|
166
|
+
#
|
167
|
+
# Convenience method for instansiating one RichText object from another. The
|
168
|
+
# methods only purpose is to make that intent more clear, and to make the
|
169
|
+
# creation from another RichText object explicit.
|
170
|
+
|
171
|
+
def self.from doc
|
172
|
+
unless Document === doc
|
173
|
+
raise TypeError,
|
174
|
+
"Can only create a #{self.name} from other RichText objects"
|
175
|
+
end
|
176
|
+
|
177
|
+
self.new doc
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# Node
|
2
|
+
#
|
3
|
+
# A Node can have children, which themselvs can have children. A tree like
|
4
|
+
# structure can thus be formed by composing multiple Nodes. An example of such a
|
5
|
+
# tree structure can be seen below.
|
6
|
+
#
|
7
|
+
# The Node class implements some convenience methods for iterating, left to
|
8
|
+
# right, over either all
|
9
|
+
# - nodes in the tree
|
10
|
+
# - leafs in the tree
|
11
|
+
# - direct decendant of a node
|
12
|
+
#
|
13
|
+
# In addition to having children a Node can also have attributes, represented by # simple key => value pairs.
|
14
|
+
#
|
15
|
+
# Example Tree
|
16
|
+
# +--------------------------+
|
17
|
+
# A <- Root Node | Left to right order: ABC |
|
18
|
+
# / \ +--------------------------+
|
19
|
+
# Leaf Node -> B C <- Child to A
|
20
|
+
# (no children) /|\
|
21
|
+
# ...
|
22
|
+
#
|
23
|
+
|
24
|
+
module RichText
|
25
|
+
class Node
|
26
|
+
include Enumerable
|
27
|
+
|
28
|
+
attr_reader :attributes
|
29
|
+
|
30
|
+
def initialize **attributes
|
31
|
+
@children = []
|
32
|
+
@attributes = attributes
|
33
|
+
#@attributes[:text] = text if text
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def initialize_copy original
|
38
|
+
@children = original.children.map(&:dup)
|
39
|
+
@attributes = original.attributes.dup
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Leaf?
|
44
|
+
#
|
45
|
+
# Returns true if this node a leaf (childless) node.
|
46
|
+
|
47
|
+
def leaf?
|
48
|
+
@children.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Children
|
53
|
+
#
|
54
|
+
# Protected accessor for the children array. This array should never be
|
55
|
+
# mutated from the outside and is only protected rather than private to be
|
56
|
+
# accessable to ther Nodes.
|
57
|
+
|
58
|
+
protected def children
|
59
|
+
@children
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Add child
|
64
|
+
#
|
65
|
+
# A child is either another node or any object that respond to #to_s.
|
66
|
+
|
67
|
+
def add *new_children
|
68
|
+
new_children.each do |c|
|
69
|
+
@children << ((Node === c) ? c : self.class.new(c))
|
70
|
+
end
|
71
|
+
|
72
|
+
@children
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :<<, :add
|
76
|
+
|
77
|
+
|
78
|
+
# Add (+)
|
79
|
+
#
|
80
|
+
# Combines two nodes by creating a new root and adding the two as children.
|
81
|
+
|
82
|
+
def + other
|
83
|
+
self.class.new.tap {|root| root.add self, other }
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Each
|
88
|
+
#
|
89
|
+
# Iterate over each node in the tree, including self.
|
90
|
+
|
91
|
+
def each &block
|
92
|
+
return to_enum(__callee__) unless block_given?
|
93
|
+
|
94
|
+
yield self
|
95
|
+
|
96
|
+
@children.each do |child|
|
97
|
+
yield child
|
98
|
+
child.each(&block) unless child.leaf?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Each Leaf
|
104
|
+
#
|
105
|
+
# Iterate over each leaf in the tree. This method will yield the leaf nodes
|
106
|
+
# of the tree from left to right.
|
107
|
+
|
108
|
+
def each_leaf &block
|
109
|
+
return to_enum(__callee__) unless block_given?
|
110
|
+
return yield self if leaf?
|
111
|
+
|
112
|
+
@children.each do |child|
|
113
|
+
if child.leaf?
|
114
|
+
yield child
|
115
|
+
else
|
116
|
+
child.each_leaf(&block)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# Each child
|
123
|
+
#
|
124
|
+
# Iterate over the children of this node.
|
125
|
+
|
126
|
+
def each_child &block
|
127
|
+
@children.each(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# Attribute accessor
|
132
|
+
#
|
133
|
+
# Read and write an attribute of the node. Attributes are simply key-value
|
134
|
+
# pairs stored internally in a hash.
|
135
|
+
|
136
|
+
def [] attribute
|
137
|
+
@attributes[attribute]
|
138
|
+
end
|
139
|
+
|
140
|
+
def []= attribute, value
|
141
|
+
@attributes[attribute] = value
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Count
|
146
|
+
#
|
147
|
+
# Returns the child count of this node.
|
148
|
+
|
149
|
+
def count
|
150
|
+
@children.size
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Size
|
155
|
+
#
|
156
|
+
# Returns the size of the tree where this node is the root.
|
157
|
+
|
158
|
+
def size
|
159
|
+
@children.reduce(1) {|total, child| total + child.size }
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# Minimal?
|
164
|
+
#
|
165
|
+
# Test if the tree under this node is minimal or not. A non minimal tree
|
166
|
+
# contains children which themselvs only have one child.
|
167
|
+
|
168
|
+
def minimal?
|
169
|
+
all? {|node| node.count != 1 }
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# Optimize!
|
174
|
+
#
|
175
|
+
# Go through each child and merge any node that a) is not a lead node and b)
|
176
|
+
# only has one child, with its child. The attributes of the child will
|
177
|
+
# override those of the parent.
|
178
|
+
|
179
|
+
def optimize!
|
180
|
+
# If the node is a leaf it cannot be optimized further
|
181
|
+
return if leaf?
|
182
|
+
|
183
|
+
# First optimize each of the children
|
184
|
+
@children.map(&:optimize!)
|
185
|
+
|
186
|
+
# If we only have one child it is superfluous and
|
187
|
+
# should be merged. That means this node will inherrit
|
188
|
+
# the children of the single child as well as its
|
189
|
+
# attributes
|
190
|
+
if count == 1
|
191
|
+
child = @children[0]
|
192
|
+
# Move the children over
|
193
|
+
@children = child.children
|
194
|
+
@attributes.merge! child.attributes
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# Shallow equality (exclude children)
|
200
|
+
#
|
201
|
+
# Returns true if the other node has the exact same attributes.
|
202
|
+
|
203
|
+
def === other
|
204
|
+
@attributes == other.attributes
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
# Deep equality (include children)
|
209
|
+
#
|
210
|
+
# Returns true if the other node has the same attributes and its children
|
211
|
+
# are also identical.
|
212
|
+
|
213
|
+
def == other
|
214
|
+
# First make sure the nodes child count matches
|
215
|
+
return false unless count == other.count
|
216
|
+
|
217
|
+
# The nodes are not equal if their attributes do not
|
218
|
+
# match
|
219
|
+
return false unless self === other
|
220
|
+
|
221
|
+
# Lastly make sure all of the children are equal
|
222
|
+
each_child.zip(other.each_child).all? {|c| c[0] == c[1] }
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
def inspect
|
227
|
+
children = @children.reduce(''){|s, c|
|
228
|
+
s + "\n" + c.inspect.gsub(/(^)/) { $1 + ' ' }}
|
229
|
+
|
230
|
+
"#<%{name} %<a>p:%<id>#x>%{children}" % {
|
231
|
+
name: self.class.name, id: self.object_id, a: @attributes, children: children}
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
data/lib/richtext.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'richtext/version'
|
2
|
+
require 'richtext/node'
|
3
|
+
require 'richtext/document/entry'
|
4
|
+
require 'richtext/document'
|
5
|
+
|
6
|
+
module RichText
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# RichText
|
12
|
+
#
|
13
|
+
# Convenience method for creating RichText objects. Calling RichText(obj) is
|
14
|
+
# equivalent to RichText::Document.new(obj).
|
15
|
+
|
16
|
+
def RichText string
|
17
|
+
RichText::Document.new string
|
18
|
+
end
|
data/richtext.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'richtext/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "richtext"
|
8
|
+
spec.version = RichText::VERSION
|
9
|
+
spec.authors = ["Sebastian Lindberg"]
|
10
|
+
spec.email = ["seb.lindberg@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{This gem provides a basic way of representing formatting within strings.}
|
13
|
+
#spec.description = %q{TODO: Write a longer description or delete this line.}
|
14
|
+
spec.homepage = "https://github.com/seblindberg/ruby-richtext"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: richtext
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Lindberg
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- seb.lindberg@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".travis.yml"
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/console
|
69
|
+
- bin/setup
|
70
|
+
- lib/richtext.rb
|
71
|
+
- lib/richtext/document.rb
|
72
|
+
- lib/richtext/document/entry.rb
|
73
|
+
- lib/richtext/node.rb
|
74
|
+
- lib/richtext/version.rb
|
75
|
+
- richtext.gemspec
|
76
|
+
homepage: https://github.com/seblindberg/ruby-richtext
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.5.1
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: This gem provides a basic way of representing formatting within strings.
|
100
|
+
test_files: []
|