philiprehberger-string_kit 0.3.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: 4a290f7b9aa5a9534b5f0f55086b416671e860b89bd58df4dae1558e8451f489
4
- data.tar.gz: e4162f06038b8da1ad5f3b67249e3c1776fe9213014c2287a305addc163a978e
3
+ metadata.gz: 72e46f8edcca8e48d21af91556ac0e4126553b361787cb01a3afd322a1549b1f
4
+ data.tar.gz: 0dfb5daf94589ea718e366fa58e4bee558a5bd082940b0cf6b32d5c6cd53226a
5
5
  SHA512:
6
- metadata.gz: df68e8c923b8ab6693894e01068317abbb4254ab10063d5859c81dbc2e1c585160e7ec4fa94189329b25ad86ae8b6b6728fa153c990fd74537b22653796abd51
7
- data.tar.gz: d41749d919b01bff185fe0bd555b65a0b7c2989e27fb9cb7d0b33cd301fdeabb1491467637eee0ac09b5eb9d29c0d4481d47c73f0a9fbadc7c2fa2e93cf2f84e
6
+ metadata.gz: 0a4f5c0b411e7f40dcb455e199c93bc6cda99486a7286ce80cc17ca82d499a8f502f14c879863de900abe241cd2980c2ba26bf29d90fe0bd97cc7b2271c44d94
7
+ data.tar.gz: 451a1b8a585ec486fee920b9ef1f881e8b8ff1a1a8c264d94f65acbcb0baea4e803063bb8ffca006b3a10b79f28cd013a0788f579e2aba98b40af69f9a6d2a96
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-05-31
11
+
12
+ ### Added
13
+ - `StringKit.word_wrap(str, width)` wraps a string to lines of at most `width` characters on word boundaries. Words longer than `width` are kept intact on their own line; existing newlines are honored as forced breaks. Raises `Error` for non-positive widths.
14
+
15
+ ## [0.4.0] - 2026-05-30
16
+
17
+ ### Added
18
+ - `StringKit.mask(str, show_first:, show_last:, mask_char:)` for partial string obfuscation
19
+ - `StringKit.between(str, left, right)` to extract text between delimiters
20
+ - `StringKit.truncate_words(str, max_words, omission:)` for word-aware truncation
21
+
22
+ ### Fixed
23
+ - README now includes the standard package card image after the badges
24
+
10
25
  ## [0.3.0] - 2026-04-25
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-string_kit.svg)](https://rubygems.org/gems/philiprehberger-string_kit)
5
5
  [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-string-kit)](https://github.com/philiprehberger/rb-string-kit/commits/main)
6
6
 
7
+ ![philiprehberger-string_kit](https://raw.githubusercontent.com/philiprehberger/rb-string-kit/main/package-card.webp)
8
+
7
9
  Comprehensive string utilities without ActiveSupport dependency
8
10
 
9
11
  ## Requirements
@@ -91,6 +93,45 @@ Philiprehberger::StringKit.levenshtein('kitten', 'sitting') # => 3
91
93
  Philiprehberger::StringKit.similarity('kitten', 'sitting') # => ~0.571
92
94
  ```
93
95
 
96
+ ### Masking
97
+
98
+ ```ruby
99
+ Philiprehberger::StringKit.mask('4242424242424242', show_last: 4) # => "************4242"
100
+ Philiprehberger::StringKit.mask('alice@example.com', show_first: 2, show_last: 4) # => "al***********.com"
101
+ Philiprehberger::StringKit.mask('password123', show_last: 3, mask_char: '#') # => "########123"
102
+ ```
103
+
104
+ ### Between Delimiters
105
+
106
+ ```ruby
107
+ Philiprehberger::StringKit.between('hello [world] there', '[', ']') # => "world"
108
+ Philiprehberger::StringKit.between('a(b)c(d)', '(', ')') # => "b"
109
+ Philiprehberger::StringKit.between('no brackets here', '[', ']') # => nil
110
+ ```
111
+
112
+ ### Word-Aware Truncation
113
+
114
+ ```ruby
115
+ Philiprehberger::StringKit.truncate_words('The quick brown fox jumps', 3) # => "The quick brown…"
116
+ Philiprehberger::StringKit.truncate_words('Two words', 5) # => "Two words"
117
+ Philiprehberger::StringKit.truncate_words('a b c d e', 2, omission: '...') # => "a b..."
118
+ ```
119
+
120
+ ### Word Wrap
121
+
122
+ ```ruby
123
+ Philiprehberger::StringKit.word_wrap('The quick brown fox jumps over the lazy dog', 15)
124
+ # => "The quick brown\nfox jumps over\nthe lazy dog"
125
+
126
+ # Words longer than `width` are kept intact on their own line
127
+ Philiprehberger::StringKit.word_wrap('Supercalifragilistic words', 10)
128
+ # => "Supercalifragilistic\nwords"
129
+
130
+ # Existing newlines are honored as forced breaks
131
+ Philiprehberger::StringKit.word_wrap("one two\nthree four", 7)
132
+ # => "one two\nthree\nfour"
133
+ ```
134
+
94
135
  ## API
95
136
 
96
137
  | Method | Description |
@@ -118,6 +159,10 @@ Philiprehberger::StringKit.similarity('kitten', 'sitting') # => ~0.571
118
159
  | `.strip_zero_width(str)` | Remove zero-width and invisible Unicode characters |
119
160
  | `.levenshtein(a, b)` | Edit distance between two strings |
120
161
  | `.similarity(a, b)` | 0.0–1.0 similarity derived from Levenshtein distance |
162
+ | `StringKit.mask(str, show_first:, show_last:, mask_char:)` | Mask the middle of a string for partial obfuscation |
163
+ | `StringKit.between(str, left, right)` | Extract text between the first occurrence of two delimiters |
164
+ | `StringKit.truncate_words(str, max_words, omission:)` | Truncate to the first `max_words` words with an omission marker |
165
+ | `StringKit.word_wrap(str, width)` | Wrap to lines of at most `width` characters on word boundaries |
121
166
 
122
167
  ## Development
123
168
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module StringKit
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -325,6 +325,77 @@ module Philiprehberger
325
325
  1.0 - (levenshtein(a, b).to_f / max)
326
326
  end
327
327
 
328
+ # Mask a string by replacing its middle portion with `mask_char`,
329
+ # leaving `show_first` characters at the start and `show_last` at the end.
330
+ # Returns `str` unchanged when there is not enough room to mask at least
331
+ # two characters in the middle.
332
+ #
333
+ # @param str [String]
334
+ # @param show_first [Integer] number of characters to leave visible at the start
335
+ # @param show_last [Integer] number of characters to leave visible at the end
336
+ # @param mask_char [String] character used to mask the hidden portion (default: '*')
337
+ # @return [String]
338
+ def self.mask(str, show_first: 0, show_last: 0, mask_char: '*')
339
+ validate!(str)
340
+ return str if show_first + show_last >= str.length - 1
341
+
342
+ masked_length = str.length - show_first - show_last
343
+ str[0, show_first] + (mask_char * masked_length) + str[str.length - show_last, show_last].to_s
344
+ end
345
+
346
+ # Returns the substring strictly between the first occurrence of `left`
347
+ # and the first occurrence of `right` after `left`. Returns `nil` when
348
+ # either delimiter is missing.
349
+ #
350
+ # @param str [String]
351
+ # @param left [String]
352
+ # @param right [String]
353
+ # @return [String, nil]
354
+ def self.between(str, left, right)
355
+ validate!(str)
356
+ left_index = str.index(left)
357
+ return nil if left_index.nil?
358
+
359
+ start_pos = left_index + left.length
360
+ right_index = str.index(right, start_pos)
361
+ return nil if right_index.nil?
362
+
363
+ str[start_pos...right_index]
364
+ end
365
+
366
+ # Truncate a string to the first `max_words` words. When truncation
367
+ # happens, append `omission` to the result. The string is unchanged
368
+ # when the word count is less than or equal to `max_words`.
369
+ #
370
+ # @param str [String]
371
+ # @param max_words [Integer] maximum number of words to keep (must be positive)
372
+ # @param omission [String] string appended when truncation occurs (default: '…')
373
+ # @return [String]
374
+ def self.truncate_words(str, max_words, omission: '…')
375
+ validate!(str)
376
+ raise Error, 'max_words must be a positive Integer' unless max_words.is_a?(Integer) && max_words.positive?
377
+
378
+ words = str.split(/\s+/).reject(&:empty?)
379
+ return str if words.length <= max_words
380
+
381
+ "#{words.first(max_words).join(' ')}#{omission}"
382
+ end
383
+
384
+ # Wrap the string to lines of at most `width` characters, breaking on
385
+ # word boundaries. Words longer than `width` are kept intact on their own
386
+ # line rather than split mid-word. Existing newlines are honored as
387
+ # forced breaks — each input line is wrapped independently.
388
+ #
389
+ # @param str [String]
390
+ # @param width [Integer] maximum line width (must be positive)
391
+ # @return [String] newline-joined wrapped lines
392
+ def self.word_wrap(str, width)
393
+ validate!(str)
394
+ raise Error, 'width must be a positive Integer' unless width.is_a?(Integer) && width.positive?
395
+
396
+ str.each_line.map { |line| wrap_line(line.chomp, width) }.join("\n")
397
+ end
398
+
328
399
  class << self
329
400
  private
330
401
 
@@ -340,6 +411,28 @@ module Philiprehberger
340
411
  .downcase
341
412
  .split
342
413
  end
414
+
415
+ def wrap_line(line, width)
416
+ return '' if line.empty?
417
+
418
+ words = line.split(/\s+/).reject(&:empty?)
419
+ return '' if words.empty?
420
+
421
+ lines = []
422
+ current = +''
423
+ words.each do |word|
424
+ if current.empty?
425
+ current = word.dup
426
+ elsif current.length + 1 + word.length <= width
427
+ current << ' ' << word
428
+ else
429
+ lines << current
430
+ current = word.dup
431
+ end
432
+ end
433
+ lines << current unless current.empty?
434
+ lines.join("\n")
435
+ end
343
436
  end
344
437
  end
345
438
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-string_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-26 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: String case conversion, slug generation, transliteration, padding, HTML
14
14
  stripping, whitespace normalization, word counting, reading time estimation, excerpt