attributed_string 0.1.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.
@@ -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