string_magic 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +59 -53
- data/lib/string_magic/advanced/security.rb +99 -20
- data/lib/string_magic/core/analysis.rb +115 -24
- data/lib/string_magic/core/transformation.rb +84 -36
- data/lib/string_magic/core/validation.rb +88 -0
- data/lib/string_magic/formatting/highlighting.rb +55 -8
- data/lib/string_magic/formatting/truncation.rb +65 -12
- data/lib/string_magic/utilities/inflection.rb +71 -0
- data/lib/string_magic/utilities/slug.rb +91 -0
- data/lib/string_magic/version.rb +1 -1
- data/lib/string_magic.rb +19 -63
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c860708ae5aa73e721fc45b4aa173a79c75a2645bcf244a416e7f758e089d2cd
|
4
|
+
data.tar.gz: ded9547fb9a55ab72c7f4a15cfee03a707811d4c4a7bbf46f66d4c1dc99f8b3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4efe0e495f4dcbeb8ffffe2f8f049c79b79c81ab991049698a659a9d795d4c11988e0b060017734d60f1cd201732c9bcdb9f23f85c3d4cf8a050757ea1287626
|
7
|
+
data.tar.gz: 3ef437f7a57cfab7e031aadbbf64cbf76851ab684fcf5a4e0059fb0c74e312e34c53c44da42ef0acd5984c2748c3ba0c97bb0752a42816c1bd441c39503a4c02
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## [0.5.0] - 2025-08-12
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- **StringMagic::Core::Validation** - Email, URL, phone, credit card validation and more
|
6
|
+
- **StringMagic::Utilities::Slug** - Convert text to URL-safe slugs and filename-safe strings
|
7
|
+
- **StringMagic::Utilities::Inflection** - Pluralize, singularize, ordinalize, and humanize text
|
8
|
+
|
9
|
+
### Enhanced
|
10
|
+
|
11
|
+
- **StringMagic::Core::Transformation** - New case conversions and text manipulation methods
|
12
|
+
- **StringMagic::Core::Analysis** - Improved entity extraction and sentiment analysis
|
13
|
+
- **StringMagic::Formatting::Truncation** - Smart word, sentence, and character truncation
|
14
|
+
- **StringMagic::Formatting::Highlighting** - HTML highlighting and URL auto-linking
|
15
|
+
- **StringMagic::Advanced::Security** - Mask sensitive data like credit cards and emails
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
|
19
|
+
- Modular code organization
|
20
|
+
- All methods now available as both String instance methods and module methods
|
21
|
+
|
1
22
|
## [0.4.0] - 2025-01-01
|
2
23
|
|
3
24
|
### Added
|
data/README.md
CHANGED
@@ -1,75 +1,81 @@
|
|
1
|
-
# StringMagic
|
1
|
+
# StringMagic - v0.5.0
|
2
|
+
[](https://badge.fury.io/rb/string_magic)
|
3
|
+
[](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
|
4
|
+
[](LICENSE)
|
2
5
|
|
3
|
-
|
4
|
-
[](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
|
5
|
-
[](https://opensource.org/licenses/MIT)
|
6
|
+
**StringMagic** super-charges Ruby's `String` with batteries-included helpers for formatting, validation, analysis, transformation, and security all in one tidy gem.
|
6
7
|
|
7
|
-
##
|
8
|
+
## Why StringMagic?
|
9
|
+
- Zero dependencies – pure Ruby
|
10
|
+
- Non-destructive: original strings are never mutated
|
11
|
+
- Thread-safe & fully tested (160+ RSpec examples, ~98% coverage)
|
12
|
+
- Drop-in: optionally auto-mixes into String, or use module methods (`StringMagic.to_slug(...)`)
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
## Description
|
12
|
-
|
13
|
-
The StringMagic gem enriches Ruby's string class by adding a suite of versatile methods. These methods offer extended capabilities for formatting, manipulating, and querying text, making string operations more efficient and expressive.
|
14
|
-
|
15
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/string_magic`. To experiment with that code, run `bin/console` for an interactive prompt.
|
16
|
-
|
17
|
-
## Installation
|
18
|
-
|
19
|
-
Add this line to your Gemfile:
|
14
|
+
## Quickstart Cheatsheet
|
20
15
|
|
21
16
|
```ruby
|
22
|
-
|
17
|
+
# Formatting
|
18
|
+
"Hello world".truncate_words(1) # => "Hello..."
|
19
|
+
"user@domain.com".mask_emails # => "u********@domain.com"
|
20
|
+
"Ruby on Rails".highlight('Ruby') # => "<mark>Ruby</mark> on Rails"
|
21
|
+
|
22
|
+
# Validation
|
23
|
+
"test@example.com".email? # => true
|
24
|
+
"4111111111111111".credit_card? # => true
|
25
|
+
"racecar".palindrome? # => true
|
26
|
+
|
27
|
+
# Transformation
|
28
|
+
"CamelCase".to_snake_case # => "camel_case"
|
29
|
+
"user input".to_filename_safe # => "user_input"
|
30
|
+
"second".ordinalize # => "2nd"
|
31
|
+
|
32
|
+
# Analysis
|
33
|
+
"Visit https://example.com".extract_urls # => ["https://example.com"]
|
34
|
+
"Great product!".sentiment_indicators # => {:positive=>1.0, :negative=>0.0, :neutral=>0}
|
23
35
|
```
|
24
36
|
|
25
|
-
|
26
|
-
|
27
|
-
```ruby
|
28
|
-
bundle install
|
29
|
-
```
|
30
|
-
|
31
|
-
Or install it globally:
|
32
|
-
|
33
|
-
```ruby
|
37
|
+
## Installation
|
38
|
+
```bash
|
34
39
|
gem install string_magic
|
35
40
|
```
|
36
|
-
|
37
|
-
## Usage
|
38
|
-
|
41
|
+
Or add to your Gemfile:
|
39
42
|
```ruby
|
40
|
-
|
41
|
-
|
42
|
-
# Example usage
|
43
|
-
string = "hello world"
|
44
|
-
puts StringMagic.palindrome?(string)
|
43
|
+
gem 'string_magic'
|
45
44
|
```
|
46
45
|
|
47
|
-
##
|
46
|
+
## Full Documentation
|
48
47
|
|
49
|
-
|
48
|
+
### Core Operations
|
49
|
+
- **Case conversion**: `to_snake_case`, `to_kebab_case`, `to_camel_case`, `to_pascal_case`, `to_title_case`
|
50
|
+
- **Text manipulation**: `reverse_words`, `alternating_case`, `remove_duplicate_chars`, `remove_duplicate_words`
|
51
|
+
- **HTML handling**: `remove_html_tags`, `escape_html`
|
50
52
|
|
51
|
-
|
53
|
+
### Validation
|
54
|
+
- Format checks: `email?`, `url?`, `phone?`, `credit_card?`
|
55
|
+
- Text relations: `palindrome?`, `anagram_of?`
|
56
|
+
- Password strength: `strong_password?`
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
58
|
+
### Analysis
|
59
|
+
- **Entity extraction**: `extract_emails`, `extract_urls`, `extract_phones`, `extract_dates`
|
60
|
+
- **Text metrics**: `readability_score`, `word_frequency`, `sentiment_indicators`
|
56
61
|
|
57
|
-
|
62
|
+
### Formatting
|
63
|
+
- **Truncation**: `truncate_words`, `truncate_sentences`, `truncate_characters`, `smart_truncate`
|
64
|
+
- **Highlighting**: `highlight`, `remove_highlights`, `highlight_urls`
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
66
|
+
### Security
|
67
|
+
- **Data masking**: `mask_sensitive_data`, `mask_credit_cards`, `mask_emails`, `mask_phones`
|
68
|
+
- **Detection**: `contains_sensitive_data?`
|
62
69
|
|
63
|
-
|
70
|
+
### Utilities
|
71
|
+
- **Inflection**: `to_plural`, `to_singular`, `ordinalize`, `humanize`
|
72
|
+
- **Slug generation**: `to_slug`, `to_url_slug`, `to_filename_safe`
|
64
73
|
|
65
74
|
## Contributing
|
66
|
-
|
67
|
-
|
75
|
+
1. Fork → `git checkout -b feature/awesome`
|
76
|
+
2. Add specs for your change
|
77
|
+
3. `bundle exec rspec`
|
78
|
+
4. PR ✉️ – we love improvements!
|
68
79
|
|
69
80
|
## License
|
70
|
-
|
71
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
72
|
-
|
73
|
-
## Code of Conduct
|
74
|
-
|
75
|
-
Everyone interacting in the StringMagic project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/erscript/string-magic/blob/main/CODE_OF_CONDUCT.md).
|
81
|
+
MIT - See [LICENSE](LICENSE) for details.
|
@@ -1,38 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StringMagic
|
2
4
|
module Advanced
|
3
5
|
module Security
|
4
|
-
|
5
|
-
|
6
|
+
# ------------------------------------------------------------
|
7
|
+
# Public API
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
|
10
|
+
# Detects and masks sensitive data in the string.
|
11
|
+
#
|
12
|
+
# options:
|
13
|
+
# :mask_char → character to use for masking (default '*')
|
14
|
+
# :preserve_count → trailing digits to leave clear (default 4)
|
15
|
+
# :types → array of types to mask (default [:credit_card, :ssn, :email, :phone])
|
16
|
+
#
|
17
|
+
def mask_sensitive_data(options = {})
|
18
|
+
return '' if empty?
|
6
19
|
|
7
|
-
mask_char
|
8
|
-
options
|
20
|
+
mask_char = options.fetch(:mask_char, '*')
|
21
|
+
preserve_count = options.fetch(:preserve_count, 4)
|
22
|
+
types = options.fetch(:types, %i[credit_card ssn email phone])
|
9
23
|
|
10
24
|
patterns = {
|
11
|
-
credit_card: /\b
|
12
|
-
ssn:
|
13
|
-
email:
|
25
|
+
credit_card: /\b(?:\d{4}[-\s]?){3}\d{4}\b/,
|
26
|
+
ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/,
|
27
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
|
28
|
+
phone: /(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/
|
14
29
|
}
|
15
30
|
|
16
|
-
result =
|
31
|
+
result = dup
|
32
|
+
types.each do |type|
|
33
|
+
next unless patterns[type]
|
17
34
|
|
18
|
-
|
19
|
-
result.gsub!(pattern) do |match|
|
35
|
+
result.gsub!(patterns[type]) do |match|
|
20
36
|
case type
|
21
|
-
when :credit_card
|
22
|
-
|
23
|
-
|
24
|
-
when :
|
25
|
-
digits = match.gsub(/[-\s]/, "")
|
26
|
-
mask_char * 5 + digits[-4..]
|
27
|
-
when :email
|
28
|
-
local, domain = match.split("@")
|
29
|
-
"#{mask_char * local.length}@#{domain}"
|
37
|
+
when :credit_card then mask_credit_card(match, mask_char, preserve_count)
|
38
|
+
when :ssn then mask_ssn(match, mask_char, preserve_count)
|
39
|
+
when :email then mask_email(match, mask_char)
|
40
|
+
when :phone then mask_phone(match, mask_char, preserve_count)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
33
|
-
|
34
44
|
result
|
35
45
|
end
|
46
|
+
|
47
|
+
# Convenience wrappers
|
48
|
+
def mask_credit_cards(mask_char: '*', preserve_count: 4)
|
49
|
+
mask_sensitive_data(types: [:credit_card], mask_char: mask_char, preserve_count: preserve_count)
|
50
|
+
end
|
51
|
+
|
52
|
+
def mask_emails(mask_char: '*')
|
53
|
+
mask_sensitive_data(types: [:email], mask_char: mask_char)
|
54
|
+
end
|
55
|
+
|
56
|
+
def mask_phones(mask_char: '*', preserve_count: 4)
|
57
|
+
mask_sensitive_data(types: [:phone], mask_char: mask_char, preserve_count: preserve_count)
|
58
|
+
end
|
59
|
+
|
60
|
+
def contains_sensitive_data?
|
61
|
+
return false if empty?
|
62
|
+
|
63
|
+
[
|
64
|
+
/\b(?:\d{4}[-\s]?){3}\d{4}\b/, # Credit card
|
65
|
+
/\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/, # SSN
|
66
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ # Email
|
67
|
+
].any? { |re| match?(re) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# ------------------------------------------------------------
|
71
|
+
# Private helpers
|
72
|
+
# ------------------------------------------------------------
|
73
|
+
private
|
74
|
+
|
75
|
+
# Mask a phone number while preserving formatting characters and last n digits.
|
76
|
+
def mask_phone(original, mask_char, preserve_count)
|
77
|
+
digits_only = original.gsub(/\D/, '')
|
78
|
+
return original if digits_only.length <= preserve_count
|
79
|
+
|
80
|
+
masked_part = mask_char * (digits_only.length - preserve_count)
|
81
|
+
preserved_part = digits_only[-preserve_count..]
|
82
|
+
replacement_pool = (masked_part + preserved_part).chars
|
83
|
+
index = 0
|
84
|
+
|
85
|
+
original.gsub(/\d/) { replacement_pool[index].tap { index += 1 } }
|
86
|
+
end
|
87
|
+
|
88
|
+
def mask_credit_card(original, mask_char, preserve_count)
|
89
|
+
digits = original.gsub(/\D/, '')
|
90
|
+
masked_len = [digits.length - preserve_count, 0].max
|
91
|
+
mask_char * masked_len + digits[-preserve_count..]
|
92
|
+
end
|
93
|
+
|
94
|
+
def mask_ssn(original, mask_char, preserve_count)
|
95
|
+
mask_credit_card(original, mask_char, preserve_count)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Email masking policy:
|
99
|
+
# - If local part length <= 4 → keep first char, then 8 mask chars (e.g. "mail" => "m********")
|
100
|
+
# - Else → keep first char, mask the remainder (no last-char preservation)
|
101
|
+
def mask_email(original, mask_char)
|
102
|
+
local, domain = original.split('@', 2)
|
103
|
+
if local.length <= 4
|
104
|
+
masked_local = local[0] + (mask_char * 8)
|
105
|
+
else
|
106
|
+
masked_local = local[0] + (mask_char * (local.length - 1))
|
107
|
+
end
|
108
|
+
"#{masked_local}@#{domain}"
|
109
|
+
end
|
36
110
|
end
|
37
111
|
end
|
38
112
|
end
|
113
|
+
|
114
|
+
# Optional automatic mix-in
|
115
|
+
class String
|
116
|
+
include StringMagic::Advanced::Security
|
117
|
+
end
|
@@ -1,49 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StringMagic
|
2
4
|
module Core
|
3
5
|
module Analysis
|
4
|
-
|
5
|
-
|
6
|
+
# ------------------------------------------------------------------
|
7
|
+
# Entity extraction
|
8
|
+
# ------------------------------------------------------------------
|
9
|
+
|
10
|
+
def extract_entities
|
11
|
+
return default_entities_hash if empty?
|
6
12
|
|
7
13
|
{
|
8
|
-
emails:
|
9
|
-
urls:
|
10
|
-
phone_numbers:
|
11
|
-
dates:
|
12
|
-
hashtags:
|
13
|
-
mentions:
|
14
|
+
emails: extract_emails,
|
15
|
+
urls: extract_urls,
|
16
|
+
phone_numbers: extract_phones,
|
17
|
+
dates: extract_dates,
|
18
|
+
hashtags: extract_hashtags,
|
19
|
+
mentions: extract_mentions
|
14
20
|
}
|
15
21
|
end
|
16
22
|
|
17
|
-
def
|
18
|
-
return
|
23
|
+
def extract_emails
|
24
|
+
return [] if empty?
|
25
|
+
scan(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/).uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
def extract_urls
|
29
|
+
return [] if empty?
|
30
|
+
|
31
|
+
# initial capture
|
32
|
+
urls = scan(%r{https?://[^\s<>"']+|www\.[^\s<>"']+})
|
33
|
+
|
34
|
+
# strip trailing punctuation like . , ; : ! ? )
|
35
|
+
urls.map { |u| u.gsub(/[\.,;:!?)+]+\z/, '') }.uniq
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_phones
|
39
|
+
return [] if empty?
|
40
|
+
phone_re = /(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/
|
41
|
+
scan(phone_re).uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
def extract_dates
|
45
|
+
return [] if empty?
|
46
|
+
|
47
|
+
patterns = [
|
48
|
+
%r{\b\d{1,2}[-/]\d{1,2}[-/]\d{2,4}\b}, # 01/31/2025 or 31-01-25
|
49
|
+
%r{\b\d{4}[-/]\d{1,2}[-/]\d{1,2}\b}, # 2025-01-31
|
50
|
+
/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4}\b/i,
|
51
|
+
/\b\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{4}\b/i
|
52
|
+
]
|
53
|
+
|
54
|
+
patterns.flat_map { |re| scan(re) }.uniq
|
55
|
+
end
|
56
|
+
|
57
|
+
def extract_hashtags
|
58
|
+
return [] if empty?
|
59
|
+
scan(/#(\w+)/).flatten.uniq
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_mentions
|
63
|
+
return [] if empty?
|
64
|
+
scan(/(?:^|\s)@([A-Za-z0-9_]+)/).flatten.uniq
|
65
|
+
end
|
66
|
+
|
67
|
+
# ------------------------------------------------------------------
|
68
|
+
# Text statistics
|
69
|
+
# ------------------------------------------------------------------
|
19
70
|
|
20
|
-
|
21
|
-
|
22
|
-
.reject(&:empty?)
|
71
|
+
def readability_score
|
72
|
+
return 0.0 if empty?
|
23
73
|
|
24
|
-
|
74
|
+
sentences = split(/[.!?]+/).map(&:strip).reject(&:empty?)
|
75
|
+
return 0.0 if sentences.empty?
|
25
76
|
|
26
|
-
words =
|
27
|
-
return 0 if words.empty?
|
77
|
+
words = scan(/\b[\p{L}\p{N}'-]+\b/)
|
78
|
+
return 0.0 if words.empty?
|
28
79
|
|
29
|
-
syllables = words.sum { |
|
80
|
+
syllables = words.sum { |w| calculate_syllables(w) }
|
81
|
+
return 0.0 if syllables.zero?
|
30
82
|
|
31
|
-
score =
|
32
|
-
11.8 * (syllables.to_f / words.
|
83
|
+
score = 0.39 * (words.size.to_f / sentences.size) +
|
84
|
+
11.8 * (syllables.to_f / words.size) - 15.59
|
33
85
|
|
34
|
-
|
86
|
+
score.round(1).clamp(0, Float::INFINITY)
|
35
87
|
end
|
36
88
|
|
89
|
+
def word_frequency
|
90
|
+
return {} if empty?
|
91
|
+
downcase.scan(/\b[\p{L}\p{N}'-]+\b/).tally
|
92
|
+
end
|
93
|
+
|
94
|
+
def sentiment_indicators
|
95
|
+
return { positive: 0, negative: 0, neutral: 1 } if empty?
|
96
|
+
|
97
|
+
positive_words = %w[good great excellent amazing wonderful fantastic happy joy love like best awesome]
|
98
|
+
negative_words = %w[bad terrible awful horrible sad hate dislike worst annoying frustrating]
|
99
|
+
|
100
|
+
words = downcase.scan(/\b[\p{L}\p{N}'-]+\b/)
|
101
|
+
pos = words.count { |w| positive_words.include?(w) }
|
102
|
+
neg = words.count { |w| negative_words.include?(w) }
|
103
|
+
total = pos + neg
|
104
|
+
|
105
|
+
if total.zero?
|
106
|
+
{ positive: 0, negative: 0, neutral: 1 }
|
107
|
+
else
|
108
|
+
{ positive: (pos.to_f / total).round(2),
|
109
|
+
negative: (neg.to_f / total).round(2),
|
110
|
+
neutral: 0 }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# ------------------------------------------------------------------
|
115
|
+
# Private helpers
|
116
|
+
# ------------------------------------------------------------------
|
37
117
|
private
|
38
118
|
|
119
|
+
def default_entities_hash
|
120
|
+
{ emails: [], urls: [], phone_numbers: [], dates: [], hashtags: [], mentions: [] }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Rough syllable estimator (en-US)
|
39
124
|
def calculate_syllables(word)
|
40
|
-
|
41
|
-
return
|
125
|
+
w = word.downcase.gsub(/[^a-z]/, '')
|
126
|
+
return 1 if w.length <= 2
|
42
127
|
|
43
|
-
count =
|
44
|
-
count -= 1 if
|
128
|
+
count = w.scan(/[aeiouy]+/).size
|
129
|
+
count -= 1 if w.end_with?('e') && count > 1
|
130
|
+
count += 1 if w.end_with?('le') && w[-3] !~ /[aeiouy]/i
|
45
131
|
[count, 1].max
|
46
132
|
end
|
47
133
|
end
|
48
134
|
end
|
49
135
|
end
|
136
|
+
|
137
|
+
# Optional auto-mix-in
|
138
|
+
class String
|
139
|
+
include StringMagic::Core::Analysis
|
140
|
+
end
|
@@ -1,54 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi' # for HTML escaping
|
4
|
+
|
1
5
|
module StringMagic
|
2
6
|
module Core
|
3
7
|
module Transformation
|
4
|
-
|
5
|
-
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
# Case conversions
|
10
|
+
# ------------------------------------------------------------
|
11
|
+
|
12
|
+
def to_snake_case
|
13
|
+
return '' if empty?
|
14
|
+
|
15
|
+
str = dup
|
16
|
+
str.gsub!(/::/, '/')
|
17
|
+
str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
18
|
+
str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
19
|
+
str.tr!('-', '_')
|
20
|
+
str.gsub!(/\s+/, '_')
|
21
|
+
str.downcase!
|
22
|
+
str.squeeze('_').gsub(/^_+|_+$/, '')
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_kebab_case
|
26
|
+
to_snake_case.tr('_', '-')
|
27
|
+
end
|
28
|
+
|
29
|
+
# first_letter: :lower (default) or :upper
|
30
|
+
def to_camel_case(first_letter: :lower)
|
31
|
+
return '' if empty?
|
32
|
+
|
33
|
+
words = to_snake_case.split('_').map(&:capitalize)
|
34
|
+
words[0].downcase! if first_letter == :lower && words.any?
|
35
|
+
words.join
|
36
|
+
end
|
6
37
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
38
|
+
def to_pascal_case
|
39
|
+
to_camel_case(first_letter: :upper)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_title_case
|
43
|
+
return '' if empty?
|
44
|
+
|
45
|
+
small = %w[a an and as at but by for if in nor of on or so the to up yet]
|
46
|
+
words = downcase.split(/\s+/)
|
47
|
+
words.map!.with_index do |w, i|
|
48
|
+
(i.zero? || i == words.size - 1 || !small.include?(w)) ? w.capitalize : w
|
49
|
+
end
|
50
|
+
words.join(' ')
|
51
|
+
end
|
52
|
+
|
53
|
+
# ------------------------------------------------------------
|
54
|
+
# Simple word / char utilities
|
55
|
+
# ------------------------------------------------------------
|
15
56
|
|
16
|
-
|
17
|
-
|
57
|
+
def reverse_words
|
58
|
+
return '' if empty?
|
59
|
+
split(/\s+/).reverse.join(' ')
|
60
|
+
end
|
18
61
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
special_cases[word]
|
23
|
-
elsif prefixes.include?(word) && !index.zero?
|
24
|
-
word
|
25
|
-
elsif articles.include?(word) && !index.zero? # Add this condition
|
26
|
-
word.downcase
|
27
|
-
else
|
28
|
-
word.capitalize
|
29
|
-
end
|
30
|
-
end.join(" ")
|
62
|
+
def alternating_case
|
63
|
+
return '' if empty?
|
64
|
+
each_char.with_index.map { |c, i| i.even? ? c.upcase : c.downcase }.join
|
31
65
|
end
|
32
66
|
|
33
|
-
def
|
34
|
-
return
|
67
|
+
def remove_duplicate_chars
|
68
|
+
return '' if empty?
|
69
|
+
each_char.to_a.uniq.join
|
70
|
+
end
|
35
71
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
.downcase
|
72
|
+
def remove_duplicate_words
|
73
|
+
return '' if empty?
|
74
|
+
split(/\s+/).uniq.join(' ')
|
40
75
|
end
|
41
76
|
|
42
|
-
|
43
|
-
|
77
|
+
# ------------------------------------------------------------
|
78
|
+
# Misc. text transformations
|
79
|
+
# ------------------------------------------------------------
|
80
|
+
|
81
|
+
def squeeze_whitespace
|
82
|
+
return '' if empty?
|
83
|
+
gsub(/\s+/, ' ').strip
|
44
84
|
end
|
45
85
|
|
46
|
-
def
|
47
|
-
return
|
48
|
-
|
86
|
+
def remove_html_tags
|
87
|
+
return '' if empty?
|
88
|
+
gsub(/<[^>]*>/, '')
|
89
|
+
end
|
49
90
|
|
50
|
-
|
91
|
+
def escape_html
|
92
|
+
return '' if empty?
|
93
|
+
CGI.escapeHTML(self)
|
51
94
|
end
|
52
95
|
end
|
53
96
|
end
|
54
97
|
end
|
98
|
+
|
99
|
+
# Optional automatic mix-in
|
100
|
+
class String
|
101
|
+
include StringMagic::Core::Transformation
|
102
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module StringMagic
|
6
|
+
module Core
|
7
|
+
module Validation
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
# Basic format checks
|
10
|
+
# ------------------------------------------------------------
|
11
|
+
|
12
|
+
def email?
|
13
|
+
!!(self =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
14
|
+
end
|
15
|
+
|
16
|
+
def url?
|
17
|
+
!!(self =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/)
|
18
|
+
end
|
19
|
+
|
20
|
+
def phone?
|
21
|
+
digits = gsub(/\D/, '')
|
22
|
+
(10..15).cover?(digits.length)
|
23
|
+
end
|
24
|
+
|
25
|
+
# ------------------------------------------------------------
|
26
|
+
# Credit-card number (Luhn)
|
27
|
+
# ------------------------------------------------------------
|
28
|
+
|
29
|
+
def credit_card?
|
30
|
+
digits = gsub(/\D/, '')
|
31
|
+
return false unless (13..19).cover?(digits.length)
|
32
|
+
|
33
|
+
sum = digits.reverse.chars.each_with_index.sum do |ch, idx|
|
34
|
+
n = ch.to_i
|
35
|
+
n *= 2 if idx.odd?
|
36
|
+
n > 9 ? n - 9 : n
|
37
|
+
end
|
38
|
+
(sum % 10).zero?
|
39
|
+
end
|
40
|
+
|
41
|
+
# ------------------------------------------------------------
|
42
|
+
# Text relationships
|
43
|
+
# ------------------------------------------------------------
|
44
|
+
|
45
|
+
def palindrome?
|
46
|
+
cleaned = downcase.gsub(/[^a-z0-9]/, '')
|
47
|
+
cleaned == cleaned.reverse && !cleaned.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def anagram_of?(other)
|
51
|
+
return false if other.nil? || other.empty?
|
52
|
+
|
53
|
+
norm = ->(s) { s.downcase.gsub(/[^a-z0-9]/, '').chars.sort.join }
|
54
|
+
!empty? && norm.call(self) == norm.call(other)
|
55
|
+
end
|
56
|
+
|
57
|
+
# ------------------------------------------------------------
|
58
|
+
# Number checks
|
59
|
+
# ------------------------------------------------------------
|
60
|
+
|
61
|
+
def numeric?
|
62
|
+
!!(self =~ /\A-?(?:\d+\.?\d*|\.\d+)\z/)
|
63
|
+
end
|
64
|
+
|
65
|
+
def integer?
|
66
|
+
!!(self =~ /\A-?\d+\z/)
|
67
|
+
end
|
68
|
+
|
69
|
+
# ------------------------------------------------------------
|
70
|
+
# Password strength
|
71
|
+
# ------------------------------------------------------------
|
72
|
+
|
73
|
+
def strong_password?(min_length: 8)
|
74
|
+
return false if length < min_length
|
75
|
+
|
76
|
+
/[A-Z]/.match?(self) &&
|
77
|
+
/[a-z]/.match?(self) &&
|
78
|
+
/\d/.match?(self) &&
|
79
|
+
/[^A-Za-z0-9]/.match?(self)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Optional auto-mix-in
|
86
|
+
class String
|
87
|
+
include StringMagic::Core::Validation
|
88
|
+
end
|
@@ -1,19 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StringMagic
|
2
4
|
module Formatting
|
3
5
|
module Highlighting
|
4
|
-
|
5
|
-
|
6
|
+
# ------------------------------------------------------------
|
7
|
+
# Generic phrase highlighting
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
#
|
10
|
+
# highlight(%w[ruby rails], tag: 'span',
|
11
|
+
# css_class: 'hit', case_sensitive: false)
|
12
|
+
#
|
13
|
+
def highlight(phrases, tag: 'mark', css_class: nil, case_sensitive: true)
|
14
|
+
return '' if empty?
|
15
|
+
|
16
|
+
phrases = Array(phrases).compact.reject(&:empty?)
|
17
|
+
return self if phrases.empty?
|
6
18
|
|
7
|
-
|
8
|
-
|
9
|
-
|
19
|
+
klass = css_class ? %( class="#{css_class}") : ''
|
20
|
+
esc = phrases.sort_by(&:length).reverse.map { |p| Regexp.escape(p) }
|
21
|
+
regex = Regexp.union(esc)
|
22
|
+
regex = Regexp.new(regex.source, Regexp::IGNORECASE) unless case_sensitive
|
10
23
|
|
11
|
-
|
24
|
+
gsub(regex) { |m| "<#{tag}#{klass}>#{m}</#{tag}>" }
|
25
|
+
end
|
12
26
|
|
13
|
-
|
14
|
-
|
27
|
+
# ------------------------------------------------------------
|
28
|
+
# Remove previously added highlighting
|
29
|
+
# ------------------------------------------------------------
|
30
|
+
#
|
31
|
+
# remove_highlights # strips ALL html tags
|
32
|
+
# remove_highlights('mark') # strips only <mark>…</mark>
|
33
|
+
#
|
34
|
+
def remove_highlights(tag = nil)
|
35
|
+
return '' if empty?
|
36
|
+
|
37
|
+
if tag
|
38
|
+
gsub(/<#{Regexp.escape(tag)}[^>]*>(.*?)<\/#{Regexp.escape(tag)}>/im, '\1')
|
39
|
+
else
|
40
|
+
gsub(/<[^>]+>/, '')
|
15
41
|
end
|
16
42
|
end
|
43
|
+
|
44
|
+
# ------------------------------------------------------------
|
45
|
+
# Auto-link / highlight URLs
|
46
|
+
# ------------------------------------------------------------
|
47
|
+
#
|
48
|
+
# highlight_urls(css_class: 'link', target: '_blank')
|
49
|
+
#
|
50
|
+
def highlight_urls(tag: 'a', css_class: nil, target: nil)
|
51
|
+
return '' if empty?
|
52
|
+
|
53
|
+
klass = css_class ? %( class="#{css_class}") : ''
|
54
|
+
tgt = target ? %( target="#{target}") : ''
|
55
|
+
url_re = %r{https?://[^\s<>"']+}i
|
56
|
+
|
57
|
+
gsub(url_re) { |url| %(<#{tag} href="#{url}"#{klass}#{tgt}>#{url}</#{tag}>) }
|
58
|
+
end
|
17
59
|
end
|
18
60
|
end
|
19
61
|
end
|
62
|
+
|
63
|
+
# Optional automatic mix-in
|
64
|
+
class String
|
65
|
+
include StringMagic::Formatting::Highlighting
|
66
|
+
end
|
@@ -1,25 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StringMagic
|
2
4
|
module Formatting
|
3
5
|
module Truncation
|
4
|
-
|
5
|
-
|
6
|
+
# ------------------------------------------------------------
|
7
|
+
# Word-based
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
def truncate_words(limit, suffix: '...', separator: ' ')
|
10
|
+
return '' if empty?
|
11
|
+
words = split(separator)
|
12
|
+
return self if words.size <= limit
|
13
|
+
words.first(limit).join(separator) + suffix
|
14
|
+
end
|
15
|
+
|
16
|
+
# ------------------------------------------------------------
|
17
|
+
# Sentence-based
|
18
|
+
# ------------------------------------------------------------
|
19
|
+
def truncate_sentences(limit, suffix: '...')
|
20
|
+
return '' if empty?
|
21
|
+
sentences = split(/(?<=[.!?])\s+/)
|
22
|
+
return self if sentences.size <= limit
|
23
|
+
result = sentences.first(limit).join(' ')
|
24
|
+
result.chomp!('.')
|
25
|
+
result.chomp!('!')
|
26
|
+
result.chomp!('?')
|
27
|
+
result += suffix unless result == self
|
28
|
+
end
|
6
29
|
|
7
|
-
|
8
|
-
|
9
|
-
|
30
|
+
# ------------------------------------------------------------
|
31
|
+
# Character-based
|
32
|
+
# ------------------------------------------------------------
|
33
|
+
#
|
34
|
+
# break_on_word: true → keep whole words
|
35
|
+
# break_on_word: false → may cut mid-word (default)
|
36
|
+
#
|
37
|
+
def truncate_characters(limit, suffix: '...', break_on_word: false)
|
38
|
+
return '' if empty? || length <= limit
|
39
|
+
|
40
|
+
# When break_on_word is true, we want the result to be exactly `limit` chars total
|
41
|
+
target_content_len = limit - suffix.length
|
42
|
+
return suffix[0, limit] if target_content_len <= 0
|
10
43
|
|
11
|
-
|
44
|
+
if break_on_word
|
45
|
+
cut = rindex(' ', target_content_len) || target_content_len
|
46
|
+
self[0, cut].rstrip + suffix
|
47
|
+
else
|
48
|
+
self[0, target_content_len] + suffix
|
49
|
+
end
|
12
50
|
end
|
13
51
|
|
14
|
-
|
15
|
-
|
52
|
+
# ------------------------------------------------------------
|
53
|
+
# "Smart" truncate: sentence ▸ word ▸ hard cut
|
54
|
+
# ------------------------------------------------------------
|
55
|
+
def smart_truncate(limit, suffix: '...')
|
56
|
+
return '' if empty? || length <= limit
|
57
|
+
hard_len = limit - suffix.length
|
58
|
+
return suffix[0, limit] if hard_len.negative?
|
59
|
+
|
60
|
+
# Try sentence boundary
|
61
|
+
sent_break = rindex(/[.!?]\s/, hard_len)
|
62
|
+
return self[0..sent_break].rstrip + suffix if sent_break && sent_break > limit * 0.5
|
16
63
|
|
17
|
-
|
18
|
-
|
19
|
-
return
|
64
|
+
# Try word boundary
|
65
|
+
word_break = rindex(' ', hard_len)
|
66
|
+
return self[0, word_break].rstrip + suffix if word_break && word_break > limit * 0.3
|
20
67
|
|
21
|
-
|
68
|
+
# Fallback
|
69
|
+
self[0, hard_len] + suffix
|
22
70
|
end
|
23
71
|
end
|
24
72
|
end
|
25
73
|
end
|
74
|
+
|
75
|
+
# Optional auto-mix-in
|
76
|
+
class String
|
77
|
+
include StringMagic::Formatting::Truncation
|
78
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StringMagic
|
4
|
+
module Utilities
|
5
|
+
module Inflection
|
6
|
+
# ------------------------------------------------------------
|
7
|
+
# Plural ↔ singular
|
8
|
+
# ------------------------------------------------------------
|
9
|
+
|
10
|
+
def to_plural
|
11
|
+
return '' if empty?
|
12
|
+
|
13
|
+
case downcase
|
14
|
+
when /(s|sh|ch|x|z)\z/ then self + 'es'
|
15
|
+
when /[^aeiou]y\z/i then chop + 'ies'
|
16
|
+
when /(f|fe)\z/ then sub(/(f|fe)\z/, 'ves')
|
17
|
+
else self + 's'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_singular
|
22
|
+
return '' if empty?
|
23
|
+
|
24
|
+
down = downcase
|
25
|
+
if down.end_with?('ies') then sub(/ies\z/i, 'y')
|
26
|
+
elsif down.end_with?('ves') && length > 3
|
27
|
+
sub(/ves\z/i, down[-4] == 'i' ? 'fe' : 'f') # knives → knife, leaves → leaf
|
28
|
+
elsif down =~ /(ses|shes|ches|xes|zes)\z/i then sub(/es\z/i, '')
|
29
|
+
elsif down.end_with?('s') && length > 1 then chop
|
30
|
+
else self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# ------------------------------------------------------------
|
35
|
+
# Ordinalisation
|
36
|
+
# ------------------------------------------------------------
|
37
|
+
#
|
38
|
+
# '1'.ordinalize #=> '1st'
|
39
|
+
#
|
40
|
+
def ordinalize
|
41
|
+
return self unless /\A-?\d+\z/.match?(self)
|
42
|
+
|
43
|
+
num = to_i
|
44
|
+
suffix = if (11..13).cover?(num % 100)
|
45
|
+
'th'
|
46
|
+
else
|
47
|
+
{ 1 => 'st', 2 => 'nd', 3 => 'rd' }.fetch(num % 10, 'th')
|
48
|
+
end
|
49
|
+
self + suffix
|
50
|
+
end
|
51
|
+
|
52
|
+
# ------------------------------------------------------------
|
53
|
+
# Human-readable
|
54
|
+
# ------------------------------------------------------------
|
55
|
+
|
56
|
+
def humanize
|
57
|
+
return '' if empty?
|
58
|
+
gsub('_', ' ')
|
59
|
+
.gsub(/([a-z])([A-Z])/, '\1 \2')
|
60
|
+
.downcase
|
61
|
+
.strip
|
62
|
+
.capitalize
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Optional auto-mix-in
|
69
|
+
class String
|
70
|
+
include StringMagic::Utilities::Inflection
|
71
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StringMagic
|
4
|
+
module Utilities
|
5
|
+
module Slug
|
6
|
+
# ------------------------------------------------------------------
|
7
|
+
# Public helpers
|
8
|
+
# ------------------------------------------------------------------
|
9
|
+
|
10
|
+
def to_slug(separator: '-', preserve_case: false)
|
11
|
+
return '' if empty?
|
12
|
+
|
13
|
+
result = dup
|
14
|
+
result.downcase! unless preserve_case
|
15
|
+
result = transliterate_accents(result)
|
16
|
+
result.gsub!(/<[^>]*>/, '') # strip HTML
|
17
|
+
result.gsub!(/[^A-Za-z0-9]+/, separator) # non-alnum → sep
|
18
|
+
result.gsub!(/^#{Regexp.escape(separator)}+|#{Regexp.escape(separator)}+$/, '')
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_url_slug
|
23
|
+
to_slug(separator: '-', preserve_case: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_filename_safe(replacement: '_', preserve_case: false)
|
27
|
+
return '' if empty?
|
28
|
+
result = transliterate_accents(dup)
|
29
|
+
result.downcase! unless preserve_case
|
30
|
+
result.gsub!(/[^0-9A-Za-z\.\-]+/, replacement) # keep only safe chars
|
31
|
+
# convert file extension: ".txt" → "_txt"
|
32
|
+
result.gsub!(/\.([a-z0-9]{1,5})\z/i, "#{replacement}\\1")
|
33
|
+
result.gsub!(/\.+$/, '') # drop any remaining trailing dots
|
34
|
+
result.gsub!(/#{Regexp.escape(replacement)}+/, replacement) # collapse runs
|
35
|
+
result.gsub!(/^#{Regexp.escape(replacement)}+|#{Regexp.escape(replacement)}+$/, '') # trim
|
36
|
+
result = 'untitled' if result.empty?
|
37
|
+
result = "#{result}_file" if reserved_filename?(result)
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def slugify_path(separator: '/')
|
42
|
+
return '' if empty?
|
43
|
+
split('/').map { |seg| seg.to_slug }.reject(&:empty?).join(separator)
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_slug_from_url
|
47
|
+
return '' if empty?
|
48
|
+
clean = split('/').last.to_s.split(/[?#]/).first
|
49
|
+
clean.gsub!(/\.\w+$/, '') if clean.match?(/\.\w+$/)
|
50
|
+
clean
|
51
|
+
end
|
52
|
+
|
53
|
+
# ------------------------------------------------------------------
|
54
|
+
# Private helpers
|
55
|
+
# ------------------------------------------------------------------
|
56
|
+
private
|
57
|
+
|
58
|
+
def transliterate_accents(text)
|
59
|
+
accents = {
|
60
|
+
'À'=>'A','Á'=>'A','Â'=>'A','Ã'=>'A','Ä'=>'A','Å'=>'A',
|
61
|
+
'à'=>'a','á'=>'a','â'=>'a','ã'=>'a','ä'=>'a','å'=>'a',
|
62
|
+
'È'=>'E','É'=>'E','Ê'=>'E','Ë'=>'E',
|
63
|
+
'è'=>'e','é'=>'e','ê'=>'e','ë'=>'e',
|
64
|
+
'Ì'=>'I','Í'=>'I','Î'=>'I','Ï'=>'I',
|
65
|
+
'ì'=>'i','í'=>'i','î'=>'i','ï'=>'i',
|
66
|
+
'Ò'=>'O','Ó'=>'O','Ô'=>'O','Õ'=>'O','Ö'=>'O',
|
67
|
+
'ò'=>'o','ó'=>'o','ô'=>'o','õ'=>'o','ö'=>'o',
|
68
|
+
'Ù'=>'U','Ú'=>'U','Û'=>'U','Ü'=>'U',
|
69
|
+
'ù'=>'u','ú'=>'u','û'=>'u','ü'=>'u',
|
70
|
+
'Ñ'=>'N','ñ'=>'n',
|
71
|
+
'Ç'=>'C','ç'=>'c',
|
72
|
+
'ß'=>'ss'
|
73
|
+
}
|
74
|
+
accents.each { |from, to| text.gsub!(from, to) }
|
75
|
+
text
|
76
|
+
end
|
77
|
+
|
78
|
+
def reserved_filename?(name)
|
79
|
+
%w[
|
80
|
+
CON PRN AUX NUL COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9
|
81
|
+
LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9
|
82
|
+
].include?(name.upcase)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Optional auto-mix-in
|
89
|
+
class String
|
90
|
+
include StringMagic::Utilities::Slug
|
91
|
+
end
|
data/lib/string_magic/version.rb
CHANGED
data/lib/string_magic.rb
CHANGED
@@ -2,10 +2,24 @@
|
|
2
2
|
|
3
3
|
require_relative "string_magic/version"
|
4
4
|
require_relative "string_magic/core/analysis"
|
5
|
-
require_relative "string_magic/core/transformation"
|
5
|
+
require_relative "string_magic/core/transformation"
|
6
|
+
require_relative "string_magic/core/validation"
|
6
7
|
require_relative "string_magic/formatting/highlighting"
|
7
8
|
require_relative "string_magic/formatting/truncation"
|
8
9
|
require_relative "string_magic/advanced/security"
|
10
|
+
require_relative "string_magic/utilities/slug"
|
11
|
+
require_relative "string_magic/utilities/inflection"
|
12
|
+
|
13
|
+
class String
|
14
|
+
include StringMagic::Core::Analysis
|
15
|
+
include StringMagic::Core::Transformation
|
16
|
+
include StringMagic::Core::Validation
|
17
|
+
include StringMagic::Formatting::Highlighting
|
18
|
+
include StringMagic::Formatting::Truncation
|
19
|
+
include StringMagic::Advanced::Security
|
20
|
+
include StringMagic::Utilities::Slug
|
21
|
+
include StringMagic::Utilities::Inflection
|
22
|
+
end
|
9
23
|
|
10
24
|
module StringMagic
|
11
25
|
class Error < StandardError; end
|
@@ -13,68 +27,10 @@ module StringMagic
|
|
13
27
|
|
14
28
|
extend Core::Analysis
|
15
29
|
extend Core::Transformation
|
30
|
+
extend Core::Validation
|
16
31
|
extend Formatting::Highlighting
|
17
32
|
extend Formatting::Truncation
|
18
33
|
extend Advanced::Security
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.word_count(string)
|
25
|
-
string.split.size
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.palindrome?(string)
|
29
|
-
cleaned = string.downcase.gsub(/[^a-z0-9]/, "")
|
30
|
-
cleaned == cleaned.reverse
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.capitalize_words(string)
|
34
|
-
string.split.map(&:capitalize).join(" ")
|
35
|
-
end
|
36
|
-
|
37
|
-
def self.reverse_words(string)
|
38
|
-
string.split.reverse.join(" ")
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.remove_duplicates(string)
|
42
|
-
string.chars.uniq.join
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.count_vowels(string)
|
46
|
-
string.downcase.count("aeiou")
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.to_pig_latin(string)
|
50
|
-
string.split.map do |word|
|
51
|
-
if word[0] =~ /[aeiou]/i
|
52
|
-
word + "ay"
|
53
|
-
else
|
54
|
-
word[1..-1] + word[0] + "ay"
|
55
|
-
end
|
56
|
-
end.join(" ")
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.alternating_case(string)
|
60
|
-
string.chars.map.with_index { |char, i| i.even? ? char.upcase : char.downcase }.join
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.camel_case(string)
|
64
|
-
string.split.map(&:capitalize).join
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.snake_case(string)
|
68
|
-
string.downcase.gsub(/\s+/, "_")
|
69
|
-
end
|
70
|
-
|
71
|
-
def self.title_case(string)
|
72
|
-
string.split.map(&:capitalize).join(" ")
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.anagram?(string1, string2)
|
76
|
-
processed_string1 = string1.downcase.gsub(/[^a-z0-9]/, "").chars.sort.join
|
77
|
-
processed_string2 = string2.downcase.gsub(/[^a-z0-9]/, "").chars.sort.join
|
78
|
-
processed_string1 == processed_string2
|
79
|
-
end
|
80
|
-
end
|
34
|
+
extend Utilities::Slug
|
35
|
+
extend Utilities::Inflection
|
36
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: string_magic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emad Rahimi
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rspec
|
@@ -72,8 +71,11 @@ files:
|
|
72
71
|
- lib/string_magic/advanced/security.rb
|
73
72
|
- lib/string_magic/core/analysis.rb
|
74
73
|
- lib/string_magic/core/transformation.rb
|
74
|
+
- lib/string_magic/core/validation.rb
|
75
75
|
- lib/string_magic/formatting/highlighting.rb
|
76
76
|
- lib/string_magic/formatting/truncation.rb
|
77
|
+
- lib/string_magic/utilities/inflection.rb
|
78
|
+
- lib/string_magic/utilities/slug.rb
|
77
79
|
- lib/string_magic/version.rb
|
78
80
|
- sig/string_magic.rbs
|
79
81
|
homepage: https://github.com/erscript/string-magic
|
@@ -83,7 +85,6 @@ metadata:
|
|
83
85
|
homepage_uri: https://github.com/erscript/string-magic
|
84
86
|
source_code_uri: https://github.com/erscript/string-magic
|
85
87
|
changelog_uri: https://github.com/erscript/string-magic/blob/main/CHANGELOG.md
|
86
|
-
post_install_message:
|
87
88
|
rdoc_options: []
|
88
89
|
require_paths:
|
89
90
|
- lib
|
@@ -98,8 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
99
|
- !ruby/object:Gem::Version
|
99
100
|
version: '0'
|
100
101
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
102
|
-
signing_key:
|
102
|
+
rubygems_version: 3.6.9
|
103
103
|
specification_version: 4
|
104
104
|
summary: The StringMagic gem enriches Ruby's string class with an array of versatile
|
105
105
|
methods designed for enhanced text formatting, manipulation, and analysis.
|