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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c2974ab8c5a68c297377233418f680d5f92ddc66d1c2c4490744c136ac6982a
4
- data.tar.gz: da5732a7df509fb6d1ff3c73990a477f9b1417fb0ffd50e4c18819fd495b17f8
3
+ metadata.gz: c860708ae5aa73e721fc45b4aa173a79c75a2645bcf244a416e7f758e089d2cd
4
+ data.tar.gz: ded9547fb9a55ab72c7f4a15cfee03a707811d4c4a7bbf46f66d4c1dc99f8b3c
5
5
  SHA512:
6
- metadata.gz: b82312455117a5efdf8cc12d58dd825e0cc268c351199185c5430125eede62d4180ec71f5497bbec627dca9a3ebffee177d5a8480fd3fedfb469709145530f2b
7
- data.tar.gz: 2520fb7095a7ccdfc3ea91e9e702e0906c37dc6f810b1f5e9231c4aeb8759747ccfce1a05a20488187463627e537c298bd864647a7b3309ddc894c5dd69c551e
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
+ [![Gem Version](https://badge.fury.io/rb/string_magic.svg)](https://badge.fury.io/rb/string_magic)
3
+ [![Build Status](https://dl.circleci.com/status-badge/img/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-lightgreen.svg)](LICENSE)
2
5
 
3
- [![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=rb&r=r&ts=1683906897&type=6e&v=0.4.0&x2=0)](https://badge.fury.io/rb/string_magic)
4
- [![CircleCI](https://dl.circleci.com/status-badge/img/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main.svg?style=svg&circle-token=CCIPRJ_PF8xu3Svcj2Ro4D8jhjCi7_71b7c0a7c781e09fc7194cd58cca67aecdc111b5)](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ## Summary
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
- A gem that enhances Ruby's string class with an array of versatile methods designed for enhanced text formatting, manipulation, and analysis.
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
- gem 'string_magic'
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
- And then execute:
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
- require 'string_magic'
41
-
42
- # Example usage
43
- string = "hello world"
44
- puts StringMagic.palindrome?(string)
43
+ gem 'string_magic'
45
44
  ```
46
45
 
47
- ## Development
46
+ ## Full Documentation
48
47
 
49
- After checking out the repo, run `bin/setup` to install dependencies. Then, run rake spec to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run:
53
+ ### Validation
54
+ - Format checks: `email?`, `url?`, `phone?`, `credit_card?`
55
+ - Text relations: `palindrome?`, `anagram_of?`
56
+ - Password strength: `strong_password?`
52
57
 
53
- ```ruby
54
- bundle exec rake install
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
- To release a new version, update the version number in version.rb, and then run:
62
+ ### Formatting
63
+ - **Truncation**: `truncate_words`, `truncate_sentences`, `truncate_characters`, `smart_truncate`
64
+ - **Highlighting**: `highlight`, `remove_highlights`, `highlight_urls`
58
65
 
59
- ```ruby
60
- bundle exec rake release
61
- ```
66
+ ### Security
67
+ - **Data masking**: `mask_sensitive_data`, `mask_credit_cards`, `mask_emails`, `mask_phones`
68
+ - **Detection**: `contains_sensitive_data?`
62
69
 
63
- This will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/erscript/string-magic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/erscript/string-magic/blob/main/CODE_OF_CONDUCT.md).
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
- def mask_sensitive_data(text, options = {})
5
- return text if text.nil? || text.empty?
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 = options[:mask_char] || "*"
8
- options[:preserve_count] || 4
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\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/,
12
- ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/,
13
- email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/
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 = text.dup
31
+ result = dup
32
+ types.each do |type|
33
+ next unless patterns[type]
17
34
 
18
- patterns.each do |type, pattern|
19
- result.gsub!(pattern) do |match|
35
+ result.gsub!(patterns[type]) do |match|
20
36
  case type
21
- when :credit_card
22
- digits = match.gsub(/[-\s]/, "")
23
- mask_char * 12 + digits[-4..]
24
- when :ssn
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
- def extract_entities(text)
5
- raise MalformedInputError, "Input must be a string" unless text.is_a?(String)
6
+ # ------------------------------------------------------------------
7
+ # Entity extraction
8
+ # ------------------------------------------------------------------
9
+
10
+ def extract_entities
11
+ return default_entities_hash if empty?
6
12
 
7
13
  {
8
- emails: text.scan(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/).uniq,
9
- urls: text.scan(%r{(?:https?:)?//[^\s/$.?#][^\s,]*}).uniq,
10
- phone_numbers: text.scan(/(?:\+\d{1,3}\s?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/).uniq,
11
- dates: text.scan(%r{\b(?:\d{1,2}[-/]\d{1,2}[-/]\d{2,4}|(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*|(?:January|February|March|April|May|June|July|August|September|October|November|December)) \d{1,2},? \d{4}|\d{4}[-/]\d{1,2}[-/]\d{1,2})\b}i).uniq,
12
- hashtags: text.scan(/#[[:word:]]+/).map { |tag| tag[1..] }.uniq,
13
- mentions: text.scan(/@[[:word:]]+/).map { |mention| mention[1..] }.uniq
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 readability_score(text)
18
- return 0 if text.empty?
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
- sentences = text.split(/[.!?]+/)
21
- .map(&:strip)
22
- .reject(&:empty?)
71
+ def readability_score
72
+ return 0.0 if empty?
23
73
 
24
- return 0 if sentences.empty? || !text.match?(/[.!?]/)
74
+ sentences = split(/[.!?]+/).map(&:strip).reject(&:empty?)
75
+ return 0.0 if sentences.empty?
25
76
 
26
- words = text.split(/\s+/)
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 { |word| calculate_syllables(word) }
80
+ syllables = words.sum { |w| calculate_syllables(w) }
81
+ return 0.0 if syllables.zero?
30
82
 
31
- score = (0.39 * (words.length.to_f / sentences.length) +
32
- 11.8 * (syllables.to_f / words.length) - 15.59)
83
+ score = 0.39 * (words.size.to_f / sentences.size) +
84
+ 11.8 * (syllables.to_f / words.size) - 15.59
33
85
 
34
- [score.round(1), 0].max
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
- word = word.downcase.gsub(/[^a-z]/, "")
41
- return 0 if word.empty?
125
+ w = word.downcase.gsub(/[^a-z]/, '')
126
+ return 1 if w.length <= 2
42
127
 
43
- count = word.scan(/[aeiou]+/i).size
44
- count -= 1 if word.end_with?("e")
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
- def titleize_names(text)
5
- return text if text.nil? || text.empty?
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
- special_cases = {
8
- "mcdonald" => "McDonald",
9
- "o'reilly" => "O'Reilly",
10
- "macbook" => "MacBook",
11
- "iphone" => "iPhone",
12
- "ipad" => "iPad",
13
- "ebay" => "eBay"
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
- prefixes = Set.new(%w[van de la du das dos di da delle degli delle])
17
- articles = Set.new(%w[a an the]) # Add this line
57
+ def reverse_words
58
+ return '' if empty?
59
+ split(/\s+/).reverse.join(' ')
60
+ end
18
61
 
19
- words = text.downcase.split(/\s+/)
20
- words.map.with_index do |word, index|
21
- if special_cases.key?(word)
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 to_snake_case(string)
34
- return "" if string.nil?
67
+ def remove_duplicate_chars
68
+ return '' if empty?
69
+ each_char.to_a.uniq.join
70
+ end
35
71
 
36
- string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
37
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
38
- .tr("-", "_")
39
- .downcase
72
+ def remove_duplicate_words
73
+ return '' if empty?
74
+ split(/\s+/).uniq.join(' ')
40
75
  end
41
76
 
42
- def to_kebab_case(string)
43
- to_snake_case(string).tr("_", "-")
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 to_pascal_case(string)
47
- return "" if string.nil?
48
- return string if string.match?(/^[A-Z][a-z]*([A-Z][a-z]*)*$/)
86
+ def remove_html_tags
87
+ return '' if empty?
88
+ gsub(/<[^>]*>/, '')
89
+ end
49
90
 
50
- string.split(/[-_\s]+/).map(&:capitalize).join
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
- def highlight(text, phrases, options = {})
5
- return text if text.nil? || phrases.nil?
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
- tag = options[:tag] || "mark"
8
- css_class = options[:class]
9
- phrases = Array(phrases)
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
- class_attr = css_class ? %( class="#{css_class}") : ""
24
+ gsub(regex) { |m| "<#{tag}#{klass}>#{m}</#{tag}>" }
25
+ end
12
26
 
13
- phrases.reduce(text) do |result, phrase|
14
- result.gsub(/(#{Regexp.escape(phrase)})/i, "<#{tag}#{class_attr}>\\1</#{tag}>")
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
- def truncate_words(text, count, options = {})
5
- return text if text.nil? || count.nil? || count < 1
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
- suffix = options[:suffix] || "..."
8
- words = text.split(/\s+/)
9
- return text if words.length <= count
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
- words[0...count].join(" ") + suffix
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
- def truncate_sentences(text, count, options = {})
15
- return text if text.nil? || count.nil? || count < 1
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
- suffix = options[:suffix] || "..."
18
- sentences = text.split(/(?<=[.!?])\s+/)
19
- return text if sentences.length <= count
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
- sentences[0...count].join(" ") + suffix
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StringMagic
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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
- def self.hello_world
21
- "hello world!"
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.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: 2025-01-02 00:00:00.000000000 Z
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.5.11
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.