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.
- checksums.yaml +7 -0
- data/README.md +23 -0
- data/Rakefile +8 -0
- data/lib/attributed_string.rb +68 -0
- data/lib/attributed_string/attribute.rb +16 -0
- data/lib/attributed_string/version.rb +3 -0
- data/spec/attribute_spec.rb +15 -0
- data/spec/attributed_string_spec.rb +205 -0
- data/spec/spec_helper.rb +1 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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
|
+
```
|
data/Rakefile
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|