]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/core/utils/extends.rb
plugin(search): fix search and gcalc, closes #28, #29
[user/henk/code/ruby/rbot.git] / lib / rbot / core / utils / extends.rb
index dcc257a2df85330f073377ca95e38f21bf64250c..08278261fdfa435b21991a069cd66cb7664b9d31 100644 (file)
@@ -4,8 +4,6 @@
 # :title: Standard classes extensions
 #
 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006,2007 Giuseppe Bilotta
-# License:: GPL v2
 #
 # This file collects extensions to standard Ruby classes and to some core rbot
 # classes to be used by the various plugins
@@ -38,6 +36,50 @@ class ::Module
 end
 
 
+# DottedIndex mixin: extend a Hash or Array class with this module
+# to achieve [] and []= methods that automatically split indices
+# at dots (indices are automatically converted to symbols, too)
+#
+# You have to define the single_retrieve(_key_) and
+# single_assign(_key_,_value_) methods (usually aliased at the
+# original :[] and :[]= methods)
+#
+module ::DottedIndex
+  def rbot_index_split(*ar)
+    keys = ([] << ar).flatten
+    keys.map! { |k|
+      k.to_s.split('.').map { |kk| kk.to_sym rescue nil }.compact
+    }.flatten
+  end
+
+  def [](*ar)
+    keys = self.rbot_index_split(ar)
+    return self.single_retrieve(keys.first) if keys.length == 1
+    h = self
+    while keys.length > 1
+      k = keys.shift
+      h[k] ||= self.class.new
+      h = h[k]
+    end
+    h[keys.last]
+  end
+
+  def []=(*arr)
+    val = arr.last
+    ar = arr[0..-2]
+    keys = self.rbot_index_split(ar)
+    return self.single_assign(keys.first, val) if keys.length == 1
+    h = self
+    while keys.length > 1
+      k = keys.shift
+      h[k] ||= self.class.new
+      h = h[k]
+    end
+    h[keys.last] = val
+  end
+end
+
+
 # Extensions to the Array class
 #
 class ::Array
@@ -49,6 +91,94 @@ class ::Array
     return nil if self.empty?
     self[rand(self.length)]
   end
+
+  # This method returns a given element from the array, deleting it from the
+  # array itself. The method returns nil if the element couldn't be found.
+  #
+  # If nil is specified, a random element is returned and deleted.
+  #
+  def delete_one(val=nil)
+    return nil if self.empty?
+    if val.nil?
+      index = rand(self.length)
+    else
+      index = self.index(val)
+      return nil unless index
+    end
+    self.delete_at(index)
+  end
+
+  # This method deletes a given object _el_ if it's found at the given
+  # position _pos_. The position can be either an integer or a range.
+  def delete_if_at(el, pos)
+    idx = self.index(el)
+    if pos === idx
+      self.delete_at(idx)
+    else
+      nil
+    end
+  end
+
+  # Taken from Ruby backports:
+  # shuffle and shuffle! are defined in Ruby >= 1.8.7
+
+  # This method returns a new array with the
+  # same items as the receiver, but shuffled
+  def shuffle
+    dup.shuffle!
+  end unless method_defined? :shuffle
+
+  def shuffle!
+    size.times do |i|
+      r = i + Kernel.rand(size - i)
+      self[i], self[r] = self[r], self[i]
+    end
+    self
+  end unless method_defined? :shuffle!
+end
+
+module ::Enumerable
+  # This method is an advanced version of #join
+  # allowing fine control of separators:
+  #
+  #   [1,2,3].conjoin(', ', ' and ')
+  #   => "1, 2 and 3
+  #
+  #   [1,2,3,4].conjoin{ |i, a, b| i % 2 == 0 ? '.' : '-' }
+  #   => "1.2-3.4"
+  #
+  # Code lifted from the ruby facets project:
+  # <http://facets.rubyforge.org>
+  # git-rev: c8b7395255b977d3c7de268ff563e3c5bc7f1441
+  # file: lib/core/facets/array/conjoin.rb
+  def conjoin(*args, &block)
+    num = count - 1
+
+    return first.to_s if num < 1
+
+    sep = []
+
+    if block_given?
+      num.times do |i|
+        sep << yield(i, *slice(i, 2))
+      end
+    else
+      options = (Hash === args.last) ? args.pop : {}
+      separator = args.shift || ""
+      options[-1] = args.shift unless args.empty?
+
+      sep = [separator] * num
+
+      if options.key?(:last)
+        options[-1] = options.delete(:last)
+      end
+      options[-1] ||= _(" and ")
+
+      options.each{ |i, s| sep[i] = s }
+    end
+
+    zip(sep).join
+  end
 end
 
 # Extensions to the Range class
@@ -126,6 +256,29 @@ class ::String
       warning "unknown :a_href option #{val} passed to ircify_html" if val
     end
 
+    # If opts[:img] is defined, it should be a String. Each image
+    # will be replaced by the string itself, replacing occurrences of
+    # %{alt} %{dimensions} and %{src} with the alt text, image dimensions
+    # and URL
+    if val = opts[:img]
+      if val.kind_of? String
+        txt.gsub!(/<img\s+(.*?)\s*\/?>/) do |imgtag|
+          attrs = Hash.new
+          imgtag.scan(/([[:alpha:]]+)\s*=\s*(['"])?(.*?)\2/) do |key, quote, value|
+            k = key.downcase.intern rescue 'junk'
+            attrs[k] = value
+          end
+          attrs[:alt] ||= attrs[:title]
+          attrs[:width] ||= '...'
+          attrs[:height] ||= '...'
+          attrs[:dimensions] ||= "#{attrs[:width]}x#{attrs[:height]}"
+          val % attrs
+        end
+      else
+        warning ":img option is not a string"
+      end
+    end
+
     # Paragraph and br tags are converted to whitespace
     txt.gsub!(/<\/?(p|br)(?:\s+[^>]*)?\s*\/?\s*>/i, ' ')
     txt.gsub!("\n", ' ')
@@ -137,6 +290,10 @@ class ::String
     txt.gsub!(/<sub>(.*?)<\/sub>/, '_{\1}')
     txt.gsub!(/(^|_)\{(.)\}/, '\1\2')
 
+    # List items are converted to *). We don't have special support for
+    # nested or ordered lists.
+    txt.gsub!(/<li>/, ' *) ')
+
     # All other tags are just removed
     txt.gsub!(/<[^>]+>/, '')
 
@@ -144,6 +301,14 @@ class ::String
     # such as &nbsp;
     txt = Utils.decode_html_entities(txt)
 
+    # Keep unbreakable spaces or conver them to plain spaces?
+    case val = opts[:nbsp]
+    when :space, ' '
+      txt.gsub!([160].pack('U'), ' ')
+    else
+      warning "unknown :nbsp option #{val} passed to ircify_html" if val
+    end
+
     # Remove double formatting options, since they only waste bytes
     txt.gsub!(/#{Bold}(\s*)#{Bold}/, '\1')
     txt.gsub!(/#{Underline}(\s*)#{Underline}/, '\1')
@@ -176,7 +341,7 @@ class ::String
   # This method will strip all HTML crud from the receiver
   #
   def riphtml
-    self.gsub(/<[^>]+>/, '').gsub(/&amp;/,'&').gsub(/&quot;/,'"').gsub(/&lt;/,'<').gsub(/&gt;/,'>').gsub(/&ellip;/,'...').gsub(/&apos;/, "'").gsub("\n",'')
+    Utils.decode_html_entities(self.gsub("\n",' ').gsub(/<\s*br\s*\/?\s*>/, ' ').gsub(/<[^>]+>/, '')).gsub(/\s+/,' ')
   end
 
   # This method tries to find an HTML title in the string,
@@ -195,8 +360,39 @@ class ::String
   def ircify_html_title
     self.get_html_title.ircify_html rescue nil
   end
-end
 
+  # This method is used to wrap a nonempty String by adding
+  # the prefix and postfix
+  def wrap_nonempty(pre, post, opts={})
+    if self.empty?
+      String.new
+    else
+      "#{pre}#{self}#{post}"
+    end
+  end
+
+  # Format a string using IRC colors
+  #
+  def colorformat
+    txt = self.dup
+
+    txt.gsub!(/\*([^\*]+)\*/, Bold + '\\1' + NormalText)
+
+    return txt
+  end
+  
+  # Removes non-ASCII symbols from string
+  def remove_nonascii(replace='')
+    encoding_options = {
+      :invalid           => :replace,  # Replace invalid byte sequences
+      :undef             => :replace,  # Replace anything not defined in ASCII
+      :replace           => replace,   
+      :universal_newline => true       # Always break lines with \n
+    }
+
+    self.encode(Encoding.find('ASCII'), encoding_options)
+  end
+end
 
 # Extensions to the Regexp class, with some common and/or complex regular
 # expressions.
@@ -275,5 +471,86 @@ module ::Irc
         end
       }.uniq
     end
+
+    # The recurse depth of a message, for fake messages. 0 means an original
+    # message
+    def recurse_depth
+      unless defined? @recurse_depth
+        @recurse_depth = 0
+      end
+      @recurse_depth
+    end
+
+    # Set the recurse depth of a message, for fake messages. 0 should only
+    # be used by original messages
+    def recurse_depth=(val)
+      @recurse_depth = val
+    end
+  end
+
+  class Bot
+    module Plugins
+
+      # Maximum fake message recursion
+      MAX_RECURSE_DEPTH = 10
+
+      class RecurseTooDeep < RuntimeError
+      end
+
+      class BotModule
+        # Sometimes plugins need to create a new fake message based on an existing
+        # message: for example, this is done by alias, linkbot, reaction and remotectl.
+        #
+        # This method simplifies the message creation, including a recursion depth
+        # check.
+        #
+        # In the options you can specify the :bot, the :server, the :source,
+        # the :target, the message :class and whether or not to :delegate. To
+        # initialize these entries from an existing message, you can use :from
+        #
+        # Additionally, if :from is given, the reply method of created message
+        # is overriden to reply to :from instead. The #in_thread attribute
+        # for created mesage is also copied from :from
+        #
+        # If you don't specify a :from you should specify a :source.
+        #
+        def fake_message(string, opts={})
+          if from = opts[:from]
+            o = {
+              :bot => from.bot, :server => from.server, :source => from.source,
+              :target => from.target, :class => from.class, :delegate => true,
+              :depth => from.recurse_depth + 1
+            }.merge(opts)
+          else
+            o = {
+              :bot => @bot, :server => @bot.server, :target => @bot.myself,
+              :class => PrivMessage, :delegate => true, :depth => 1
+            }.merge(opts)
+          end
+          raise RecurseTooDeep if o[:depth] > MAX_RECURSE_DEPTH
+          new_m = o[:class].new(o[:bot], o[:server], o[:source], o[:target], string)
+          new_m.recurse_depth = o[:depth]
+          if from
+            # the created message will reply to the originating message, but we
+            # must remember to set 'replied' on the fake message too, to
+            # prevent infinite loops that could be created for example with the reaction
+            # plugin by setting up a reaction to ping with cmd:ping
+            class << new_m
+              self
+            end.send(:define_method, :reply) do |*args|
+              debug "replying to '#{from.message}' with #{args.first}"
+              from.reply(*args)
+              new_m.replied = true
+            end
+            # the created message will follow originating message's in_thread
+            new_m.in_thread = from.in_thread if from.respond_to?(:in_thread)
+          end
+          return new_m unless o[:delegate]
+          method = o[:class].to_s.gsub(/^Irc::|Message$/,'').downcase
+          method = 'privmsg' if method == 'priv'
+          o[:bot].plugins.irc_delegate(method, new_m)
+        end
+      end
+    end
   end
 end