rage_flip 2.3.0 → 2.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: db5be08744369388bbc29752411e903e25ad018bcf024f49401f0181519b5642
4
- data.tar.gz: b9117dc053c098d74625f71cdbf32cd4ef50542fe2c5d315f93d0f37f584af91
3
+ metadata.gz: 67b5dd4341d2242b6f1372e026865302c6e2ca16a1005cb0801a4a6e1de4a7e1
4
+ data.tar.gz: 134945b628dbb3a591b22ae544f338022c12dd7065affa53b7a35ae30dd67137
5
5
  SHA512:
6
- metadata.gz: ed8150e74a80e424f5c97c153bf349a1a25261c67af53c26b2cce71849e31406ddbf8db651fb6532d16ffd80a71cb9a9ef9e7c5e1e154709ad3f25c716980b5f
7
- data.tar.gz: 2bc6ba4473e0a81a8a9dbf86e18921d04d693bf3fcc566297ee477e3d7fc5287ceb8dbe9e55c6b7615ba620f0b890d5c9e6858c8fe82d19e7722a71ab6261c91
6
+ metadata.gz: 822af43cd588ffe11a99bde6aa24d0de90d7b99b80c271d09d038b563dbc6a52eff1fdf5e136073e763e8a567a48a24a3c9ac7dee59da09af86e0c9a1f84b501
7
+ data.tar.gz: 04a1906d61a7093b3586657009dbfb19b82b528bd08bd58bb18e45bdb841c0681067b278477bfa4bad7040423f8d7adf43a4bf83c8365a9f5327d6a3e88fbf5f
data/README.md CHANGED
@@ -145,6 +145,15 @@ frustrated "one more day"
145
145
  # Output: ONE. MORE. DAY.
146
146
  ```
147
147
 
148
+ ### confusable
149
+
150
+ Swaps every letter for a random lookalike glyph: homoglyphs from other scripts (Cyrillic, Greek, Armenian, Cherokee), unaccented letter variants (dotless ı, small capitals), and allographs (fullwidth, bold, script, fraktur, monospace):
151
+
152
+ ```bash
153
+ confusable "hello world"
154
+ # Output (random each time): һ𝐞𝓁ⅼ𝔬 ԝoг𝚕ԁ
155
+ ```
156
+
148
157
  ### strikethrough
149
158
 
150
159
  Adds strikethrough formatting to text using Unicode combining characters:
@@ -272,6 +281,10 @@ RageFlip::Sarcasm.process("hello world")
272
281
  RageFlip::Frustrated.process("one more day")
273
282
  # => "ONE. MORE. DAY."
274
283
 
284
+ # Confusable (random lookalike glyphs)
285
+ RageFlip::Confusable.process("hello")
286
+ # => "һ𝐞𝓁ⅼ𝔬"
287
+
275
288
  # Strikethrough
276
289
  RageFlip::Strikethrough.process("text")
277
290
  # => "t̶e̶x̶t̶"
data/exe/confusable ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/rage_flip"
4
+
5
+ text = ARGV.join(" ")
6
+
7
+ if text.empty?
8
+ puts "Usage: confusable <text_to_confuse>"
9
+ exit 1
10
+ end
11
+
12
+ result = RageFlip::Confusable.process(text)
13
+
14
+ if RageFlip::Clipboard.copy(result)
15
+ else
16
+ puts "Failed to copy to clipboard, but here's your confusable text:"
17
+ end
18
+ puts result
@@ -0,0 +1,64 @@
1
+ module RageFlip
2
+ class Confusable
3
+ # Homoglyphs (Cyrillic, Greek, Armenian, Cherokee, etc.), unaccented letter
4
+ # variants, and allographs (fullwidth, bold, script, fraktur, monospace).
5
+ CONFUSABLES = {
6
+ "a" => %w[а ɑ α ⍺ a 𝐚 𝒶 𝔞 𝚊],
7
+ "b" => %w[Ь ᖯ b 𝐛 𝒷 𝔟 𝚋],
8
+ "c" => %w[с ϲ ᴄ ⅽ c 𝐜 𝒸 𝔠 𝚌],
9
+ "d" => %w[ԁ ⅾ d 𝐝 𝒹 𝔡 𝚍],
10
+ "e" => %w[е ҽ ℮ e 𝐞 ℯ 𝔢 𝚎],
11
+ "f" => %w[ꬵ f 𝐟 𝒻 𝔣 𝚏],
12
+ "g" => %w[ɡ ց g 𝐠 ℊ 𝔤 𝚐],
13
+ "h" => %w[һ հ h 𝐡 𝒽 𝔥 𝚑],
14
+ "i" => %w[і ı ɩ ι ⅰ i 𝐢 𝒾 𝔦 𝚒],
15
+ "j" => %w[ј ϳ j 𝐣 𝒿 𝔧 𝚓],
16
+ "k" => %w[ĸ κ k 𝐤 𝓀 𝔨 𝚔],
17
+ "l" => %w[ӏ ǀ ⅼ ℓ l 𝐥 𝓁 𝔩 𝚕],
18
+ "m" => %w[ⅿ m 𝐦 𝓂 𝔪 𝚖],
19
+ "n" => %w[ո ռ n 𝐧 𝓃 𝔫 𝚗],
20
+ "o" => %w[о ο օ ᴏ o 𝐨 ℴ 𝔬 𝚘],
21
+ "p" => %w[р ρ p 𝐩 𝓅 𝔭 𝚙],
22
+ "q" => %w[ԛ զ q 𝐪 𝓆 𝔮 𝚚],
23
+ "r" => %w[г ꭇ r 𝐫 𝓇 𝔯 𝚛],
24
+ "s" => %w[ѕ ꜱ s 𝐬 𝓈 𝔰 𝚜],
25
+ "t" => %w[t 𝐭 𝓉 𝔱 𝚝],
26
+ "u" => %w[υ ս ᴜ u 𝐮 𝓊 𝔲 𝚞],
27
+ "v" => %w[ν ѵ ᴠ ⅴ v 𝐯 𝓋 𝔳 𝚟],
28
+ "w" => %w[ԝ ᴡ w 𝐰 𝓌 𝔴 𝚠],
29
+ "x" => %w[х ⅹ x 𝐱 𝓍 𝔵 𝚡],
30
+ "y" => %w[у ү ɣ y 𝐲 𝓎 𝔶 𝚢],
31
+ "z" => %w[ᴢ z 𝐳 𝓏 𝔷 𝚣],
32
+ "A" => %w[А Α Ꭺ ᗅ A 𝐀 𝒜 𝔄 𝙰],
33
+ "B" => %w[В Β Ᏼ ℬ B 𝐁 𝔅 𝙱],
34
+ "C" => %w[С Ϲ Ꮯ Ⅽ ℭ C 𝐂 𝒞 𝙲],
35
+ "D" => %w[Ꭰ Ⅾ D 𝐃 𝒟 𝔇 𝙳],
36
+ "E" => %w[Е Ε Ꭼ E 𝐄 ℰ 𝔈 𝙴],
37
+ "F" => %w[Ϝ ᖴ F 𝐅 ℱ 𝔉 𝙵],
38
+ "G" => %w[Ԍ Ꮐ G 𝐆 𝒢 𝔊 𝙶],
39
+ "H" => %w[Н Η Ꮋ ℋ ℌ H 𝐇 𝙷],
40
+ "I" => %w[І Ι Ӏ Ⅰ I 𝐈 ℐ ℑ 𝙸],
41
+ "J" => %w[Ј Ꭻ J 𝐉 𝒥 𝔍 𝙹],
42
+ "K" => %w[К Κ Ꮶ K K 𝐊 𝒦 𝔎 𝙺],
43
+ "L" => %w[Ꮮ ᒪ Ⅼ L 𝐋 ℒ 𝔏 𝙻],
44
+ "M" => %w[М Μ Ꮇ Ⅿ M 𝐌 ℳ 𝔐 𝙼],
45
+ "N" => %w[Ν N 𝐍 𝒩 𝔑 𝙽],
46
+ "O" => %w[О Ο Օ O 𝐎 𝒪 𝔒 𝙾],
47
+ "P" => %w[Р Ρ Ꮲ P 𝐏 𝒫 𝔓 𝙿],
48
+ "Q" => %w[Ԛ Q 𝐐 𝒬 𝔔 𝚀],
49
+ "R" => %w[Ꭱ Ꮢ ℛ ℜ R 𝐑 𝚁],
50
+ "S" => %w[Ѕ Ꮪ S 𝐒 𝒮 𝔖 𝚂],
51
+ "T" => %w[Т Τ Ꭲ T 𝐓 𝒯 𝔗 𝚃],
52
+ "U" => %w[Ս ᑌ U 𝐔 𝒰 𝔘 𝚄],
53
+ "V" => %w[Ѵ Ꮩ Ⅴ V 𝐕 𝒱 𝔙 𝚅],
54
+ "W" => %w[Ԝ Ꮃ W 𝐖 𝒲 𝔚 𝚆],
55
+ "X" => %w[Х Χ Ⅹ X 𝐗 𝒳 𝔛 𝚇],
56
+ "Y" => %w[Υ Ү Y 𝐘 𝒴 𝔜 𝚈],
57
+ "Z" => %w[Ζ Ꮓ ℤ Z 𝐙 𝒵 ℨ 𝚉]
58
+ }.freeze
59
+
60
+ def self.process(text)
61
+ text.each_char.map { |c| CONFUSABLES[c]&.sample || c }.join
62
+ end
63
+ end
64
+ end
@@ -1911,6 +1911,18 @@ module RageFlip
1911
1911
  def self.refresh_emotes
1912
1912
  @all_emotes = nil
1913
1913
  @emoji_only_emotes = nil
1914
+ @emote_by_first_char = nil
1915
+ @compound_memo = nil
1916
+ end
1917
+
1918
+ def self.emote_by_first_char
1919
+ @emote_by_first_char ||= emoji_only_emotes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, value), h|
1920
+ h[name[0]] << [name, value]
1921
+ end
1922
+ end
1923
+
1924
+ def self.compound_memo
1925
+ @compound_memo ||= {}
1914
1926
  end
1915
1927
 
1916
1928
  # Check if an emote value contains only emoji (no ASCII art or text)
@@ -1941,22 +1953,21 @@ module RageFlip
1941
1953
  end
1942
1954
 
1943
1955
  # Find a valid combination of emoji-only emotes that match the compound name
1944
- def self.find_emote_combination(name, start = 0, memo = {})
1956
+ def self.find_emote_combination(name, start = 0, memo = compound_memo)
1945
1957
  return [] if start >= name.length
1946
1958
  return memo[[name, start]] if memo.key?([name, start])
1947
1959
 
1948
- # Try each emoji-only emote starting at this position
1949
- emoji_only_emotes.each do |emote_name, emote_value|
1950
- next unless name[start..-1].start_with?(emote_name)
1960
+ # Only check emotes whose name starts with the same character — avoids O(n) scan
1961
+ candidates = emote_by_first_char[name[start]] || []
1962
+ candidates.each do |emote_name, emote_value|
1963
+ next unless name[start..].start_with?(emote_name)
1951
1964
 
1952
1965
  rest_start = start + emote_name.length
1953
1966
  if rest_start >= name.length
1954
- # We've matched the entire string
1955
1967
  memo[[name, start]] = [emote_value]
1956
1968
  return [emote_value]
1957
1969
  end
1958
1970
 
1959
- # Try to match the rest
1960
1971
  rest = find_emote_combination(name, rest_start, memo)
1961
1972
  if rest
1962
1973
  result = [emote_value] + rest
@@ -1,3 +1,3 @@
1
1
  module RageFlip
2
- VERSION = "2.3.0"
2
+ VERSION = "2.5.0"
3
3
  end
data/lib/rage_flip.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "rage_flip/clipboard"
8
8
  require_relative "rage_flip/emote"
9
9
  require_relative "rage_flip/text_substitution"
10
10
  require_relative "rage_flip/frustrated"
11
+ require_relative "rage_flip/confusable"
11
12
  require "shellwords"
12
13
 
13
14
  module RageFlip
@@ -64,6 +64,29 @@ RSpec.describe RageFlip do
64
64
  end
65
65
  end
66
66
 
67
+ describe RageFlip::Confusable do
68
+ describe ".process" do
69
+ it "replaces every letter with a different glyph" do
70
+ input = ("a".."z").to_a.join + ("A".."Z").to_a.join
71
+ result = RageFlip::Confusable.process(input)
72
+ expect(result.each_char.count).to eq(input.length)
73
+ result.each_char.zip(input.each_char).each do |out, orig|
74
+ expect(out).not_to eq(orig)
75
+ expect(RageFlip::Confusable::CONFUSABLES[orig]).to include(out)
76
+ end
77
+ end
78
+
79
+ it "leaves non-letters untouched" do
80
+ expect(RageFlip::Confusable.process("123 !?")).to eq("123 !?")
81
+ end
82
+
83
+ it "has confusables for every ASCII letter" do
84
+ letters = ("a".."z").to_a + ("A".."Z").to_a
85
+ expect(RageFlip::Confusable::CONFUSABLES.keys).to match_array(letters)
86
+ end
87
+ end
88
+ end
89
+
67
90
  describe RageFlip::Strikethrough do
68
91
  describe ".process" do
69
92
  it "adds strikethrough characters" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage_flip
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Powell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -45,6 +45,7 @@ email:
45
45
  executables:
46
46
  - chaos
47
47
  - chaos_level
48
+ - confusable
48
49
  - disapproval
49
50
  - doubleunderline
50
51
  - emote
@@ -69,6 +70,7 @@ files:
69
70
  - bin/version
70
71
  - exe/chaos
71
72
  - exe/chaos_level
73
+ - exe/confusable
72
74
  - exe/disapproval
73
75
  - exe/doubleunderline
74
76
  - exe/emote
@@ -83,6 +85,7 @@ files:
83
85
  - lib/rage_flip.rb
84
86
  - lib/rage_flip/chaos.rb
85
87
  - lib/rage_flip/clipboard.rb
88
+ - lib/rage_flip/confusable.rb
86
89
  - lib/rage_flip/emote.rb
87
90
  - lib/rage_flip/flipper.rb
88
91
  - lib/rage_flip/frustrated.rb