attributed-string 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/LICENSE +202 -0
- data/README.md +45 -0
- data/lib/attributed-string.rb +9 -0
- data/lib/attributed_string/attachment.rb +66 -0
- data/lib/attributed_string/filter_result.rb +108 -0
- data/lib/attributed_string/inspect.rb +95 -0
- data/lib/attributed_string/klass.rb +113 -0
- data/lib/attributed_string/needs_testing.rb +8 -0
- data/lib/attributed_string/refine.rb +42 -0
- data/lib/attributed_string/string_ops.rb +270 -0
- data/lib/attributed_string/unsupported.rb +221 -0
- data/lib/attributed_string/version.rb +3 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 82923e8039feb4f88aed1efcddc7df11bf2a620c2320d602f8d105c57b510a8a
|
4
|
+
data.tar.gz: 0aa653c860bd4329d28464fd2513df2ce3c733cda59f77739c4c6f9eb45cc809
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 15758243087810b97934ddc122f4bb60e473ece3a338d5a642e272b08adad982701d2791c71acf46e42a20422a7ff7411e31d748875faa243a5e508eec8a136d
|
7
|
+
data.tar.gz: d6c2f8655e93f1536724a321e17fd90da6f82cca842eecfdaa82b20630cafd243e7d5a40ec60a890ae8c3f973359252742e90568347746c355b04ff5d9fc53ce
|
data/LICENSE
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
Copyright 2024-, Andrew Mackross
|
2
|
+
Apache License
|
3
|
+
Version 2.0, January 2004
|
4
|
+
http://www.apache.org/licenses/
|
5
|
+
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
7
|
+
|
8
|
+
1. Definitions.
|
9
|
+
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
12
|
+
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
14
|
+
the copyright owner that is granting the License.
|
15
|
+
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
17
|
+
other entities that control, are controlled by, or are under common
|
18
|
+
control with that entity. For the purposes of this definition,
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
20
|
+
direction or management of such entity, whether by contract or
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
23
|
+
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
25
|
+
exercising permissions granted by this License.
|
26
|
+
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
28
|
+
including but not limited to software source code, documentation
|
29
|
+
source, and configuration files.
|
30
|
+
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
32
|
+
transformation or translation of a Source form, including but
|
33
|
+
not limited to compiled object code, generated documentation,
|
34
|
+
and conversions to other media types.
|
35
|
+
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
37
|
+
Object form, made available under the License, as indicated by a
|
38
|
+
copyright notice that is included in or attached to the work
|
39
|
+
(an example is provided in the Appendix below).
|
40
|
+
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
47
|
+
the Work and Derivative Works thereof.
|
48
|
+
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
50
|
+
the original version of the Work and any modifications or additions
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
62
|
+
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
65
|
+
subsequently incorporated within the Work.
|
66
|
+
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
73
|
+
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
79
|
+
where such license applies only to those patent claims licensable
|
80
|
+
by such Contributor that are necessarily infringed by their
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
83
|
+
institute patent litigation against any entity (including a
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
86
|
+
or contributory patent infringement, then any patent licenses
|
87
|
+
granted to You under this License for that Work shall terminate
|
88
|
+
as of the date such litigation is filed.
|
89
|
+
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
92
|
+
modifications, and in Source or Object form, provided that You
|
93
|
+
meet the following conditions:
|
94
|
+
|
95
|
+
(a) You must give any other recipients of the Work or
|
96
|
+
Derivative Works a copy of this License; and
|
97
|
+
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
99
|
+
stating that You changed the files; and
|
100
|
+
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
103
|
+
attribution notices from the Source form of the Work,
|
104
|
+
excluding those notices that do not pertain to any part of
|
105
|
+
the Derivative Works; and
|
106
|
+
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
109
|
+
include a readable copy of the attribution notices contained
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
112
|
+
of the following places: within a NOTICE text file distributed
|
113
|
+
as part of the Derivative Works; within the Source form or
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
115
|
+
within a display generated by the Derivative Works, if and
|
116
|
+
wherever such third-party notices normally appear. The contents
|
117
|
+
of the NOTICE file are for informational purposes only and
|
118
|
+
do not modify the License. You may add Your own attribution
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
121
|
+
that such additional attribution notices cannot be construed
|
122
|
+
as modifying the License.
|
123
|
+
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
125
|
+
may provide additional or different license terms and conditions
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
129
|
+
the conditions stated in this License.
|
130
|
+
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
134
|
+
this License, without any additional terms or conditions.
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
136
|
+
the terms of any separate license agreement you may have executed
|
137
|
+
with Licensor regarding such Contributions.
|
138
|
+
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
141
|
+
except as required for reasonable and customary use in describing the
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
143
|
+
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
153
|
+
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
159
|
+
incidental, or consequential damages of any character arising as a
|
160
|
+
result of this License or out of the use or inability to use the
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
163
|
+
other commercial damages or losses), even if such Contributor
|
164
|
+
has been advised of the possibility of such damages.
|
165
|
+
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
169
|
+
or other liability obligations and/or rights consistent with this
|
170
|
+
License. However, in accepting such obligations, You may act only
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
175
|
+
of your accepting any such warranty or additional liability.
|
176
|
+
|
177
|
+
END OF TERMS AND CONDITIONS
|
178
|
+
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
180
|
+
|
181
|
+
To apply the Apache License to your work, attach the following
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
183
|
+
replaced with your own identifying information. (Don't include
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
185
|
+
comment syntax for the file format. We also recommend that a
|
186
|
+
file or class name and description of purpose be included on the
|
187
|
+
same "printed page" as the copyright notice for easier
|
188
|
+
identification within third-party archives.
|
189
|
+
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
191
|
+
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
193
|
+
you may not use this file except in compliance with the License.
|
194
|
+
You may obtain a copy of the License at
|
195
|
+
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
197
|
+
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
201
|
+
See the License for the specific language governing permissions and
|
202
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# AttributedString
|
2
|
+
|
3
|
+
An attributed string implementation for Ruby.
|
4
|
+
|
5
|
+
An attributed string contains key-value pairs known as attributes that specify additional information related to ranges of characters within the string. Attributed strings support any key-value pair, but are often used for:
|
6
|
+
|
7
|
+
- Rendering attributes such as font, color, and other details.
|
8
|
+
- Attributes for inline-attachments such as images, videos, files, etc.
|
9
|
+
- Semantic attributes such as link URLs or tool-tip information
|
10
|
+
- Language attributes to support automatic gender agreement or verb agreement.
|
11
|
+
- Accessibility attributes that provide information for assistive technologies
|
12
|
+
- Custom attributes you define
|
13
|
+
|
14
|
+
You will typically need to create a presenter for an attributed string, as the default shows no attribute information and inspect shows all attributes.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'attributed-string', github: 'instruct-rb/attributed-string', branch: 'main'
|
22
|
+
```
|
23
|
+
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
🚧 This gem is a work in progress and the API may change before 1.0.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
using AttributedString::Refinements
|
31
|
+
hello_world1 = AttributedString.new('Hello, World!', color: :red, font: 'Helvetica' )
|
32
|
+
hello_world2 = 'Hello, World!'.to_attr_s(color: :red, font: 'Helvetica')
|
33
|
+
puts hello_world1 == hello_world2 # true
|
34
|
+
```
|
35
|
+
|
36
|
+
## Attachments
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
hello_world = AttributedString.new('Hello, World!')
|
40
|
+
hello_world.add_attachment("any ruby object", position: string.length)
|
41
|
+
puts hello_world # => "Hello, World! "
|
42
|
+
puts hello_world.attachments # => ["any ruby object"]
|
43
|
+
hello_world[-1..-1] = ''
|
44
|
+
puts hello_world.attachments # => []
|
45
|
+
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require_relative "attributed_string/filter_result"
|
2
|
+
require_relative "attributed_string/inspect"
|
3
|
+
require_relative "attributed_string/klass"
|
4
|
+
require_relative "attributed_string/attachment"
|
5
|
+
require_relative "attributed_string/unsupported"
|
6
|
+
require_relative "attributed_string/needs_testing"
|
7
|
+
require_relative "attributed_string/refine"
|
8
|
+
require_relative "attributed_string/string_ops"
|
9
|
+
require_relative "attributed_string/version"
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class AttributedString < String
|
2
|
+
# This character Object Replacement Character is used to represent an attachment in the string.
|
3
|
+
ATTACHMENT_CHARACTER = "\u{FFFC}".freeze
|
4
|
+
|
5
|
+
# Adds an attachment to the given position.
|
6
|
+
# @param attachment [Object] The attachment to add.
|
7
|
+
# @param position [Integer] The position to add the attachment to.
|
8
|
+
# @return [AttributedString] self for chaining
|
9
|
+
def add_attachment(attachment, position: self.length)
|
10
|
+
self.insert(position, ATTACHMENT_CHARACTER)
|
11
|
+
|
12
|
+
range = normalize_range(position..position)
|
13
|
+
raise ArgumentError, "Position out of bounds" if range.nil?
|
14
|
+
@store << { range:, attachment: }
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Deletes the attachment at a specific position.
|
19
|
+
# @param position [Integer] The index in the string.
|
20
|
+
# @return [Object] The attachment that was removed.
|
21
|
+
def delete_attachment(position)
|
22
|
+
attachment = attachment_at(position)
|
23
|
+
self[position] = ''
|
24
|
+
attachment
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if the string has an attachment
|
28
|
+
# @param range [Range] The range to check for attachments.
|
29
|
+
# @return [Boolean] Whether the string has an attachment at the given position.
|
30
|
+
def has_attachment?(range: 0...self.length)
|
31
|
+
range = normalize_range(range)
|
32
|
+
return false if range.nil?
|
33
|
+
(self.to_s[range]||'').include?(ATTACHMENT_CHARACTER)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the attachments at a specific position.
|
37
|
+
# @param position [Integer] The index in the string.
|
38
|
+
# @return <Object> The attachments at the given position.
|
39
|
+
def attachment_at(position)
|
40
|
+
result = nil
|
41
|
+
@store.each do |stored_val|
|
42
|
+
if stored_val[:range].begin == position && stored_val[:attachment]
|
43
|
+
result = stored_val[:attachment]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of attachments in the given range.
|
50
|
+
# @param range [Range] The range to check for attachments.
|
51
|
+
# @return [Array<Object>] The attachments in the given range.
|
52
|
+
def attachments(range: 0...self.length)
|
53
|
+
attachments_with_positions(range: range).map { |attachment| attachment[:attachment] }
|
54
|
+
end
|
55
|
+
|
56
|
+
# TODO: needs a test
|
57
|
+
def attachments_with_positions(range: 0...self.length)
|
58
|
+
range = normalize_range(range)
|
59
|
+
return [] if range.nil?
|
60
|
+
attachments = []
|
61
|
+
self.chars[range].map.with_index do |char, i|
|
62
|
+
attachments << { attachment: attachment_at(range.begin + i), position: range.begin + i } if char == ATTACHMENT_CHARACTER
|
63
|
+
end
|
64
|
+
attachments
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
class AttributedString < String
|
2
|
+
|
3
|
+
# Returns a filtered string the block will be called with each attribute,
|
4
|
+
# value pair. It's an inclusive filter, so if the block returns true, any
|
5
|
+
# character with that attribute will be included.
|
6
|
+
#
|
7
|
+
# This method has been slightly optimized to minimize allocations.
|
8
|
+
# @see AttributedString#filter
|
9
|
+
# @param attr_string [AttributedString]
|
10
|
+
# @param block [Proc] the block to filter the attributes.
|
11
|
+
# @return [AttributedString::FilterResult] a filtered string.
|
12
|
+
def filter(&block)
|
13
|
+
AttributedString::FilterResult.new(self, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
class FilterResult < String
|
17
|
+
# @see AttributedString#filter
|
18
|
+
def initialize(attr_string, &block)
|
19
|
+
filtered_positions = []
|
20
|
+
cached_block_calls = {}
|
21
|
+
|
22
|
+
# TODO: this can be optimized to use the same method that inspect uses which doesn't go through
|
23
|
+
# every character (it goes through each substring span with different ranges)
|
24
|
+
# A presenter type architecture that inspect, rainbow print, and filter can share would be ideal
|
25
|
+
attr_string.each_char.with_index do |char, index|
|
26
|
+
attrs = attr_string.attrs_at(index)
|
27
|
+
# Use the attrs object ID as the cache key to handle different attribute hashes
|
28
|
+
cache_key = attrs.hash
|
29
|
+
cached_result = cached_block_calls.fetch(cache_key) do
|
30
|
+
result = block.call(attrs)
|
31
|
+
cached_block_calls[cache_key] = result
|
32
|
+
result
|
33
|
+
end
|
34
|
+
if cached_result
|
35
|
+
filtered_positions << index
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Group adjacent positions into ranges to minimize allocations
|
40
|
+
ranges = []
|
41
|
+
unless filtered_positions.empty?
|
42
|
+
start_pos = filtered_positions.first
|
43
|
+
prev_pos = start_pos
|
44
|
+
filtered_positions.each_with_index do |pos, idx|
|
45
|
+
next if idx == 0
|
46
|
+
if pos == prev_pos + 1
|
47
|
+
# Continue the current range
|
48
|
+
prev_pos = pos
|
49
|
+
else
|
50
|
+
# End the current range and start a new one
|
51
|
+
ranges << (start_pos..prev_pos)
|
52
|
+
start_pos = pos
|
53
|
+
prev_pos = pos
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# Add the final range
|
57
|
+
ranges << (start_pos..prev_pos)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Concatenate substrings from the original string based on the ranges
|
61
|
+
result_string = ranges.map { |range| attr_string.send(:original_slice,range) }.join
|
62
|
+
|
63
|
+
# Build the list of original positions
|
64
|
+
original_positions = ranges.flat_map { |range| range.to_a }
|
65
|
+
|
66
|
+
super(result_string)
|
67
|
+
@original_positions = original_positions
|
68
|
+
freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
def original_position_at(index)
|
72
|
+
@original_positions.fetch(index)
|
73
|
+
end
|
74
|
+
|
75
|
+
def original_ranges_for(filtered_range)
|
76
|
+
# TODO: this doesn't work for excluded end range
|
77
|
+
raise ArgumentError, "Invalid range" unless filtered_range.is_a?(Range)
|
78
|
+
raise ArgumentError, "Range out of bounds" if filtered_range.end >= length
|
79
|
+
if filtered_range.begin > filtered_range.end
|
80
|
+
raise ArgumentError, "Reverse range is not allowed"
|
81
|
+
end
|
82
|
+
if filtered_range.begin == filtered_range.end && filtered_range.exclude_end?
|
83
|
+
return []
|
84
|
+
end
|
85
|
+
|
86
|
+
original_positions = @original_positions[filtered_range]
|
87
|
+
ranges = []
|
88
|
+
start_pos = original_positions.first
|
89
|
+
prev_pos = start_pos
|
90
|
+
|
91
|
+
original_positions.each_with_index do |pos, idx|
|
92
|
+
next if idx == 0
|
93
|
+
if pos == prev_pos + 1
|
94
|
+
# Continue the current range
|
95
|
+
prev_pos = pos
|
96
|
+
else
|
97
|
+
# End the current range and start a new one
|
98
|
+
ranges << (start_pos..prev_pos)
|
99
|
+
start_pos = pos
|
100
|
+
prev_pos = pos
|
101
|
+
end
|
102
|
+
end
|
103
|
+
# Add the final range
|
104
|
+
ranges << (start_pos..prev_pos)
|
105
|
+
ranges
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class AttributedString
|
2
|
+
# Inspect prints the attributed string in an easily readable way.
|
3
|
+
# An example inspect output "{ k1: 1 }these have the attributes k1: 1 { k1: 2, k2: true }these have 2 attrs k1: 2, and k2: true { -k2 }and these have k1: 2 { -k1 }and these have none"
|
4
|
+
# could be constructed as:
|
5
|
+
#
|
6
|
+
# ("these have the attributes k1: 1 ".to_attr_s(k1: 1) +
|
7
|
+
# "these have 2 attrs k1: 2, and k2: true ".to_attr_s(k1: 2, k2: true) +
|
8
|
+
# "and these have k1: 2 ".to_attr_s(k1: 2) +
|
9
|
+
# "and these have none").inspect
|
10
|
+
#
|
11
|
+
def inspect(color: false)
|
12
|
+
# Collect all positions where attributes change
|
13
|
+
positions = Set.new
|
14
|
+
|
15
|
+
@store.each do |attr|
|
16
|
+
range = attr[:range]
|
17
|
+
positions << range.begin
|
18
|
+
positions << range.begin + range.size
|
19
|
+
end
|
20
|
+
|
21
|
+
# Include the start and end positions of the string
|
22
|
+
positions << 0
|
23
|
+
positions << self.length
|
24
|
+
|
25
|
+
# Sort all positions
|
26
|
+
positions = positions.to_a.sort
|
27
|
+
|
28
|
+
result = ""
|
29
|
+
last_attrs = {} # Initialize as empty hash
|
30
|
+
|
31
|
+
positions.each_cons(2) do |start_pos, end_pos|
|
32
|
+
next if start_pos >= end_pos # Skip invalid ranges
|
33
|
+
|
34
|
+
substring = self.to_s[start_pos...end_pos]
|
35
|
+
attrs_before = last_attrs
|
36
|
+
attachment = attachment_at(start_pos)
|
37
|
+
attrs_after = attrs_at(start_pos)
|
38
|
+
|
39
|
+
# Determine attribute changes
|
40
|
+
ended_attrs = {}
|
41
|
+
started_attrs = {}
|
42
|
+
|
43
|
+
# Attributes that have ended or changed
|
44
|
+
attrs_before.each do |key, value|
|
45
|
+
if !attrs_after.key?(key)
|
46
|
+
# Attribute has ended
|
47
|
+
ended_attrs[key] = value
|
48
|
+
elsif attrs_after[key] != value
|
49
|
+
# Attribute value has changed; treat as ending old and starting new
|
50
|
+
ended_attrs[key] = value
|
51
|
+
started_attrs[key] = attrs_after[key]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Attributes that have started
|
56
|
+
attrs_after.each do |key, value|
|
57
|
+
if !attrs_before.key?(key)
|
58
|
+
started_attrs[key] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Remove attributes that both ended and started (value change)
|
63
|
+
ended_attrs.delete_if { |k, _| started_attrs.key?(k) }
|
64
|
+
|
65
|
+
unless ended_attrs.empty? && started_attrs.empty?
|
66
|
+
attrs_str = ended_attrs.keys.sort.map{ |k| "-#{k}" }
|
67
|
+
attrs_str += started_attrs.to_a.sort{ |a,b| a[0] <=> b[0] }.map{ |a| "#{a[0]}: #{a[1]}" }
|
68
|
+
attrs_str = "{ #{attrs_str.join(', ')} }"
|
69
|
+
result += dim(attrs_str, color: color)
|
70
|
+
end
|
71
|
+
|
72
|
+
if attachment
|
73
|
+
substring = dim("[#{attachment}]", color: color)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Append the substring
|
77
|
+
result += substring
|
78
|
+
|
79
|
+
last_attrs = attrs_after
|
80
|
+
end
|
81
|
+
|
82
|
+
# Close any remaining attributes
|
83
|
+
unless last_attrs.empty?
|
84
|
+
result += dim("{ #{last_attrs.keys.sort.map{ |k| "-#{k}" }.join(", ")} }", color: color)
|
85
|
+
end
|
86
|
+
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def dim(string, color: true)
|
92
|
+
color ? "\e[2m#{string}\e[22m" : string
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class AttributedString < String
|
2
|
+
|
3
|
+
alias_method :original_slice, :slice
|
4
|
+
private :original_slice
|
5
|
+
|
6
|
+
# Returns a new instance of AttributedString with the kwargs passed in as
|
7
|
+
# attributes for the whole string.
|
8
|
+
# @param string [String]
|
9
|
+
def initialize(string = "", **attrs)
|
10
|
+
super(string)
|
11
|
+
@store = []
|
12
|
+
if string.is_a?(AttributedString)
|
13
|
+
string.instance_variable_get(:@store).each do |entry|
|
14
|
+
@store << entry.dup
|
15
|
+
end
|
16
|
+
end
|
17
|
+
add_attrs(**attrs)
|
18
|
+
end
|
19
|
+
|
20
|
+
def dup
|
21
|
+
super.tap do |copy|
|
22
|
+
new_store = @store.map do |entry|
|
23
|
+
entry[:range] = entry[:range].dup
|
24
|
+
entry.dup
|
25
|
+
end
|
26
|
+
copy.instance_variable_set(:@store, new_store)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the given attributes in the hash to the range.
|
31
|
+
# @param range [Range] The range to apply the attributes to.
|
32
|
+
# @param attributes [Hash<Symbol, Object>] The attributes to apply to the range.
|
33
|
+
# @return [AttributedString] self for chaining
|
34
|
+
def add_attrs(range = 0..self.length - 1, **attributes)
|
35
|
+
range = normalize_range(range)
|
36
|
+
return self if attributes.empty? || self.empty? || range.nil? || range.size.zero?
|
37
|
+
@store << { range: range, attributes: attributes }
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adds the given attributes in the hash to the range.
|
42
|
+
#
|
43
|
+
# If the attribute is already set, it will be converted to an array and the new value will be appended.
|
44
|
+
#
|
45
|
+
# @param range [Range] The range to apply the attributes to.
|
46
|
+
# @param attributes [Hash<Symbol, Object>] The attributes to apply to the range.
|
47
|
+
# @return [AttributedString] self for chaining
|
48
|
+
def add_arr_attrs(range = 0..self.length - 1, **attributes)
|
49
|
+
range = normalize_range(range)
|
50
|
+
return self if attributes.empty? || self.empty? || range.nil? || range.size.zero?
|
51
|
+
@store << { range: range, arr_attributes: attributes }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
# Need to think about how this works as the attachment store is an array
|
58
|
+
# def remove_attachment(position)
|
59
|
+
# raise Todo
|
60
|
+
# end
|
61
|
+
|
62
|
+
|
63
|
+
# Removes the given attributes from a range.
|
64
|
+
# @param range_or_key [Range] The range to remove the attributes from, if its not a range, it will be treated as a key to remove.
|
65
|
+
# @param attribute_keys [Array<Symbol>] The keys of the attributes to remove.
|
66
|
+
# @return [AttributedString] self for chaining
|
67
|
+
def remove_attrs(range_or_key, *attribute_keys)
|
68
|
+
if !range_or_key.is_a?(Range)
|
69
|
+
attribute_keys << range_or_key
|
70
|
+
range_or_key = 0..self.length - 1
|
71
|
+
end
|
72
|
+
@store << { range: range_or_key, delete: attribute_keys }
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the attributes at a specific position.
|
77
|
+
# @param position [Integer] The index in the string.
|
78
|
+
# @return [Hash] The attributes at the given position.
|
79
|
+
def attrs_at(position)
|
80
|
+
result = {}
|
81
|
+
@store.each do |stored_val|
|
82
|
+
if stored_val[:range].include?(position)
|
83
|
+
if stored_val[:delete]
|
84
|
+
stored_val[:delete].each do |key|
|
85
|
+
result.delete(key)
|
86
|
+
end
|
87
|
+
elsif stored_val[:arr_attributes]
|
88
|
+
stored_val[:arr_attributes].each do |key, value|
|
89
|
+
result[key] = [result[key]] if result.key?(key) && !result[key].is_a?(Array)
|
90
|
+
if value.is_a?(Array)
|
91
|
+
(result[key] ||= []).concat(value)
|
92
|
+
else
|
93
|
+
(result[key] ||= []).push(value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
elsif stored_val[:attributes]
|
97
|
+
result.merge!(stored_val[:attributes])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
result
|
102
|
+
end
|
103
|
+
|
104
|
+
def ==(other)
|
105
|
+
return false unless other.is_a?(AttributedString)
|
106
|
+
# not super efficient, but it works for now
|
107
|
+
(0...length).all? { |i| attrs_at(i) == other.attrs_at(i) && attachment_at(i) == other.attachment_at(i) } && super
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class AttributedString
|
2
|
+
module Refinements
|
3
|
+
refine String do
|
4
|
+
unless self.class == AttributedString
|
5
|
+
# @param attrs [Hash] A hash of attributes to apply to the string.
|
6
|
+
# @return [Instruct::AttributedString] A new AttributedString with the given attributes.
|
7
|
+
def to_attr_s(**attrs)
|
8
|
+
AttributedString.new(self, **attrs)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias_method :original_eq, :==
|
12
|
+
private :original_eq
|
13
|
+
def ==(other)
|
14
|
+
return false if other.is_a?(AttributedString)
|
15
|
+
original_eq(other)
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :original_plus, :+
|
19
|
+
private :original_plus
|
20
|
+
def +(other)
|
21
|
+
if other.is_a?(AttributedString)
|
22
|
+
other.class.new(self) + other
|
23
|
+
else
|
24
|
+
original_plus(other)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :original_concat, :concat
|
29
|
+
private :original_concat
|
30
|
+
def concat(*args)
|
31
|
+
args.each do |arg|
|
32
|
+
if arg.is_a?(AttributedString)
|
33
|
+
raise ArgumentError, "Cannot concatenate AttributedString to String"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
original_concat(*args)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
class AttributedString < String
|
2
|
+
|
3
|
+
def insert(index, other)
|
4
|
+
return super if other.empty?
|
5
|
+
other = AttributedString.new(other) unless other.is_a?(AttributedString)
|
6
|
+
index = normalize_index(index, len: self.length + 1)
|
7
|
+
split_ranges(@store, index, other.length, self.length + other.length)
|
8
|
+
store = other.instance_variable_get(:@store).map { |obj| obj.dup }
|
9
|
+
translate_ranges(store, distance: index)
|
10
|
+
@store.concat(store)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def slice(arg, *args)
|
15
|
+
result = super
|
16
|
+
raise Todo, "Regular expression not implemented for slice, consider raising a pull request" if arg.is_a?(Regexp)
|
17
|
+
|
18
|
+
range = arg.is_a?(Range) ? normalize_range(arg) : normalize_integer_slice_args(arg, *args)
|
19
|
+
|
20
|
+
return range if range.nil?
|
21
|
+
raise RuntimeError if range.size != result.length
|
22
|
+
store = @store.map do |obj|
|
23
|
+
clamped = clamp_range(obj[:range], range)
|
24
|
+
next nil if clamped.nil?
|
25
|
+
obj.dup.tap do |obj|
|
26
|
+
obj[:range] = translate_range(clamped, distance: -range.begin)
|
27
|
+
end
|
28
|
+
end.compact
|
29
|
+
|
30
|
+
new_string = self.class.new(result)
|
31
|
+
new_string.instance_variable_set(:@store, store)
|
32
|
+
new_string
|
33
|
+
end
|
34
|
+
alias_method :[], :slice
|
35
|
+
|
36
|
+
def slice!(arg, *args)
|
37
|
+
result = slice(arg, *args)
|
38
|
+
return result if result.nil?
|
39
|
+
|
40
|
+
range = arg.is_a?(Range) ? normalize_range(arg) : normalize_integer_slice_args(arg, *args)
|
41
|
+
return result if range.nil?
|
42
|
+
|
43
|
+
super(range)
|
44
|
+
|
45
|
+
split_ranges(@store, range.begin, -range.size, self.length)
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO: this should take args not other
|
51
|
+
# can be implemented as (other, *args)
|
52
|
+
def concat(other)
|
53
|
+
if other.is_a?(AttributedString)
|
54
|
+
store = other.instance_variable_get(:@store).map { |obj| obj.dup }
|
55
|
+
translate_ranges(store, distance: self.length)
|
56
|
+
@store.concat(store)
|
57
|
+
end
|
58
|
+
super
|
59
|
+
end
|
60
|
+
alias_method :<<, :concat
|
61
|
+
|
62
|
+
def prepend(other)
|
63
|
+
if other.is_a?(AttributedString)
|
64
|
+
store = other.instance_variable_get(:@store).map { |obj| obj.dup }
|
65
|
+
translate_ranges(@store, distance: other.length)
|
66
|
+
@store.concat(store)
|
67
|
+
end
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# modifies the string or returns a modified string
|
73
|
+
def replace(other)
|
74
|
+
super
|
75
|
+
other_store = other.instance_variable_get(:@store) || []
|
76
|
+
@store = other_store.map { |obj| obj.dup }
|
77
|
+
end
|
78
|
+
|
79
|
+
def +(other)
|
80
|
+
self.dup.concat(other)
|
81
|
+
end
|
82
|
+
|
83
|
+
def []=(arg, *args, value)
|
84
|
+
if arg.is_a?(Regexp)
|
85
|
+
raise Todo, "Regular expression assignment not implemented for []=, consider raising a pull request"
|
86
|
+
end
|
87
|
+
value ||= ""
|
88
|
+
value = value.is_a?(AttributedString) ? value : self.class.new(value)
|
89
|
+
|
90
|
+
range = arg.is_a?(Range) ? normalize_range(arg) : normalize_integer_slice_args(arg, *args)
|
91
|
+
return nil if range.nil?
|
92
|
+
slice!(range)
|
93
|
+
insert(range.begin, value)
|
94
|
+
value
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def normalize_range(range, len: self.length)
|
100
|
+
return nil unless range
|
101
|
+
|
102
|
+
b = range.begin
|
103
|
+
e = range.end
|
104
|
+
exclude_end = range.exclude_end?
|
105
|
+
|
106
|
+
# Handle nil for begin and end
|
107
|
+
b = b.nil? ? 0 : b
|
108
|
+
e = e.nil? ? len : e
|
109
|
+
|
110
|
+
# Convert negative indices to positive
|
111
|
+
b += len if b < 0
|
112
|
+
e += len if e < 0
|
113
|
+
|
114
|
+
# Return nil if adjusted begin index is out of bounds
|
115
|
+
return nil if b < 0 || b > len
|
116
|
+
|
117
|
+
# Adjust indices to be within bounds
|
118
|
+
b = b.clamp(0, len)
|
119
|
+
e = e.clamp(-1, len)
|
120
|
+
|
121
|
+
# Adjust end for exclusive ranges unless both begin and end are nil
|
122
|
+
unless range.begin.nil? && range.end.nil?
|
123
|
+
e -= 1 if exclude_end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Ensure the end index does not exceed len - 1
|
127
|
+
e = [e, len - 1].min
|
128
|
+
|
129
|
+
# Return nil if begin index is outside the string
|
130
|
+
return nil if b >= len
|
131
|
+
|
132
|
+
# Return empty range if begin > end
|
133
|
+
return b...b if b > e
|
134
|
+
|
135
|
+
# Return the normalized range
|
136
|
+
b..e
|
137
|
+
end
|
138
|
+
|
139
|
+
def normalize_index(index, len: self.length)
|
140
|
+
# Convert negative index to positive equivalent
|
141
|
+
normalized_index = index < 0 ? len + index : index
|
142
|
+
|
143
|
+
# Clamp the index within string bounds
|
144
|
+
[[normalized_index, 0].max, len].min
|
145
|
+
end
|
146
|
+
|
147
|
+
def normalize_integer_slice_args(start, *args, len: self.length)
|
148
|
+
length_set = args.length == 1
|
149
|
+
length = length_set ? args[0] : 1
|
150
|
+
# Return nil if start is nil or length is nil or negative
|
151
|
+
return nil if !length_set && start >= len
|
152
|
+
|
153
|
+
# Adjust negative start index
|
154
|
+
start += len if start < 0
|
155
|
+
|
156
|
+
# Return nil if start is still negative
|
157
|
+
return nil if start < 0
|
158
|
+
|
159
|
+
# Adjust length if it extends beyond the string
|
160
|
+
max_length = len - start
|
161
|
+
length = [length, max_length].min
|
162
|
+
|
163
|
+
# Return nil if length is negative
|
164
|
+
return nil if length < 0
|
165
|
+
|
166
|
+
# Return the normalized arguments
|
167
|
+
start...(start + length)
|
168
|
+
end
|
169
|
+
|
170
|
+
# @api private
|
171
|
+
# splits the ranges in the store at the given index
|
172
|
+
# if the index is negative, it will be counted from the end of the string
|
173
|
+
# @param index [Integer] the index to split the ranges at
|
174
|
+
# @param gap [Integer] the gap to insert at the index, if its negative, it will delete
|
175
|
+
# @param new_length [Integer] the new length of the string, used to remove ranges that are out of bounds
|
176
|
+
def split_ranges(store, index, gap, new_length)
|
177
|
+
new_ranges = []
|
178
|
+
store.each_with_index do |obj, idx|
|
179
|
+
range = obj[:range]
|
180
|
+
|
181
|
+
if gap > 0
|
182
|
+
if index <= range.begin
|
183
|
+
# Shift the range
|
184
|
+
obj[:range] = translate_range(range, distance: gap)
|
185
|
+
elsif index > range.begin && index <= range.end
|
186
|
+
# Attribute spans over the insertion point; split into two
|
187
|
+
original_end = range.end
|
188
|
+
obj[:range] = range.begin..(index - 1)
|
189
|
+
new_range = (index + gap)..(original_end + gap)
|
190
|
+
new_obj = obj.dup
|
191
|
+
new_obj[:range] = new_range
|
192
|
+
new_ranges << new_obj
|
193
|
+
end
|
194
|
+
else
|
195
|
+
# Deletion logic
|
196
|
+
deletion_end = index - gap - 1 # Since gap is negative
|
197
|
+
if range.end < index
|
198
|
+
# Case 1: Attribute entirely before deletion; no change needed
|
199
|
+
next
|
200
|
+
elsif range.begin > deletion_end
|
201
|
+
# Case 2: Attribute entirely after deletion; shift it
|
202
|
+
obj[:range] = translate_range(range, distance: gap)
|
203
|
+
elsif range.begin >= index && range.end <= deletion_end
|
204
|
+
# Case 5: Attribute entirely within deletion; remove it
|
205
|
+
store[idx] = nil
|
206
|
+
elsif range.begin >= index && range.end > deletion_end
|
207
|
+
# Case 3: Attribute starts within deletion, ends after deletion
|
208
|
+
obj[:range] = (index)..(range.end + gap)
|
209
|
+
elsif range.begin < index && range.end >= index && range.end <= deletion_end
|
210
|
+
# Case 4: Attribute starts before deletion, ends within deletion
|
211
|
+
obj[:range] = range.begin..(index - 1)
|
212
|
+
elsif range.begin < index && range.end > deletion_end
|
213
|
+
# Case 6: Attribute spans over deletion
|
214
|
+
obj[:range] = range.begin..(range.end + gap)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Remove ranges that are now invalid
|
218
|
+
if obj[:range].end < obj[:range].begin || obj[:range].end < 0 || obj[:range].begin >= new_length
|
219
|
+
store[idx] = nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
store.concat(new_ranges)
|
224
|
+
store.compact!
|
225
|
+
end
|
226
|
+
|
227
|
+
def translate_ranges(store, distance:)
|
228
|
+
store.each do |obj|
|
229
|
+
obj[:range] = translate_range(obj[:range], distance:)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def translate_range(range, distance:)
|
234
|
+
range.exclude_end? ? (range.begin + distance)...(range.end + distance) : (range.begin + distance)..(range.end + distance)
|
235
|
+
end
|
236
|
+
|
237
|
+
def clamp_range(range, clamp_range)
|
238
|
+
# Determine the new start index
|
239
|
+
new_start = [range.begin, clamp_range.begin].max
|
240
|
+
|
241
|
+
# Adjust ends based on exclude_end?
|
242
|
+
range_end_adj = range.exclude_end? ? range.end - 1 : range.end
|
243
|
+
clamp_end_adj = clamp_range.exclude_end? ? clamp_range.end - 1 : clamp_range.end
|
244
|
+
|
245
|
+
# Determine the new adjusted end index
|
246
|
+
new_end_adj = [range_end_adj, clamp_end_adj].min
|
247
|
+
|
248
|
+
# Check if ranges do not overlap
|
249
|
+
if new_start > new_end_adj
|
250
|
+
return nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# Check if each range includes new_end_adj
|
254
|
+
range_includes_new_end = range.cover?(new_end_adj)
|
255
|
+
clamp_includes_new_end = clamp_range.cover?(new_end_adj)
|
256
|
+
|
257
|
+
# Determine if the new range should exclude the end
|
258
|
+
new_exclude_end = !(range_includes_new_end && clamp_includes_new_end)
|
259
|
+
|
260
|
+
# Construct the new range
|
261
|
+
if new_exclude_end
|
262
|
+
# For exclusive ranges, the end index is one beyond the last included index
|
263
|
+
new_range = new_start... (new_end_adj + 1)
|
264
|
+
else
|
265
|
+
new_range = new_start..new_end_adj
|
266
|
+
end
|
267
|
+
|
268
|
+
new_range
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
class AttributedString < String
|
2
|
+
class Todo < NotImplementedError
|
3
|
+
def message = "Not implemented, consider adding a pull request"
|
4
|
+
end
|
5
|
+
|
6
|
+
# Unsupported.
|
7
|
+
# Consider opening pull request
|
8
|
+
def gsub(*args, **kwargs, &block) = raise Todo
|
9
|
+
|
10
|
+
# Unsupported.
|
11
|
+
# Consider opening pull request
|
12
|
+
def gsub!(*args, **kwargs, &block) = raise Todo
|
13
|
+
|
14
|
+
# Unsupported.
|
15
|
+
# Consider opening pull request
|
16
|
+
def sub(*args, **kwargs, &block) = raise Todo
|
17
|
+
|
18
|
+
# Unsupported.
|
19
|
+
# Consider opening pull request
|
20
|
+
def sub!(*args, **kwargs, &block) = raise Todo
|
21
|
+
|
22
|
+
# Unsupported.
|
23
|
+
# Consider opening pull request
|
24
|
+
def succ(*args, **kwargs, &block) = raise Todo
|
25
|
+
|
26
|
+
# Unsupported.
|
27
|
+
# Consider opening pull request
|
28
|
+
def succ!(*args, **kwargs, &block) = raise Todo
|
29
|
+
|
30
|
+
# Unsupported.
|
31
|
+
# Consider opening pull request
|
32
|
+
def reverse(*args, **kwargs, &block) = raise Todo
|
33
|
+
|
34
|
+
# Unsupported.
|
35
|
+
# Consider opening pull request
|
36
|
+
def reverse!(*args, **kwargs, &block) = raise Todo
|
37
|
+
|
38
|
+
# Unsupported.
|
39
|
+
# Consider opening pull request
|
40
|
+
def setbyte(*args, **kwargs, &block) = raise Todo
|
41
|
+
|
42
|
+
# Unsupported.
|
43
|
+
# Consider opening pull request
|
44
|
+
def tr(*args, **kwargs, &block) = raise Todo
|
45
|
+
|
46
|
+
# Unsupported.
|
47
|
+
# Consider opening pull request
|
48
|
+
def tr!(*args, **kwargs, &block) = raise Todo
|
49
|
+
|
50
|
+
# Unsupported.
|
51
|
+
# Consider opening pull request
|
52
|
+
def tr_s(*args, **kwargs, &block) = raise Todo
|
53
|
+
|
54
|
+
# Unsupported.
|
55
|
+
# Consider opening pull request
|
56
|
+
def tr_s!(*args, **kwargs, &block) = raise Todo
|
57
|
+
|
58
|
+
# Unsupported.
|
59
|
+
# Consider opening pull request
|
60
|
+
def squeeze(*args, **kwargs, &block) = raise Todo
|
61
|
+
|
62
|
+
# Unsupported.
|
63
|
+
# Consider opening pull request
|
64
|
+
def squeeze!(*args, **kwargs, &block) = raise Todo
|
65
|
+
|
66
|
+
# Unsupported.
|
67
|
+
# Consider opening pull request
|
68
|
+
def lstrip(*args, **kwargs, &block) = raise Todo
|
69
|
+
|
70
|
+
# Unsupported.
|
71
|
+
# Consider opening pull request
|
72
|
+
def lstrip!(*args, **kwargs, &block) = raise Todo
|
73
|
+
|
74
|
+
# Unsupported.
|
75
|
+
# Consider opening pull request
|
76
|
+
def rstrip(*args, **kwargs, &block) = raise Todo
|
77
|
+
|
78
|
+
# Unsupported.
|
79
|
+
# Consider opening pull request
|
80
|
+
def rstrip!(*args, **kwargs, &block) = raise Todo
|
81
|
+
|
82
|
+
# Unsupported.
|
83
|
+
# Consider opening pull request
|
84
|
+
def strip(*args, **kwargs, &block) = raise Todo
|
85
|
+
|
86
|
+
# Unsupported.
|
87
|
+
# Consider opening pull request
|
88
|
+
def strip!(*args, **kwargs, &block) = raise Todo
|
89
|
+
|
90
|
+
# Unsupported.
|
91
|
+
# Consider opening pull request
|
92
|
+
def chomp(*args, **kwargs, &block) = raise Todo
|
93
|
+
|
94
|
+
# Unsupported.
|
95
|
+
# Consider opening pull request
|
96
|
+
def chomp!(*args, **kwargs, &block) = raise Todo
|
97
|
+
|
98
|
+
# Unsupported.
|
99
|
+
# Consider opening pull request
|
100
|
+
def chop(*args, **kwargs, &block) = raise Todo
|
101
|
+
|
102
|
+
# Unsupported.
|
103
|
+
# Consider opening pull request
|
104
|
+
def chop!(*args, **kwargs, &block) = raise Todo
|
105
|
+
|
106
|
+
# converts string
|
107
|
+
|
108
|
+
# Unsupported.
|
109
|
+
# Consider opening pull request
|
110
|
+
def center(*args, **kwargs, &block) = raise Todo
|
111
|
+
|
112
|
+
# Unsupported.
|
113
|
+
# Consider opening pull request
|
114
|
+
def ljust(*args, **kwargs, &block) = raise Todo
|
115
|
+
|
116
|
+
# Unsupported.
|
117
|
+
# Consider opening pull request
|
118
|
+
def rjust(*args, **kwargs, &block) = raise Todo
|
119
|
+
|
120
|
+
# Unsupported.
|
121
|
+
# Consider opening pull request
|
122
|
+
def b(*args, **kwargs, &block) = raise Todo
|
123
|
+
|
124
|
+
# Unsupported.
|
125
|
+
# Consider opening pull request
|
126
|
+
def scrub(*args, **kwargs, &block) = raise Todo
|
127
|
+
|
128
|
+
# Unsupported.
|
129
|
+
# Consider opening pull request
|
130
|
+
def unicode_normalize(*args, **kwargs, &block) = raise Todo
|
131
|
+
|
132
|
+
# Unsupported.
|
133
|
+
# Consider opening pull request
|
134
|
+
def encode(*args, **kwargs, &block) = raise Todo
|
135
|
+
|
136
|
+
# substitutions
|
137
|
+
|
138
|
+
# Unsupported.
|
139
|
+
# Consider opening pull request
|
140
|
+
def dump(*args, **kwargs, &block) = raise Todo
|
141
|
+
|
142
|
+
# Unsupported.
|
143
|
+
# Consider opening pull request
|
144
|
+
def undump(*args, **kwargs, &block) = raise Todo
|
145
|
+
|
146
|
+
# Unsupported.
|
147
|
+
# Consider opening pull request
|
148
|
+
def %(*args, **kwargs, &block) = raise Todo
|
149
|
+
|
150
|
+
# more deletes
|
151
|
+
|
152
|
+
# Unsupported.
|
153
|
+
# Consider opening pull request
|
154
|
+
def delete(*args, **kwargs, &block) = raise Todo
|
155
|
+
|
156
|
+
# Unsupported.
|
157
|
+
# Consider opening pull request
|
158
|
+
def delete!(*args, **kwargs, &block) = raise Todo
|
159
|
+
|
160
|
+
# Unsupported.
|
161
|
+
# Consider opening pull request
|
162
|
+
def delete_prefix(*args, **kwargs, &block) = raise Todo
|
163
|
+
|
164
|
+
# Unsupported.
|
165
|
+
# Consider opening pull request
|
166
|
+
def delete_prefix!(*args, **kwargs, &block) = raise Todo
|
167
|
+
|
168
|
+
# Unsupported.
|
169
|
+
# Consider opening pull request
|
170
|
+
def delete_suffix(*args, **kwargs, &block) = raise Todo
|
171
|
+
|
172
|
+
# Unsupported.
|
173
|
+
# Consider opening pull request
|
174
|
+
def delete_suffix!(*args, **kwargs, &block) = raise Todo
|
175
|
+
|
176
|
+
# Unsupported.
|
177
|
+
# Consider opening pull request
|
178
|
+
def byteslice(*args, **kwargs, &block) = raise Todo
|
179
|
+
|
180
|
+
# Unsupported.
|
181
|
+
# Consider opening pull request
|
182
|
+
def chr(*args, **kwargs, &block) = raise Todo
|
183
|
+
|
184
|
+
# Unsupported.
|
185
|
+
# Consider opening pull request
|
186
|
+
def *(*args, **kwargs, &block) = raise Todo
|
187
|
+
|
188
|
+
|
189
|
+
# Unsupported.
|
190
|
+
# Consider opening pull request
|
191
|
+
def lines(*args, **kwargs, &block) = raise Todo
|
192
|
+
|
193
|
+
# Unsupported.
|
194
|
+
# Consider opening pull request
|
195
|
+
def partition(*args, **kwargs, &block) = raise Todo
|
196
|
+
|
197
|
+
# Unsupported.
|
198
|
+
# Consider opening pull request
|
199
|
+
def rpartition(*args, **kwargs, &block) = raise Todo
|
200
|
+
|
201
|
+
# Unsupported.
|
202
|
+
# Consider opening pull request
|
203
|
+
def split(*args, **kwargs, &block) = raise Todo
|
204
|
+
|
205
|
+
# Unsupported.
|
206
|
+
# Consider opening pull request
|
207
|
+
def scan(*args, **kwargs, &block) = raise Todo
|
208
|
+
|
209
|
+
# Unsupported.
|
210
|
+
# Consider opening pull request
|
211
|
+
def unpack(*args, **kwargs, &block) = raise Todo
|
212
|
+
|
213
|
+
# Unsupported.
|
214
|
+
# Consider opening pull request
|
215
|
+
def unpack1(*args, **kwargs, &block) = raise Todo
|
216
|
+
|
217
|
+
# Unsupported.
|
218
|
+
# Consider opening pull request
|
219
|
+
def upto(*args, **kwargs, &block) = raise Todo
|
220
|
+
|
221
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
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
|
+
- Andrew Mackross
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-20 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: andrew@mackross.net
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE
|
20
|
+
- README.md
|
21
|
+
- lib/attributed-string.rb
|
22
|
+
- lib/attributed_string/attachment.rb
|
23
|
+
- lib/attributed_string/filter_result.rb
|
24
|
+
- lib/attributed_string/inspect.rb
|
25
|
+
- lib/attributed_string/klass.rb
|
26
|
+
- lib/attributed_string/needs_testing.rb
|
27
|
+
- lib/attributed_string/refine.rb
|
28
|
+
- lib/attributed_string/string_ops.rb
|
29
|
+
- lib/attributed_string/unsupported.rb
|
30
|
+
- lib/attributed_string/version.rb
|
31
|
+
homepage: https://github.com/instruct-rb/attributed-string
|
32
|
+
licenses:
|
33
|
+
- Apache-2.0
|
34
|
+
metadata:
|
35
|
+
source_code_uri: https://github.com/instruct-rb/attributed-string
|
36
|
+
homepage_uri: https://github.com/instruct-rb/attributed-string
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.2.3
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubygems_version: 3.5.16
|
53
|
+
signing_key:
|
54
|
+
specification_version: 4
|
55
|
+
summary: An attributed string implementation for Ruby.
|
56
|
+
test_files: []
|