attributed_string 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a4dae773b79cef9b5c9f997c44f7965874c3cabf
4
+ data.tar.gz: 755fede5186b248a7b69e494572718fe540c436a
5
+ SHA512:
6
+ metadata.gz: 29b01509657b7a0860f2e3d6a76aeec09ef0c26445cabfa44a8cc669a65d03b1065e3baa23f40fb33799b95ce323e4bda6fc5f2dc1385d5f45f3c9755a6c79c5
7
+ data.tar.gz: b5a0b00339d6997c39d3b81c8a5b43971c70bbafdfa952c001c86bab868826593fed391b64bd9031ee6fa20d05a44cd475b2540d943f32cda21b40c490f86262
@@ -0,0 +1,23 @@
1
+ # attributed_string
2
+
3
+ Similar to a `NSAttributedString` in Objective-C, this provides an `AttributedString` class. It's similar to a normal `String`, but tracks an array of `AttributedString::Attribute` instances, each of which references a range and a blob of data about that range.
4
+
5
+ This is useful for storing string formatting information separately from the plain text, while still being about to do transforms like prepend and append operations.
6
+
7
+ ## Example
8
+
9
+ ```ruby
10
+ string = "This is a test"
11
+ range = Range.new(0, 3)
12
+ data = { weight: :bold }
13
+ # This marks the first word as being bold. The implementation of the data is up to you, but here we use a simple Hash.
14
+ attribute = AttributedString::Attribute.new(range, data)
15
+
16
+ attributed_string = AttributedString.new(string, [attribute])
17
+
18
+ # This prepends another AttributedString, but keeps track of the position of the original attributes.
19
+ attributed_string.prepend(another_attributed_string)
20
+
21
+ # Coalesces overlapping attributes of identical data
22
+ attributed_string.fix
23
+ ```
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc "Run specs"
4
+ task :spec do
5
+ sh "bundle exec rspec"
6
+ end
7
+
8
+ task :default => :spec
@@ -0,0 +1,68 @@
1
+ require 'attributed_string/version'
2
+ require 'attributed_string/attribute'
3
+
4
+ class AttributedString
5
+
6
+ attr_reader :string, :attributes
7
+
8
+ def initialize(string, attributes = [])
9
+ @string = string
10
+ @attributes = attributes
11
+ end
12
+
13
+ def <<(attributed_string)
14
+ @string << attributed_string.string
15
+ @attributes += attributed_string.attributes
16
+ end
17
+
18
+ def prepend(attributed_string)
19
+ @string.prepend(attributed_string.string)
20
+ move_attributes(attributed_string.length)
21
+ @attributes += attributed_string.attributes
22
+ end
23
+
24
+ def length
25
+ @string.length
26
+ end
27
+
28
+ def fix
29
+ # Group into identical data structures
30
+ grouped_attributes = @attributes.group_by { |a| a.data }.values
31
+
32
+ # Sort each group into order
33
+ grouped_attributes.each do |group|
34
+ group.sort_by! { |a| a.range.begin }
35
+ end
36
+
37
+ # Merge attributes in identical groups together
38
+ merged_attributes = grouped_attributes.map do |group|
39
+ group.inject([]) do |attrs, a|
40
+ if !attrs.empty? && attributes_overlap?(attrs.last, a)
41
+ attrs[0...-1] + [merge_attributes(attrs.last, a)]
42
+ else
43
+ attrs + [a]
44
+ end
45
+ end
46
+ end
47
+
48
+ @attributes = merged_attributes.flatten
49
+ end
50
+
51
+ private
52
+
53
+ def move_attributes(offset)
54
+ @attributes.each do |attribute|
55
+ attribute.move(offset)
56
+ end
57
+ end
58
+
59
+ def attributes_overlap?(a, b)
60
+ a.data == b.data && (a.range.include?(b.range.begin) || b.range.include?(a.range.begin))
61
+ end
62
+
63
+ def merge_attributes(a, b)
64
+ range = [a.range.begin, b.range.begin].min..[a.range.end, b.range.end].max
65
+ AttributedString::Attribute.new(range, a.data)
66
+ end
67
+
68
+ end
@@ -0,0 +1,16 @@
1
+ class AttributedString
2
+ class Attribute
3
+
4
+ attr_reader :range, :data
5
+
6
+ def initialize(range, data)
7
+ @range = range
8
+ @data = data
9
+ end
10
+
11
+ def move(offset)
12
+ @range = Range.new(@range.begin + offset, @range.end + offset, @range.exclude_end?)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class AttributedString
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe AttributedString::Attribute do
4
+ let(:range) { Range.new(0, 5) }
5
+ let(:data) { { foo: :bar } }
6
+ subject { AttributedString::Attribute.new(range, data) }
7
+
8
+ describe '.move' do
9
+ before { subject.move(3) }
10
+
11
+ it "moves the range up" do
12
+ expect(subject.range).to eq(Range.new(3, 8))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+
3
+ describe AttributedString do
4
+ let(:string) { "Hello brave new world" }
5
+ let(:attributes) do
6
+ [
7
+ AttributedString::Attribute.new(Range.new(0, 5), { weight: :bold }),
8
+ AttributedString::Attribute.new(Range.new(6, 11), { weight: :bold }),
9
+ ]
10
+ end
11
+
12
+ let(:alt_string) { "Testing" }
13
+ let(:alt_attributes) do
14
+ [
15
+ AttributedString::Attribute.new(Range.new(0, 7), { style: :italic }),
16
+ ]
17
+ end
18
+ let(:alt_attributed_string) { AttributedString.new(alt_string.dup, alt_attributes.dup) }
19
+
20
+ subject { AttributedString.new(string.dup, attributes.dup) }
21
+
22
+ it "should have a length of 21" do
23
+ expect(subject.length).to eq(21)
24
+ end
25
+
26
+ it "should have the correct attributes" do
27
+ expect(subject.attributes).to eq(attributes)
28
+ end
29
+
30
+ it "should have the correct string" do
31
+ expect(subject.string).to eq("Hello brave new world")
32
+ end
33
+
34
+ describe 'appending an AttributedString' do
35
+ before do
36
+ subject << alt_attributed_string
37
+ end
38
+
39
+ it "should have the right length" do
40
+ expect(subject.length).to eq(28)
41
+ end
42
+
43
+ it "should have a concatenated string" do
44
+ expect(subject.string).to eq("Hello brave new worldTesting")
45
+ end
46
+
47
+ it "should have 3 attributes" do
48
+ expect(subject.attributes.length).to eq(3)
49
+ end
50
+ end
51
+
52
+ describe 'prepending an AttributedString' do
53
+ before do
54
+ subject.prepend(alt_attributed_string)
55
+ end
56
+
57
+ it "should have the right length" do
58
+ expect(subject.length).to eq(28)
59
+ end
60
+
61
+ it "should have a concatenated string" do
62
+ expect(subject.string).to eq("TestingHello brave new world")
63
+ end
64
+
65
+ it "should have 3 attributes" do
66
+ expect(subject.attributes.length).to eq(3)
67
+ end
68
+
69
+ it "the first and second attributes should have moved up" do
70
+ expect(subject.attributes[0].range).to eq(Range.new(7, 12))
71
+ expect(subject.attributes[1].range).to eq(Range.new(13, 18))
72
+ end
73
+
74
+ it "the third attribute should be at the beginning" do
75
+ expect(subject.attributes[2].range).to eq(Range.new(0, 7))
76
+ end
77
+ end
78
+
79
+ describe '.fix' do
80
+ before do
81
+ subject.fix
82
+ end
83
+
84
+ context 'with non-overlapping attributes' do
85
+ it "preserves the attributes" do
86
+ expect(subject.attributes).to eq(attributes)
87
+ end
88
+ end
89
+
90
+ context 'with two overlapping attributes' do
91
+ let(:attributes) do
92
+ [
93
+ AttributedString::Attribute.new(Range.new(0, 8), { weight: :bold }),
94
+ AttributedString::Attribute.new(Range.new(6, 11), { weight: :bold }),
95
+ ]
96
+ end
97
+
98
+ it "reduces to 1 attribute" do
99
+ expect(subject.attributes.length).to eq(1)
100
+ end
101
+
102
+ it "has the right data" do
103
+ expect(subject.attributes.first.data).to eq({ weight: :bold })
104
+ end
105
+
106
+ it "has the right range" do
107
+ expect(subject.attributes.first.range).to eq(Range.new(0, 11))
108
+ end
109
+ end
110
+
111
+ context 'with two touching attributes' do
112
+ let(:attributes) do
113
+ [
114
+ AttributedString::Attribute.new(Range.new(0, 8), { weight: :bold }),
115
+ AttributedString::Attribute.new(Range.new(8, 11), { weight: :bold }),
116
+ ]
117
+ end
118
+
119
+ it "reduces to 1 attribute" do
120
+ expect(subject.attributes.length).to eq(1)
121
+ end
122
+
123
+ it "has the right data" do
124
+ expect(subject.attributes.first.data).to eq({ weight: :bold })
125
+ end
126
+
127
+ it "has the right range" do
128
+ expect(subject.attributes.first.range).to eq(Range.new(0, 11))
129
+ end
130
+ end
131
+
132
+ context 'with two overlapping attributes in reverse order' do
133
+ let(:attributes) do
134
+ [
135
+ AttributedString::Attribute.new(Range.new(6, 11), { weight: :bold }),
136
+ AttributedString::Attribute.new(Range.new(0, 8), { weight: :bold }),
137
+ ]
138
+ end
139
+
140
+ it "reduces to 1 attribute" do
141
+ expect(subject.attributes.length).to eq(1)
142
+ end
143
+
144
+ it "has the right data" do
145
+ expect(subject.attributes.first.data).to eq({ weight: :bold })
146
+ end
147
+
148
+ it "has the right range" do
149
+ expect(subject.attributes.first.range).to eq(Range.new(0, 11))
150
+ end
151
+ end
152
+
153
+ context 'with three overlapping attributes' do
154
+ let(:attributes) do
155
+ [
156
+ AttributedString::Attribute.new(Range.new(0, 8), { weight: :bold }),
157
+ AttributedString::Attribute.new(Range.new(6, 11), { weight: :bold }),
158
+ AttributedString::Attribute.new(Range.new(10, 11), { weight: :bold }),
159
+ ]
160
+ end
161
+
162
+ it "reduces to 1 attribute" do
163
+ expect(subject.attributes.length).to eq(1)
164
+ end
165
+
166
+ it "has the right data" do
167
+ expect(subject.attributes.first.data).to eq({ weight: :bold })
168
+ end
169
+
170
+ it "has the right range" do
171
+ expect(subject.attributes.first.range).to eq(Range.new(0, 11))
172
+ end
173
+ end
174
+
175
+ context 'with three attributes, two of which are the same' do
176
+ let(:attributes) do
177
+ [
178
+ AttributedString::Attribute.new(Range.new(0, 8), { weight: :bold }),
179
+ AttributedString::Attribute.new(Range.new(6, 13), { style: :italic }),
180
+ AttributedString::Attribute.new(Range.new(8, 11), { weight: :bold }),
181
+ ]
182
+ end
183
+
184
+ it "reduces to 2 attributes" do
185
+ expect(subject.attributes.length).to eq(2)
186
+ end
187
+
188
+ it "has the right data for the first" do
189
+ expect(subject.attributes.first.data).to eq({ weight: :bold })
190
+ end
191
+
192
+ it "has the right range for the first" do
193
+ expect(subject.attributes.first.range).to eq(Range.new(0, 11))
194
+ end
195
+
196
+ it "has the right data for the second" do
197
+ expect(subject.attributes[1].data).to eq({ style: :italic })
198
+ end
199
+
200
+ it "has the right range for the first" do
201
+ expect(subject.attributes[1].range).to eq(Range.new(6, 13))
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1 @@
1
+ require 'attributed_string'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attributed_string
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Taylor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description:
42
+ email:
43
+ - tom@newspaperclub.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - Rakefile
50
+ - lib/attributed_string.rb
51
+ - lib/attributed_string/attribute.rb
52
+ - lib/attributed_string/version.rb
53
+ - spec/attribute_spec.rb
54
+ - spec/attributed_string_spec.rb
55
+ - spec/spec_helper.rb
56
+ homepage: https://github.com/newspaperclub/attributed_string
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.2.2
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: A String class, but with attributes marking data at specific ranges.
80
+ test_files:
81
+ - spec/attribute_spec.rb
82
+ - spec/attributed_string_spec.rb
83
+ - spec/spec_helper.rb