IRC refactoring: casemaps
[rbot] / lib / irc / casemap.rb
1 #-- vim:sw=2:et
2 #++
3 # :title: IRC casemaps
4 #
5 # This module defines the IRC casemap class and the most common casemaps
6 #
7 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
8
9 require 'singleton'
10
11 module Irc
12
13   # Due to its Scandinavian origins, IRC has strange case mappings, which
14   # consider the characters <tt>{}|^</tt> as the uppercase
15   # equivalents of # <tt>[]\~</tt>.
16   #
17   # This is however not the same on all IRC servers: some use standard ASCII
18   # casemapping, other do not consider <tt>^</tt> as the uppercase of
19   # <tt>~</tt>
20   #
21   class Casemap
22     @@casemaps = {}
23
24     # Create a new casemap with name _name_, uppercase characters _upper_ and
25     # lowercase characters _lower_
26     #
27     def initialize(name, upper, lower)
28       @key = name.to_sym
29       raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)
30       @@casemaps[@key] = {
31         :upper => upper,
32         :lower => lower,
33         :casemap => self
34       }
35     end
36
37     # Returns the Casemap with the given name
38     #
39     def Casemap.get(name)
40       @@casemaps[name.to_sym][:casemap]
41     end
42
43     # Retrieve the 'uppercase characters' of this Casemap
44     #
45     def upper
46       @@casemaps[@key][:upper]
47     end
48
49     # Retrieve the 'lowercase characters' of this Casemap
50     #
51     def lower
52       @@casemaps[@key][:lower]
53     end
54
55     # Return a Casemap based on the receiver
56     #
57     def to_irc_casemap
58       self
59     end
60
61     # A Casemap is represented by its lower/upper mappings
62     #
63     def inspect
64       self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"
65     end
66
67     # As a String we return our name
68     #
69     def to_s
70       @key.to_s
71     end
72
73     # Two Casemaps are equal if they have the same upper and lower ranges
74     #
75     def ==(arg)
76       other = arg.to_irc_casemap
77       return self.upper == other.upper && self.lower == other.lower
78     end
79
80     # Give a warning if _arg_ and self are not the same Casemap
81     #
82     def must_be(arg)
83       other = arg.to_irc_casemap
84       if self == other
85         return true
86       else
87         warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
88         return false
89       end
90     end
91
92   end
93
94   # The rfc1459 casemap
95   #
96   class RfcCasemap < Casemap
97     include Singleton
98
99     def initialize
100       super('rfc1459', "\x41-\x5e", "\x61-\x7e")
101     end
102
103   end
104   RfcCasemap.instance
105
106   # The strict-rfc1459 Casemap
107   #
108   class StrictRfcCasemap < Casemap
109     include Singleton
110
111     def initialize
112       super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
113     end
114
115   end
116   StrictRfcCasemap.instance
117
118   # The ascii Casemap
119   #
120   class AsciiCasemap < Casemap
121     include Singleton
122
123     def initialize
124       super('ascii', "\x41-\x5a", "\x61-\x7a")
125     end
126
127   end
128   AsciiCasemap.instance
129
130
131   # This module is included by all classes that are either bound to a server
132   # or should have a casemap.
133   #
134   module ServerOrCasemap
135
136     attr_reader :server
137
138     # This method initializes the instance variables @server and @casemap
139     # according to the values of the hash keys :server and :casemap in _opts_
140     #
141     def init_server_or_casemap(opts={})
142       @server = opts.fetch(:server, nil)
143       raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)
144
145       @casemap = opts.fetch(:casemap, nil)
146       if @server
147         if @casemap
148           @server.casemap.must_be(@casemap)
149           @casemap = nil
150         end
151       else
152         @casemap = (@casemap || 'rfc1459').to_irc_casemap
153       end
154     end
155
156     # This is an auxiliary method: it returns true if the receiver fits the
157     # server and casemap specified in _opts_, false otherwise.
158     #
159     def fits_with_server_and_casemap?(opts={})
160       srv = opts.fetch(:server, nil)
161       cmap = opts.fetch(:casemap, nil)
162       cmap = cmap.to_irc_casemap unless cmap.nil?
163
164       if srv.nil?
165         return true if cmap.nil? or cmap == casemap
166       else
167         return true if srv == @server and (cmap.nil? or cmap == casemap)
168       end
169       return false
170     end
171
172     # Returns the casemap of the receiver, by looking at the bound
173     # @server (if possible) or at the @casemap otherwise
174     #
175     def casemap
176       return @server.casemap if defined?(@server) and @server
177       return @casemap
178     end
179
180     # Returns a hash with the current @server and @casemap as values of
181     # :server and :casemap
182     #
183     def server_and_casemap
184       h = {}
185       h[:server] = @server if defined?(@server) and @server
186       h[:casemap] = @casemap if defined?(@casemap) and @casemap
187       return h
188     end
189
190     # We allow up/downcasing with a different casemap
191     #
192     def irc_downcase(cmap=casemap)
193       self.to_s.irc_downcase(cmap)
194     end
195
196     # Up/downcasing something that includes this module returns its
197     # Up/downcased to_s form
198     #
199     def downcase
200       self.irc_downcase
201     end
202
203     # We allow up/downcasing with a different casemap
204     #
205     def irc_upcase(cmap=casemap)
206       self.to_s.irc_upcase(cmap)
207     end
208
209     # Up/downcasing something that includes this module returns its
210     # Up/downcased to_s form
211     #
212     def upcase
213       self.irc_upcase
214     end
215
216   end
217
218 end
219
220
221 # We extend the String class with some Irc::Casemap methods
222 #
223 class String
224
225   # This method returns the Irc::Casemap whose name is the receiver
226   #
227   def to_irc_casemap
228     Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
229   end
230
231   # This method returns a string which is the downcased version of the
232   # receiver, according to the given _casemap_
233   #
234   #
235   def irc_downcase(casemap='rfc1459')
236     cmap = casemap.to_irc_casemap
237     self.tr(cmap.upper, cmap.lower)
238   end
239
240   # This is the same as the above, except that the string is altered in place
241   #
242   # See also the discussion about irc_downcase
243   #
244   def irc_downcase!(casemap='rfc1459')
245     cmap = casemap.to_irc_casemap
246     self.tr!(cmap.upper, cmap.lower)
247   end
248
249   # Upcasing functions are provided too
250   #
251   # See also the discussion about irc_downcase
252   #
253   def irc_upcase(casemap='rfc1459')
254     cmap = casemap.to_irc_casemap
255     self.tr(cmap.lower, cmap.upper)
256   end
257
258   # In-place upcasing
259   #
260   # See also the discussion about irc_downcase
261   #
262   def irc_upcase!(casemap='rfc1459')
263     cmap = casemap.to_irc_casemap
264     self.tr!(cmap.lower, cmap.upper)
265   end
266 end
267
268