Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adding Tests and Extensions

  • Loading branch information...
commit b6b833163fe1c1ec9d1010020eece74c6cf9f41c 1 parent 3a37dd0
@merbjedi authored
Showing with 5,873 additions and 0 deletions.
  1. BIN  .DS_Store
  2. BIN  lib/merb_wheels/.DS_Store
  3. +145 −0 lib/merb_wheels/core_ext/array_ext.rb
  4. +56 −0 lib/merb_wheels/core_ext/class_ext.rb
  5. +22 −0 lib/merb_wheels/core_ext/date_and_time_ext.rb
  6. +90 −0 lib/merb_wheels/core_ext/enumerable_ext.rb
  7. +128 −0 lib/merb_wheels/core_ext/hash_ext.rb
  8. +22 −0 lib/merb_wheels/core_ext/integer_ext.rb
  9. +55 −0 lib/merb_wheels/core_ext/module_ext.rb
  10. +45 −0 lib/merb_wheels/core_ext/numeric_ext.rb
  11. +69 −0 lib/merb_wheels/core_ext/object_ext.rb
  12. +35 −0 lib/merb_wheels/core_ext/range_ext.rb
  13. +228 −0 lib/merb_wheels/core_ext/string_ext.rb
  14. +17 −0 lib/merb_wheels/core_ext/symbol_ext.rb
  15. +65 −0 lib/merb_wheels/helpers/date_helpers.rb
  16. +59 −0 lib/merb_wheels/helpers/javascript_helpers.rb
  17. +263 −0 lib/merb_wheels/helpers/number_helpers.rb
  18. +232 −0 lib/merb_wheels/helpers/sanitize_helpers.rb
  19. +42 −0 lib/merb_wheels/helpers/tag_helpers.rb
  20. +337 −0 lib/merb_wheels/helpers/text_helpers.rb
  21. +7 −0 lib/merb_wheels/helpers/url_helpers.rb
  22. +63 −0 lib/merb_wheels/html-scanner/document.rb
  23. +537 −0 lib/merb_wheels/html-scanner/node.rb
  24. +173 −0 lib/merb_wheels/html-scanner/sanitizer.rb
  25. +828 −0 lib/merb_wheels/html-scanner/selector.rb
  26. +105 −0 lib/merb_wheels/html-scanner/tokenizer.rb
  27. +11 −0 lib/merb_wheels/html-scanner/version.rb
  28. +57 −0 lib/merb_wheels/ordered_hash.rb
  29. +173 −0 lib/merb_wheels/sanitizer.rb
  30. +106 −0 lib/merb_wheels/tokenizer.rb
  31. +36 −0 lib/merb_wheels/util.rb
  32. BIN  pkg/.DS_Store
  33. BIN  pkg/merb_wheels-0.1.gem
  34. +147 −0 spec/core_ext/array_spec.rb
  35. +33 −0 spec/core_ext/class_spec.rb
  36. +92 −0 spec/core_ext/enumerable_spec.rb
  37. +235 −0 spec/core_ext/hash_spec.rb
  38. +35 −0 spec/core_ext/module_spec.rb
  39. +16 −0 spec/core_ext/numeric_spec.rb
  40. +74 −0 spec/core_ext/object_spec.rb
  41. +45 −0 spec/core_ext/range_spec.rb
  42. +187 −0 spec/core_ext/string_spec.rb
  43. +10 −0 spec/core_ext/symbol_spec.rb
  44. +97 −0 spec/date_helpers_spec.rb
  45. +210 −0 spec/fixture/inflections.yml
  46. +48 −0 spec/javascript_helpers_spec.rb
  47. +137 −0 spec/number_helpers_spec.rb
  48. +66 −0 spec/ordered_hash_spec.rb
  49. +49 −0 spec/sanitize_helpers_spec.rb
  50. +52 −0 spec/tag_helpers_spec.rb
  51. +135 −0 spec/text_helpers/auto_link_spec.rb
  52. +49 −0 spec/text_helpers/excerpt_spec.rb
  53. +125 −0 spec/text_helpers_spec.rb
  54. +25 −0 spec/url_helpers_spec.rb
View
BIN  .DS_Store
Binary file not shown
View
BIN  lib/merb_wheels/.DS_Store
Binary file not shown
View
145 lib/merb_wheels/core_ext/array_ext.rb
@@ -0,0 +1,145 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module ArrayExt #:nodoc:
+ # Extracts options from a set of arguments. Removes and returns the last
+ # element in the array if it's a hash, otherwise returns a blank hash.
+ #
+ # def options(*args)
+ # args.extract_options!
+ # end
+ #
+ # options(1, 2) # => {}
+ # options(1, 2, :a => :b) # => {:a=>:b}
+ def extract_options!
+ extract_options_from_args!(self) || {}
+ end
+
+ # Returns the tail of the array from +position+.
+ #
+ # %w( a b c d ).from(0) # => %w( a b c d )
+ # %w( a b c d ).from(2) # => %w( c d )
+ # %w( a b c d ).from(10) # => nil
+ # %w().from(0) # => nil
+ def from(position)
+ self[position..-1]
+ end
+
+ # Returns the beginning of the array up to +position+.
+ #
+ # %w( a b c d ).to(0) # => %w( a )
+ # %w( a b c d ).to(2) # => %w( a b c )
+ # %w( a b c d ).to(10) # => %w( a b c d )
+ # %w().to(0) # => %w()
+ def to(position)
+ self[0..position]
+ end
+
+ # Splits or iterates over the array in groups of size +number+,
+ # padding any remaining slots with +fill_with+ unless it is +false+.
+ #
+ # %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
+ # ["1", "2", "3"]
+ # ["4", "5", "6"]
+ # ["7", nil, nil]
+ #
+ # %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
+ # ["1", "2"]
+ # ["3", " "]
+ #
+ # %w(1 2 3).in_groups_of(2, false) {|group| p group}
+ # ["1", "2"]
+ # ["3"]
+ def in_groups_of(number, fill_with = nil)
+ if fill_with == false
+ collection = self
+ else
+ # size % number gives how many extra we have;
+ # subtracting from number gives how many to add;
+ # modulo number ensures we don't add group of just fill.
+ padding = (number - size % number) % number
+ collection = dup.concat([fill_with] * padding)
+ end
+
+ if block_given?
+ collection.each_slice(number) { |slice| yield(slice) }
+ else
+ returning [] do |groups|
+ collection.each_slice(number) { |group| groups << group }
+ end
+ end
+ end
+
+ # Splits or iterates over the array in +number+ of groups, padding any
+ # remaining slots with +fill_with+ unless it is +false+.
+ #
+ # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
+ # ["1", "2", "3", "4"]
+ # ["5", "6", "7", nil]
+ # ["8", "9", "10", nil]
+ #
+ # %w(1 2 3 4 5 6 7).in_groups(3, '&nbsp;') {|group| p group}
+ # ["1", "2", "3"]
+ # ["4", "5", "&nbsp;"]
+ # ["6", "7", "&nbsp;"]
+ #
+ # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
+ # ["1", "2", "3"]
+ # ["4", "5"]
+ # ["6", "7"]
+ def in_groups(number, fill_with = nil)
+ # size / number gives minor group size;
+ # size % number gives how many objects need extra accomodation;
+ # each group hold either division or division + 1 items.
+ division = size / number
+ modulo = size % number
+
+ # create a new array avoiding dup
+ groups = []
+ start = 0
+
+ number.times do |index|
+ length = division + (modulo > 0 && modulo > index ? 1 : 0)
+ padding = fill_with != false &&
+ modulo > 0 && length == division ? 1 : 0
+ groups << slice(start, length).concat([fill_with] * padding)
+ start += length
+ end
+
+ if block_given?
+ groups.each{|g| yield(g) }
+ else
+ groups
+ end
+ end
+
+ # Divides the array into one or more subarrays based on a delimiting +value+
+ # or the result of an optional block.
+ #
+ # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
+ # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
+ def split(value = nil)
+ using_block = block_given?
+
+ inject([[]]) do |results, element|
+ if (using_block && yield(element)) || (value == element)
+ results << []
+ else
+ results.last << element
+ end
+
+ results
+ end
+ end
+
+ # Returns a random element from the array.
+ def rand
+ self[Kernel.rand(length)]
+ end
+ end
+ end
+end
+
+Array.send :include, MerbWheels::CoreExt::ArrayExt
View
56 lib/merb_wheels/core_ext/class_ext.rb
@@ -0,0 +1,56 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module ClassExt #:nodoc:
+ def cattr_reader(*syms)
+ syms.flatten.each do |sym|
+ next if sym.is_a?(Hash)
+ class_eval(<<-EOS, __FILE__, __LINE__)
+ unless defined? @@#{sym}
+ @@#{sym} = nil
+ end
+
+ def self.#{sym}
+ @@#{sym}
+ end
+
+ def #{sym}
+ @@#{sym}
+ end
+ EOS
+ end
+ end
+
+ def cattr_writer(*syms)
+ options = syms.extract_options!
+ syms.flatten.each do |sym|
+ class_eval(<<-EOS, __FILE__, __LINE__)
+ unless defined? @@#{sym}
+ @@#{sym} = nil
+ end
+
+ def self.#{sym}=(obj)
+ @@#{sym} = obj
+ end
+
+ #{"
+ def #{sym}=(obj)
+ @@#{sym} = obj
+ end
+ " unless options[:instance_writer] == false }
+ EOS
+ end
+ end
+
+ def cattr_accessor(*syms)
+ cattr_reader(*syms)
+ cattr_writer(*syms)
+ end
+
+ end
+ end
+end
+
+Class.send :include, MerbWheels::CoreExt::ClassExt
View
22 lib/merb_wheels/core_ext/date_and_time_ext.rb
@@ -0,0 +1,22 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+# TODO: add any useful activesupport datetime helpers that Merb doesnt have
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module DateExt #:nodoc:
+ end
+
+ module TimeExt #:nodoc:
+ end
+
+ module DateTimeExt #:nodoc:
+ end
+
+ end
+end
+
+
+# String.send :include, MerbWheels::CoreExt::DateExt
+# String.send :include, MerbWheels::CoreExt::TimeExt
+# String.send :include, MerbWheels::CoreExt::DateTimeExt
View
90 lib/merb_wheels/core_ext/enumerable_ext.rb
@@ -0,0 +1,90 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module Enumerable
+ # Ruby 1.8.7 introduces group_by, but the result isn't ordered. Override it.
+ remove_method(:group_by) if [].respond_to?(:group_by) && RUBY_VERSION < '1.9'
+
+ # Collect an enumerable into sets, grouped by the result of a block. Useful,
+ # for example, for grouping records by date.
+ #
+ # Example:
+ #
+ # latest_transcripts.group_by(&:day).each do |day, transcripts|
+ # p "#{day} -> #{transcripts.map(&:class).join(', ')}"
+ # end
+ # "2006-03-01 -> Transcript"
+ # "2006-02-28 -> Transcript"
+ # "2006-02-27 -> Transcript, Transcript"
+ # "2006-02-26 -> Transcript, Transcript"
+ # "2006-02-25 -> Transcript"
+ # "2006-02-24 -> Transcript, Transcript"
+ # "2006-02-23 -> Transcript"
+ def group_by
+ assoc = MerbWheels::OrderedHash.new
+
+ each do |element|
+ key = yield(element)
+
+ if assoc.has_key?(key)
+ assoc[key] << element
+ else
+ assoc[key] = [element]
+ end
+ end
+
+ assoc
+ end
+
+
+ # Calculates a sum from the elements. Examples:
+ #
+ # payments.sum { |p| p.price * p.tax_rate }
+ # payments.sum(&:price)
+ #
+ # The latter is a shortcut for:
+ #
+ # payments.inject { |sum, p| sum + p.price }
+ #
+ # It can also calculate the sum without the use of a block.
+ #
+ # [5, 15, 10].sum # => 30
+ # ["foo", "bar"].sum # => "foobar"
+ # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5]
+ #
+ # The default sum of an empty list is zero. You can override this default:
+ #
+ # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
+ #
+ def sum(identity = 0, &block)
+ return identity unless size > 0
+
+ if block_given?
+ map(&block).sum
+ else
+ inject { |sum, element| sum + element }
+ end
+ end
+
+ # Convert an enumerable to a hash. Examples:
+ #
+ # people.index_by(&:login)
+ # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
+ # people.index_by { |person| "#{person.first_name} #{person.last_name}" }
+ # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
+ #
+ def index_by
+ inject({}) do |accum, elem|
+ accum[yield(elem)] = elem
+ accum
+ end
+ end
+
+ # Returns true if the collection has more than 1 element. Functionally equivalent to collection.size > 1.
+ # Works with a block too ala any?, so people.many? { |p| p.age > 26 } # => returns true if more than 1 person is over 26.
+ def many?(&block)
+ size = block_given? ? select(&block).size : self.size
+ size > 1
+ end
+
+end
View
128 lib/merb_wheels/core_ext/hash_ext.rb
@@ -0,0 +1,128 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module HashExt #:nodoc:
+ # Returns a hash that represents the difference between two hashes.
+ #
+ # Examples:
+ #
+ # {1 => 2}.diff(1 => 2) # => {}
+ # {1 => 2}.diff(1 => 3) # => {1 => 2}
+ # {}.diff(1 => 2) # => {1 => 2}
+ # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
+ def diff(h2)
+ self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) })
+ end
+
+ # Returns a new hash without the given keys.
+ def except(*keys)
+ dup.except!(*keys)
+ end
+
+ # Replaces the hash without the given keys.
+ def except!(*keys)
+ keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
+ keys.each { |key| delete(key) }
+ self
+ end
+
+ def with_indifferent_access
+ hash = Mash.new(self)
+ hash.default = self.default
+ hash
+ end
+
+ # Return a new hash with all keys converted to strings.
+ def stringify_keys
+ inject({}) do |options, (key, value)|
+ options[key.to_s] = value
+ options
+ end
+ end
+
+ # Destructively convert all keys to strings.
+ def stringify_keys!
+ keys.each do |key|
+ self[key.to_s] = delete(key)
+ end
+ self
+ end
+
+ # Return a new hash with all keys converted to symbols.
+ def symbolize_keys
+ inject({}) do |options, (key, value)|
+ options[(key.to_sym rescue key) || key] = value
+ options
+ end
+ end
+
+ # Destructively convert all keys to symbols.
+ def symbolize_keys!
+ self.replace(self.symbolize_keys)
+ end
+
+ alias_method :to_options, :symbolize_keys
+ alias_method :to_options!, :symbolize_keys!
+
+
+ # Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
+ # in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
+ #
+ # def setup(options = {})
+ # options.reverse_merge! :size => 25, :velocity => 10
+ # end
+ #
+ # Using <tt>merge</tt>, the above example would look as follows:
+ #
+ # def setup(options = {})
+ # { :size => 25, :velocity => 10 }.merge(options)
+ # end
+ #
+ # The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
+ # have the respective key.
+ def reverse_merge(other_hash)
+ other_hash.merge(self)
+ end
+
+ # Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
+ # Modifies the receiver in place.
+ def reverse_merge!(other_hash)
+ replace(reverse_merge(other_hash))
+ end
+
+ alias_method :reverse_update, :reverse_merge!
+
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
+ def deep_merge(other_hash)
+ self.merge(other_hash) do |key, oldval, newval|
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
+ end
+ end
+
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
+ # Modifies the receiver in place.
+ def deep_merge!(other_hash)
+ replace(deep_merge(other_hash))
+ end
+
+ # Returns a new hash with only the given keys.
+ def slice(*keys)
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
+ hash = self.class.new
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
+ hash
+ end
+
+ # Replaces the hash with only the given keys.
+ def slice!(*keys)
+ replace(slice(*keys))
+ end
+ end
+ end
+end
+
+Hash.send :include, MerbWheels::CoreExt::HashExt
View
22 lib/merb_wheels/core_ext/integer_ext.rb
@@ -0,0 +1,22 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module IntegerExt #:nodoc:
+ def multiple_of?(number)
+ self % number == 0
+ end
+
+ def even?
+ multiple_of? 2
+ end if RUBY_VERSION < '1.9'
+
+ def odd?
+ !even?
+ end if RUBY_VERSION < '1.9'
+ end
+ end
+end
+
+Integer.send :include, MerbWheels::CoreExt::IntegerExt
View
55 lib/merb_wheels/core_ext/module_ext.rb
@@ -0,0 +1,55 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module ModuleExt #:nodoc:
+ def mattr_reader(*syms)
+ syms.each do |sym|
+ next if sym.is_a?(Hash)
+ class_eval(<<-EOS, __FILE__, __LINE__)
+ unless defined? @@#{sym}
+ @@#{sym} = nil
+ end
+
+ def self.#{sym}
+ @@#{sym}
+ end
+
+ def #{sym}
+ @@#{sym}
+ end
+ EOS
+ end
+ end
+
+ def mattr_writer(*syms)
+ options = syms.extract_options!
+ syms.each do |sym|
+ class_eval(<<-EOS, __FILE__, __LINE__)
+ unless defined? @@#{sym}
+ @@#{sym} = nil
+ end
+
+ def self.#{sym}=(obj)
+ @@#{sym} = obj
+ end
+
+ #{"
+ def #{sym}=(obj)
+ @@#{sym} = obj
+ end
+ " unless options[:instance_writer] == false }
+ EOS
+ end
+ end
+
+ def mattr_accessor(*syms)
+ mattr_reader(*syms)
+ mattr_writer(*syms)
+ end
+ end
+ end
+end
+
+Module.send :include, MerbWheels::CoreExt::ModuleExt
View
45 lib/merb_wheels/core_ext/numeric_ext.rb
@@ -0,0 +1,45 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module NumericExt #:nodoc:
+ def bytes
+ self
+ end
+ alias :byte :bytes
+
+ def kilobytes
+ self * 1024
+ end
+ alias :kilobyte :kilobytes
+
+ def megabytes
+ self * 1024.kilobytes
+ end
+ alias :megabyte :megabytes
+
+ def gigabytes
+ self * 1024.megabytes
+ end
+ alias :gigabyte :gigabytes
+
+ def terabytes
+ self * 1024.gigabytes
+ end
+ alias :terabyte :terabytes
+
+ def petabytes
+ self * 1024.terabytes
+ end
+ alias :petabyte :petabytes
+
+ def exabytes
+ self * 1024.petabytes
+ end
+ alias :exabyte :exabytes
+ end
+ end
+end
+
+Numeric.send :include, MerbWheels::CoreExt::NumericExt
View
69 lib/merb_wheels/core_ext/object_ext.rb
@@ -0,0 +1,69 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module ObjectExt #:nodoc:
+ # An object is present if it's not blank.
+ def present?
+ !blank?
+ end
+
+ # Returns +value+ after yielding +value+ to the block. This simplifies the
+ # process of constructing an object, performing work on the object, and then
+ # returning the object from a method. It is a Ruby-ized realization of the K
+ # combinator, courtesy of Mikael Brockman.
+ #
+ # ==== Examples
+ #
+ # # Without returning
+ # def foo
+ # values = []
+ # values << "bar"
+ # values << "baz"
+ # return values
+ # end
+ #
+ # foo # => ['bar', 'baz']
+ #
+ # # returning with a local variable
+ # def foo
+ # returning values = [] do
+ # values << 'bar'
+ # values << 'baz'
+ # end
+ # end
+ #
+ # foo # => ['bar', 'baz']
+ #
+ # # returning with a block argument
+ # def foo
+ # returning [] do |values|
+ # values << 'bar'
+ # values << 'baz'
+ # end
+ # end
+ #
+ # foo # => ['bar', 'baz']
+ def returning(value)
+ yield(value)
+ value
+ end
+
+ # Tries to send the method only if object responds to it. Return +nil+ otherwise.
+ #
+ # ==== Example :
+ #
+ # # Without try
+ # @person ? @person.name : nil
+ #
+ # With try
+ # @person.try(:name)
+ def try(method_id, *args, &block)
+ respond_to?(method_id) ? send(method_id, *args, &block) : nil
+ end
+ end
+ end
+end
+
+Object.send :include, MerbWheels::CoreExt::ObjectExt
View
35 lib/merb_wheels/core_ext/range_ext.rb
@@ -0,0 +1,35 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module RangeExt #:nodoc:
+ # Compare two ranges and see if they overlap eachother
+ # (1..5).overlaps?(4..6) # => true
+ # (1..5).overlaps?(7..9) # => false
+ def overlaps?(other)
+ include?(other.first) || other.include?(first)
+ end
+
+ # Extends the default Range#include? to support range comparisons.
+ # (1..5).include?(1..5) # => true
+ # (1..5).include?(2..3) # => true
+ # (1..5).include?(2..6) # => false
+ #
+ # The native Range#include? behavior is untouched.
+ # ("a".."f").include?("c") # => true
+ # (5..9).include?(11) # => false
+ def include_with_range?(value)
+ if value.is_a?(::Range)
+ operator = exclude_end? ? :< : :<=
+ end_value = value.exclude_end? ? last.succ : last
+ include?(value.first) && (value.last <=> end_value).send(operator, 0)
+ else
+ include_without_range?(value)
+ end
+ end
+ end
+ end
+end
+
+Range.send :include, MerbWheels::CoreExt::RangeExt
View
228 lib/merb_wheels/core_ext/string_ext.rb
@@ -0,0 +1,228 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+module MerbWheels #:nodoc:
+ module CoreExt #:nodoc:
+ module StringExt #:nodoc:
+
+ # Returns the character at the +position+ treating the string as an array (where 0 is the first character).
+ #
+ # Examples:
+ # "hello".at(0) # => "h"
+ # "hello".at(4) # => "o"
+ # "hello".at(10) # => nil
+ def at(position)
+ self[position, 1]
+ end
+
+ # Returns the remaining of the string from the +position+ treating the string as an array (where 0 is the first character).
+ #
+ # Examples:
+ # "hello".from(0) # => "hello"
+ # "hello".from(2) # => "llo"
+ # "hello".from(10) # => nil
+ def from(position)
+ self[position..-1]
+ end
+
+ # Returns the beginning of the string up to the +position+ treating the string as an array (where 0 is the first character).
+ #
+ # Examples:
+ # "hello".to(0) # => "h"
+ # "hello".to(2) # => "hel"
+ # "hello".to(10) # => "hello"
+ def to(position)
+ self[0..position]
+ end
+
+ # Returns the first character of the string or the first +limit+ characters.
+ #
+ # Examples:
+ # "hello".first # => "h"
+ # "hello".first(2) # => "he"
+ # "hello".first(10) # => "hello"
+ def first(limit = 1)
+ self[0..(limit - 1)]
+ end
+
+ # Returns the last character of the string or the last +limit+ characters.
+ #
+ # Examples:
+ # "hello".last # => "o"
+ # "hello".last(2) # => "lo"
+ # "hello".last(10) # => "hello"
+ def last(limit = 1)
+ from(-limit) || self
+ end
+
+ # 'a'.ord == 'a'[0] for Ruby 1.9 forward compatibility.
+ def ord
+ self[0]
+ end if RUBY_VERSION < '1.9'
+
+ # Returns the string, first removing all whitespace on both ends of
+ # the string, and then changing remaining consecutive whitespace
+ # groups into one space each.
+ #
+ # Examples:
+ # %{ Multi-line
+ # string }.squish # => "Multi-line string"
+ # " foo bar \n \t boo".squish # => "foo bar boo"
+ def squish
+ dup.squish!
+ end
+
+ # Performs a destructive squish. See String#squish.
+ def squish!
+ strip!
+ gsub!(/\s+/, ' ')
+ self
+ end
+
+ # By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize
+ # is set to <tt>:lower</tt> then camelize produces lowerCamelCase.
+ #
+ # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
+ #
+ # "active_record".camelize # => "ActiveRecord"
+ # "active_record".camelize(:lower) # => "activeRecord"
+ # "active_record/errors".camelize # => "ActiveRecord::Errors"
+ # "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
+ def camelize(first_letter = :upper)
+ case first_letter
+ when :upper then self.camel_case
+ when :lower then self.camel_case.to(0).downcase+self.from(1)
+ end
+ end
+
+ # Capitalizes all the words and replaces some characters in the string to create
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
+ # used in the Rails internals.
+ #
+ # +titleize+ is also aliased as +titlecase+.
+ #
+ # "man from the boondocks".titleize # => "Man From The Boondocks"
+ # "x-men: the last stand".titleize # => "X Men: The Last Stand"
+ def titleize
+ # TODO
+ # Inflector.titleize(self)
+ end
+
+ # Replaces underscores with dashes in the string.
+ #
+ # "puni_puni" # => "puni-puni"
+ def dasherize
+ # TODO
+ # Inflector.dasherize(self)
+ end
+
+ # Removes the module part from the constant expression in the string.
+ #
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
+ # "Inflections".demodulize # => "Inflections"
+ def demodulize
+ # TODO
+ # Inflector.demodulize(self)
+ end
+
+
+ # Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
+ #
+ # ==== Examples
+ #
+ # class Person
+ # def to_param
+ # "#{id}-#{name.parameterize}"
+ # end
+ # end
+ #
+ # @person = Person.find(1)
+ # # => #<Person id: 1, name: "Donald E. Knuth">
+ #
+ # <%= link_to(@person.name, person_path %>
+ # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
+ def parameterize
+ # TODO
+ # Inflector.demodulize(self)
+ end
+
+
+ # Creates the name of a table like Rails does for models to table names. This method
+ # uses the +pluralize+ method on the last word in the string.
+ #
+ # "RawScaledScorer".tableize # => "raw_scaled_scorers"
+ # "egg_and_ham".tableize # => "egg_and_hams"
+ # "fancyCategory".tableize # => "fancy_categories"
+ def tableize
+ # TODO
+ # Inflector.demodulize(self)
+ end
+
+ # Create a class name from a plural table name like Rails does for table names to models.
+ # Note that this returns a string and not a class. (To convert to an actual class
+ # follow +classify+ with +constantize+.)
+ #
+ # "egg_and_hams".classify # => "EggAndHam"
+ # "posts".classify # => "Post"
+ #
+ # Singular names are not handled correctly.
+ #
+ # "business".classify # => "Busines"
+ def classify
+ # TODO
+ # Inflector.classify(self)
+ end
+
+ # Capitalizes the first word, turns underscores into spaces, and strips '_id'.
+ # Like +titleize+, this is meant for creating pretty output.
+ #
+ # "employee_salary" # => "Employee salary"
+ # "author_id" # => "Author"
+ def humanize
+ # TODO
+ # Inflector.humanize(self)
+ end
+
+ # Creates a foreign key name from a class name.
+ # +separate_class_name_and_id_with_underscore+ sets whether
+ # the method should put '_' between the name and 'id'.
+ #
+ # Examples
+ # "Message".foreign_key # => "message_id"
+ # "Message".foreign_key(false) # => "messageid"
+ # "Admin::Post".foreign_key # => "post_id"
+ def foreign_key(separate_class_name_and_id_with_underscore = true)
+ # TODO
+ # Inflector.foreign_key(self, separate_class_name_and_id_with_underscore)
+ end
+
+ # +constantize+ tries to find a declared constant with the name specified
+ # in the string. It raises a NameError when the name is not in CamelCase
+ # or is not initialized.
+ #
+ # Examples
+ # "Module".constantize # => Module
+ # "Class".constantize # => Class
+ def constantize
+ # TODO
+ # Inflector.constantize(self)
+ end
+
+ # Does the string start with the specified +prefix+?
+ def starts_with?(prefix)
+ prefix = prefix.to_s
+ self[0, prefix.length] == prefix
+ end
+ alias_method :start_with?, :starts_with?
+
+ # Does the string end with the specified +suffix+?
+ def ends_with?(suffix)
+ suffix = suffix.to_s
+ self[-suffix.length, suffix.length] == suffix
+ end
+ alias_method :end_with?, :ends_with?
+ end
+ end
+end
+
+String.send :include, MerbWheels::CoreExt::StringExt
View
17 lib/merb_wheels/core_ext/symbol_ext.rb
@@ -0,0 +1,17 @@
+# methods copied directly from ActiveSupport
+# see included /LICENSE
+
+unless :to_proc.respond_to?(:to_proc)
+ class Symbol
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
+ #
+ # # The same as people.collect { |p| p.name }
+ # people.collect(&:name)
+ #
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
+ # people.select(&:manager?).collect(&:salary)
+ def to_proc
+ Proc.new { |*args| args.shift.__send__(self, *args) }
+ end
+ end
+end
View
65 lib/merb_wheels/helpers/date_helpers.rb
@@ -0,0 +1,65 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module DateHelpers
+ # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
+ # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
+ # Distances are reported based on the following table:
+ #
+ # 0 <-> 29 secs # => less than a minute
+ # 30 secs <-> 1 min, 29 secs # => 1 minute
+ # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
+ # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
+ # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
+ # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
+ # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
+ # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
+ # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
+ # 1 yr <-> 2 yrs minus 1 secs # => about 1 year
+ # 2 yrs <-> max time or date # => over [2..X] years
+ #
+ # With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds:
+ # 0-4 secs # => less than 5 seconds
+ # 5-9 secs # => less than 10 seconds
+ # 10-19 secs # => less than 20 seconds
+ # 20-39 secs # => half a minute
+ # 40-59 secs # => less than a minute
+ # 60-89 secs # => 1 minute
+ #
+ # ==== Examples
+ # from_time = Time.now
+ # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
+ # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
+ # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
+ # distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years
+ # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
+ # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
+ # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
+ # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years
+ #
+ # to_time = Time.now + 6.years + 19.days
+ # distance_of_time_in_words(from_time, to_time, true) # => over 6 years
+ # distance_of_time_in_words(to_time, from_time, true) # => over 6 years
+ # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
+ time_lost_in_words(from_time, to_time, include_seconds, options[:locale])
+ end
+
+ # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
+ #
+ # ==== Examples
+ # time_ago_in_words(3.minutes.from_now) # => 3 minutes
+ # time_ago_in_words(Time.now - 15.hours) # => 15 hours
+ # time_ago_in_words(Time.now) # => less than a minute
+ #
+ # from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days
+ def time_ago_in_words(from_time, include_seconds = false)
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
+ end
+
+ alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
+ end
+ end
+end
View
59 lib/merb_wheels/helpers/javascript_helpers.rb
@@ -0,0 +1,59 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module JavascriptHelpers
+
+ JS_ESCAPE_MAP = {
+ '\\' => '\\\\',
+ '</' => '<\/',
+ "\r\n" => '\n',
+ "\n" => '\n',
+ "\r" => '\n',
+ '"' => '\\"',
+ "'" => "\\'" } unless defined?(JS_ESCAPE_MAP)
+
+ # Escape carrier returns and single and double quotes for JavaScript segments.
+ def escape_javascript(javascript)
+ if javascript
+ javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] }
+ else
+ ''
+ end
+ end
+
+ # Returns a JavaScript tag with the +content+ inside. Example:
+ # javascript_tag "alert('All is good')"
+ #
+ # Returns:
+ # <script type="text/javascript">
+ # //<![CDATA[
+ # alert('All is good')
+ # //]]>
+ # </script>
+ #
+ # +html_options+ may be a hash of attributes for the <script> tag. Example:
+ # javascript_tag "alert('All is good')", :defer => 'defer'
+ # # => <script defer="defer" type="text/javascript">alert('All is good')</script>
+ #
+ # Instead of passing the content as an argument, you can also use a block
+ # in which case, you pass your +html_options+ as the first parameter.
+ # <% javascript_tag :defer => 'defer' do -%>
+ # alert('All is good')
+ # <% end -%>
+ def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
+ content =
+ if block_given?
+ html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
+ capture(&block)
+ else
+ content_or_options_with_block
+ end
+
+ tag(:script, javascript_cdata_section(content), html_options.merge(:type => "text/javascript"))
+ end
+
+ def javascript_cdata_section(content) #:nodoc:
+ "\n//#{cdata_section("\n#{content}\n//")}\n"
+ end
+ end
+ end
+end
View
263 lib/merb_wheels/helpers/number_helpers.rb
@@ -0,0 +1,263 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module NumberHelpers
+ # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format
+ # in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:area_code</tt> - Adds parentheses around the area code.
+ # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-").
+ # * <tt>:extension</tt> - Specifies an extension to add to the end of the
+ # generated number.
+ # * <tt>:country_code</tt> - Sets the country code for the phone number.
+ #
+ # ==== Examples
+ # number_to_phone(1235551234) # => 123-555-1234
+ # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234
+ # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234
+ # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555
+ # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234
+ #
+ # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".")
+ # => +1.123.555.1234 x 1343
+ def number_to_phone(number, options = {})
+ number = number.to_s.strip unless number.nil?
+ options = options.stringify_keys
+ area_code = options["area_code"] || nil
+ delimiter = options["delimiter"] || "-"
+ extension = options["extension"].to_s.strip || nil
+ country_code = options["country_code"] || nil
+
+ begin
+ str = ""
+ str << "+#{country_code}#{delimiter}" unless country_code.blank?
+ if area_code
+ number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ else
+ number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ end
+ str << number unless number.blank?
+ str << " x #{extension}" unless extension.blank?
+ str
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format
+ # in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 2).
+ # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are:
+ #
+ # %u The currency unit
+ # %n The number
+ #
+ # ==== Examples
+ # number_to_currency(1234567890.50) # => $1,234,567,890.50
+ # number_to_currency(1234567890.506) # => $1,234,567,890.51
+ # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506
+ #
+ # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
+ # # => &pound;1234567890,50
+ # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
+ # # => 1234567890,50 &pound;
+ def number_to_currency(number, options = {})
+ options.symbolize_keys!
+
+ precision = options[:precision] || 2
+ unit = options[:unit] || "$"
+ separator = precision > 0 ? options[:separator] || "." : ""
+ delimiter = options[:delimiter] || ","
+ format = options[:format] || "%u%n"
+
+ begin
+ format.gsub(/%n/, number_with_precision(number,
+ :precision => precision,
+ :delimiter => delimiter,
+ :separator => separator)
+ ).gsub(/%u/, unit)
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ as a percentage string (e.g., 65%). You can customize the
+ # format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
+ #
+ # ==== Examples
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage(100, :precision => 0) # => 100%
+ # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399%
+ def number_to_percentage(number, options = {})
+ options.symbolize_keys!
+
+ precision = options[:precision] || 3
+ separator = options[:separator] || "."
+ delimiter = options[:delimiter] || ""
+
+ begin
+ number_with_precision(number,
+ :precision => precision,
+ :separator => separator,
+ :delimiter => delimiter) + "%"
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can
+ # customize the format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ #
+ # ==== Examples
+ # number_with_delimiter(12345678) # => 12,345,678
+ # number_with_delimiter(12345678.05) # => 12,345,678.05
+ # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678
+ # number_with_delimiter(12345678, :seperator => ",") # => 12,345,678
+ # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
+ # # => 98 765 432,98
+ #
+ # You can still use <tt>number_with_delimiter</tt> with the old API that accepts the
+ # +delimiter+ as its optional second and the +separator+ as its
+ # optional third parameter:
+ # number_with_delimiter(12345678, " ") # => 12 345.678
+ # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05
+ def number_with_delimiter(number, *args)
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ unless args.empty?
+ delimiter = args[0]
+ separator = args[1]
+ end
+
+ delimiter ||= (options[:delimiter] || ",")
+ separator ||= (options[:separator] || ".")
+
+ begin
+ parts = number.to_s.split('.')
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
+ parts.join(separator)
+ rescue
+ number
+ end
+ end
+
+ # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision of 2).
+ # You can customize the format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
+ #
+ # ==== Examples
+ # number_with_precision(111.2345) # => 111.235
+ # number_with_precision(111.2345, :precision => 2) # => 111.23
+ # number_with_precision(13, :precision => 5) # => 13.00000
+ # number_with_precision(389.32314, :precision => 0) # => 389
+ # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
+ # # => 1.111,23
+ #
+ # You can still use <tt>number_with_precision</tt> with the old API that accepts the
+ # +precision+ as its optional second parameter:
+ # number_with_precision(number_with_precision(111.2345, 2) # => 111.23
+ def number_with_precision(number, *args)
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ unless args.empty?
+ precision = args[0]
+ end
+
+ precision ||= (options[:precision] || 3)
+ separator ||= (options[:separator] || ".")
+ delimiter ||= (options[:delimiter] || "")
+
+ begin
+ rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision
+ number_with_delimiter("%01.#{precision}f" % rounded_number,
+ :separator => separator,
+ :delimiter => delimiter)
+ rescue
+ number
+ end
+ end
+
+ STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze unless defined?(STORAGE_UNITS)
+
+ # Formats the bytes in +size+ into a more understandable representation
+ # (e.g., giving it 1500 yields 1.5 KB). This method is useful for
+ # reporting file sizes to users. This method returns nil if
+ # +size+ cannot be converted into a number. You can customize the
+ # format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 1).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
+ #
+ # ==== Examples
+ # number_to_human_size(123) # => 123 Bytes
+ # number_to_human_size(1234) # => 1.2 KB
+ # number_to_human_size(12345) # => 12.1 KB
+ # number_to_human_size(1234567) # => 1.2 MB
+ # number_to_human_size(1234567890) # => 1.1 GB
+ # number_to_human_size(1234567890123) # => 1.1 TB
+ # number_to_human_size(1234567, :precision => 2) # => 1.18 MB
+ # number_to_human_size(483989, :precision => 0) # => 473 KB
+ # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB
+ #
+ # You can still use <tt>number_to_human_size</tt> with the old API that accepts the
+ # +precision+ as its optional second parameter:
+ # number_to_human_size(1234567, 2) # => 1.18 MB
+ # number_to_human_size(483989, 0) # => 473 KB
+ def number_to_human_size(number, *args)
+ return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024
+
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ unless args.empty?
+ precision = args[0]
+ end
+
+ precision ||= (options[:precision] || 1)
+ separator ||= (options[:separator] || ".")
+ delimiter ||= (options[:delimiter] || "")
+
+ max_exp = STORAGE_UNITS.size - 1
+ number = Float(number)
+ exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
+ exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
+ number /= 1024 ** exponent
+ unit = STORAGE_UNITS[exponent]
+
+ begin
+ escaped_separator = Regexp.escape(separator)
+ number_with_precision(number,
+ :precision => precision,
+ :separator => separator,
+ :delimiter => delimiter
+ ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}"
+ rescue
+ number
+ end
+ end
+ end
+ end
+end
View
232 lib/merb_wheels/helpers/sanitize_helpers.rb
@@ -0,0 +1,232 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module SanitizeHelpers
+ # This +sanitize+ helper will html encode all tags and strip all attributes that aren't specifically allowed.
+ # It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any
+ # tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out
+ # the extensive test suite.
+ #
+ # <%= sanitize @article.body %>
+ #
+ # You can add or remove tags/attributes if you want to customize it a bit. See ActionView::Base for full docs on the
+ # available options. You can add tags/attributes for single uses of +sanitize+ by passing either the <tt>:attributes</tt> or <tt>:tags</tt> options:
+ #
+ # Normal Use
+ #
+ # <%= sanitize @article.body %>
+ #
+ # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
+ #
+ # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style)
+ #
+ # Add table tags to the default allowed tags
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ # Remove tags to the default allowed tags
+ #
+ # Rails::Initializer.run do |config|
+ # config.after_initialize do
+ # ActionView::Base.sanitized_allowed_tags.delete 'div'
+ # end
+ # end
+ #
+ # Change allowed default attributes
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
+ # end
+ #
+ # Please note that sanitizing user-provided text does not guarantee that the
+ # resulting markup is valid (conforming to a document type) or even well-formed.
+ # The output may still contain e.g. unescaped '<', '>', '&' characters and
+ # confuse browsers.
+ #
+ def sanitize(html, options = {})
+ Inner.white_list_sanitizer.sanitize(html, options)
+ end
+
+ # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
+ def sanitize_css(style)
+ Inner.white_list_sanitizer.sanitize_css(style)
+ end
+
+ # Strips all HTML tags from the +html+, including comments. This uses the
+ # html-scanner tokenizer and so its HTML parsing ability is limited by
+ # that of html-scanner.
+ #
+ # ==== Examples
+ #
+ # strip_tags("Strip <i>these</i> tags!")
+ # # => Strip these tags!
+ #
+ # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
+ # # => Bold no more! See more here...
+ #
+ # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
+ # # => Welcome to my website!
+ def strip_tags(html)
+ Inner.full_sanitizer.sanitize(html)
+ end
+
+ # Strips all link tags from +text+ leaving just the link text.
+ #
+ # ==== Examples
+ # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
+ # # => Ruby on Rails
+ #
+ # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
+ # # => Please e-mail me at me@email.com.
+ #
+ # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
+ # # => Blog: Visit
+ def strip_links(html)
+ Inner.link_sanitizer.sanitize(html)
+ end
+
+ module Inner #:nodoc:
+ mattr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
+
+ def sanitized_protocol_separator
+ Inner.white_list_sanitizer.protocol_separator
+ end
+
+ def sanitized_uri_attributes
+ Inner.white_list_sanitizer.uri_attributes
+ end
+
+ def sanitized_bad_tags
+ Inner.white_list_sanitizer.bad_tags
+ end
+
+ def sanitized_allowed_tags
+ Inner.white_list_sanitizer.allowed_tags
+ end
+
+ def sanitized_allowed_attributes
+ Inner.white_list_sanitizer.allowed_attributes
+ end
+
+ def sanitized_allowed_css_properties
+ Inner.white_list_sanitizer.allowed_css_properties
+ end
+
+ def sanitized_allowed_css_keywords
+ Inner.white_list_sanitizer.allowed_css_keywords
+ end
+
+ def sanitized_shorthand_css_properties
+ Inner.white_list_sanitizer.shorthand_css_properties
+ end
+
+ def sanitized_allowed_protocols
+ Inner.white_list_sanitizer.allowed_protocols
+ end
+
+ def sanitized_protocol_separator=(value)
+ Inner.white_list_sanitizer.protocol_separator = value
+ end
+
+ # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
+ # any object that responds to +sanitize+.
+ def self.full_sanitizer
+ @full_sanitizer ||= MerbWheels::FullSanitizer.new
+ end
+
+ # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
+ # any object that responds to +sanitize+.
+ def self.link_sanitizer
+ @link_sanitizer ||= MerbWheels::LinkSanitizer.new
+ end
+
+ # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
+ # Replace with any object that responds to +sanitize+.
+ def self.white_list_sanitizer
+ @white_list_sanitizer ||= MerbWheels::WhiteListSanitizer.new
+ end
+
+ # Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
+ # end
+ #
+ def sanitized_uri_attributes=(attributes)
+ MerbWheels::WhiteListSanitizer.uri_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of 'bad' tags for the +sanitize+ helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_bad_tags = 'embed', 'object'
+ # end
+ #
+ def sanitized_bad_tags=(attributes)
+ MerbWheels::WhiteListSanitizer.bad_tags.merge(attributes)
+ end
+
+ # Adds to the Set of allowed tags for the +sanitize+ helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ def sanitized_allowed_tags=(attributes)
+ MerbWheels::WhiteListSanitizer.allowed_tags.merge(attributes)
+ end
+
+ # Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc'
+ # end
+ #
+ def sanitized_allowed_attributes=(attributes)
+ MerbWheels::WhiteListSanitizer.allowed_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_css_properties = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_properties=(attributes)
+ MerbWheels::WhiteListSanitizer.allowed_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_css_keywords = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_keywords=(attributes)
+ MerbWheels::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
+ end
+
+ # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_shorthand_css_properties = 'expression'
+ # end
+ #
+ def sanitized_shorthand_css_properties=(attributes)
+ MerbWheels::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed protocols for the +sanitize+ helper.
+ #
+ # Rails::Initializer.run do |config|
+ # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
+ # end
+ #
+ def sanitized_allowed_protocols=(attributes)
+ MerbWheels::WhiteListSanitizer.allowed_protocols.merge(attributes)
+ end
+ end
+ end
+ end
+end
View
42 lib/merb_wheels/helpers/tag_helpers.rb
@@ -0,0 +1,42 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module TagHelpers
+ # Returns a CDATA section with the given +content+. CDATA sections
+ # are used to escape blocks of text containing characters which would
+ # otherwise be recognized as markup. CDATA sections begin with the string
+ # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
+ #
+ # ==== Examples
+ # cdata_section("<hello world>")
+ # # => <![CDATA[<hello world>]]>
+ #
+ # cdata_section(File.read("hello_world.txt"))
+ # # => <![CDATA[<hello from a text file]]>
+ def cdata_section(content)
+ "<![CDATA[#{content}]]>"
+ end
+
+
+ # Returns an escaped version of +html+ without affecting existing escaped entities.
+ #
+ # ==== Examples
+ # escape_once("1 > 2 &amp; 3")
+ # # => "1 &lt; 2 &amp; 3"
+ #
+ # escape_once("&lt;&lt; Accept & Checkout")
+ # # => "&lt;&lt; Accept &amp; Checkout"
+ def escape_once(html)
+ html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| MerbWheels::Util::HTML_ESCAPE[special] }
+ end
+
+ def tag_options(options, escape = true)
+ unless options.blank?
+ options.reject{|k,v|v.nil?}.to_html_attributes
+
+ # todo: add escape
+ end
+ end
+
+ end
+ end
+end
View
337 lib/merb_wheels/helpers/text_helpers.rb
@@ -0,0 +1,337 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module TextHelpers
+
+ # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
+ # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...").
+ #
+ # ==== Examples
+ #
+ # truncate("Once upon a time in a world far far away")
+ # # => Once upon a time in a world f...
+ #
+ # truncate("Once upon a time in a world far far away", :length => 14)
+ # # => Once upon a...
+ #
+ # truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)")
+ # # => And they found that many (clipped)
+ #
+ # truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15)
+ # # => And they found... (continued)
+ #
+ # You can still use <tt>truncate</tt> with the old API that accepts the
+ # +length+ as its optional second and the +ellipsis+ as its
+ # optional third parameter:
+ # truncate("Once upon a time in a world far far away", 14)
+ # # => Once upon a time in a world f...
+ #
+ # truncate("And they found that many people were sleeping better.", 15, "... (continued)")
+ # # => And they found... (continued)
+ def truncate(text, *args)
+ options = extract_options_from_args!(args) || {}
+ unless args.empty?
+ options[:length] = args[0]
+ options[:omission] = args[1]
+ end
+
+ # set defaults
+ options[:length] ||= 30
+ options[:omission] ||= "..."
+
+ if text
+ return text.to_s.truncate(options[:length], options[:omission])
+ else
+ return ""
+ end
+ end
+
+ # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
+ # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
+ # as a single-quoted string with \1 where the phrase is to be inserted (defaults to
+ # '<strong class="highlight">\1</strong>')
+ #
+ # ==== Examples
+ # highlight('You searched for: rails', 'rails')
+ # # => You searched for: <strong class="highlight">rails</strong>
+ #
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
+ # # => You searched for: ruby, rails, dhh
+ #
+ # highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
+ # # => You searched <em>for</em>: <em>rails</em>
+ #
+ # highlight('You searched for: rails', 'rails', :highlighter => '<a href="search?q=\1">\1</a>')
+ # # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # You can still use <tt>highlight</tt> with the old API that accepts the
+ # +highlighter+ as its optional third parameter:
+ # highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>') # => You searched for: <a href="search?q=rails">rails</a>
+ def highlight(text, phrases, *args)
+ options = extract_options_from_args!(args) || {}
+ unless args.empty?
+ options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>'
+ end
+ options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>')
+
+ if text.blank? || phrases.blank?
+ text
+ else
+ match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
+ text.gsub(/(#{match})/i, options[:highlighter])
+ end
+ end
+
+ # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
+ # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
+ # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
+ # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The resulting string
+ # will be stripped in any case. If the +phrase+ isn't found, nil is returned.
+ #
+ # ==== Examples
+ # excerpt('This is an example', 'an', :radius => 5)
+ # # => ...s is an exam...
+ #
+ # excerpt('This is an example', 'is', :radius => 5)
+ # # => This is a...
+ #
+ # excerpt('This is an example', 'is')
+ # # => This is an example
+ #
+ # excerpt('This next thing is an example', 'ex', :radius => 2)
+ # # => ...next...
+ #
+ # excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ')
+ # # => <chop> is also an example
+ #
+ # You can still use <tt>excerpt</tt> with the old API that accepts the
+ # +radius+ as its optional third and the +ellipsis+ as its
+ # optional forth parameter:
+ # excerpt('This is an example', 'an', 5) # => ...s is an exam...
+ # excerpt('This is also an example', 'an', 8, '<chop> ') # => <chop> is also an example
+ def excerpt(text, phrase, *args)
+ options = extract_options_from_args!(args) || {}
+ unless args.empty?
+ options[:radius] = args[0] || 100
+ options[:omission] = args[1] || "..."
+ end
+ options.reverse_merge!(:radius => 100, :omission => "...")
+
+ if text && phrase
+ phrase = Regexp.escape(phrase)
+
+ if found_pos = text =~ /(#{phrase})/i
+ start_pos = [ found_pos - options[:radius], 0 ].max
+ end_pos = [ [ found_pos + phrase.length + options[:radius] - 1, 0].max, text.length ].min
+
+ prefix = start_pos > 0 ? options[:omission] : ""
+ postfix = end_pos < text.length - 1 ? options[:omission] : ""
+
+ prefix + text[start_pos..end_pos].strip + postfix
+ else
+ nil
+ end
+ end
+ end
+
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. If
+ # +plural+ is supplied, it will use that when count is > 1, otherwise
+ # it will use the Inflector to determine the plural form
+ #
+ # ==== Examples
+ # pluralize(1, 'person')
+ # # => 1 person
+ #
+ # pluralize(2, 'person')
+ # # => 2 people
+ #
+ # pluralize(3, 'person', 'users')
+ # # => 3 users
+ #
+ # pluralize(0, 'person')
+ # # => 0 people
+ def pluralize(count, singular, plural = nil)
+ "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
+ end
+
+ # Wraps the +text+ into lines no longer than +line_width+ width. This method
+ # breaks on the first whitespace character that does not exceed +line_width+
+ # (which is 80 by default).
+ #
+ # ==== Examples
+ #
+ # word_wrap('Once upon a time')
+ # # => Once upon a time
+ #
+ # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
+ # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\n a successor to the throne turned out to be more trouble than anyone could have\n imagined...
+ #
+ # word_wrap('Once upon a time', :line_width => 8)
+ # # => Once upon\na time
+ #
+ # word_wrap('Once upon a time', :line_width => 1)
+ # # => Once\nupon\na\ntime
+ #
+ # You can still use <tt>word_wrap</tt> with the old API that accepts the
+ # +line_width+ as its optional second parameter:
+ # word_wrap('Once upon a time', 8) # => Once upon\na time
+ def word_wrap(text, *args)
+ options = extract_options_from_args!(args) || {}
+ unless args.blank?
+ options[:line_width] = args[0] || 80
+ end
+ options.reverse_merge!(:line_width => 80)
+
+ text.split("\n").collect do |line|
+ line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
+ end * "\n"
+ end
+
+
+ # Returns +text+ transformed into HTML using simple formatting rules.
+ # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
+ # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
+ # considered as a linebreak and a <tt><br /></tt> tag is appended. This
+ # method does not remove the newlines from the +text+.
+ #
+ # You can pass any HTML attributes into <tt>html_options</tt>. These
+ # will be added to all created paragraphs.
+ # ==== Examples
+ # my_text = "Here is some basic text...\n...with a line break."
+ #
+ # simple_format(my_text)
+ # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
+ #
+ # more_text = "We want to put a paragraph...\n\n...right there."
+ #
+ # simple_format(more_text)
+ # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
+ #
+ # simple_format("Look ma! A class!", :class => 'description')
+ # # => "<p class='description'>Look ma! A class!</p>"
+ def simple_format(text, html_options={})
+ start_tag = open_tag('p', html_options)
+ text = text.to_s.dup
+ text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
+ text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}") # 2+ newline -> paragraph
+ text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
+ text.insert 0, start_tag
+ text << "</p>"
+ end
+
+
+ # Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option
+ # will limit what should be linked. You can add HTML attributes to the links using
+ # <tt>:href_options</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default),
+ # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and
+ # e-mail address is yielded and the result is used as the link text.
+ #
+ # ==== Examples
+ # auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com")
+ # # => "Go to <a href=\"http://www.rubyonrails.org\">http://www.rubyonrails.org</a> and
+ # # say hello to <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
+ #
+ # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :link => :urls)
+ # # => "Visit <a href=\"http://www.loudthinking.com/\">http://www.loudthinking.com/</a>
+ # # or e-mail david@loudthinking.com"
+ #
+ # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :link => :email_addresses)
+ # # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
+ #
+ # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
+ # auto_link(post_body, :href_options => { :target => '_blank' }) do |text|
+ # truncate(text, 15)
+ # end
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
+ # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
+ #
+ #
+ # You can still use <tt>auto_link</tt> with the old API that accepts the
+ # +link+ as its optional second parameter and the +html_options+ hash
+ # as its optional third parameter:
+ # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
+ # auto_link(post_body, :urls) # => Once upon\na time
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\">http://www.myblog.com</a>.
+ # Please e-mail me at me@email.com."
+ #
+ # auto_link(post_body, :all, :target => "_blank") # => Once upon\na time
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>.
+ # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
+ def auto_link(text, *args, &block)#link = :all, href_options = {}, &block)
+ return '' if text.blank?
+
+ options = extract_options_from_args!(args) if args.size != 2
+ options ||= {}
+ unless args.empty?
+ options[:link] = args[0] || :all
+ options[:html] = args[1] || {}
+ end
+ options.reverse_merge!(:link => :all, :html => {})
+
+ case options[:link].to_sym
+ when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), &block)
+ when :email_addresses then auto_link_email_addresses(text, &block)
+ when :urls then auto_link_urls(text, options[:html], &block)
+ end
+ end
+
+ # Turns all urls into clickable links. If a block is given, each url
+ # is yielded and the result is used as the link text.
+ def auto_link_urls(text, html_options = {})
+ extra_options = Mash.new(html_options).to_html_attributes
+ extra_options = " #{extra_options}" unless extra_options.blank?
+
+ text.gsub(AUTO_LINK_RE) do
+ all, a, b, c, d = $&, $1, $2, $3, $4
+ if a =~ /<a\s/i # don't replace URL's that are already linked
+ all
+ else
+ text = b + c
+ text = yield(text) if block_given?
+ %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{text}</a>#{d})
+ end
+ end
+ end
+
+ # Turns all email addresses into clickable links. If a block is given,
+ # each email is yielded and the result is used as the link text.
+ def auto_link_email_addresses(text)
+ body = text.dup
+ text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
+ text = $1
+
+ if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/)
+ text
+ else
+ display_text = (block_given?) ? yield(text) : text
+ %{<a href="mailto:#{text}">#{display_text}</a>}
+ end
+ end
+ end
+
+
+ private
+ AUTO_LINK_RE = %r{
+ ( # leading text
+ <\w+.*?>| # leading HTML tag, or
+ [^=!:'"/]| # leading punctuation, or
+ ^ # beginning of line
+ )
+ (
+ (?:https?://)| # protocol spec, or
+ (?:www\.) # www.*
+ )
+ (
+ [-\w]+ # subdomain or domain
+ (?:\.[-\w]+)* # remaining subdomains or domain
+ (?::\d+)? # port
+ (?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path
+ (?:\?[\w\+@%&=.;:-]+)? # query string
+ (?:\#[\w\-]*)? # trailing anchor
+ )
+ ([[:punct:]]|<|$|) # trailing text
+ }x unless const_defined?(:AUTO_LINK_RE)
+
+
+ end
+ end
+end
View
7 lib/merb_wheels/helpers/url_helpers.rb
@@ -0,0 +1,7 @@
+module MerbWheels #:nodoc:
+ module Helpers #:nodoc:
+ module UrlHelpers
+
+ end
+ end
+end
View
63 lib/merb_wheels/html-scanner/document.rb
@@ -0,0 +1,63 @@
+module HTML #:nodoc:
+ # A top-level HTMl document. You give it a body of text, and it will parse that
+ # text into a tree of nodes.
+ class Document #:nodoc:
+
+ # The root of the parsed document.
+ attr_reader :root
+
+ # Create a new Document from the given text.
+ def initialize(text, strict=false, xml=false)
+ tokenizer = Tokenizer.new(text)
+ @root = Node.new(nil)
+ node_stack = [ @root ]
+ while token = tokenizer.next
+ node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
+
+ node_stack.last.children << node unless node.tag? && node.closing == :close
+ if node.tag?
+ if node_stack.length > 1 && node.closing == :close
+ if node_stack.last.name == node.name
+ if node_stack.last.children.empty?
+ node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
+ end
+ node_stack.pop
+ else
+ open_start = node_stack.last.position - 20
+ open_start = 0 if open_start < 0
+ close_start = node.position - 20
+ close_start = 0 if close_start < 0
+ msg = <<EOF.strip
+ignoring attempt to close #{node_stack.last.name} with #{node.name}
+ opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
+ closed at byte #{node.position}, line #{node.line}
+ attributes at open: #{node_stack.last.attributes.inspect}
+ text around open: #{text[open_start,40].inspect}
+ text around close: #{text[close_start,40].inspect}
+EOF
+ strict ? raise(msg) : warn(msg)
+ end
+ elsif !node.childless?(xml) && node.closing != :close
+ node_stack.push node
+ end
+ end
+ end
+ end
+
+ # Search the tree for (and return) the first node that matches the given
+ # conditions. The conditions are interpreted differently for different node
+ # types, see HTML::Text#find and HTML::Tag#find.
+ def find(conditions)
+ @root.find(conditions)
+ end
+
+ # Search the tree for (and return) all nodes that match the given
+ # conditions. The conditions are interpreted differently for different node
+ # types, see HTML::Text#find and HTML::Tag#find.
+ def find_all(conditions)
+ @root.find_all(conditions)
+ end
+
+ end
+
+end
View
537 lib/merb_wheels/html-scanner/node.rb
@@ -0,0 +1,537 @@
+require 'strscan'
+
+module HTML #:nodoc:
+
+ class Conditions < Hash #:nodoc:
+ def initialize(hash)
+ super()
+ hash = { :content => hash } unless Hash === hash
+ hash = keys_to_symbols(hash)
+ hash.each do |k,v|
+ case k
+ when :tag, :content then
+ # keys are valid, and require no further processing
+ when :attributes then
+ hash[k] = keys_to_strings(v)
+ when :parent, :child, :ancestor, :descendant, :sibling, :before,
+ :after
+ hash[k] = Conditions.new(v)
+ when :children
+ hash[k] = v = keys_to_symbols(v)
+ v.each do |k,v2|
+ case k
+ when :count, :greater_than, :less_than
+ # keys are valid, and require no further processing
+ when :only
+ v[k] = Conditions.new(v2)
+ else
+ raise "illegal key #{k.inspect} => #{v2.inspect}"
+ end
+ end
+ else
+ raise "illegal key #{k.inspect} => #{v.inspect}"
+ end
+ end
+ update hash
+ end
+
+ private
+
+ def keys_to_strings(hash)
+ hash.keys.inject({}) do |h,k|
+ h[k.to_s] = hash[k]
+ h
+ end
+ end
+
+ def keys_to_symbols(hash)
+ hash.keys.inject({}) do |h,k|
+ raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
+ h[k.to_sym] = hash[k]
+ h
+ end
+ end
+ end
+
+ # The base class of all nodes, textual and otherwise, in an HTML document.
+ class Node #:nodoc:
+ # The array of children of this node. Not all nodes have children.
+ attr_reader :children
+
+ # The parent node of this node. All nodes have a parent, except for the
+ # root node.
+ attr_reader :parent
+
+ # The line number of the input where this node was begun
+ attr_reader :line
+
+ # The byte position in the input where this node was begun
+ attr_reader :position
+
+ # Create a new node as a child of the given parent.
+ def initialize(parent, line=0, pos=0)
+ @parent = parent
+ @children = []
+ @line, @position = line, pos
+ end
+
+ # Return a textual representation of the node.
+ def to_s
+ s = ""
+ @children.each { |child| s << child.to_s }
+ s
+ end
+
+ # Return false (subclasses must override this to provide specific matching
+ # behavior.) +conditions+ may be of any type.
+ def match(conditions)
+ false
+ end
+
+ # Search the children of this node for the first node for which #find
+ # returns non +nil+. Returns the result of the #find call that succeeded.
+ def find(conditions)
+ conditions = validate_conditions(conditions)
+ @children.each do |child|
+ node = child.find(conditions)
+ return node if node
+ end
+ nil
+ end
+
+ # Search for all nodes that match the given conditions, and return them
+ # as an array.
+ def find_all(conditions)
+ conditions = validate_conditions(conditions)
+
+ matches = []
+ matches << self if match(conditions)
+ @children.each do |child|
+ matches.concat child.find_all(conditions)
+ end
+ matches
+ end
+
+ # Returns +false+. Subclasses may override this if they define a kind of
+ # tag.
+ def tag?
+ false
+ end
+
+ def validate_conditions(conditions)
+ Conditions === conditions ? conditions : Conditions.new(conditions)
+ end
+
+ def ==(node)
+ return false unless self.class == node.class && children.size == node.children.size
+
+ equivalent = true
+
+ children.size.times do |i|
+ equivalent &&= children[i] == node.children[i]
+ end
+
+ equivalent
+ end
+
+ class <<self
+ def parse(parent, line, pos, content, strict=true)
+ if content !~ /^<\S/
+ Text.new(parent, line, pos, content)
+ else
+ scanner = StringScanner.new(content)
+
+ unless scanner.skip(/</)
+ if strict
+ raise "expected <"
+ else
+ return Text.new(parent, line, pos, content)
+ end
+ end
+
+ if scanner.skip(/!\[CDATA\[/)
+ unless scanner.skip_until(/\]\]>/)
+ if strict
+ raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
+ else
+ scanner.skip_until(/\Z/)
+ end
+ end
+
+ return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
+ end
+
+ closing = ( scanner.scan(/\//) ? :close : nil )
+ return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
+ name.downcase!
+
+ unless closing
+ scanner.skip(/\s*/)
+ attributes = {}
+ while attr = scanner.scan(/[-\w:]+/)
+ value = true
+ if scanner.scan(/\s*=\s*/)
+ if delim = scanner.scan(/['"]/)
+ value = ""
+ while text = scanner.scan(/[^#{delim}\\]+|./)
+ case text
+ when "\\" then
+ value << text
+ value << scanner.getch
+ when delim
+ break
+ else value << text
+ end
+ end
+ else
+ value = scanner.scan(/[^\s>\/]+/)
+ end
+ end
+ attributes[attr.downcase] = value
+ scanner.skip(/\s*/)
+ end
+
+ closing = ( scanner.scan(/\//) ? :self : nil )
+ end
+
+ unless scanner.scan(/\s*>/)
+ if strict
+ raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
+ else
+ # throw away all text until we find what we're looking for
+ scanner.skip_until(/>/) or scanner.terminate
+ end
+ end
+
+ Tag.new(parent, line, pos, name, attributes, closing)
+ end
+ end
+ end
+ end
+
+ # A node that represents text, rather than markup.
+ class Text < Node #:nodoc:
+
+ attr_reader :content
+
+ # Creates a new text node as a child of the given parent, with the given
+ # content.
+ def initialize(parent, line, pos, content)
+ super(parent, line, pos)
+ @content = content
+ end
+
+ # Returns the content of this node.
+ def to_s
+ @content
+ end
+
+ # Returns +self+ if this node meets the given conditions. Text nodes support
+ # conditions of the following kinds:
+ #
+ # * if +conditions+ is a string, it must be a substring of the node's
+ # content
+ # * if +conditions+ is a regular expression, it must match the node's
+ # content
+ # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
+ # is either a string or a regexp, and which is interpreted as described
+ # above.
+ def find(conditions)
+ match(conditions) && self
+ end
+
+ # Returns non-+nil+ if this node meets the given conditions, or +nil+
+ # otherwise. See the discussion of #find for the valid conditions.
+ def match(conditions)
+ case conditions
+ when String
+ @content == conditions
+ when Regexp
+ @content =~ conditions
+ when Hash
+ conditions = validate_conditions(conditions)
+
+ # Text nodes only have :content, :parent, :ancestor
+ unless (conditions.keys - [:content, :parent, :ancestor]).empty?
+ return false
+ end
+
+ match(conditions[:content])
+ else
+ nil
+ end
+ end
+
+ def ==(node)
+ return false unless super
+ content == node.content
+ end
+ end
+
+ # A CDATA node is simply a text node with a specialized way of displaying
+ # itself.
+ class CDATA < Text #:nodoc:
+ def to_s
+ "<![CDATA[#{super}]]>"
+ end
+ end
+
+ # A Tag is any node that represents markup. It may be an opening tag, a
+ # closing tag, or a self-closing tag. It has a name, and may have a hash of
+ # attributes.
+ class Tag < Node #:nodoc:
+
+ # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
+ attr_reader :closing
+
+ # Either +nil+, or a hash of attributes for this node.
+ attr_reader :attributes
+
+ # The name of this tag.
+ attr_reader :name
+
+ # Create a new node as a child of the given parent, using the given content
+ # to describe the node. It will be parsed and the node name, attributes and
+ # closing status extracted.
+ def initialize(parent, line, pos, name, attributes, closing)
+ super(parent, line, pos)
+ @name = name
+ @attributes = attributes
+ @closing = closing
+ end
+
+ # A convenience for obtaining an attribute of the node. Returns +nil+ if
+ # the node has no attributes.
+ def [](attr)
+ @attributes ? @attributes[attr] : nil
+ end
+
+ # Returns non-+nil+ if this tag can contain child nodes.
+ def childless?(xml = false)
+ return false if xml && @closing.nil?
+ !@closing.nil? ||
+ @name =~ /^(img|br|hr|link|meta|area|base|basefont|
+ col|frame|input|isindex|param)$/ox
+ end
+
+ # Returns a textual representation of the node
+ def to_s
+ if @closing == :close
+ "</#{@name}>"
+ else
+ s = "<#{@name}"
+ @attributes.each do |k,v|
+ s << " #{k}"
+ s << "=\"#{v}\"" if String === v
+ end
+ s << " /" if @closing == :self
+ s << ">"
+ @children.each { |child| s << child.to_s }
+ s << "</#{@name}>" if @closing != :self && !@children.empty?
+ s
+ end
+ end
+
+ # If either the node or any of its children meet the given conditions, the
+ # matching node is returned. Otherwise, +nil+ is returned. (See the
+ # description of the valid conditions in the +match+ method.)
+ def find(conditions)
+ match(conditions) && self || super
+ end
+
+ # Returns +true+, indicating that this node represents an HTML tag.
+ def tag?
+ true
+ end
+
+ # Returns +true+ if the node meets any of the given conditions. The
+ # +conditions+ parameter must be a hash of any of the following keys
+ # (all are optional):
+ #
+ # * <tt>:tag</tt>: the node name must match the corresponding value
+ # * <tt>:attributes</tt>: a hash. The node's values must match the
+ # corresponding values in the hash.
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
+ # corresponding hash.
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
+ # must meet the criteria described by the hash.
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
+ # meet the criteria described by the hash.
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
+ # must meet the criteria described by the hash.
+ # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
+ # meet the criteria described by the hash.
+ # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
+ # keys:
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
+ # include) the number of children that match.
+ # ** <tt>:less_than</tt>: the number of matching children must be less than
+ # this number.
+ # ** <tt>:greater_than</tt>: the number of matching children must be
+ # greater than this number.
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
+ # to match on the children, and only matching children will be
+ # counted.
+ #
+ # Conditions are matched using the following algorithm:
+ #
+ # * if the condition is a string, it must be a substring of the value.
+ # * if the condition is a regexp, it must match the value.
+ # * if the condition is a number, the value must match number.to_s.
+ # * if the condition is +true+, the value must not be +nil+.
+ # * if the condition is +false+ or +nil+, the value must be +nil+.
+ #
+ # Usage:
+ #
+ # # test if the node is a "span" tag
+ # node.match :tag => "span"
+ #
+ # # test if the node's parent is a "div"
+ # node.match :parent => { :tag => "div" }
+ #
+ # # test if any of the node's ancestors are "table" tags
+ # node.match :ancestor => { :tag => "table" }
+ #
+ # # test if any of the node's immediate children are "em" tags
+ # node.match :child => { :tag => "em" }
+ #
+ # # test if any of the node's descendants are "strong" tags
+ # node.match :descendant => { :tag => "strong" }
+ #
+ # # test if the node has between 2 and 4 span tags as immediate children
+ # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
+ #
+ # # get funky: test to see if the node is a "div", has a "ul" ancestor
+ # # and an "li" parent (with "class" = "enum"), and whether or not it has
+ # # a "span" descendant that contains # text matching /hello world/:
+ # node.match :tag => "div",
+ # :ancestor => { :tag => "ul" },
+ # :parent => { :tag => "li",
+ # :attributes => { :class => "enum" } },
+ # :descendant => { :tag => "span",
+ # :child => /hello world/ }
+ def match(conditions)
+ conditions = validate_conditions(conditions)
+ # check content of child nodes
+ if conditions[:content]
+ if children.empty?
+ return false unless match_condition("", conditions[:content])
+ else
+ return false unless children.find { |child| child.match(conditions[:content]) }
+ end
+ end
+
+ # test the name
+ return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
+
+ # test attributes
+ (conditions[:attributes] || {}).each do |key, value|
+ return false unless match_condition(self[key], value)
+ end
+
+ # test parent
+ return false unless parent.match(conditions[:parent]) if conditions[:parent]
+
+ # test children
+ return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
+
+ # test ancestors
+ if conditions[:ancestor]
+ return false unless catch :found do
+ p = self
+ throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
+ end
+ end
+
+ # test descendants
+ if conditions[:descendant]
+ return false unless children.find do |child|
+ # test the child
+ child.match(conditions[:descendant]) ||
+ # test the child's descendants
+ child.match(:descendant => conditions[:descendant])
+ end
+ end
+
+ # count children
+ if opts = conditions[:children]
+ matches = children.select do |c|
+ (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
+ end
+
+ matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]