From 03520b5fd45ca45fd639e50c26e800f4779976a2 Mon Sep 17 00:00:00 2001 From: Richie Vos Date: Mon, 7 Dec 2009 20:57:41 -0600 Subject: [PATCH 001/677] Refactored the rake tasks No longer does includes against Object Uses 2 custom rake tasks to keep the task code together --- tasks/sitemap_generator_tasks.rake | 75 +++++++++++++++++++----------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index cb6dfac7..b897afb4 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,32 +1,24 @@ require 'zlib' -namespace :sitemap do - - desc "Install a default config/sitemap.rb file" - task :install do - load File.expand_path(File.join(File.dirname(__FILE__), "../rails/install.rb")) - end - - desc "Delete all Sitemap files in public/ directory" - task :clean do - sitemap_files = Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')] - FileUtils.rm sitemap_files - end +class SiteMapRefreshTask < Rake::Task + include SitemapGenerator::Helper - desc "Create Sitemap XML files in public/ directory" - desc "Create Sitemap XML files in public/ directory (rake -s for no output)" - task :refresh => ['sitemap:create'] do + def execute(*) + super ping_search_engines("sitemap_index.xml.gz") end +end - desc "Create Sitemap XML files (don't ping search engines)" - task 'refresh:no_ping' => ['sitemap:create'] do - end +class SiteMapCreateTask < Rake::Task + include SitemapGenerator::Helper + include ActionView::Helpers::NumberHelper - task :create => [:environment] do - include SitemapGenerator::Helper - include ActionView::Helpers::NumberHelper + def execute(*) + super + build_files + end + def build_files start_time = Time.now # update links from config/sitemap.rb @@ -34,12 +26,22 @@ namespace :sitemap do raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? - links_grps = SitemapGenerator::Sitemap.links.in_groups_of(50000, false) + links_grps = SitemapGenerator::Sitemap.links.in_groups_of(50_000, false) raise(ArgumentError, "TOO MANY LINKS!! I really thought 2,500,000,000 links would be enough for anybody!") if links_grps.length > 50000 Rake::Task['sitemap:clean'].invoke # render individual sitemaps + sitemap_files = render_sitemap(links_grps) + + # render index + render_index(sitemap_files) + + stop_time = Time.now + puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + end + + def render_sitemap(links_grps) sitemap_files = [] links_grps.each_with_index do |links, index| buffer = '' @@ -49,12 +51,14 @@ namespace :sitemap do Zlib::GzipWriter.open(filename) do |gz| gz.write buffer end + sitemap_files << filename puts "+ #{filename}" if verbose puts "** Sitemap too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose - sitemap_files << filename end + sitemap_files + end - # render index + def render_index(sitemap_files) buffer = '' xml = Builder::XmlMarkup.new(:target=>buffer) eval(open(SitemapGenerator.templates[:sitemap_index]).read, binding) @@ -62,10 +66,29 @@ namespace :sitemap do Zlib::GzipWriter.open(filename) do |gz| gz.write buffer end + puts "+ #{filename}" if verbose puts "** Sitemap Index too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose + end +end - stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose +namespace :sitemap do + desc "Install a default config/sitemap.rb file" + task :install do + load File.expand_path(File.join(File.dirname(__FILE__), "../rails/install.rb")) end + + desc "Delete all Sitemap files in public/ directory" + task :clean do + sitemap_files = Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')] + FileUtils.rm sitemap_files + end + + desc "Create Sitemap XML files in public/ directory (rake -s for no output)" + SiteMapRefreshTask.define_task :refresh => ['sitemap:create'] + + desc "Create Sitemap XML files (don't ping search engines)" + task 'refresh:no_ping' => ['sitemap:create'] + + SiteMapCreateTask.define_task :create => [:environment] end From 39d33ed76d813a582fa06938bb3cea94c6b0809b Mon Sep 17 00:00:00 2001 From: Richie Vos Date: Mon, 7 Dec 2009 21:02:58 -0600 Subject: [PATCH 002/677] Use UrlWriter directly instead of getting at it through ApplicationController --- lib/sitemap_generator/helper.rb | 26 +++++++++++--------------- tasks/sitemap_generator_tasks.rake | 1 + 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb index 7536bff5..650e7649 100644 --- a/lib/sitemap_generator/helper.rb +++ b/lib/sitemap_generator/helper.rb @@ -1,22 +1,18 @@ -require 'action_controller' -require 'action_controller/test_process' -begin - require 'application_controller' -rescue LoadError - # Rails < 2.3 - require 'application' -end - module SitemapGenerator module Helper + include ActionController::UrlWriter + + def self.included(base) + base.class_eval do + def self.default_url_options(options = nil) + {} + end + end + end + def load_sitemap_rb - controller = ApplicationController.new - controller.request = ActionController::TestRequest.new - controller.params = {} - controller.send(:initialize_current_url) - b = controller.instance_eval{binding} sitemap_mapper_file = File.join(RAILS_ROOT, 'config/sitemap.rb') - eval(open(sitemap_mapper_file).read, b) + eval(open(sitemap_mapper_file).read) end def url_with_hostname(path) diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index b897afb4..35ece17a 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -18,6 +18,7 @@ class SiteMapCreateTask < Rake::Task build_files end + private def build_files start_time = Time.now From cc9b14c04a92648803920e3d387c55c9465c360c Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 11 Dec 2009 16:06:47 +0800 Subject: [PATCH 003/677] Version bump to 0.2.3 * Update README * Integrate Richie's changes * Bugfixes --- README.md | 28 +++++++++++++++++++++++++++- Rakefile | 6 +++--- VERSION | 2 +- lib/sitemap_generator/helper.rb | 7 +++++-- sitemap_generator.gemspec | 10 +++++----- tasks/sitemap_generator_tasks.rake | 1 + templates/sitemap.rb | 12 ++++++++++++ 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bad4ff16..67f307cd 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ SitemapGenerator This plugin enables ['enterprise-class'][enterprise_class] Google Sitemaps to be easily generated for a Rails site as a rake task, using a simple 'Rails Routes'-like DSL. +Foreword +------- + +Unfortunately, Adam Salter passed away recently. Those who knew him know what an amazing guy he was, and what an excellent Rails programmer he was. His passing is a great loss to the Rails community. + +I will be taking over maintaining this gem from him. -- Karl + Raison d'être ------- @@ -60,7 +67,7 @@ Installation 1. Install plugin as normal - $ ./script/plugin install git://github.com/adamsalter/sitemap_generator.git + $ ./script/plugin install git://github.com/kjvarga/sitemap_generator.git ---- @@ -111,6 +118,18 @@ Example 'config/sitemap.rb' end + # Including Sitemaps from Rails Engines. + # + # These Sitemaps should be almost identical to a regular Sitemap file except + # they needn't define their own SitemapGenerator::Sitemap.default_host since + # they will undoubtedly share the host name of the application they belong to. + # + # As an example, say we have a Rails Engine in vendor/plugins/cadability_client + # We can include its Sitemap here as follows: + # + file = File.join(Rails.root, 'vendor/plugins/cadability_client/config/sitemap.rb') + eval(open(file).read, binding, file) + Notes ======= @@ -135,6 +154,8 @@ Notes end end +4) If generation of your sitemap fails for some reason, the old sitemap will remain in public/. This ensures that robots will always find a valid sitemap. Running silently (`rake -s sitemap:refresh`) and with email forwarding setup you'll only get an email if your sitemap fails to build, and no notification when everything is fine - which will be most of the time. + Known Bugs ======== @@ -142,6 +163,11 @@ Known Bugs - There's no check on the size of a URL which [isn't supposed to exceed 2,048 bytes][sitemaps_xml]. - Currently only supports one Sitemap Index file, which can contain 50,000 Sitemap files which can each contain 50,000 urls, so it _only_ supports up to 2,500,000,000 (2.5 billion) urls. I personally have no need of support for more urls, but plugin could be improved to support this. +Wishlist +======== + +- Auto coverage testing. Generate a report of broken URLs by checking the status codes of each page in the sitemap. + Thanks (in no particular order) ======== diff --git a/Rakefile b/Rakefile index bbce2126..6cd09694 100644 --- a/Rakefile +++ b/Rakefile @@ -7,9 +7,9 @@ begin s.name = "sitemap_generator" s.summary = %Q{Generate 'enterprise-class' Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} s.description = %Q{Install as a plugin or Gem to easily generate ['enterprise-class'][enterprise_class] Google Sitemaps for your Rails site, using a simple 'Rails Routes'-like DSL and a single rake task.} - s.email = "adam.salter@codebright.net " - s.homepage = "http://github.com/adamsalter/sitemap_generator" - s.authors = ["Adam Salter"] + s.email = "kjvarga@gmail.com" + s.homepage = "http://github.com/kjvarga/sitemap_generator" + s.authors = ["Adam Salter", "Karl Varga"] s.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] # s is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end diff --git a/VERSION b/VERSION index ee1372d3..71790396 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2 +0.2.3 diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb index 650e7649..19bbc4e2 100644 --- a/lib/sitemap_generator/helper.rb +++ b/lib/sitemap_generator/helper.rb @@ -1,3 +1,6 @@ +require 'sitemap_generator' +require 'action_controller' + module SitemapGenerator module Helper include ActionController::UrlWriter @@ -5,7 +8,7 @@ module Helper def self.included(base) base.class_eval do def self.default_url_options(options = nil) - {} + { :host => SitemapGenerator::Sitemap.default_host } end end end @@ -16,7 +19,7 @@ def load_sitemap_rb end def url_with_hostname(path) - URI.join(Sitemap.default_host, path).to_s + URI.join(SitemapGenerator::Sitemap.default_host, path).to_s end def w3c_date(date) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 4c8f7c9f..09273b1b 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,13 +5,13 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.2.2" + s.version = "0.2.3" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Adam Salter"] - s.date = %q{2009-11-10} + s.authors = ["Adam Salter", "Karl Varga"] + s.date = %q{2009-12-11} s.description = %q{Install as a plugin or Gem to easily generate ['enterprise-class'][enterprise_class] Google Sitemaps for your Rails site, using a simple 'Rails Routes'-like DSL and a single rake task.} - s.email = %q{adam.salter@codebright.net } + s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ "README.md" ] @@ -33,7 +33,7 @@ Gem::Specification.new do |s| "templates/sitemap_index.builder", "templates/xml_sitemap.builder" ] - s.homepage = %q{http://github.com/adamsalter/sitemap_generator} + s.homepage = %q{http://github.com/kjvarga/sitemap_generator} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.rubygems_version = %q{1.3.5} diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index 35ece17a..718e9281 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,4 +1,5 @@ require 'zlib' +require 'sitemap_generator/helper' class SiteMapRefreshTask < Rake::Task include SitemapGenerator::Helper diff --git a/templates/sitemap.rb b/templates/sitemap.rb index 9210a159..ecccb8f2 100644 --- a/templates/sitemap.rb +++ b/templates/sitemap.rb @@ -28,3 +28,15 @@ sitemap.add '/purchase', :priority => 0.7, :host => "https://www.example.com" end + +# Including Sitemaps from Rails Engines. +# +# These Sitemaps should be almost identical to a regular Sitemap file except +# they needn't define their own SitemapGenerator::Sitemap.default_host since +# they will undoubtedly share the host name of the application they belong to. +# +# As an example, say we have a Rails Engine in vendor/plugins/cadability_client +# We can include its Sitemap here as follows: +# +# file = File.join(Rails.root, 'vendor/plugins/cadability_client/config/sitemap.rb') +# eval(open(file).read, binding, file) \ No newline at end of file From b7e1555de1954ebae3c80073940d3c9b79739a48 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 11 Dec 2009 21:30:54 +0800 Subject: [PATCH 004/677] Test as a Gem install rather than a plugin. --- test/{mock_app => mock_app_gem}/.gitignore | 0 test/{mock_app => mock_app_gem}/README | 0 test/mock_app_gem/Rakefile | 11 + .../app/controllers/application_controller.rb | 0 .../app/controllers/contents_controller.rb | 0 .../app/models/content.rb | 0 test/mock_app_gem/config/boot.rb | 110 + .../config/database.yml | 0 test/mock_app_gem/config/environment.rb | 42 + .../config/environments/development.rb | 0 .../config/environments/production.rb | 0 .../config/environments/test.rb | 0 .../initializers/backtrace_silencers.rb | 0 .../config/initializers/inflections.rb | 0 .../config/initializers/mime_types.rb | 0 .../config/initializers/new_rails_defaults.rb | 0 .../config/initializers/session_store.rb | 0 .../config/locales/en.yml | 0 .../config/routes.rb | 0 .../config/sitemap.rb | 0 .../migrate/20090826121911_create_contents.rb | 0 test/{mock_app => mock_app_gem}/db/schema.rb | 0 .../db/test.sqlite3 | Bin .../public/index.html | 0 .../public/javascripts/application.js | 0 .../public/javascripts/controls.js | 963 ++++ .../public/javascripts/dragdrop.js | 973 ++++ .../public/javascripts/effects.js | 1128 +++++ .../public/javascripts/prototype.js | 4320 +++++++++++++++++ test/mock_app_gem/script/about | 4 + .../{mock_app => mock_app_gem}/script/console | 0 test/mock_app_gem/script/dbconsole | 3 + test/mock_app_gem/script/destroy | 3 + test/mock_app_gem/script/generate | 3 + .../script/performance/benchmarker | 3 + test/mock_app_gem/script/performance/profiler | 3 + test/mock_app_gem/script/plugin | 3 + test/mock_app_gem/script/runner | 3 + test/mock_app_gem/script/server | 3 + .../vendor/gems/sitemap_generator-1.2.3 | 1 + test/mock_app_plugin/.gitignore | 3 + test/mock_app_plugin/README | 243 + test/{mock_app => mock_app_plugin}/Rakefile | 0 .../app/controllers/application_controller.rb | 10 + .../app/controllers/contents_controller.rb | 85 + test/mock_app_plugin/app/models/content.rb | 2 + .../config/boot.rb | 0 test/mock_app_plugin/config/database.yml | 5 + .../config/environment.rb | 0 .../config/environments/development.rb | 17 + .../config/environments/production.rb | 28 + .../config/environments/test.rb | 28 + .../initializers/backtrace_silencers.rb | 7 + .../config/initializers/inflections.rb | 10 + .../config/initializers/mime_types.rb | 5 + .../config/initializers/new_rails_defaults.rb | 19 + .../config/initializers/session_store.rb | 15 + test/mock_app_plugin/config/locales/en.yml | 5 + test/mock_app_plugin/config/routes.rb | 45 + test/mock_app_plugin/config/sitemap.rb | 13 + .../migrate/20090826121911_create_contents.rb | 12 + test/mock_app_plugin/db/schema.rb | 20 + test/mock_app_plugin/db/test.sqlite3 | Bin 0 -> 5120 bytes test/mock_app_plugin/public/index.html | 275 ++ test/mock_app_plugin/script/console | 3 + .../vendor/plugins/sitemap_generator | 0 test/test_helper.rb | 12 +- 67 files changed, 8432 insertions(+), 6 deletions(-) rename test/{mock_app => mock_app_gem}/.gitignore (100%) rename test/{mock_app => mock_app_gem}/README (100%) create mode 100644 test/mock_app_gem/Rakefile rename test/{mock_app => mock_app_gem}/app/controllers/application_controller.rb (100%) rename test/{mock_app => mock_app_gem}/app/controllers/contents_controller.rb (100%) rename test/{mock_app => mock_app_gem}/app/models/content.rb (100%) create mode 100644 test/mock_app_gem/config/boot.rb rename test/{mock_app => mock_app_gem}/config/database.yml (100%) create mode 100644 test/mock_app_gem/config/environment.rb rename test/{mock_app => mock_app_gem}/config/environments/development.rb (100%) rename test/{mock_app => mock_app_gem}/config/environments/production.rb (100%) rename test/{mock_app => mock_app_gem}/config/environments/test.rb (100%) rename test/{mock_app => mock_app_gem}/config/initializers/backtrace_silencers.rb (100%) rename test/{mock_app => mock_app_gem}/config/initializers/inflections.rb (100%) rename test/{mock_app => mock_app_gem}/config/initializers/mime_types.rb (100%) rename test/{mock_app => mock_app_gem}/config/initializers/new_rails_defaults.rb (100%) rename test/{mock_app => mock_app_gem}/config/initializers/session_store.rb (100%) rename test/{mock_app => mock_app_gem}/config/locales/en.yml (100%) rename test/{mock_app => mock_app_gem}/config/routes.rb (100%) rename test/{mock_app => mock_app_gem}/config/sitemap.rb (100%) rename test/{mock_app => mock_app_gem}/db/migrate/20090826121911_create_contents.rb (100%) rename test/{mock_app => mock_app_gem}/db/schema.rb (100%) rename test/{mock_app => mock_app_gem}/db/test.sqlite3 (100%) rename test/{mock_app => mock_app_gem}/public/index.html (100%) create mode 100644 test/mock_app_gem/public/javascripts/application.js create mode 100644 test/mock_app_gem/public/javascripts/controls.js create mode 100644 test/mock_app_gem/public/javascripts/dragdrop.js create mode 100644 test/mock_app_gem/public/javascripts/effects.js create mode 100644 test/mock_app_gem/public/javascripts/prototype.js create mode 100755 test/mock_app_gem/script/about rename test/{mock_app => mock_app_gem}/script/console (100%) create mode 100755 test/mock_app_gem/script/dbconsole create mode 100755 test/mock_app_gem/script/destroy create mode 100755 test/mock_app_gem/script/generate create mode 100755 test/mock_app_gem/script/performance/benchmarker create mode 100755 test/mock_app_gem/script/performance/profiler create mode 100755 test/mock_app_gem/script/plugin create mode 100755 test/mock_app_gem/script/runner create mode 100755 test/mock_app_gem/script/server create mode 120000 test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 create mode 100644 test/mock_app_plugin/.gitignore create mode 100644 test/mock_app_plugin/README rename test/{mock_app => mock_app_plugin}/Rakefile (100%) create mode 100644 test/mock_app_plugin/app/controllers/application_controller.rb create mode 100644 test/mock_app_plugin/app/controllers/contents_controller.rb create mode 100644 test/mock_app_plugin/app/models/content.rb rename test/{mock_app => mock_app_plugin}/config/boot.rb (100%) create mode 100644 test/mock_app_plugin/config/database.yml rename test/{mock_app => mock_app_plugin}/config/environment.rb (100%) create mode 100644 test/mock_app_plugin/config/environments/development.rb create mode 100644 test/mock_app_plugin/config/environments/production.rb create mode 100644 test/mock_app_plugin/config/environments/test.rb create mode 100644 test/mock_app_plugin/config/initializers/backtrace_silencers.rb create mode 100644 test/mock_app_plugin/config/initializers/inflections.rb create mode 100644 test/mock_app_plugin/config/initializers/mime_types.rb create mode 100644 test/mock_app_plugin/config/initializers/new_rails_defaults.rb create mode 100644 test/mock_app_plugin/config/initializers/session_store.rb create mode 100644 test/mock_app_plugin/config/locales/en.yml create mode 100644 test/mock_app_plugin/config/routes.rb create mode 100644 test/mock_app_plugin/config/sitemap.rb create mode 100644 test/mock_app_plugin/db/migrate/20090826121911_create_contents.rb create mode 100644 test/mock_app_plugin/db/schema.rb create mode 100644 test/mock_app_plugin/db/test.sqlite3 create mode 100644 test/mock_app_plugin/public/index.html create mode 100755 test/mock_app_plugin/script/console rename test/{mock_app => mock_app_plugin}/vendor/plugins/sitemap_generator (100%) diff --git a/test/mock_app/.gitignore b/test/mock_app_gem/.gitignore similarity index 100% rename from test/mock_app/.gitignore rename to test/mock_app_gem/.gitignore diff --git a/test/mock_app/README b/test/mock_app_gem/README similarity index 100% rename from test/mock_app/README rename to test/mock_app_gem/README diff --git a/test/mock_app_gem/Rakefile b/test/mock_app_gem/Rakefile new file mode 100644 index 00000000..3782e6aa --- /dev/null +++ b/test/mock_app_gem/Rakefile @@ -0,0 +1,11 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' +require 'sitemap_generator/tasks' diff --git a/test/mock_app/app/controllers/application_controller.rb b/test/mock_app_gem/app/controllers/application_controller.rb similarity index 100% rename from test/mock_app/app/controllers/application_controller.rb rename to test/mock_app_gem/app/controllers/application_controller.rb diff --git a/test/mock_app/app/controllers/contents_controller.rb b/test/mock_app_gem/app/controllers/contents_controller.rb similarity index 100% rename from test/mock_app/app/controllers/contents_controller.rb rename to test/mock_app_gem/app/controllers/contents_controller.rb diff --git a/test/mock_app/app/models/content.rb b/test/mock_app_gem/app/models/content.rb similarity index 100% rename from test/mock_app/app/models/content.rb rename to test/mock_app_gem/app/models/content.rb diff --git a/test/mock_app_gem/config/boot.rb b/test/mock_app_gem/config/boot.rb new file mode 100644 index 00000000..dd5e3b69 --- /dev/null +++ b/test/mock_app_gem/config/boot.rb @@ -0,0 +1,110 @@ +# Don't change this file! +# Configure your app in config/environment.rb and config/environments/*.rb + +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + Rails::GemDependency.add_frozen_gem_path + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + min_version = '1.3.2' + require 'rubygems' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/test/mock_app/config/database.yml b/test/mock_app_gem/config/database.yml similarity index 100% rename from test/mock_app/config/database.yml rename to test/mock_app_gem/config/database.yml diff --git a/test/mock_app_gem/config/environment.rb b/test/mock_app_gem/config/environment.rb new file mode 100644 index 00000000..c36e8435 --- /dev/null +++ b/test/mock_app_gem/config/environment.rb @@ -0,0 +1,42 @@ +# Be sure to restart your server when you modify this file + +# Specifies gem version of Rails to use when vendor/rails is not present +RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # Specify gems that this application depends on and have them installed with rake gems:install + # config.gem "bj" + # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" + # config.gem "sqlite3-ruby", :lib => "sqlite3" + # config.gem "aws-s3", :lib => "aws/s3" + config.gem 'sitemap_generator', :lib => false + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Skip frameworks you're not going to use. To use Rails without a database, + # you must remove the Active Record framework. + # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. + config.time_zone = 'UTC' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] + # config.i18n.default_locale = :de +end diff --git a/test/mock_app/config/environments/development.rb b/test/mock_app_gem/config/environments/development.rb similarity index 100% rename from test/mock_app/config/environments/development.rb rename to test/mock_app_gem/config/environments/development.rb diff --git a/test/mock_app/config/environments/production.rb b/test/mock_app_gem/config/environments/production.rb similarity index 100% rename from test/mock_app/config/environments/production.rb rename to test/mock_app_gem/config/environments/production.rb diff --git a/test/mock_app/config/environments/test.rb b/test/mock_app_gem/config/environments/test.rb similarity index 100% rename from test/mock_app/config/environments/test.rb rename to test/mock_app_gem/config/environments/test.rb diff --git a/test/mock_app/config/initializers/backtrace_silencers.rb b/test/mock_app_gem/config/initializers/backtrace_silencers.rb similarity index 100% rename from test/mock_app/config/initializers/backtrace_silencers.rb rename to test/mock_app_gem/config/initializers/backtrace_silencers.rb diff --git a/test/mock_app/config/initializers/inflections.rb b/test/mock_app_gem/config/initializers/inflections.rb similarity index 100% rename from test/mock_app/config/initializers/inflections.rb rename to test/mock_app_gem/config/initializers/inflections.rb diff --git a/test/mock_app/config/initializers/mime_types.rb b/test/mock_app_gem/config/initializers/mime_types.rb similarity index 100% rename from test/mock_app/config/initializers/mime_types.rb rename to test/mock_app_gem/config/initializers/mime_types.rb diff --git a/test/mock_app/config/initializers/new_rails_defaults.rb b/test/mock_app_gem/config/initializers/new_rails_defaults.rb similarity index 100% rename from test/mock_app/config/initializers/new_rails_defaults.rb rename to test/mock_app_gem/config/initializers/new_rails_defaults.rb diff --git a/test/mock_app/config/initializers/session_store.rb b/test/mock_app_gem/config/initializers/session_store.rb similarity index 100% rename from test/mock_app/config/initializers/session_store.rb rename to test/mock_app_gem/config/initializers/session_store.rb diff --git a/test/mock_app/config/locales/en.yml b/test/mock_app_gem/config/locales/en.yml similarity index 100% rename from test/mock_app/config/locales/en.yml rename to test/mock_app_gem/config/locales/en.yml diff --git a/test/mock_app/config/routes.rb b/test/mock_app_gem/config/routes.rb similarity index 100% rename from test/mock_app/config/routes.rb rename to test/mock_app_gem/config/routes.rb diff --git a/test/mock_app/config/sitemap.rb b/test/mock_app_gem/config/sitemap.rb similarity index 100% rename from test/mock_app/config/sitemap.rb rename to test/mock_app_gem/config/sitemap.rb diff --git a/test/mock_app/db/migrate/20090826121911_create_contents.rb b/test/mock_app_gem/db/migrate/20090826121911_create_contents.rb similarity index 100% rename from test/mock_app/db/migrate/20090826121911_create_contents.rb rename to test/mock_app_gem/db/migrate/20090826121911_create_contents.rb diff --git a/test/mock_app/db/schema.rb b/test/mock_app_gem/db/schema.rb similarity index 100% rename from test/mock_app/db/schema.rb rename to test/mock_app_gem/db/schema.rb diff --git a/test/mock_app/db/test.sqlite3 b/test/mock_app_gem/db/test.sqlite3 similarity index 100% rename from test/mock_app/db/test.sqlite3 rename to test/mock_app_gem/db/test.sqlite3 diff --git a/test/mock_app/public/index.html b/test/mock_app_gem/public/index.html similarity index 100% rename from test/mock_app/public/index.html rename to test/mock_app_gem/public/index.html diff --git a/test/mock_app_gem/public/javascripts/application.js b/test/mock_app_gem/public/javascripts/application.js new file mode 100644 index 00000000..e69de29b diff --git a/test/mock_app_gem/public/javascripts/controls.js b/test/mock_app_gem/public/javascripts/controls.js new file mode 100644 index 00000000..ca29aefd --- /dev/null +++ b/test/mock_app_gem/public/javascripts/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { }; +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element); + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index--; + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++; + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); + return "
      " + ret.join('') + "
    "; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +}; + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML.unescapeHTML(); + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw('Server returned an invalid collection representation.'); + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}); \ No newline at end of file diff --git a/test/mock_app_gem/public/javascripts/dragdrop.js b/test/mock_app_gem/public/javascripts/dragdrop.js new file mode 100644 index 00000000..07229f98 --- /dev/null +++ b/test/mock_app_gem/public/javascripts/dragdrop.js @@ -0,0 +1,973 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +}; + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +}; + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this._originallyAbsolute) + Position.relativize(this.element); + delete this._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)); + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight; + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + element = $(element); + var s = Sortable.sortables[element.id]; + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + }; + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + }; + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + }; + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child); + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + }; + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +}; + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +}; + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +}; + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +}; \ No newline at end of file diff --git a/test/mock_app_gem/public/javascripts/effects.js b/test/mock_app_gem/public/javascripts/effects.js new file mode 100644 index 00000000..5a639d2d --- /dev/null +++ b/test/mock_app_gem/public/javascripts/effects.js @@ -0,0 +1,1128 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + .5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; + }, + pulse: function(pos, pulses) { + return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()); } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }); + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}); }}); }}); }}); }}); }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ); + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }, + oldOpacity = element.getInlineOpacity(), + transition = options.transition || Effect.Transitions.linear, + reverser = function(pos){ + return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); + }; + + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + }; + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ); + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ); + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '
    '; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { + results[property] = css[property]; + return results; + }); + if (!styles.opacity) styles.opacity = element.getOpacity(); + return styles; + }; +} + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element); + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + }; + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/test/mock_app_gem/public/javascripts/prototype.js b/test/mock_app_gem/public/javascripts/prototype.js new file mode 100644 index 00000000..dfe8ab4e --- /dev/null +++ b/test/mock_app_gem/public/javascripts/prototype.js @@ -0,0 +1,4320 @@ +/* Prototype JavaScript framework, version 1.6.0.3 + * (c) 2005-2008 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0.3', + + Browser: { + IE: !!(window.attachEvent && + navigator.userAgent.indexOf('Opera') === -1), + Opera: navigator.userAgent.indexOf('Opera') > -1, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && + navigator.userAgent.indexOf('KHTML') === -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div')['__proto__'] && + document.createElement('div')['__proto__'] !== + document.createElement('form')['__proto__'] + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return !!(object && object.nodeType == 1); + }, + + isArray: function(object) { + return object != null && typeof object == "object" && + 'splice' in object && 'join' in object; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + defer: function() { + var args = [0.01].concat($A(arguments)); + return this.delay.apply(this, args); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(//g,'>'); + }, + unescapeHTML: function() { + return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + try { + this._each(function(value) { + iterator.call(context, value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + $A = function(iterable) { + if (!iterable) return []; + // In Safari, only use the `toArray` method if it's not a NodeList. + // A NodeList is a function, has an function `item` property, and a numeric + // `length` property. Adapted from Google Doctype. + if (!(typeof iterable === 'function' && typeof iterable.length === + 'number' && typeof iterable.item === 'function') && iterable.toArray) + return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + }; +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + // simulating poorly supported hasOwnProperty + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); + if (element) this.Element.prototype = element.prototype; +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; + }, + + show: function(element) { + element = $(element); + element.style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).select("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? element.descendants()[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = element.getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (Prototype.Browser.Opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return element; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + // IE doesn't report offsets correctly for static elements, so we change them + // to "relative" to get the values, then change them back. + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + // IE throws an error if element is not in document + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + // Trigger hasLayout on the offset parent so that IE6 reports + // accurate offsetTop and offsetLeft values for position: fixed. + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if ('outerHTML' in document.createElement('div')) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['', '
    ', 1], + TBODY: ['', '
    ', 2], + TR: ['', '
    ', 3], + TD: ['
    ', '
    ', 4], + SELECT: ['', 1] + } +}; + +(function() { + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div')['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div')['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(), property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName)['__proto__']; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }, B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + if (B.WebKit && !document.evaluate) { + // Safari <3.0 needs self.innerWidth/Height + dimensions[d] = self['inner' + D]; + } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { + // Opera <9.5 needs document.body.clientWidth/Height + dimensions[d] = document.body['client' + D] + } else { + dimensions[d] = document.documentElement['client' + D]; + } + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + + }, + + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + return true; + }, + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + var e = this.expression, results; + + switch (this.mode) { + case 'selectorsAPI': + // querySelectorAll queries document-wide, then filters to descendants + // of the context element. That's not what we want. + // Add an explicit context to the selector if necessary. + if (root !== document) { + var oldId = root.id, id = $(root).identify(); + e = "#" + id + " " + e; + } + + results = $A(root.querySelectorAll(e)).map(Element.extend); + root.id = oldId; + + return results; + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: +/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, + attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._countedByPrototype) { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }, + + // IE improperly serializes _countedByPrototype in (inner|outer)HTML. + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } + }); +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + event = Event.extend(event); + + var node = event.target, + type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + // Firefox screws up the "click" event when moving between radio buttons + // via arrow keys. It also screws up the "load" and "error" events on images, + // reporting the document as the target instead of the original image. + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; + return Element.extend(node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0, scrollTop: 0 }; + return { + x: event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)), + y: event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._prototypeEventID) return element._prototypeEventID[0]; + arguments.callee.id = arguments.callee.id || 1; + return element._prototypeEventID = [++arguments.callee.id]; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event); + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + + // Internet Explorer needs to remove event handlers on page unload + // in order to avoid memory leaks. + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + // Safari has a dummy event handler on page unload so that it won't + // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" + // object when page is returned to via the back button using its bfcache. + if (Prototype.Browser.WebKit) { + window.addEventListener('unload', Prototype.emptyFunction, false); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize(), + loaded: false +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + document.loaded = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write(" + + + + +
    + + +
    + + + + +
    +

    Getting started

    +

    Here’s how to get rolling:

    + +
      +
    1. +

      Use script/generate to create your models and controllers

      +

      To see all available options, run it without parameters.

      +
    2. + +
    3. +

      Set up a default route and remove or rename this file

      +

      Routes are set up in config/routes.rb.

      +
    4. + +
    5. +

      Create your database

      +

      Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

      +
    6. +
    +
    +
    + + +
    + + \ No newline at end of file diff --git a/test/mock_app_plugin/script/console b/test/mock_app_plugin/script/console new file mode 100755 index 00000000..498077ab --- /dev/null +++ b/test/mock_app_plugin/script/console @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/console' diff --git a/test/mock_app/vendor/plugins/sitemap_generator b/test/mock_app_plugin/vendor/plugins/sitemap_generator similarity index 100% rename from test/mock_app/vendor/plugins/sitemap_generator rename to test/mock_app_plugin/vendor/plugins/sitemap_generator diff --git a/test/test_helper.rb b/test/test_helper.rb index b041c6de..43fb9546 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,12 +1,12 @@ ENV['RAILS_ENV'] = 'test' -ENV['RAILS_ROOT'] ||= File.join(File.dirname(__FILE__), 'mock_app') -require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config', 'environment.rb')) - -require 'fileutils' +# This is basically the contents of mock_app_gems's Rakefile +require File.join(File.dirname(__FILE__), 'mock_app_gem', 'config', 'boot') require 'rake' -require 'shoulda' - require 'rake/testtask' require 'rake/rdoctask' require 'tasks/rails' +require 'sitemap_generator/tasks' + +# Testing +require 'shoulda' \ No newline at end of file From 7c90742b5bb2d4ad9e52b39409de6447c699ffbf Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Wed, 17 Mar 2010 22:16:29 -0300 Subject: [PATCH 005/677] Clean up bootstrap code --- lib/sitemap_generator.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 4bde6c9f..0adebfa0 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -4,16 +4,18 @@ require 'sitemap_generator/helper' module SitemapGenerator - class < File.join(self.root, 'templates/sitemap_index.builder'), - :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), + :sitemap_index => File.join(self.root, 'templates/sitemap_index.builder'), + :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), :sitemap_sample => File.join(self.root, 'templates/sitemap.rb'), } - + Sitemap = LinkSet.new end - From da2a33fe6b5a0c37afe50a3e9fc704b10af88d82 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Wed, 17 Mar 2010 22:50:04 -0300 Subject: [PATCH 006/677] Extract method from create task --- lib/sitemap_generator.rb | 2 ++ lib/sitemap_generator/link_set.rb | 13 +++++++++---- tasks/sitemap_generator_tasks.rake | 10 +++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 0adebfa0..e2b7f064 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -6,6 +6,8 @@ module SitemapGenerator VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip + MAX_ENTRIES = 50_000 + class << self attr_accessor :root, :templates end diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 4e76e019..30de3763 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,7 +1,7 @@ module SitemapGenerator class LinkSet attr_accessor :default_host, :yahoo_app_id, :links - + def initialize @links = [] end @@ -16,13 +16,18 @@ def add_default_links @links << Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) @links << Link.generate('/sitemap_index.xml.gz', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) end - + def add_links yield Mapper.new(self) end - + def add_link(link) @links << link end + + # Return groups with no more than maximum allowed links. + def link_groups + links.in_groups_of(SitemapGenerator::MAX_ENTRIES, false) + end end -end \ No newline at end of file +end diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index 718e9281..bd5468bc 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -28,13 +28,13 @@ class SiteMapCreateTask < Rake::Task raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? - links_grps = SitemapGenerator::Sitemap.links.in_groups_of(50_000, false) - raise(ArgumentError, "TOO MANY LINKS!! I really thought 2,500,000,000 links would be enough for anybody!") if links_grps.length > 50000 + link_groups = SitemapGenerator::Sitemap.link_groups + raise(ArgumentError, "TOO MANY LINKS!! I really thought 2,500,000,000 links would be enough for anybody!") if link_groups.length > SitemapGenerator::MAX_ENTRIES Rake::Task['sitemap:clean'].invoke # render individual sitemaps - sitemap_files = render_sitemap(links_grps) + sitemap_files = render_sitemap(link_groups) # render index render_index(sitemap_files) @@ -43,9 +43,9 @@ class SiteMapCreateTask < Rake::Task puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose end - def render_sitemap(links_grps) + def render_sitemap(link_groups) sitemap_files = [] - links_grps.each_with_index do |links, index| + link_groups.each_with_index do |links, index| buffer = '' xml = Builder::XmlMarkup.new(:target=>buffer) eval(open(SitemapGenerator.templates[:sitemap_xml]).read, binding) From 360ee4802ab6105445db1ccc7feff393cba86cb1 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Wed, 17 Mar 2010 23:11:52 -0300 Subject: [PATCH 007/677] Move render methods from task to class --- lib/sitemap_generator/link_set.rb | 40 +++++++++++++++++++++++++++- tasks/sitemap_generator_tasks.rake | 42 +++--------------------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 30de3763..f30d5e60 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,9 +1,15 @@ +require File.dirname(__FILE__) + '/helper' + module SitemapGenerator class LinkSet + include SitemapGenerator::Helper + attr_accessor :default_host, :yahoo_app_id, :links + attr_accessor :sitemap_files def initialize - @links = [] + self.links = [] + self.sitemap_files = [] end def default_host=(host) @@ -29,5 +35,37 @@ def add_link(link) def link_groups links.in_groups_of(SitemapGenerator::MAX_ENTRIES, false) end + + # Render individual sitemap files. + def render_sitemaps(verbose = true) + sitemap_files.clear + link_groups.each_with_index do |links, index| + buffer = '' + xml = Builder::XmlMarkup.new(:target => buffer) + eval(open(SitemapGenerator.templates[:sitemap_xml]).read, binding) + filename = File.join(RAILS_ROOT, "public/sitemap#{index+1}.xml.gz") + Zlib::GzipWriter.open(filename) do |gz| + gz.write buffer + end + sitemap_files.push filename + puts "+ #{filename}" if verbose + puts "** Sitemap too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose + end + sitemap_files + end + + # Render sitemap index file. + def render_index(verbose = true) + buffer = '' + xml = Builder::XmlMarkup.new(:target => buffer) + eval(open(SitemapGenerator.templates[:sitemap_index]).read, binding) + filename = File.join(RAILS_ROOT, "public/sitemap_index.xml.gz") + Zlib::GzipWriter.open(filename) do |gz| + gz.write buffer + end + + puts "+ #{filename}" if verbose + puts "** Sitemap Index too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose + end end end diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index bd5468bc..f0ac4cb3 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -20,6 +20,7 @@ class SiteMapCreateTask < Rake::Task end private + def build_files start_time = Time.now @@ -28,50 +29,15 @@ class SiteMapCreateTask < Rake::Task raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? - link_groups = SitemapGenerator::Sitemap.link_groups - raise(ArgumentError, "TOO MANY LINKS!! I really thought 2,500,000,000 links would be enough for anybody!") if link_groups.length > SitemapGenerator::MAX_ENTRIES - Rake::Task['sitemap:clean'].invoke - # render individual sitemaps - sitemap_files = render_sitemap(link_groups) + SitemapGenerator::Sitemap.render_sitemaps(verbose) + + SitemapGenerator::Sitemap.render_index(verbose) - # render index - render_index(sitemap_files) - stop_time = Time.now puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose end - - def render_sitemap(link_groups) - sitemap_files = [] - link_groups.each_with_index do |links, index| - buffer = '' - xml = Builder::XmlMarkup.new(:target=>buffer) - eval(open(SitemapGenerator.templates[:sitemap_xml]).read, binding) - filename = File.join(RAILS_ROOT, "public/sitemap#{index+1}.xml.gz") - Zlib::GzipWriter.open(filename) do |gz| - gz.write buffer - end - sitemap_files << filename - puts "+ #{filename}" if verbose - puts "** Sitemap too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose - end - sitemap_files - end - - def render_index(sitemap_files) - buffer = '' - xml = Builder::XmlMarkup.new(:target=>buffer) - eval(open(SitemapGenerator.templates[:sitemap_index]).read, binding) - filename = File.join(RAILS_ROOT, "public/sitemap_index.xml.gz") - Zlib::GzipWriter.open(filename) do |gz| - gz.write buffer - end - - puts "+ #{filename}" if verbose - puts "** Sitemap Index too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose - end end namespace :sitemap do From 9401d39a3923b7c86b94ee3c3d2475351f757999 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Wed, 17 Mar 2010 23:14:58 -0300 Subject: [PATCH 008/677] Extract common code as methods --- lib/sitemap_generator/link_set.rb | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index f30d5e60..a8466a5c 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -44,12 +44,8 @@ def render_sitemaps(verbose = true) xml = Builder::XmlMarkup.new(:target => buffer) eval(open(SitemapGenerator.templates[:sitemap_xml]).read, binding) filename = File.join(RAILS_ROOT, "public/sitemap#{index+1}.xml.gz") - Zlib::GzipWriter.open(filename) do |gz| - gz.write buffer - end - sitemap_files.push filename - puts "+ #{filename}" if verbose - puts "** Sitemap too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose + write_file(filename, buffer) + show_progress("Sitemap", filename, buffer) if verbose end sitemap_files end @@ -60,12 +56,19 @@ def render_index(verbose = true) xml = Builder::XmlMarkup.new(:target => buffer) eval(open(SitemapGenerator.templates[:sitemap_index]).read, binding) filename = File.join(RAILS_ROOT, "public/sitemap_index.xml.gz") - Zlib::GzipWriter.open(filename) do |gz| - gz.write buffer - end + write_file(filename, buffer) + show_progress("Sitemap Index", filename, buffer) if verbose + end + + # Commit buffer to gzipped file. + def write_file(name, buffer) + Zlib::GzipWriter.open(name) { |gz| gz.write buffer } + end - puts "+ #{filename}" if verbose - puts "** Sitemap Index too big! The uncompressed size exceeds 10Mb" if (buffer.size > 10 * 1024 * 1024) && verbose + # Report progress line. + def show_progress(title, filename, buffer) + puts "+ #{filename}" + puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes end end end From 8799a1c254e80d65464e9189eaf3bcf38aef44e3 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Wed, 17 Mar 2010 23:20:13 -0300 Subject: [PATCH 009/677] Move over install and uninstall code --- lib/sitemap_generator/link_set.rb | 17 +++++++++++++++++ rails/install.rb | 12 +----------- rails/uninstall.rb | 4 +--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index a8466a5c..6bfbd26c 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -70,5 +70,22 @@ def show_progress(title, filename, buffer) puts "+ #{filename}" puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes end + + # Copy templates/sitemap.rb to config if not there yet. + def install_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "already exists: config/sitemap.rb, file not copied" + else + FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "created: config/sitemap.rb" + end + end + + # Remove config/sitemap.rb if exists. + def uninstall_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) + end + end end end diff --git a/rails/install.rb b/rails/install.rb index c49f1591..9d2abd61 100644 --- a/rails/install.rb +++ b/rails/install.rb @@ -1,12 +1,2 @@ # Install hook code here - -# Copy sitemap_template.rb to config/sitemap.rb -require 'fileutils' -sitemap_template = File.join(File.dirname(__FILE__), '../templates/sitemap.rb') -new_sitemap = File.join(RAILS_ROOT, 'config/sitemap.rb') -if File.exist?(new_sitemap) - puts "already exists: config/sitemap.rb, file not copied" -else - puts "created: config/sitemap.rb" - FileUtils.cp(sitemap_template, new_sitemap) -end \ No newline at end of file +SitemapGenerator::Sitemap.install_sitemap_rb diff --git a/rails/uninstall.rb b/rails/uninstall.rb index ffe744bf..2715ac46 100644 --- a/rails/uninstall.rb +++ b/rails/uninstall.rb @@ -1,4 +1,2 @@ # Uninstall hook code here - -new_sitemap = File.join(RAILS_ROOT, 'config/sitemap.rb') -File.rm(new_sitemap) if File.exist?(new_sitemap) \ No newline at end of file +SitemapGenerator::Sitemap.uninstall_sitemap_rb From 5c1701f06e13a36de9973089a4fd03fc217d378a Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 00:09:33 -0300 Subject: [PATCH 010/677] Extract leftover code from tasks --- lib/sitemap_generator/link_set.rb | 23 +++++++++++++ tasks/sitemap_generator_tasks.rake | 52 +++++------------------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 6bfbd26c..3846819f 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -3,6 +3,7 @@ module SitemapGenerator class LinkSet include SitemapGenerator::Helper + include ActionView::Helpers::NumberHelper attr_accessor :default_host, :yahoo_app_id, :links attr_accessor :sitemap_files @@ -87,5 +88,27 @@ def uninstall_sitemap_rb File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) end end + + # Clean sitemap files in output directory. + def clean_files + FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + end + + # Ping search engines passing sitemap location. + def ping_search_engines + super "sitemap_index.xml.gz" + end + + # Create sitemap files in output directory. + def create_files(verbose = true) + start_time = Time.now + load_sitemap_rb + raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? + clean_files + render_sitemaps(verbose) + render_index(verbose) + stop_time = Time.now + puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + end end end diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index f0ac4cb3..55443d39 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,62 +1,26 @@ require 'zlib' require 'sitemap_generator/helper' -class SiteMapRefreshTask < Rake::Task - include SitemapGenerator::Helper - - def execute(*) - super - ping_search_engines("sitemap_index.xml.gz") - end -end - -class SiteMapCreateTask < Rake::Task - include SitemapGenerator::Helper - include ActionView::Helpers::NumberHelper - - def execute(*) - super - build_files - end - - private - - def build_files - start_time = Time.now - - # update links from config/sitemap.rb - load_sitemap_rb - - raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? - - Rake::Task['sitemap:clean'].invoke - - SitemapGenerator::Sitemap.render_sitemaps(verbose) - - SitemapGenerator::Sitemap.render_index(verbose) - - stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose - end -end - namespace :sitemap do desc "Install a default config/sitemap.rb file" task :install do - load File.expand_path(File.join(File.dirname(__FILE__), "../rails/install.rb")) + SitemapGenerator::Sitemap.install_sitemap_rb end desc "Delete all Sitemap files in public/ directory" task :clean do - sitemap_files = Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')] - FileUtils.rm sitemap_files + SitemapGenerator::Sitemap.clean_files end desc "Create Sitemap XML files in public/ directory (rake -s for no output)" - SiteMapRefreshTask.define_task :refresh => ['sitemap:create'] + task :refresh => ['sitemap:create'] do + SitemapGenerator::Sitemap.ping_search_engines + end desc "Create Sitemap XML files (don't ping search engines)" task 'refresh:no_ping' => ['sitemap:create'] - SiteMapCreateTask.define_task :create => [:environment] + task :create => [:environment] do + SitemapGenerator::Sitemap.create_files + end end From c2b3804091af7ea1ff6e613345d8a4a35d986645 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 00:15:52 -0300 Subject: [PATCH 011/677] Change link count to use a method --- lib/sitemap_generator/link_set.rb | 7 ++++++- test/sitemap_generator_test.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 3846819f..df10ec26 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -108,7 +108,12 @@ def create_files(verbose = true) render_sitemaps(verbose) render_index(verbose) stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.links.length)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + end + + # Return total link count. + def link_count + links.length end end end diff --git a/test/sitemap_generator_test.rb b/test/sitemap_generator_test.rb index c614cca5..4e98adab 100644 --- a/test/sitemap_generator_test.rb +++ b/test/sitemap_generator_test.rb @@ -62,7 +62,7 @@ class SitemapGeneratorTest < Test::Unit::TestCase end should "be have x elements" do - assert_equal 14, SitemapGenerator::Sitemap.links.size + assert_equal 14, SitemapGenerator::Sitemap.link_count end end From c1d794faff07deea6f74c5a0e88c18fde1dbfa92 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 15:29:56 -0300 Subject: [PATCH 012/677] Replace symbolic links with file copies --- .gitignore | 4 ++- Rakefile | 27 ++++++++++++++----- sitemap_generator.gemspec | 25 ++--------------- .../vendor/gems/sitemap_generator-1.2.3 | 1 - .../vendor/plugins/sitemap_generator | 1 - 5 files changed, 26 insertions(+), 32 deletions(-) delete mode 120000 test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 delete mode 120000 test/mock_app_plugin/vendor/plugins/sitemap_generator diff --git a/.gitignore b/.gitignore index 341b0ec4..774e6fef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -pkg *.swp +pkg +test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 +test/mock_app_plugin/vendor/plugins/sitemap_generator diff --git a/Rakefile b/Rakefile index 6cd09694..ff6d9a8d 100644 --- a/Rakefile +++ b/Rakefile @@ -11,6 +11,7 @@ begin s.homepage = "http://github.com/kjvarga/sitemap_generator" s.authors = ["Adam Salter", "Karl Varga"] s.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] + s.test_files = [] # s is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end Jeweler::GemcutterTasks.new @@ -18,13 +19,27 @@ rescue LoadError puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" end -desc 'Default: run unit tests.' task :default => :test -desc 'Test.' -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true +desc "Run tests" +task :test do + Rake::Task["test:prepare"].invoke + Rake::Task["test:sitemap_generator"].invoke end +namespace :test do + desc "Copy sitemap_generator files to mock apps" + task :prepare do + %w(test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 test/mock_app_plugin/vendor/plugins/sitemap_generator).each do |path| + rm_rf path + mkdir_p path + cp_r FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}"], path + end + end + + Rake::TestTask.new(:sitemap_generator) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true + end +end diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 09273b1b..87bc7bcb 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2009-12-11} + s.date = %q{2010-03-18} s.description = %q{Install as a plugin or Gem to easily generate ['enterprise-class'][enterprise_class] Google Sitemaps for your Rails site, using a simple 'Rails Routes'-like DSL and a single rake task.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ @@ -36,29 +36,8 @@ Gem::Specification.new do |s| s.homepage = %q{http://github.com/kjvarga/sitemap_generator} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.5} + s.rubygems_version = %q{1.3.6} s.summary = %q{Generate 'enterprise-class' Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} - s.test_files = [ - "test/mock_app/app/controllers/application_controller.rb", - "test/mock_app/app/controllers/contents_controller.rb", - "test/mock_app/app/models/content.rb", - "test/mock_app/config/boot.rb", - "test/mock_app/config/environment.rb", - "test/mock_app/config/environments/development.rb", - "test/mock_app/config/environments/production.rb", - "test/mock_app/config/environments/test.rb", - "test/mock_app/config/initializers/backtrace_silencers.rb", - "test/mock_app/config/initializers/inflections.rb", - "test/mock_app/config/initializers/mime_types.rb", - "test/mock_app/config/initializers/new_rails_defaults.rb", - "test/mock_app/config/initializers/session_store.rb", - "test/mock_app/config/routes.rb", - "test/mock_app/config/sitemap.rb", - "test/mock_app/db/migrate/20090826121911_create_contents.rb", - "test/mock_app/db/schema.rb", - "test/sitemap_generator_test.rb", - "test/test_helper.rb" - ] if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION diff --git a/test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 b/test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 deleted file mode 120000 index 11a54ed3..00000000 --- a/test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 +++ /dev/null @@ -1 +0,0 @@ -../../../../ \ No newline at end of file diff --git a/test/mock_app_plugin/vendor/plugins/sitemap_generator b/test/mock_app_plugin/vendor/plugins/sitemap_generator deleted file mode 120000 index c866b868..00000000 --- a/test/mock_app_plugin/vendor/plugins/sitemap_generator +++ /dev/null @@ -1 +0,0 @@ -../../../.. \ No newline at end of file From 41b5b3fabf093d17f01a5bd25209e0f32267d08d Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 15:49:07 -0300 Subject: [PATCH 013/677] Version bump to 0.2.4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 71790396..abd41058 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.3 +0.2.4 From 3f1bf88aaced157e76ae3a3d132da56c15bbfbf1 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 15:50:15 -0300 Subject: [PATCH 014/677] Change to write as soon as possible --- lib/sitemap_generator/link_set.rb | 152 +++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 45 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index df10ec26..e6d9849e 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -6,59 +6,129 @@ class LinkSet include ActionView::Helpers::NumberHelper attr_accessor :default_host, :yahoo_app_id, :links - attr_accessor :sitemap_files + attr_accessor :sitemaps + attr_accessor :max_entries + attr_accessor :link_count - def initialize - self.links = [] - self.sitemap_files = [] - end + alias :sitemap_files :sitemaps - def default_host=(host) - @default_host = host - add_default_links + # Create new link set instance. + def initialize + self.links = [] + self.sitemaps = [] + self.max_entries = SitemapGenerator::MAX_ENTRIES + self.link_count = 0 end + # Add default links to sitemap files. def add_default_links - # Add default links - @links << Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - @links << Link.generate('/sitemap_index.xml.gz', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + links.push Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + links.push Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + self.link_count += 2 end + # Add links to sitemap files passing a block. def add_links + raise ArgumentError, "Default hostname not set" if default_host.blank? + add_default_links if first_link? yield Mapper.new(self) end + # Add links from mapper to sitemap files. def add_link(link) - @links << link + write_upcoming if enough_links? + links.push link + self.link_count += 1 end - # Return groups with no more than maximum allowed links. - def link_groups - links.in_groups_of(SitemapGenerator::MAX_ENTRIES, false) + # Write links to sitemap file. + def write + write_pending end - # Render individual sitemap files. - def render_sitemaps(verbose = true) - sitemap_files.clear - link_groups.each_with_index do |links, index| - buffer = '' - xml = Builder::XmlMarkup.new(:target => buffer) - eval(open(SitemapGenerator.templates[:sitemap_xml]).read, binding) - filename = File.join(RAILS_ROOT, "public/sitemap#{index+1}.xml.gz") - write_file(filename, buffer) - show_progress("Sitemap", filename, buffer) if verbose - end - sitemap_files + # Write links to upcoming sitemap file. + def write_upcoming + write_sitemap(upcoming_file) end - # Render sitemap index file. - def render_index(verbose = true) - buffer = '' + # Write pending links to sitemap, write index file if needed. + def write_pending + write_upcoming + write_index + end + + # Write links to sitemap file. + def write_sitemap(file = upcoming_file) + buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(open(SitemapGenerator.templates[:sitemap_index]).read, binding) - filename = File.join(RAILS_ROOT, "public/sitemap_index.xml.gz") + eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) + filename = File.join(RAILS_ROOT, "public", file) + write_file(filename, buffer) + show_progress("Sitemap", filename, buffer) if verbose + links.clear + sitemaps.push filename + end + + # Write sitemap links to sitemap index file. + def write_index + buffer = "" + xml = Builder::XmlMarkup.new(:target => buffer) + eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) + filename = File.join(RAILS_ROOT, "public", index_file) write_file(filename, buffer) show_progress("Sitemap Index", filename, buffer) if verbose + links.clear + sitemaps.clear + end + + # Return sitemap or sitemap index main name. + def index_file + "sitemap_index.xml.gz" + end + + # Return upcoming sitemap name with index. + def upcoming_file + "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? + end + + # Return upcoming sitemap index, first is 1. + def upcoming_index + sitemaps.length + 1 unless enough_sitemaps? + end + + # Return true if upcoming is first sitemap. + def first_sitemap? + sitemaps.empty? + end + + # Return true if sitemap index needed. + def multiple_sitemaps? + !first_sitemap? + end + + # Return true if more sitemaps can be added. + def more_sitemaps? + sitemaps.length < max_entries + end + + # Return true if no sitemaps can be added. + def enough_sitemaps? + !more_sitemaps? + end + + # Return true if this is the first link added. + def first_link? + links.empty? && first_sitemap? + end + + # Return true if more links can be added. + def more_links? + links.length < max_entries + end + + # Return true if no further links can be added. + def enough_links? + !more_links? end # Commit buffer to gzipped file. @@ -94,26 +164,18 @@ def clean_files FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) end - # Ping search engines passing sitemap location. - def ping_search_engines - super "sitemap_index.xml.gz" - end + # Ping search engines passing sitemap location. + def ping_search_engines + super index_file + end # Create sitemap files in output directory. def create_files(verbose = true) start_time = Time.now load_sitemap_rb - raise(ArgumentError, "Default hostname not defined") if SitemapGenerator::Sitemap.default_host.blank? - clean_files - render_sitemaps(verbose) - render_index(verbose) + write stop_time = Time.now puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose end - - # Return total link count. - def link_count - links.length - end end end From 8d77309e793fc2ae4bea47e4cfe2a693f7790313 Mon Sep 17 00:00:00 2001 From: Adrian Mugnolo Date: Thu, 18 Mar 2010 15:50:38 -0300 Subject: [PATCH 015/677] Update gemspec file --- sitemap_generator.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 87bc7bcb..e71828eb 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.2.3" + s.version = "0.2.4" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] From 2e6777edd7e4bc7bcde859b53d7e4a95bd477ba2 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 1 Apr 2010 18:08:58 -0700 Subject: [PATCH 016/677] Move to spec tests. --- {test => spec}/mock_app_gem/.gitignore | 0 {test => spec}/mock_app_gem/README | 0 {test => spec}/mock_app_gem/Rakefile | 0 .../app/controllers/application_controller.rb | 0 .../app/controllers/contents_controller.rb | 0 {test => spec}/mock_app_gem/app/models/content.rb | 0 {test => spec}/mock_app_gem/config/boot.rb | 0 {test => spec}/mock_app_gem/config/database.yml | 0 {test => spec}/mock_app_gem/config/environment.rb | 0 .../mock_app_gem/config/environments/development.rb | 0 .../mock_app_gem/config/environments/production.rb | 0 .../mock_app_gem/config/environments/test.rb | 0 .../config/initializers/backtrace_silencers.rb | 0 .../mock_app_gem/config/initializers/inflections.rb | 0 .../mock_app_gem/config/initializers/mime_types.rb | 0 .../config/initializers/new_rails_defaults.rb | 0 .../config/initializers/session_store.rb | 0 {test => spec}/mock_app_gem/config/locales/en.yml | 0 {test => spec}/mock_app_gem/config/routes.rb | 0 {test => spec}/mock_app_gem/config/sitemap.rb | 0 .../db/migrate/20090826121911_create_contents.rb | 0 {test => spec}/mock_app_gem/db/schema.rb | 0 {test => spec}/mock_app_gem/db/test.sqlite3 | Bin {test => spec}/mock_app_gem/public/index.html | 0 .../mock_app_gem/public/javascripts/application.js | 0 .../mock_app_gem/public/javascripts/controls.js | 0 .../mock_app_gem/public/javascripts/dragdrop.js | 0 .../mock_app_gem/public/javascripts/effects.js | 0 .../mock_app_gem/public/javascripts/prototype.js | 0 {test => spec}/mock_app_gem/script/about | 0 {test => spec}/mock_app_gem/script/console | 0 {test => spec}/mock_app_gem/script/dbconsole | 0 {test => spec}/mock_app_gem/script/destroy | 0 {test => spec}/mock_app_gem/script/generate | 0 .../mock_app_gem/script/performance/benchmarker | 0 .../mock_app_gem/script/performance/profiler | 0 {test => spec}/mock_app_gem/script/plugin | 0 {test => spec}/mock_app_gem/script/runner | 0 {test => spec}/mock_app_gem/script/server | 0 {test => spec}/mock_app_plugin/.gitignore | 0 {test => spec}/mock_app_plugin/README | 0 {test => spec}/mock_app_plugin/Rakefile | 0 .../app/controllers/application_controller.rb | 0 .../app/controllers/contents_controller.rb | 0 .../mock_app_plugin/app/models/content.rb | 0 {test => spec}/mock_app_plugin/config/boot.rb | 0 {test => spec}/mock_app_plugin/config/database.yml | 0 .../mock_app_plugin/config/environment.rb | 0 .../config/environments/development.rb | 0 .../config/environments/production.rb | 0 .../mock_app_plugin/config/environments/test.rb | 0 .../config/initializers/backtrace_silencers.rb | 0 .../config/initializers/inflections.rb | 0 .../config/initializers/mime_types.rb | 0 .../config/initializers/new_rails_defaults.rb | 0 .../config/initializers/session_store.rb | 0 .../mock_app_plugin/config/locales/en.yml | 0 {test => spec}/mock_app_plugin/config/routes.rb | 0 {test => spec}/mock_app_plugin/config/sitemap.rb | 0 .../db/migrate/20090826121911_create_contents.rb | 0 {test => spec}/mock_app_plugin/db/schema.rb | 0 {test => spec}/mock_app_plugin/db/test.sqlite3 | Bin {test => spec}/mock_app_plugin/public/index.html | 0 {test => spec}/mock_app_plugin/script/console | 0 {test => spec}/sitemap.file | 0 {test => spec}/sitemap_generator_test.rb | 0 test/test_helper.rb => spec/spec_helper.rb | 0 67 files changed, 0 insertions(+), 0 deletions(-) rename {test => spec}/mock_app_gem/.gitignore (100%) rename {test => spec}/mock_app_gem/README (100%) rename {test => spec}/mock_app_gem/Rakefile (100%) rename {test => spec}/mock_app_gem/app/controllers/application_controller.rb (100%) rename {test => spec}/mock_app_gem/app/controllers/contents_controller.rb (100%) rename {test => spec}/mock_app_gem/app/models/content.rb (100%) rename {test => spec}/mock_app_gem/config/boot.rb (100%) rename {test => spec}/mock_app_gem/config/database.yml (100%) rename {test => spec}/mock_app_gem/config/environment.rb (100%) rename {test => spec}/mock_app_gem/config/environments/development.rb (100%) rename {test => spec}/mock_app_gem/config/environments/production.rb (100%) rename {test => spec}/mock_app_gem/config/environments/test.rb (100%) rename {test => spec}/mock_app_gem/config/initializers/backtrace_silencers.rb (100%) rename {test => spec}/mock_app_gem/config/initializers/inflections.rb (100%) rename {test => spec}/mock_app_gem/config/initializers/mime_types.rb (100%) rename {test => spec}/mock_app_gem/config/initializers/new_rails_defaults.rb (100%) rename {test => spec}/mock_app_gem/config/initializers/session_store.rb (100%) rename {test => spec}/mock_app_gem/config/locales/en.yml (100%) rename {test => spec}/mock_app_gem/config/routes.rb (100%) rename {test => spec}/mock_app_gem/config/sitemap.rb (100%) rename {test => spec}/mock_app_gem/db/migrate/20090826121911_create_contents.rb (100%) rename {test => spec}/mock_app_gem/db/schema.rb (100%) rename {test => spec}/mock_app_gem/db/test.sqlite3 (100%) rename {test => spec}/mock_app_gem/public/index.html (100%) rename {test => spec}/mock_app_gem/public/javascripts/application.js (100%) rename {test => spec}/mock_app_gem/public/javascripts/controls.js (100%) rename {test => spec}/mock_app_gem/public/javascripts/dragdrop.js (100%) rename {test => spec}/mock_app_gem/public/javascripts/effects.js (100%) rename {test => spec}/mock_app_gem/public/javascripts/prototype.js (100%) rename {test => spec}/mock_app_gem/script/about (100%) rename {test => spec}/mock_app_gem/script/console (100%) rename {test => spec}/mock_app_gem/script/dbconsole (100%) rename {test => spec}/mock_app_gem/script/destroy (100%) rename {test => spec}/mock_app_gem/script/generate (100%) rename {test => spec}/mock_app_gem/script/performance/benchmarker (100%) rename {test => spec}/mock_app_gem/script/performance/profiler (100%) rename {test => spec}/mock_app_gem/script/plugin (100%) rename {test => spec}/mock_app_gem/script/runner (100%) rename {test => spec}/mock_app_gem/script/server (100%) rename {test => spec}/mock_app_plugin/.gitignore (100%) rename {test => spec}/mock_app_plugin/README (100%) rename {test => spec}/mock_app_plugin/Rakefile (100%) rename {test => spec}/mock_app_plugin/app/controllers/application_controller.rb (100%) rename {test => spec}/mock_app_plugin/app/controllers/contents_controller.rb (100%) rename {test => spec}/mock_app_plugin/app/models/content.rb (100%) rename {test => spec}/mock_app_plugin/config/boot.rb (100%) rename {test => spec}/mock_app_plugin/config/database.yml (100%) rename {test => spec}/mock_app_plugin/config/environment.rb (100%) rename {test => spec}/mock_app_plugin/config/environments/development.rb (100%) rename {test => spec}/mock_app_plugin/config/environments/production.rb (100%) rename {test => spec}/mock_app_plugin/config/environments/test.rb (100%) rename {test => spec}/mock_app_plugin/config/initializers/backtrace_silencers.rb (100%) rename {test => spec}/mock_app_plugin/config/initializers/inflections.rb (100%) rename {test => spec}/mock_app_plugin/config/initializers/mime_types.rb (100%) rename {test => spec}/mock_app_plugin/config/initializers/new_rails_defaults.rb (100%) rename {test => spec}/mock_app_plugin/config/initializers/session_store.rb (100%) rename {test => spec}/mock_app_plugin/config/locales/en.yml (100%) rename {test => spec}/mock_app_plugin/config/routes.rb (100%) rename {test => spec}/mock_app_plugin/config/sitemap.rb (100%) rename {test => spec}/mock_app_plugin/db/migrate/20090826121911_create_contents.rb (100%) rename {test => spec}/mock_app_plugin/db/schema.rb (100%) rename {test => spec}/mock_app_plugin/db/test.sqlite3 (100%) rename {test => spec}/mock_app_plugin/public/index.html (100%) rename {test => spec}/mock_app_plugin/script/console (100%) rename {test => spec}/sitemap.file (100%) rename {test => spec}/sitemap_generator_test.rb (100%) rename test/test_helper.rb => spec/spec_helper.rb (100%) diff --git a/test/mock_app_gem/.gitignore b/spec/mock_app_gem/.gitignore similarity index 100% rename from test/mock_app_gem/.gitignore rename to spec/mock_app_gem/.gitignore diff --git a/test/mock_app_gem/README b/spec/mock_app_gem/README similarity index 100% rename from test/mock_app_gem/README rename to spec/mock_app_gem/README diff --git a/test/mock_app_gem/Rakefile b/spec/mock_app_gem/Rakefile similarity index 100% rename from test/mock_app_gem/Rakefile rename to spec/mock_app_gem/Rakefile diff --git a/test/mock_app_gem/app/controllers/application_controller.rb b/spec/mock_app_gem/app/controllers/application_controller.rb similarity index 100% rename from test/mock_app_gem/app/controllers/application_controller.rb rename to spec/mock_app_gem/app/controllers/application_controller.rb diff --git a/test/mock_app_gem/app/controllers/contents_controller.rb b/spec/mock_app_gem/app/controllers/contents_controller.rb similarity index 100% rename from test/mock_app_gem/app/controllers/contents_controller.rb rename to spec/mock_app_gem/app/controllers/contents_controller.rb diff --git a/test/mock_app_gem/app/models/content.rb b/spec/mock_app_gem/app/models/content.rb similarity index 100% rename from test/mock_app_gem/app/models/content.rb rename to spec/mock_app_gem/app/models/content.rb diff --git a/test/mock_app_gem/config/boot.rb b/spec/mock_app_gem/config/boot.rb similarity index 100% rename from test/mock_app_gem/config/boot.rb rename to spec/mock_app_gem/config/boot.rb diff --git a/test/mock_app_gem/config/database.yml b/spec/mock_app_gem/config/database.yml similarity index 100% rename from test/mock_app_gem/config/database.yml rename to spec/mock_app_gem/config/database.yml diff --git a/test/mock_app_gem/config/environment.rb b/spec/mock_app_gem/config/environment.rb similarity index 100% rename from test/mock_app_gem/config/environment.rb rename to spec/mock_app_gem/config/environment.rb diff --git a/test/mock_app_gem/config/environments/development.rb b/spec/mock_app_gem/config/environments/development.rb similarity index 100% rename from test/mock_app_gem/config/environments/development.rb rename to spec/mock_app_gem/config/environments/development.rb diff --git a/test/mock_app_gem/config/environments/production.rb b/spec/mock_app_gem/config/environments/production.rb similarity index 100% rename from test/mock_app_gem/config/environments/production.rb rename to spec/mock_app_gem/config/environments/production.rb diff --git a/test/mock_app_gem/config/environments/test.rb b/spec/mock_app_gem/config/environments/test.rb similarity index 100% rename from test/mock_app_gem/config/environments/test.rb rename to spec/mock_app_gem/config/environments/test.rb diff --git a/test/mock_app_gem/config/initializers/backtrace_silencers.rb b/spec/mock_app_gem/config/initializers/backtrace_silencers.rb similarity index 100% rename from test/mock_app_gem/config/initializers/backtrace_silencers.rb rename to spec/mock_app_gem/config/initializers/backtrace_silencers.rb diff --git a/test/mock_app_gem/config/initializers/inflections.rb b/spec/mock_app_gem/config/initializers/inflections.rb similarity index 100% rename from test/mock_app_gem/config/initializers/inflections.rb rename to spec/mock_app_gem/config/initializers/inflections.rb diff --git a/test/mock_app_gem/config/initializers/mime_types.rb b/spec/mock_app_gem/config/initializers/mime_types.rb similarity index 100% rename from test/mock_app_gem/config/initializers/mime_types.rb rename to spec/mock_app_gem/config/initializers/mime_types.rb diff --git a/test/mock_app_gem/config/initializers/new_rails_defaults.rb b/spec/mock_app_gem/config/initializers/new_rails_defaults.rb similarity index 100% rename from test/mock_app_gem/config/initializers/new_rails_defaults.rb rename to spec/mock_app_gem/config/initializers/new_rails_defaults.rb diff --git a/test/mock_app_gem/config/initializers/session_store.rb b/spec/mock_app_gem/config/initializers/session_store.rb similarity index 100% rename from test/mock_app_gem/config/initializers/session_store.rb rename to spec/mock_app_gem/config/initializers/session_store.rb diff --git a/test/mock_app_gem/config/locales/en.yml b/spec/mock_app_gem/config/locales/en.yml similarity index 100% rename from test/mock_app_gem/config/locales/en.yml rename to spec/mock_app_gem/config/locales/en.yml diff --git a/test/mock_app_gem/config/routes.rb b/spec/mock_app_gem/config/routes.rb similarity index 100% rename from test/mock_app_gem/config/routes.rb rename to spec/mock_app_gem/config/routes.rb diff --git a/test/mock_app_gem/config/sitemap.rb b/spec/mock_app_gem/config/sitemap.rb similarity index 100% rename from test/mock_app_gem/config/sitemap.rb rename to spec/mock_app_gem/config/sitemap.rb diff --git a/test/mock_app_gem/db/migrate/20090826121911_create_contents.rb b/spec/mock_app_gem/db/migrate/20090826121911_create_contents.rb similarity index 100% rename from test/mock_app_gem/db/migrate/20090826121911_create_contents.rb rename to spec/mock_app_gem/db/migrate/20090826121911_create_contents.rb diff --git a/test/mock_app_gem/db/schema.rb b/spec/mock_app_gem/db/schema.rb similarity index 100% rename from test/mock_app_gem/db/schema.rb rename to spec/mock_app_gem/db/schema.rb diff --git a/test/mock_app_gem/db/test.sqlite3 b/spec/mock_app_gem/db/test.sqlite3 similarity index 100% rename from test/mock_app_gem/db/test.sqlite3 rename to spec/mock_app_gem/db/test.sqlite3 diff --git a/test/mock_app_gem/public/index.html b/spec/mock_app_gem/public/index.html similarity index 100% rename from test/mock_app_gem/public/index.html rename to spec/mock_app_gem/public/index.html diff --git a/test/mock_app_gem/public/javascripts/application.js b/spec/mock_app_gem/public/javascripts/application.js similarity index 100% rename from test/mock_app_gem/public/javascripts/application.js rename to spec/mock_app_gem/public/javascripts/application.js diff --git a/test/mock_app_gem/public/javascripts/controls.js b/spec/mock_app_gem/public/javascripts/controls.js similarity index 100% rename from test/mock_app_gem/public/javascripts/controls.js rename to spec/mock_app_gem/public/javascripts/controls.js diff --git a/test/mock_app_gem/public/javascripts/dragdrop.js b/spec/mock_app_gem/public/javascripts/dragdrop.js similarity index 100% rename from test/mock_app_gem/public/javascripts/dragdrop.js rename to spec/mock_app_gem/public/javascripts/dragdrop.js diff --git a/test/mock_app_gem/public/javascripts/effects.js b/spec/mock_app_gem/public/javascripts/effects.js similarity index 100% rename from test/mock_app_gem/public/javascripts/effects.js rename to spec/mock_app_gem/public/javascripts/effects.js diff --git a/test/mock_app_gem/public/javascripts/prototype.js b/spec/mock_app_gem/public/javascripts/prototype.js similarity index 100% rename from test/mock_app_gem/public/javascripts/prototype.js rename to spec/mock_app_gem/public/javascripts/prototype.js diff --git a/test/mock_app_gem/script/about b/spec/mock_app_gem/script/about similarity index 100% rename from test/mock_app_gem/script/about rename to spec/mock_app_gem/script/about diff --git a/test/mock_app_gem/script/console b/spec/mock_app_gem/script/console similarity index 100% rename from test/mock_app_gem/script/console rename to spec/mock_app_gem/script/console diff --git a/test/mock_app_gem/script/dbconsole b/spec/mock_app_gem/script/dbconsole similarity index 100% rename from test/mock_app_gem/script/dbconsole rename to spec/mock_app_gem/script/dbconsole diff --git a/test/mock_app_gem/script/destroy b/spec/mock_app_gem/script/destroy similarity index 100% rename from test/mock_app_gem/script/destroy rename to spec/mock_app_gem/script/destroy diff --git a/test/mock_app_gem/script/generate b/spec/mock_app_gem/script/generate similarity index 100% rename from test/mock_app_gem/script/generate rename to spec/mock_app_gem/script/generate diff --git a/test/mock_app_gem/script/performance/benchmarker b/spec/mock_app_gem/script/performance/benchmarker similarity index 100% rename from test/mock_app_gem/script/performance/benchmarker rename to spec/mock_app_gem/script/performance/benchmarker diff --git a/test/mock_app_gem/script/performance/profiler b/spec/mock_app_gem/script/performance/profiler similarity index 100% rename from test/mock_app_gem/script/performance/profiler rename to spec/mock_app_gem/script/performance/profiler diff --git a/test/mock_app_gem/script/plugin b/spec/mock_app_gem/script/plugin similarity index 100% rename from test/mock_app_gem/script/plugin rename to spec/mock_app_gem/script/plugin diff --git a/test/mock_app_gem/script/runner b/spec/mock_app_gem/script/runner similarity index 100% rename from test/mock_app_gem/script/runner rename to spec/mock_app_gem/script/runner diff --git a/test/mock_app_gem/script/server b/spec/mock_app_gem/script/server similarity index 100% rename from test/mock_app_gem/script/server rename to spec/mock_app_gem/script/server diff --git a/test/mock_app_plugin/.gitignore b/spec/mock_app_plugin/.gitignore similarity index 100% rename from test/mock_app_plugin/.gitignore rename to spec/mock_app_plugin/.gitignore diff --git a/test/mock_app_plugin/README b/spec/mock_app_plugin/README similarity index 100% rename from test/mock_app_plugin/README rename to spec/mock_app_plugin/README diff --git a/test/mock_app_plugin/Rakefile b/spec/mock_app_plugin/Rakefile similarity index 100% rename from test/mock_app_plugin/Rakefile rename to spec/mock_app_plugin/Rakefile diff --git a/test/mock_app_plugin/app/controllers/application_controller.rb b/spec/mock_app_plugin/app/controllers/application_controller.rb similarity index 100% rename from test/mock_app_plugin/app/controllers/application_controller.rb rename to spec/mock_app_plugin/app/controllers/application_controller.rb diff --git a/test/mock_app_plugin/app/controllers/contents_controller.rb b/spec/mock_app_plugin/app/controllers/contents_controller.rb similarity index 100% rename from test/mock_app_plugin/app/controllers/contents_controller.rb rename to spec/mock_app_plugin/app/controllers/contents_controller.rb diff --git a/test/mock_app_plugin/app/models/content.rb b/spec/mock_app_plugin/app/models/content.rb similarity index 100% rename from test/mock_app_plugin/app/models/content.rb rename to spec/mock_app_plugin/app/models/content.rb diff --git a/test/mock_app_plugin/config/boot.rb b/spec/mock_app_plugin/config/boot.rb similarity index 100% rename from test/mock_app_plugin/config/boot.rb rename to spec/mock_app_plugin/config/boot.rb diff --git a/test/mock_app_plugin/config/database.yml b/spec/mock_app_plugin/config/database.yml similarity index 100% rename from test/mock_app_plugin/config/database.yml rename to spec/mock_app_plugin/config/database.yml diff --git a/test/mock_app_plugin/config/environment.rb b/spec/mock_app_plugin/config/environment.rb similarity index 100% rename from test/mock_app_plugin/config/environment.rb rename to spec/mock_app_plugin/config/environment.rb diff --git a/test/mock_app_plugin/config/environments/development.rb b/spec/mock_app_plugin/config/environments/development.rb similarity index 100% rename from test/mock_app_plugin/config/environments/development.rb rename to spec/mock_app_plugin/config/environments/development.rb diff --git a/test/mock_app_plugin/config/environments/production.rb b/spec/mock_app_plugin/config/environments/production.rb similarity index 100% rename from test/mock_app_plugin/config/environments/production.rb rename to spec/mock_app_plugin/config/environments/production.rb diff --git a/test/mock_app_plugin/config/environments/test.rb b/spec/mock_app_plugin/config/environments/test.rb similarity index 100% rename from test/mock_app_plugin/config/environments/test.rb rename to spec/mock_app_plugin/config/environments/test.rb diff --git a/test/mock_app_plugin/config/initializers/backtrace_silencers.rb b/spec/mock_app_plugin/config/initializers/backtrace_silencers.rb similarity index 100% rename from test/mock_app_plugin/config/initializers/backtrace_silencers.rb rename to spec/mock_app_plugin/config/initializers/backtrace_silencers.rb diff --git a/test/mock_app_plugin/config/initializers/inflections.rb b/spec/mock_app_plugin/config/initializers/inflections.rb similarity index 100% rename from test/mock_app_plugin/config/initializers/inflections.rb rename to spec/mock_app_plugin/config/initializers/inflections.rb diff --git a/test/mock_app_plugin/config/initializers/mime_types.rb b/spec/mock_app_plugin/config/initializers/mime_types.rb similarity index 100% rename from test/mock_app_plugin/config/initializers/mime_types.rb rename to spec/mock_app_plugin/config/initializers/mime_types.rb diff --git a/test/mock_app_plugin/config/initializers/new_rails_defaults.rb b/spec/mock_app_plugin/config/initializers/new_rails_defaults.rb similarity index 100% rename from test/mock_app_plugin/config/initializers/new_rails_defaults.rb rename to spec/mock_app_plugin/config/initializers/new_rails_defaults.rb diff --git a/test/mock_app_plugin/config/initializers/session_store.rb b/spec/mock_app_plugin/config/initializers/session_store.rb similarity index 100% rename from test/mock_app_plugin/config/initializers/session_store.rb rename to spec/mock_app_plugin/config/initializers/session_store.rb diff --git a/test/mock_app_plugin/config/locales/en.yml b/spec/mock_app_plugin/config/locales/en.yml similarity index 100% rename from test/mock_app_plugin/config/locales/en.yml rename to spec/mock_app_plugin/config/locales/en.yml diff --git a/test/mock_app_plugin/config/routes.rb b/spec/mock_app_plugin/config/routes.rb similarity index 100% rename from test/mock_app_plugin/config/routes.rb rename to spec/mock_app_plugin/config/routes.rb diff --git a/test/mock_app_plugin/config/sitemap.rb b/spec/mock_app_plugin/config/sitemap.rb similarity index 100% rename from test/mock_app_plugin/config/sitemap.rb rename to spec/mock_app_plugin/config/sitemap.rb diff --git a/test/mock_app_plugin/db/migrate/20090826121911_create_contents.rb b/spec/mock_app_plugin/db/migrate/20090826121911_create_contents.rb similarity index 100% rename from test/mock_app_plugin/db/migrate/20090826121911_create_contents.rb rename to spec/mock_app_plugin/db/migrate/20090826121911_create_contents.rb diff --git a/test/mock_app_plugin/db/schema.rb b/spec/mock_app_plugin/db/schema.rb similarity index 100% rename from test/mock_app_plugin/db/schema.rb rename to spec/mock_app_plugin/db/schema.rb diff --git a/test/mock_app_plugin/db/test.sqlite3 b/spec/mock_app_plugin/db/test.sqlite3 similarity index 100% rename from test/mock_app_plugin/db/test.sqlite3 rename to spec/mock_app_plugin/db/test.sqlite3 diff --git a/test/mock_app_plugin/public/index.html b/spec/mock_app_plugin/public/index.html similarity index 100% rename from test/mock_app_plugin/public/index.html rename to spec/mock_app_plugin/public/index.html diff --git a/test/mock_app_plugin/script/console b/spec/mock_app_plugin/script/console similarity index 100% rename from test/mock_app_plugin/script/console rename to spec/mock_app_plugin/script/console diff --git a/test/sitemap.file b/spec/sitemap.file similarity index 100% rename from test/sitemap.file rename to spec/sitemap.file diff --git a/test/sitemap_generator_test.rb b/spec/sitemap_generator_test.rb similarity index 100% rename from test/sitemap_generator_test.rb rename to spec/sitemap_generator_test.rb diff --git a/test/test_helper.rb b/spec/spec_helper.rb similarity index 100% rename from test/test_helper.rb rename to spec/spec_helper.rb From 0ff782e6edf69b0d6a16a04066151d6f3936a9de Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 1 Apr 2010 18:09:13 -0700 Subject: [PATCH 017/677] Update readme --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 67f307cd..eff32501 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ This plugin enables ['enterprise-class'][enterprise_class] Google Sitemaps to be Foreword ------- -Unfortunately, Adam Salter passed away recently. Those who knew him know what an amazing guy he was, and what an excellent Rails programmer he was. His passing is a great loss to the Rails community. +Unfortunately, Adam Salter passed away in 2009. Those who knew him know what an amazing guy he was, and what an excellent Rails programmer he was. His passing is a great loss to the Rails community. -I will be taking over maintaining this gem from him. -- Karl +[Karl Varga](http://github.com/kjvarga) has taken over development of SitemapGenerator. The canonical repository is [http://github.com/kjvarga/sitemap_generator][canonical_repo] Raison d'être ------- @@ -171,20 +171,15 @@ Wishlist Thanks (in no particular order) ======== -- [Karl Varga (aka Bear Grylls)](http://github.com/kjvarga) - [Dan Pickett](http://github.com/dpickett) - [Rob Biedenharn](http://github.com/rab) - [Richie Vos](http://github.com/jerryvos) +- [Adrian Mugnolo](http://github.com/xymbol) -Follow me on: ---------- - -> Twitter: [twitter.com/adamsalter](http://twitter.com/adamsalter) -> Github: [github.com/adamsalter](http://github.com/adamsalter) - Copyright (c) 2009 Adam @ [Codebright.net][cb], released under the MIT license +[canonical_repo]:http://github.com/kjvarga/sitemap_generator [enterprise_class]:https://twitter.com/dhh/status/1631034662 "I use enterprise in the same sense the Phusion guys do - i.e. Enterprise Ruby. Please don't look down on my use of the word 'enterprise' to represent being a cut above. It doesn't mean you ever have to work for a company the size of IBM. Or constantly fight inertia, writing crappy software, adhering to change management practices and spending hours in meetings... Not that there's anything wrong with that - Wait, what?" [sitemap_engines]:http://en.wikipedia.org/wiki/Sitemap_index "http://en.wikipedia.org/wiki/Sitemap_index" [sitemaps_org]:http://www.sitemaps.org/protocol.php "http://www.sitemaps.org/protocol.php" From efc43eaaebeefe42cd51da6a08a0052694c34e9a Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 1 Apr 2010 18:14:25 -0700 Subject: [PATCH 018/677] Reworked test rake tasks. Call test:plugin or test:gem. Couldn't get testing both working. The spec task cannot be run twice apparently. --- Rakefile | 90 +++++++++++++++++++++--------- spec/sitemap_generator_spec.rb | 77 +++++++++++++++++++++++++ spec/sitemap_generator_test.rb | 83 --------------------------- spec/spec_helper.rb | 32 +++++++---- spec/support/file_macros.rb | 28 ++++++++++ tasks/sitemap_generator_tasks.rake | 2 +- 6 files changed, 189 insertions(+), 123 deletions(-) create mode 100644 spec/sitemap_generator_spec.rb delete mode 100644 spec/sitemap_generator_test.rb create mode 100644 spec/support/file_macros.rb diff --git a/Rakefile b/Rakefile index ff6d9a8d..7bdc7ec5 100644 --- a/Rakefile +++ b/Rakefile @@ -1,18 +1,19 @@ -require 'rake/testtask' -require 'find' +require 'rake' +require 'rake/rdoctask' +require 'spec/rake/spectask' begin require 'jeweler' - Jeweler::Tasks.new do |s| - s.name = "sitemap_generator" - s.summary = %Q{Generate 'enterprise-class' Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} - s.description = %Q{Install as a plugin or Gem to easily generate ['enterprise-class'][enterprise_class] Google Sitemaps for your Rails site, using a simple 'Rails Routes'-like DSL and a single rake task.} - s.email = "kjvarga@gmail.com" - s.homepage = "http://github.com/kjvarga/sitemap_generator" - s.authors = ["Adam Salter", "Karl Varga"] - s.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] - s.test_files = [] - # s is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings + Jeweler::Tasks.new do |gem| + gem.name = "sitemap_generator" + gem.summary = %Q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} + gem.description = %Q{Installs as a plugin or Gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} + gem.email = "kjvarga@gmail.com" + gem.homepage = "http://github.com/kjvarga/sitemap_generator" + gem.authors = ["Adam Salter", "Karl Varga"] + gem.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] + gem.test_files = [] + gem.add_development_dependency "rspec" end Jeweler::GemcutterTasks.new rescue LoadError @@ -21,25 +22,60 @@ end task :default => :test -desc "Run tests" -task :test do - Rake::Task["test:prepare"].invoke - Rake::Task["test:sitemap_generator"].invoke -end - namespace :test do - desc "Copy sitemap_generator files to mock apps" - task :prepare do - %w(test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 test/mock_app_plugin/vendor/plugins/sitemap_generator).each do |path| + task :gem => ['test:prepare:gem', 'multi_spec'] + task :plugin => ['test:prepare:plugin', 'multi_spec'] + + task :multi_spec do + Rake::Task['spec'].invoke + Rake::Task['spec'].reenable + end + + namespace :prepare do + task :gem do + ENV["SITEMAP_RAILS"] = 'gem' + prepare_path(local_path('spec/mock_app_gem/vendor/gems/sitemap_generator-1.2.3')) + rm_rf(local_path('spec/mock_app_gem/public/sitemap*')) + end + + task :plugin do + ENV["SITEMAP_RAILS"] = 'plugin' + prepare_path(local_path('spec/mock_app_plugin/vendor/plugins/sitemap_generator-1.2.3')) + rm_rf(local_path('spec/mock_app_plugin/public/sitemap*')) + end + + def local_path(path) + File.join(File.dirname(__FILE__), path) + end + + def prepare_path(path) rm_rf path mkdir_p path - cp_r FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}"], path + cp_r(FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}"], path) end end +end - Rake::TestTask.new(:sitemap_generator) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true - end +desc "Run all tests both as a plugin and gem" +task :test => ['test:plugin', 'test:gem'] + +Spec::Rake::SpecTask.new(:spec) do |spec| + spec.libs << 'lib' << 'spec' + spec.spec_files = FileList['spec/**/*_spec.rb'] +end +task :spec => :check_dependencies + +Spec::Rake::SpecTask.new(:rcov) do |spec| + spec.libs << 'lib' << 'spec' + spec.pattern = 'spec/**/*_spec.rb' + spec.rcov = true +end + +desc 'Generate documentation' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'SitemapGenerator' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README.md') + rdoc.rdoc_files.include('lib/**/*.rb') end diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb new file mode 100644 index 00000000..e579097e --- /dev/null +++ b/spec/sitemap_generator_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe "SitemapGenerator" do + + context "clean task" do + before :all do + copy_sitemap_file_to_rails_app + FileUtils.touch(rails_path('/public/sitemap_index.xml.gz')) + Rake::Task['sitemap:clean'].invoke + end + + it "should delete the sitemaps" do + file_should_not_exist(rails_path('/public/sitemap_index.xml.gz')) + end + end + + context "fresh install" do + before :all do + delete_sitemap_file_from_rails_app + Rake::Task['sitemap:install'].invoke + end + + it "should create config/sitemap.rb" do + file_should_exist(rails_path('config/sitemap.rb')) + end + + it "should create config/sitemap.rb matching template" do + sitemap_template = SitemapGenerator.templates[:sitemap_sample] + files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) + end + + context "install multiple times" do + before :all do + copy_sitemap_file_to_rails_app + Rake::Task['sitemap:install'].invoke + end + + it "should not overwrite config/sitemap.rb" do + sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') + files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) + end + end + end + + context "generate sitemap" do + before :each do + Rake::Task['sitemap:refresh:no_ping'].invoke + end + + it "should create sitemaps" do + file_should_exist(rails_path('/public/sitemap_index.xml.gz')) + file_should_exist(rails_path('/public/sitemap1.xml.gz')) + end + + it "should have 14 links" do + SitemapGenerator::Sitemap.link_count.should == 14 + end + end + + protected + + # + # Helpers + # + + def rails_path(file) + File.join(RAILS_ROOT, file) + end + + def copy_sitemap_file_to_rails_app + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) + end + + def delete_sitemap_file_from_rails_app + FileUtils.remove(File.join(RAILS_ROOT, '/config/sitemap.rb')) rescue nil + end +end \ No newline at end of file diff --git a/spec/sitemap_generator_test.rb b/spec/sitemap_generator_test.rb deleted file mode 100644 index 4e98adab..00000000 --- a/spec/sitemap_generator_test.rb +++ /dev/null @@ -1,83 +0,0 @@ -require File.dirname(__FILE__) + '/test_helper' - -class SitemapGeneratorTest < Test::Unit::TestCase - context "SitemapGenerator Rake Tasks" do - - context "when running the clean task" do - setup do - copy_sitemap_file_to_rails_app - FileUtils.touch(File.join(RAILS_ROOT, '/public/sitemap_index.xml.gz')) - Rake::Task['sitemap:clean'].invoke - end - - should "the sitemap xml files be deleted" do - assert !File.exists?(File.join(RAILS_ROOT, '/public/sitemap_index.xml.gz')) - end - end - - # For some reason I just couldn't get this to work! It seemed to delete the - # file before calling the second *should* assertion. - context "when installed to a clean Rails app" do - setup do - #delete_sitemap_file_from_rails_app - #Rake::Task['sitemap:install'].invoke - end - - should "a sitemap.rb is created" do - #assert File.exists?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - end - - should "the sitemap.rb file matches the template" do - #assert identical_files?(File.join(RAILS_ROOT, 'config/sitemap.rb'), SitemapGenerator.templates[:sitemap_sample]) - end - end - - context "when installed multiple times" do - setup do - copy_sitemap_file_to_rails_app - Rake::Task['sitemap:install'].invoke - end - - should "not overwrite existing sitemap.rb file" do - assert identical_files?(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) - end - end - - context "when sitemap generated" do - setup do - copy_sitemap_file_to_rails_app - Rake::Task['sitemap:refresh'].invoke - end - - should "not create sitemap xml files" do - assert File.exists?(File.join(RAILS_ROOT, '/public/sitemap_index.xml.gz')) - assert File.exists?(File.join(RAILS_ROOT, '/public/sitemap1.xml.gz')) - end - end - end - - context "SitemapGenerator library" do - setup do - copy_sitemap_file_to_rails_app - end - - should "be have x elements" do - assert_equal 14, SitemapGenerator::Sitemap.link_count - end - end - - def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) - end - - def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(RAILS_ROOT, '/config/sitemap.rb')) rescue nil - end - - def identical_files?(first, second) - first = open(first, 'r').read - second = open(second, 'r').read - first == second - end -end - diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 43fb9546..2737419f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,20 @@ -ENV['RAILS_ENV'] = 'test' - -# This is basically the contents of mock_app_gems's Rakefile -require File.join(File.dirname(__FILE__), 'mock_app_gem', 'config', 'boot') -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' -require 'tasks/rails' -require 'sitemap_generator/tasks' - -# Testing -require 'shoulda' \ No newline at end of file +ENV["RAILS_ENV"] ||= 'test' + +sitemap_rails = ENV["SITEMAP_RAILS"] ? "mock_app_#{ENV["SITEMAP_RAILS"]}" : 'mock_app_gem' + +# Boot the environment +require File.join(File.dirname(__FILE__), sitemap_rails, 'config', 'boot') + +# Load the app's Rakefile so we know everything is being loaded correctly +load(File.join(File.dirname(__FILE__), sitemap_rails, 'Rakefile')) + +require 'ruby-debug' +# debugger + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f} + +Spec::Runner.configure do |config| + config.include(FileMacros) +end \ No newline at end of file diff --git a/spec/support/file_macros.rb b/spec/support/file_macros.rb new file mode 100644 index 00000000..db9a0ab7 --- /dev/null +++ b/spec/support/file_macros.rb @@ -0,0 +1,28 @@ +module FileMacros + module ExampleMethods + + def files_should_be_identical(first, second) + identical_files?(first, second).should be(true) + end + + def files_should_not_be_identical(first, second) + identical_files?(first, second).should be(false) + end + + def file_should_exist(file) + File.exists?(file).should be(true) + end + + def file_should_not_exist(file) + File.exists?(file).should be(false) + end + + def identical_files?(first, second) + open(second, 'r').read.should == open(first, 'r').read + end + end + + def self.included(receiver) + receiver.send :include, ExampleMethods + end +end \ No newline at end of file diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index 55443d39..a679d47d 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,5 +1,5 @@ require 'zlib' -require 'sitemap_generator/helper' +require 'sitemap_generator' namespace :sitemap do desc "Install a default config/sitemap.rb file" From 50cdbfde2450758d038b760886656b2351ea0065 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 1 Apr 2010 18:16:42 -0700 Subject: [PATCH 019/677] Update gemspec with new description etc. --- sitemap_generator.gemspec | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index e71828eb..c82b1b24 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -9,8 +9,8 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-03-18} - s.description = %q{Install as a plugin or Gem to easily generate ['enterprise-class'][enterprise_class] Google Sitemaps for your Rails site, using a simple 'Rails Routes'-like DSL and a single rake task.} + s.date = %q{2010-04-01} + s.description = %q{Installs as a plugin or Gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ "README.md" @@ -36,17 +36,20 @@ Gem::Specification.new do |s| s.homepage = %q{http://github.com/kjvarga/sitemap_generator} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.6} - s.summary = %q{Generate 'enterprise-class' Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} + s.rubygems_version = %q{1.3.5} + s.summary = %q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION s.specification_version = 3 if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then + s.add_development_dependency(%q, [">= 0"]) else + s.add_dependency(%q, [">= 0"]) end else + s.add_dependency(%q, [">= 0"]) end end From 2852bfffc1376cae9ebf093df24146e9df2d0141 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 1 Apr 2010 18:24:29 -0700 Subject: [PATCH 020/677] Silence warnings if the SitemapGenerator constants have already been defined. --- lib/sitemap_generator.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index e2b7f064..6d94c99b 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -4,10 +4,12 @@ require 'sitemap_generator/helper' module SitemapGenerator - VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip - - MAX_ENTRIES = 50_000 - + silence_warnings do + VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip + MAX_ENTRIES = 50_000 + Sitemap = LinkSet.new + end + class << self attr_accessor :root, :templates end @@ -18,6 +20,4 @@ class << self :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), :sitemap_sample => File.join(self.root, 'templates/sitemap.rb'), } - - Sitemap = LinkSet.new end From 346227e4ddef1a9b34aeb614c3f2e4bbc2fc5565 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 7 Apr 2010 14:58:12 -0700 Subject: [PATCH 021/677] Fix the rescue clause in the Rakefile example. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eff32501..e1c32e14 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,11 @@ Installation 3. Add the following line to your RAILS_ROOT/Rakefile - require 'sitemap_generator/tasks' rescue LoadError + begin + require 'sitemap_generator/tasks' + rescue Exception => e + puts "Warning, couldn't load gem tasks: #{e.message}! Skipping..." + end 4. `$ rake sitemap:install` From de5d2a7579298c8e2bd3cfe8c9f7e977e48b1b13 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 7 Apr 2010 15:08:32 -0700 Subject: [PATCH 022/677] Improve README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e1c32e14..71589ea4 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,19 @@ Installation **As a gem** -1. Add the gem as a dependency in your config/environment.rb +1. Add the gem as a dependency in your config/environment.rb config.gem 'sitemap_generator', :lib => false, :source => 'http://gemcutter.org' 2. `$ rake gems:install` -3. Add the following line to your RAILS_ROOT/Rakefile +3. Add the following to your RAILS_ROOT/Rakefile - begin +
    begin
           require 'sitemap_generator/tasks'
         rescue Exception => e
           puts "Warning, couldn't load gem tasks: #{e.message}! Skipping..."
    -    end
    +    end
    4. `$ rake sitemap:install` @@ -75,17 +75,17 @@ Installation ---- -Installation should create a 'config/sitemap.rb' file which will contain your logic for generation of the Sitemap files. (If you want to recreate this file manually run `rake sitemap:install`) +Installation creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. If you want to create this file manually run rake sitemap:install. -You can run `rake sitemap:refresh` as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. (if you want to disable all non-essential output run the rake task thusly `rake -s sitemap:refresh`) +You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. Sitemaps with many urls (100,000+) take quite a long time to generate, so if you need to refresh your Sitemaps regularly you can set the rake task up as a cron job. Most cron agents will only send you an email if there is output from the cron task. -Optionally, you can add the following to your robots.txt file, so that robots can find the sitemap file. +Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file. Sitemap: /sitemap_index.xml.gz -The robots.txt Sitemap URL should be the complete URL to the Sitemap Index, such as: `http://www.example.org/sitemap_index.xml.gz` +The Sitemap URL in the robots file should be the complete URL to the Sitemap Index, such as http://www.example.org/sitemap_index.xml.gz Example 'config/sitemap.rb' From ba859f7f31039af338291b8fa1eb09e2713985e6 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 7 Apr 2010 15:12:16 -0700 Subject: [PATCH 023/677] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71589ea4..55424875 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,13 @@ Installation ---- -Installation creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. If you want to create this file manually run rake sitemap:install. +Installation creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. If you want to create this file manually run rake sitemap:install. -You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. +You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. Sitemaps with many urls (100,000+) take quite a long time to generate, so if you need to refresh your Sitemaps regularly you can set the rake task up as a cron job. Most cron agents will only send you an email if there is output from the cron task. -Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file. +Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file. Sitemap: /sitemap_index.xml.gz From 46a6ef8ad8e18269f53273d5bb81c7e119005c97 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Sun, 11 Apr 2010 01:53:07 -0700 Subject: [PATCH 024/677] Move template handling to a module. Only read template files once. Separate LinkSet generation and writing in preparation for multiple hosts. --- lib/sitemap_generator.rb | 15 +- lib/sitemap_generator/link_set.rb | 182 +--------------------- lib/sitemap_generator/link_set/builder.rb | 24 +++ lib/sitemap_generator/link_set/writer.rb | 178 +++++++++++++++++++++ lib/sitemap_generator/mapper.rb | 15 -- lib/sitemap_generator/templates.rb | 36 +++++ spec/sitemap_generator/templates_spec.rb | 24 +++ spec/sitemap_generator_spec.rb | 10 +- 8 files changed, 276 insertions(+), 208 deletions(-) create mode 100644 lib/sitemap_generator/link_set/builder.rb create mode 100644 lib/sitemap_generator/link_set/writer.rb delete mode 100644 lib/sitemap_generator/mapper.rb create mode 100644 lib/sitemap_generator/templates.rb create mode 100644 spec/sitemap_generator/templates_spec.rb diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 6d94c99b..5df9dafc 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -1,23 +1,20 @@ -require 'sitemap_generator/mapper' require 'sitemap_generator/link' require 'sitemap_generator/link_set' +require 'sitemap_generator/link_set/builder' require 'sitemap_generator/helper' +require 'sitemap_generator/templates' module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip MAX_ENTRIES = 50_000 - Sitemap = LinkSet.new + Sitemap = SitemapGenerator::LinkSet::Builder.new end class << self - attr_accessor :root, :templates + attr_accessor :root, :templates, :template, :x end self.root = File.expand_path(File.join(File.dirname(__FILE__), '../')) - self.templates = { - :sitemap_index => File.join(self.root, 'templates/sitemap_index.builder'), - :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), - :sitemap_sample => File.join(self.root, 'templates/sitemap.rb'), - } -end + self.templates = SitemapGenerator::Templates.new(self.root) +end \ No newline at end of file diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index e6d9849e..2792ca7c 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,181 +1,5 @@ -require File.dirname(__FILE__) + '/helper' - module SitemapGenerator - class LinkSet - include SitemapGenerator::Helper - include ActionView::Helpers::NumberHelper - - attr_accessor :default_host, :yahoo_app_id, :links - attr_accessor :sitemaps - attr_accessor :max_entries - attr_accessor :link_count - - alias :sitemap_files :sitemaps - - # Create new link set instance. - def initialize - self.links = [] - self.sitemaps = [] - self.max_entries = SitemapGenerator::MAX_ENTRIES - self.link_count = 0 - end - - # Add default links to sitemap files. - def add_default_links - links.push Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - links.push Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - self.link_count += 2 - end - - # Add links to sitemap files passing a block. - def add_links - raise ArgumentError, "Default hostname not set" if default_host.blank? - add_default_links if first_link? - yield Mapper.new(self) - end - - # Add links from mapper to sitemap files. - def add_link(link) - write_upcoming if enough_links? - links.push link - self.link_count += 1 - end - - # Write links to sitemap file. - def write - write_pending - end - - # Write links to upcoming sitemap file. - def write_upcoming - write_sitemap(upcoming_file) - end - - # Write pending links to sitemap, write index file if needed. - def write_pending - write_upcoming - write_index - end - - # Write links to sitemap file. - def write_sitemap(file = upcoming_file) - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) - filename = File.join(RAILS_ROOT, "public", file) - write_file(filename, buffer) - show_progress("Sitemap", filename, buffer) if verbose - links.clear - sitemaps.push filename - end - - # Write sitemap links to sitemap index file. - def write_index - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) - filename = File.join(RAILS_ROOT, "public", index_file) - write_file(filename, buffer) - show_progress("Sitemap Index", filename, buffer) if verbose - links.clear - sitemaps.clear - end - - # Return sitemap or sitemap index main name. - def index_file - "sitemap_index.xml.gz" - end - - # Return upcoming sitemap name with index. - def upcoming_file - "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? - end - - # Return upcoming sitemap index, first is 1. - def upcoming_index - sitemaps.length + 1 unless enough_sitemaps? - end - - # Return true if upcoming is first sitemap. - def first_sitemap? - sitemaps.empty? - end - - # Return true if sitemap index needed. - def multiple_sitemaps? - !first_sitemap? - end - - # Return true if more sitemaps can be added. - def more_sitemaps? - sitemaps.length < max_entries - end - - # Return true if no sitemaps can be added. - def enough_sitemaps? - !more_sitemaps? - end - - # Return true if this is the first link added. - def first_link? - links.empty? && first_sitemap? - end - - # Return true if more links can be added. - def more_links? - links.length < max_entries - end - - # Return true if no further links can be added. - def enough_links? - !more_links? - end - - # Commit buffer to gzipped file. - def write_file(name, buffer) - Zlib::GzipWriter.open(name) { |gz| gz.write buffer } - end - - # Report progress line. - def show_progress(title, filename, buffer) - puts "+ #{filename}" - puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes - end - - # Copy templates/sitemap.rb to config if not there yet. - def install_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "already exists: config/sitemap.rb, file not copied" - else - FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "created: config/sitemap.rb" - end - end - - # Remove config/sitemap.rb if exists. - def uninstall_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) - end - end - - # Clean sitemap files in output directory. - def clean_files - FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) - end - - # Ping search engines passing sitemap location. - def ping_search_engines - super index_file - end - - # Create sitemap files in output directory. - def create_files(verbose = true) - start_time = Time.now - load_sitemap_rb - write - stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose - end + module LinkSet + end -end +end \ No newline at end of file diff --git a/lib/sitemap_generator/link_set/builder.rb b/lib/sitemap_generator/link_set/builder.rb new file mode 100644 index 00000000..f9ed39f0 --- /dev/null +++ b/lib/sitemap_generator/link_set/builder.rb @@ -0,0 +1,24 @@ +module SitemapGenerator + module LinkSet + # The object passed to SitemapGenerator::Sitemap.add_links block in + # config/sitemap.rb is a SetBuilder instance. + class Builder + attr_accessor :host, :default_host + + # Add links to sitemap files. + # + # Pass a block which takes as its argument a LinkSet::Builder instance. + # + # Pass optional host list of host symbols (or a single symbol) + # to add the links to sitemap files for those hosts. + def add_links(host) + @host = host.is_a?(Array) ? host : [host || default_host].compact! + raise ArgumentError, "Default hostname not set" if @host.empty? + + set = LinkSet.new + add_default_links if first_link? + yield Mapper.new(self) + end + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/link_set/writer.rb b/lib/sitemap_generator/link_set/writer.rb new file mode 100644 index 00000000..9ccc4508 --- /dev/null +++ b/lib/sitemap_generator/link_set/writer.rb @@ -0,0 +1,178 @@ +require File.dirname(__FILE__) + '/helper' + +module SitemapGenerator + module LinkSet + module Writer + include SitemapGenerator::Helper + include ActionView::Helpers::NumberHelper + + attr_accessor :default_host, :yahoo_app_id, :links + attr_accessor :sitemaps, :max_entries, :link_count + attr_accessor :host + + alias :sitemap_files :sitemaps + + # Create new link set instance. + # + # Optional host is the host symbol. Defines the subdirectory + # in which to place sitemap files. If nil, no subdirectory is used. + def initialize(host) + self.host = host.to_s + self.links = [] + self.sitemaps = [] + self.max_entries = SitemapGenerator::MAX_ENTRIES + self.link_count = 0 + end + + # Add default links to sitemap files. + def add_default_links + add_link Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + add_link Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + end + + # Add links from mapper to sitemap files. + def add_link(link) + write_upcoming if enough_links? + links.push link + self.link_count += 1 + end + + # Write links to sitemap file. + def write + write_pending + end + + # Write links to upcoming sitemap file. + def write_upcoming + write_sitemap(upcoming_file) + end + + # Write pending links to sitemap, write index file if needed. + def write_pending + write_upcoming + write_index + end + + # Write links to sitemap file. + def write_sitemap(file = upcoming_file) + buffer = "" + xml = Builder::XmlMarkup.new(:target => buffer) + eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) + filename = File.join(RAILS_ROOT, "tmp", file) + write_file(filename, buffer) + show_progress("Sitemap", filename, buffer) if verbose + links.clear + sitemaps.push filename + end + + # Write sitemap links to sitemap index file. + def write_index + buffer = "" + xml = Builder::XmlMarkup.new(:target => buffer) + eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) + filename = File.join(RAILS_ROOT, "public", index_file) + write_file(filename, buffer) + show_progress("Sitemap Index", filename, buffer) if verbose + links.clear + sitemaps.clear + end + + # Return sitemap or sitemap index main name. + def index_file + "sitemap_index.xml.gz" + end + + # Return upcoming sitemap name with index. + def upcoming_file + "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? + end + + # Return upcoming sitemap index, first is 1. + def upcoming_index + sitemaps.length + 1 unless enough_sitemaps? + end + + # Return true if upcoming is first sitemap. + def first_sitemap? + sitemaps.empty? + end + + # Return true if sitemap index needed. + def multiple_sitemaps? + !first_sitemap? + end + + # Return true if more sitemaps can be added. + def more_sitemaps? + sitemaps.length < max_entries + end + + # Return true if no sitemaps can be added. + def enough_sitemaps? + !more_sitemaps? + end + + # Return true if this is the first link added. + def first_link? + links.empty? && first_sitemap? + end + + # Return true if more links can be added. + def more_links? + links.length < max_entries + end + + # Return true if no further links can be added. + def enough_links? + !more_links? + end + + # Commit buffer to gzipped file. + def write_file(name, buffer) + Zlib::GzipWriter.open(name) { |gz| gz.write buffer } + end + + # Report progress line. + def show_progress(title, filename, buffer) + puts "+ #{filename}" + puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes + end + + # Copy templates/sitemap.rb to config if not there yet. + def install_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "already exists: config/sitemap.rb, file not copied" + else + FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "created: config/sitemap.rb" + end + end + + # Remove config/sitemap.rb if exists. + def uninstall_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) + end + end + + # Clean sitemap files in output directory. + def clean_files + FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + end + + # Ping search engines passing sitemap location. + def ping_search_engines + super index_file + end + + # Create sitemap files in output directory. + def create_files(verbose = true) + start_time = Time.now + load_sitemap_rb + write + stop_time = Time.now + puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + end + end + end +end diff --git a/lib/sitemap_generator/mapper.rb b/lib/sitemap_generator/mapper.rb deleted file mode 100644 index 16cb1302..00000000 --- a/lib/sitemap_generator/mapper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module SitemapGenerator - # Generator instances are used to build links. - # The object passed to the add_links block in config/sitemap.rb is a Generator instance. - class Mapper - attr_accessor :set - - def initialize(set) - @set = set - end - - def add(loc, options = {}) - set.add_link Link.generate(loc, options) - end - end -end \ No newline at end of file diff --git a/lib/sitemap_generator/templates.rb b/lib/sitemap_generator/templates.rb new file mode 100644 index 00000000..7744c00c --- /dev/null +++ b/lib/sitemap_generator/templates.rb @@ -0,0 +1,36 @@ +module SitemapGenerator + # Provide convenient access to template files. + # + # Only read the template file once and store the result. + # Define accessor methods for each template file. + class Templates + FILES = { + :sitemap_index => 'sitemap_index.builder', + :sitemap_xml => 'xml_sitemap.builder', + :sitemap_sample => 'sitemap.rb', + } + + attr_accessor *FILES.keys + FILES.keys.each do |name| + eval <<-END + define_method(:#{name}) do + @#{name} ||= read_template(:#{name}) + end + END + end + + def initialize(root = SitemapGenerator.root) + @root = root + end + + protected + + def template_path(file) + File.join(@root, 'templates', file) + end + + def read_template(template) + File.read(template_path(self.class::FILES[template])) + end + end +end \ No newline at end of file diff --git a/spec/sitemap_generator/templates_spec.rb b/spec/sitemap_generator/templates_spec.rb new file mode 100644 index 00000000..94daa0cf --- /dev/null +++ b/spec/sitemap_generator/templates_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe "Templates class" do + + it "should provide method access to each template" do + SitemapGenerator::Templates::FILES.each do |name, file| + SitemapGenerator.templates.send(name).should_not be(nil) + SitemapGenerator.templates.send(name).should == File.read(File.join(SitemapGenerator.root, 'templates', file)) + end + end + + describe "templates" do + before :each do + SitemapGenerator.templates.sitemap_xml = nil + File.stub!(:read).and_return('read file') + end + + it "should only be read once" do + File.should_receive(:read).once + SitemapGenerator.templates.sitemap_xml + SitemapGenerator.templates.sitemap_xml + end + end +end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index e579097e..d62a2080 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe "SitemapGenerator" do - + context "clean task" do before :all do copy_sitemap_file_to_rails_app @@ -19,11 +19,11 @@ delete_sitemap_file_from_rails_app Rake::Task['sitemap:install'].invoke end - + it "should create config/sitemap.rb" do file_should_exist(rails_path('config/sitemap.rb')) end - + it "should create config/sitemap.rb matching template" do sitemap_template = SitemapGenerator.templates[:sitemap_sample] files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) @@ -41,7 +41,7 @@ end end end - + context "generate sitemap" do before :each do Rake::Task['sitemap:refresh:no_ping'].invoke @@ -56,7 +56,7 @@ SitemapGenerator::Sitemap.link_count.should == 14 end end - + protected # From a7382303bd06d0800539107ff63edb34df5578b7 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Sun, 11 Apr 2010 01:56:13 -0700 Subject: [PATCH 025/677] Fix the .gitignore for the new mock apps location --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 774e6fef..6d6542dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.swp pkg -test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 -test/mock_app_plugin/vendor/plugins/sitemap_generator +spec/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 +spec/mock_app_plugin/vendor/plugins/sitemap_generator-1.2.3 From ff3d293974014d6f2454b621a08aadce030ee62d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Sun, 11 Apr 2010 13:23:52 -0700 Subject: [PATCH 026/677] Fix specs. Refactoring WIP. --- lib/sitemap_generator.rb | 2 +- lib/sitemap_generator/link_set/builder.rb | 7 ++++++- lib/sitemap_generator/link_set/writer.rb | 25 ++--------------------- lib/sitemap_generator/templates.rb | 4 ++-- lib/sitemap_generator/utilities.rb | 23 +++++++++++++++++++++ spec/sitemap_generator_spec.rb | 2 +- 6 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 lib/sitemap_generator/utilities.rb diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 5df9dafc..74780d7f 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -12,7 +12,7 @@ module SitemapGenerator end class << self - attr_accessor :root, :templates, :template, :x + attr_accessor :root, :templates end self.root = File.expand_path(File.join(File.dirname(__FILE__), '../')) diff --git a/lib/sitemap_generator/link_set/builder.rb b/lib/sitemap_generator/link_set/builder.rb index f9ed39f0..ad2a5d4c 100644 --- a/lib/sitemap_generator/link_set/builder.rb +++ b/lib/sitemap_generator/link_set/builder.rb @@ -18,7 +18,12 @@ def add_links(host) set = LinkSet.new add_default_links if first_link? yield Mapper.new(self) - end + end + + # Clean sitemap files from output directory. + #def clean_files + # FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + #end end end end \ No newline at end of file diff --git a/lib/sitemap_generator/link_set/writer.rb b/lib/sitemap_generator/link_set/writer.rb index 9ccc4508..4b444ae8 100644 --- a/lib/sitemap_generator/link_set/writer.rb +++ b/lib/sitemap_generator/link_set/writer.rb @@ -57,7 +57,7 @@ def write_pending def write_sitemap(file = upcoming_file) buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) + eval(SitemapGenerator.templates.sitemap_xml, binding) filename = File.join(RAILS_ROOT, "tmp", file) write_file(filename, buffer) show_progress("Sitemap", filename, buffer) if verbose @@ -69,7 +69,7 @@ def write_sitemap(file = upcoming_file) def write_index buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) + eval(SitemapGenerator.templates.sitemap_index, binding) filename = File.join(RAILS_ROOT, "public", index_file) write_file(filename, buffer) show_progress("Sitemap Index", filename, buffer) if verbose @@ -138,27 +138,6 @@ def show_progress(title, filename, buffer) puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes end - # Copy templates/sitemap.rb to config if not there yet. - def install_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "already exists: config/sitemap.rb, file not copied" - else - FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "created: config/sitemap.rb" - end - end - - # Remove config/sitemap.rb if exists. - def uninstall_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) - end - end - - # Clean sitemap files in output directory. - def clean_files - FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) - end # Ping search engines passing sitemap location. def ping_search_engines diff --git a/lib/sitemap_generator/templates.rb b/lib/sitemap_generator/templates.rb index 7744c00c..a051b9bf 100644 --- a/lib/sitemap_generator/templates.rb +++ b/lib/sitemap_generator/templates.rb @@ -23,12 +23,12 @@ def initialize(root = SitemapGenerator.root) @root = root end - protected - def template_path(file) File.join(@root, 'templates', file) end + protected + def read_template(template) File.read(template_path(self.class::FILES[template])) end diff --git a/lib/sitemap_generator/utilities.rb b/lib/sitemap_generator/utilities.rb new file mode 100644 index 00000000..e19fe179 --- /dev/null +++ b/lib/sitemap_generator/utilities.rb @@ -0,0 +1,23 @@ +module SitemapGenerator + module Utilities + + # Copy templates/sitemap.rb to config if not there yet. + def install_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "already exists: config/sitemap.rb, file not copied" + else + FileUtils.cp( + SitemapGenerator.templates.template_path(:sitemap_sample), + File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "created: config/sitemap.rb" + end + end + + # Remove config/sitemap.rb if exists. + def uninstall_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) + end + end + end +end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index d62a2080..6c2577df 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -25,7 +25,7 @@ end it "should create config/sitemap.rb matching template" do - sitemap_template = SitemapGenerator.templates[:sitemap_sample] + sitemap_template = SitemapGenerator.templates.template_path(:sitemap_sample) files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) end From 04039d50c7fa5328bd99a2e402c0d47c53ae617f Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 11:49:27 -0700 Subject: [PATCH 027/677] Upgrade the mock apps to 2.3.5. Tests still broken because of changes to API. --- lib/sitemap_generator/templates.rb | 8 +++++--- spec/mock_app_gem/config/environment.rb | 2 +- spec/mock_app_plugin/config/environment.rb | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/sitemap_generator/templates.rb b/lib/sitemap_generator/templates.rb index a051b9bf..20939f12 100644 --- a/lib/sitemap_generator/templates.rb +++ b/lib/sitemap_generator/templates.rb @@ -1,8 +1,10 @@ module SitemapGenerator - # Provide convenient access to template files. + # Provide convenient access to template files. E.g. # - # Only read the template file once and store the result. - # Define accessor methods for each template file. + # SitemapGenerator.templates.sitemap_index + # + # Lazy-load and cache for efficient access. + # Define an accessor method for each template file. class Templates FILES = { :sitemap_index => 'sitemap_index.builder', diff --git a/spec/mock_app_gem/config/environment.rb b/spec/mock_app_gem/config/environment.rb index c36e8435..1f6f5cfa 100644 --- a/spec/mock_app_gem/config/environment.rb +++ b/spec/mock_app_gem/config/environment.rb @@ -1,7 +1,7 @@ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') diff --git a/spec/mock_app_plugin/config/environment.rb b/spec/mock_app_plugin/config/environment.rb index 1de84373..029a58b3 100644 --- a/spec/mock_app_plugin/config/environment.rb +++ b/spec/mock_app_plugin/config/environment.rb @@ -1,7 +1,7 @@ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') From 2efb7dfd4d3cc5779e5c358773bb0e813fb81cda Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 13:15:04 -0700 Subject: [PATCH 028/677] Add mock Rails 3 app --- spec/mock_rails3_gem/.gitignore | 4 + spec/mock_rails3_gem/Gemfile | 27 + spec/mock_rails3_gem/README | 244 + spec/mock_rails3_gem/Rakefile | 10 + .../app/controllers/application_controller.rb | 4 + .../app/helpers/application_helper.rb | 2 + .../app/views/layouts/application.html.erb | 14 + spec/mock_rails3_gem/config.ru | 4 + spec/mock_rails3_gem/config/application.rb | 46 + spec/mock_rails3_gem/config/boot.rb | 6 + spec/mock_rails3_gem/config/database.yml | 22 + spec/mock_rails3_gem/config/environment.rb | 5 + .../config/environments/development.rb | 19 + .../config/environments/production.rb | 42 + .../config/environments/test.rb | 32 + .../initializers/backtrace_silencers.rb | 7 + .../config/initializers/inflections.rb | 10 + .../config/initializers/mime_types.rb | 5 + .../config/initializers/secret_token.rb | 7 + .../config/initializers/session_store.rb | 8 + spec/mock_rails3_gem/config/locales/en.yml | 5 + spec/mock_rails3_gem/config/routes.rb | 58 + spec/mock_rails3_gem/db/seeds.rb | 7 + spec/mock_rails3_gem/doc/README_FOR_APP | 2 + spec/mock_rails3_gem/lib/tasks/.gitkeep | 0 spec/mock_rails3_gem/public/404.html | 26 + spec/mock_rails3_gem/public/422.html | 26 + spec/mock_rails3_gem/public/500.html | 26 + spec/mock_rails3_gem/public/favicon.ico | 0 spec/mock_rails3_gem/public/images/rails.png | Bin 0 -> 6646 bytes .../public/javascripts/application.js | 2 + .../public/javascripts/controls.js | 965 ++++ .../public/javascripts/dragdrop.js | 974 ++++ .../public/javascripts/effects.js | 1123 ++++ .../public/javascripts/prototype.js | 4874 +++++++++++++++++ .../public/javascripts/rails.js | 118 + spec/mock_rails3_gem/public/robots.txt | 5 + .../public/stylesheets/.gitkeep | 0 spec/mock_rails3_gem/script/rails | 9 + .../test/performance/browsing_test.rb | 9 + spec/mock_rails3_gem/test/test_helper.rb | 13 + spec/mock_rails3_gem/vendor/plugins/.gitkeep | 0 42 files changed, 8760 insertions(+) create mode 100644 spec/mock_rails3_gem/.gitignore create mode 100644 spec/mock_rails3_gem/Gemfile create mode 100644 spec/mock_rails3_gem/README create mode 100644 spec/mock_rails3_gem/Rakefile create mode 100644 spec/mock_rails3_gem/app/controllers/application_controller.rb create mode 100644 spec/mock_rails3_gem/app/helpers/application_helper.rb create mode 100644 spec/mock_rails3_gem/app/views/layouts/application.html.erb create mode 100644 spec/mock_rails3_gem/config.ru create mode 100644 spec/mock_rails3_gem/config/application.rb create mode 100644 spec/mock_rails3_gem/config/boot.rb create mode 100644 spec/mock_rails3_gem/config/database.yml create mode 100644 spec/mock_rails3_gem/config/environment.rb create mode 100644 spec/mock_rails3_gem/config/environments/development.rb create mode 100644 spec/mock_rails3_gem/config/environments/production.rb create mode 100644 spec/mock_rails3_gem/config/environments/test.rb create mode 100644 spec/mock_rails3_gem/config/initializers/backtrace_silencers.rb create mode 100644 spec/mock_rails3_gem/config/initializers/inflections.rb create mode 100644 spec/mock_rails3_gem/config/initializers/mime_types.rb create mode 100644 spec/mock_rails3_gem/config/initializers/secret_token.rb create mode 100644 spec/mock_rails3_gem/config/initializers/session_store.rb create mode 100644 spec/mock_rails3_gem/config/locales/en.yml create mode 100644 spec/mock_rails3_gem/config/routes.rb create mode 100644 spec/mock_rails3_gem/db/seeds.rb create mode 100644 spec/mock_rails3_gem/doc/README_FOR_APP create mode 100644 spec/mock_rails3_gem/lib/tasks/.gitkeep create mode 100644 spec/mock_rails3_gem/public/404.html create mode 100644 spec/mock_rails3_gem/public/422.html create mode 100644 spec/mock_rails3_gem/public/500.html create mode 100644 spec/mock_rails3_gem/public/favicon.ico create mode 100644 spec/mock_rails3_gem/public/images/rails.png create mode 100644 spec/mock_rails3_gem/public/javascripts/application.js create mode 100644 spec/mock_rails3_gem/public/javascripts/controls.js create mode 100644 spec/mock_rails3_gem/public/javascripts/dragdrop.js create mode 100644 spec/mock_rails3_gem/public/javascripts/effects.js create mode 100644 spec/mock_rails3_gem/public/javascripts/prototype.js create mode 100644 spec/mock_rails3_gem/public/javascripts/rails.js create mode 100644 spec/mock_rails3_gem/public/robots.txt create mode 100644 spec/mock_rails3_gem/public/stylesheets/.gitkeep create mode 100755 spec/mock_rails3_gem/script/rails create mode 100644 spec/mock_rails3_gem/test/performance/browsing_test.rb create mode 100644 spec/mock_rails3_gem/test/test_helper.rb create mode 100644 spec/mock_rails3_gem/vendor/plugins/.gitkeep diff --git a/spec/mock_rails3_gem/.gitignore b/spec/mock_rails3_gem/.gitignore new file mode 100644 index 00000000..af64fae5 --- /dev/null +++ b/spec/mock_rails3_gem/.gitignore @@ -0,0 +1,4 @@ +.bundle +db/*.sqlite3 +log/*.log +tmp/**/* diff --git a/spec/mock_rails3_gem/Gemfile b/spec/mock_rails3_gem/Gemfile new file mode 100644 index 00000000..8ed08813 --- /dev/null +++ b/spec/mock_rails3_gem/Gemfile @@ -0,0 +1,27 @@ +source 'http://rubygems.org' + +gem 'rails', '3.0.0.beta3' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' + +gem 'sqlite3-ruby', :require => 'sqlite3' +gem 'sitemap_generator', :require => false + +# Use unicorn as the web server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + +# Bundle the extra gems: +# gem 'bj' +# gem 'nokogiri', '1.4.1' +# gem 'sqlite3-ruby', :require => 'sqlite3' +# gem 'aws-s3', :require => 'aws/s3' + +# Bundle gems for certain environments: +# gem 'rspec', :group => :test +# group :test do +# gem 'webrat' +# end diff --git a/spec/mock_rails3_gem/README b/spec/mock_rails3_gem/README new file mode 100644 index 00000000..ded8570c --- /dev/null +++ b/spec/mock_rails3_gem/README @@ -0,0 +1,244 @@ +== Welcome to Rails + +Rails is a web-application framework that includes everything needed to create +database-backed web applications according to the Model-View-Control pattern. + +This pattern splits the view (also called the presentation) into "dumb" templates +that are primarily responsible for inserting pre-built data in between HTML tags. +The model contains the "smart" domain objects (such as Account, Product, Person, +Post) that holds all the business logic and knows how to persist themselves to +a database. The controller handles the incoming requests (such as Save New Account, +Update Product, Show Post) by manipulating the model and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting Started + +1. At the command prompt, start a new Rails application using the rails command + and your application name. Ex: rails myapp +2. Change directory into myapp and start the web server: rails server (run with --help for options) +3. Go to http://localhost:3000/ and get "Welcome aboard: You're riding the Rails!" +4. Follow the guidelines to start developing your application + + +== Web Servers + +By default, Rails will try to use Mongrel if it's installed when started with rails server, otherwise +Rails will use WEBrick, the webserver that ships with Ruby. But you can also use Rails +with a variety of other web servers. + +Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is +suitable for development and deployment of Rails applications. If you have Ruby Gems installed, +getting up and running with mongrel is as easy as: gem install mongrel. +More info at: http://mongrel.rubyforge.org + +Say other Ruby web servers like Thin and Ebb or regular web servers like Apache or LiteSpeed or +Lighttpd or IIS. The Ruby web servers are run through Rack and the latter can either be setup to use +FCGI or proxy to a pack of Mongrels/Thin/Ebb servers. + +== Apache .htaccess example for FCGI/CGI + +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

    Application error

    Rails application failed to start properly" + + +== Debugging Rails + +Sometimes your application goes wrong. Fortunately there are a lot of tools that +will help you debug it and get it back on the rails. + +First area to check is the application log files. Have "tail -f" commands running +on the server.log and development.log. Rails will automatically display debugging +and runtime information to these files. Debugging info will also be shown in the +browser on requests from 127.0.0.1. + +You can also log your own messages directly into the log file from your code using +the Ruby logger class from inside your controllers. Example: + + class WeblogController < ActionController::Base + def destroy + @weblog = Weblog.find(params[:id]) + @weblog.destroy + logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") + end + end + +The result will be a message in your log file along the lines of: + + Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1 + +More information on how to use the logger is at http://www.ruby-doc.org/core/ + +Also, Ruby documentation can be found at http://www.ruby-lang.org/ including: + +* The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/ +* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) + +These two online (and free) books will bring you up to speed on the Ruby language +and also on programming in general. + + +== Debugger + +Debugger support is available through the debugger command when you start your Mongrel or +Webrick server with --debugger. This means that you can break out of execution at any point +in the code, investigate and change the model, AND then resume execution! +You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug' +Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find(:all) + debugger + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the server window. Here you can do things like: + + >> @posts.inspect + => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, + #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" + >> @posts.first.title = "hello from a debugger" + => "hello from a debugger" + +...and even better is that you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you enter "cont" + + +== Console + +You can interact with the domain model by starting the console through rails console. +Here you'll have all parts of the application configured, just like it is when the +application is running. You can inspect domain models, change values, and save to the +database. Starting the script without arguments will launch it in the development environment. +Passing an argument will specify a different environment, like rails console production. + +To reload your controllers and models after launching the console run reload! + +== dbconsole + +You can go to the command line of your database directly through rails dbconsole. +You would be connected to the database with the credentials defined in database.yml. +Starting the script without arguments will connect you to the development database. Passing an +argument will connect you to a different database, like rails dbconsole production. +Currently works for mysql, postgresql and sqlite. + +== Description of Contents + +app + Holds all the code that's specific to this particular application. + +app/controllers + Holds controllers that should be named like weblogs_controller.rb for + automated URL mapping. All controllers should descend from ApplicationController + which itself descends from ActionController::Base. + +app/models + Holds models that should be named like post.rb. + Most models will descend from ActiveRecord::Base. + +app/views + Holds the template files for the view that should be named like + weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby + syntax. + +app/views/layouts + Holds the template files for layouts to be used with views. This models the common + header/footer method of wrapping views. In your views, define a layout using the + layout :default and create a file named default.html.erb. Inside default.html.erb, + call <% yield %> to render the view using this layout. + +app/helpers + Holds view helpers that should be named like weblogs_helper.rb. These are generated + for you automatically when using rails generate for controllers. Helpers can be used to + wrap functionality for your views into methods. + +config + Configuration files for the Rails environment, the routing map, the database, and other dependencies. + +db + Contains the database schema in schema.rb. db/migrate contains all + the sequence of Migrations for your schema. + +doc + This directory is where your application documentation will be stored when generated + using rake doc:app + +lib + Application specific libraries. Basically, any kind of custom code that doesn't + belong under controllers, models, or helpers. This directory is in the load path. + +public + The directory available for the web server. Contains subdirectories for images, stylesheets, + and javascripts. Also contains the dispatchers and the default HTML files. This should be + set as the DOCUMENT_ROOT of your web server. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. When using the rails generate command, template + test files will be generated for you and placed in this directory. + +vendor + External libraries that the application depends on. Also includes the plugins subdirectory. + If the app has frozen rails, those gems also go here, under vendor/rails/. + This directory is in the load path. diff --git a/spec/mock_rails3_gem/Rakefile b/spec/mock_rails3_gem/Rakefile new file mode 100644 index 00000000..9cb20464 --- /dev/null +++ b/spec/mock_rails3_gem/Rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +Rails::Application.load_tasks diff --git a/spec/mock_rails3_gem/app/controllers/application_controller.rb b/spec/mock_rails3_gem/app/controllers/application_controller.rb new file mode 100644 index 00000000..f2569b3a --- /dev/null +++ b/spec/mock_rails3_gem/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::Base + protect_from_forgery + layout 'application' +end diff --git a/spec/mock_rails3_gem/app/helpers/application_helper.rb b/spec/mock_rails3_gem/app/helpers/application_helper.rb new file mode 100644 index 00000000..de6be794 --- /dev/null +++ b/spec/mock_rails3_gem/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/spec/mock_rails3_gem/app/views/layouts/application.html.erb b/spec/mock_rails3_gem/app/views/layouts/application.html.erb new file mode 100644 index 00000000..c59dd1df --- /dev/null +++ b/spec/mock_rails3_gem/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ + + + + MockRails3Gem + <%= stylesheet_link_tag :all %> + <%= javascript_include_tag :defaults %> + <%= csrf_meta_tag %> + + + +<%= yield %> + + + diff --git a/spec/mock_rails3_gem/config.ru b/spec/mock_rails3_gem/config.ru new file mode 100644 index 00000000..c0935f66 --- /dev/null +++ b/spec/mock_rails3_gem/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run MockRails3Gem::Application diff --git a/spec/mock_rails3_gem/config/application.rb b/spec/mock_rails3_gem/config/application.rb new file mode 100644 index 00000000..888af406 --- /dev/null +++ b/spec/mock_rails3_gem/config/application.rb @@ -0,0 +1,46 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +# If you have a Gemfile, require the gems listed there, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(:default, Rails.env) if defined?(Bundler) + +module MockRails3Gem + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{config.root}/extras ) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure generators values. Many other options are available, be sure to check the documentation. + # config.generators do |g| + # g.orm :active_record + # g.template_engine :erb + # g.test_framework :test_unit, :fixture => true + # end + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + end +end diff --git a/spec/mock_rails3_gem/config/boot.rb b/spec/mock_rails3_gem/config/boot.rb new file mode 100644 index 00000000..712b0981 --- /dev/null +++ b/spec/mock_rails3_gem/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' +# Set up gems listed in the Gemfile. +if File.exist?(File.expand_path('../../Gemfile', __FILE__)) + require 'bundler' + Bundler.setup +end diff --git a/spec/mock_rails3_gem/config/database.yml b/spec/mock_rails3_gem/config/database.yml new file mode 100644 index 00000000..025d62a8 --- /dev/null +++ b/spec/mock_rails3_gem/config/database.yml @@ -0,0 +1,22 @@ +# SQLite version 3.x +# gem install sqlite3-ruby (not necessary on OS X Leopard) +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + adapter: sqlite3 + database: db/test.sqlite3 + pool: 5 + timeout: 5000 + +production: + adapter: sqlite3 + database: db/production.sqlite3 + pool: 5 + timeout: 5000 diff --git a/spec/mock_rails3_gem/config/environment.rb b/spec/mock_rails3_gem/config/environment.rb new file mode 100644 index 00000000..57974d58 --- /dev/null +++ b/spec/mock_rails3_gem/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +MockRails3Gem::Application.initialize! diff --git a/spec/mock_rails3_gem/config/environments/development.rb b/spec/mock_rails3_gem/config/environments/development.rb new file mode 100644 index 00000000..3e4a3f05 --- /dev/null +++ b/spec/mock_rails3_gem/config/environments/development.rb @@ -0,0 +1,19 @@ +MockRails3Gem::Application.configure do + # Settings specified here will take precedence over those in config/environment.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the webserver when you make code changes. + config.cache_classes = false + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_view.debug_rjs = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + config.action_mailer.raise_delivery_errors = false +end diff --git a/spec/mock_rails3_gem/config/environments/production.rb b/spec/mock_rails3_gem/config/environments/production.rb new file mode 100644 index 00000000..8cf5c260 --- /dev/null +++ b/spec/mock_rails3_gem/config/environments/production.rb @@ -0,0 +1,42 @@ +MockRails3Gem::Application.configure do + # Settings specified here will take precedence over those in config/environment.rb + + # The production environment is meant for finished, "live" apps. + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Specifies the header that your server uses for sending files + config.action_dispatch.x_sendfile_header = "X-Sendfile" + + # For nginx: + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' + + # If you have no front-end server that supports something like X-Sendfile, + # just comment this out and Rails will serve the files + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Use a different logger for distributed setups + # config.logger = SyslogLogger.new + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Disable Rails's static asset server + # In production, Apache or nginx will already do this + config.serve_static_assets = false + + # Enable serving of images, stylesheets, and javascripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! +end diff --git a/spec/mock_rails3_gem/config/environments/test.rb b/spec/mock_rails3_gem/config/environments/test.rb new file mode 100644 index 00000000..496a726c --- /dev/null +++ b/spec/mock_rails3_gem/config/environments/test.rb @@ -0,0 +1,32 @@ +MockRails3Gem::Application.configure do + # Settings specified here will take precedence over those in config/environment.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql +end diff --git a/spec/mock_rails3_gem/config/initializers/backtrace_silencers.rb b/spec/mock_rails3_gem/config/initializers/backtrace_silencers.rb new file mode 100644 index 00000000..59385cdf --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/spec/mock_rails3_gem/config/initializers/inflections.rb b/spec/mock_rails3_gem/config/initializers/inflections.rb new file mode 100644 index 00000000..d531b8bb --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/inflections.rb @@ -0,0 +1,10 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format +# (all these examples are active by default): +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end diff --git a/spec/mock_rails3_gem/config/initializers/mime_types.rb b/spec/mock_rails3_gem/config/initializers/mime_types.rb new file mode 100644 index 00000000..72aca7e4 --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register_alias "text/html", :iphone diff --git a/spec/mock_rails3_gem/config/initializers/secret_token.rb b/spec/mock_rails3_gem/config/initializers/secret_token.rb new file mode 100644 index 00000000..dea00273 --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/secret_token.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +Rails.application.config.secret_token = '0be394414161860f4e3f68b42b4b19ddbbbd69261bd930bd67912097061264cc478c77b4b77d0e8f1c4e0f094c7783c037bef2882cd5960a02b6c158c3637554' diff --git a/spec/mock_rails3_gem/config/initializers/session_store.rb b/spec/mock_rails3_gem/config/initializers/session_store.rb new file mode 100644 index 00000000..cd4a5bbb --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/session_store.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, :key => '_mock_rails3_gem_session' + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rake db:sessions:create") +# Rails.application.config.session_store :active_record_store diff --git a/spec/mock_rails3_gem/config/locales/en.yml b/spec/mock_rails3_gem/config/locales/en.yml new file mode 100644 index 00000000..a747bfa6 --- /dev/null +++ b/spec/mock_rails3_gem/config/locales/en.yml @@ -0,0 +1,5 @@ +# Sample localization file for English. Add more files in this directory for other locales. +# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en: + hello: "Hello world" diff --git a/spec/mock_rails3_gem/config/routes.rb b/spec/mock_rails3_gem/config/routes.rb new file mode 100644 index 00000000..4bb199ad --- /dev/null +++ b/spec/mock_rails3_gem/config/routes.rb @@ -0,0 +1,58 @@ +MockRails3Gem::Application.routes.draw do |map| + # The priority is based upon order of creation: + # first created -> highest priority. + + # Sample of regular route: + # match 'products/:id' => 'catalog#view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase + # This route can be invoked with purchase_url(:id => product.id) + + # Sample resource route (maps HTTP verbs to controller actions automatically): + # resources :products + + # Sample resource route with options: + # resources :products do + # member do + # get :short + # post :toggle + # end + # + # collection do + # get :sold + # end + # end + + # Sample resource route with sub-resources: + # resources :products do + # resources :comments, :sales + # resource :seller + # end + + # Sample resource route with more complex sub-resources + # resources :products do + # resources :comments + # resources :sales do + # get :recent, :on => :collection + # end + # end + + # Sample resource route within a namespace: + # namespace :admin do + # # Directs /admin/products/* to Admin::ProductsController + # # (app/controllers/admin/products_controller.rb) + # resources :products + # end + + # You can have the root of your site routed with "root" + # just remember to delete public/index.html. + # root :to => "welcome#index" + + # See how all your routes lay out with "rake routes" + + # This is a legacy wild controller route that's not recommended for RESTful applications. + # Note: This route will make all actions in every controller accessible via GET requests. + # match ':controller(/:action(/:id(.:format)))' +end diff --git a/spec/mock_rails3_gem/db/seeds.rb b/spec/mock_rails3_gem/db/seeds.rb new file mode 100644 index 00000000..664d8c74 --- /dev/null +++ b/spec/mock_rails3_gem/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# +# Examples: +# +# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) +# Mayor.create(:name => 'Daley', :city => cities.first) diff --git a/spec/mock_rails3_gem/doc/README_FOR_APP b/spec/mock_rails3_gem/doc/README_FOR_APP new file mode 100644 index 00000000..fe41f5cc --- /dev/null +++ b/spec/mock_rails3_gem/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/spec/mock_rails3_gem/lib/tasks/.gitkeep b/spec/mock_rails3_gem/lib/tasks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/spec/mock_rails3_gem/public/404.html b/spec/mock_rails3_gem/public/404.html new file mode 100644 index 00000000..9a48320a --- /dev/null +++ b/spec/mock_rails3_gem/public/404.html @@ -0,0 +1,26 @@ + + + + The page you were looking for doesn't exist (404) + + + + + +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    + + diff --git a/spec/mock_rails3_gem/public/422.html b/spec/mock_rails3_gem/public/422.html new file mode 100644 index 00000000..83660ab1 --- /dev/null +++ b/spec/mock_rails3_gem/public/422.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (422) + + + + + +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    + + diff --git a/spec/mock_rails3_gem/public/500.html b/spec/mock_rails3_gem/public/500.html new file mode 100644 index 00000000..b80307fc --- /dev/null +++ b/spec/mock_rails3_gem/public/500.html @@ -0,0 +1,26 @@ + + + + We're sorry, but something went wrong (500) + + + + + +
    +

    We're sorry, but something went wrong.

    +

    We've been notified about this issue and we'll take a look at it shortly.

    +
    + + diff --git a/spec/mock_rails3_gem/public/favicon.ico b/spec/mock_rails3_gem/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/spec/mock_rails3_gem/public/images/rails.png b/spec/mock_rails3_gem/public/images/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..d5edc04e65f555e3ba4dcdaad39dc352e75b575e GIT binary patch literal 6646 zcmVpVcQya!6@Dsmj@#jv7C*qh zIhOJ6_K0n?*d`*T7TDuW-}m`9Kz3~>+7`DUkbAraU%yi+R{N~~XA2B%zt-4=tLimUer9!2M~N{G5bftFij_O&)a zsHnOppFIzebQ`RA0$!yUM-lg#*o@_O2wf422iLnM6cU(ktYU8#;*G!QGhIy9+ZfzKjLuZo%@a z-i@9A`X%J{^;2q&ZHY3C(B%gqCPW!8{9C0PMcNZccefK){s|V5-xxtHQc@uf>XqhD z7#N^siWqetgq29aX>G^olMf=bbRF6@Y(}zYxw6o!9WBdG1unP}<(V;zKlcR2p86fq zYjaqB^;Ycq>Wy@5T1xOzG3tucG3e%nPvajaN{CrFbnzv^9&K3$NrDm*eQe4`BGQ2bI;dFEwyt>hK%X!L6)82aOZp zsrGcJ#7PoX7)s|~t6is?FfX*7vWdREi58tiY4S)t6u*|kv?J)d_$r+CH#eZ?Ef+I_ z(eVlX8dh~4QP?o*E`_MgaNFIKj*rtN(0Raj3ECjSXcWfd#27NYs&~?t`QZFT}!Zaf=ldZIhi}LhQlqLo+o5(Pvui&{7PD__^53f9j>HW`Q z_V8X5j~$|GP9qXu0C#!@RX2}lXD35@3N5{BkUi%jtaPQ*H6OX2zIz4QPuqmTv3`vG{zc>l3t0B9E75h< z8&twGh%dp7WPNI+tRl%#gf2}Epg8st+~O4GjtwJsXfN;EjAmyr6z5dnaFU(;IV~QK zW62fogF~zA``(Q>_SmD!izc6Y4zq*97|NAPHp1j5X7Op2%;GLYm>^HEMyObo6s7l) zE3n|aOHi5~B84!}b^b*-aL2E)>OEJX_tJ~t<#VJ?bT?lDwyDB&5SZ$_1aUhmAY}#* zs@V1I+c5md9%R-o#_DUfqVtRk>59{+Opd5Yu%dAU#VQW}^m}x-30ftBx#527{^pI4 z6l2C6C7QBG$~NLYb3rVdLD#Z{+SleOp`(Lg5J}`kxdTHe(nV5BdpLrD=l|)e$gEqA zwI6vuX-PFCtcDIH>bGY2dwq&^tf+&R?)nY-@7_j%4CMRAF}C9w%p86W<2!aSY$p+k zrkFtG=cGo38RnrG28;?PNk%7a@faaXq&MS*&?1Z`7Ojw7(#>}ZG4nMAs3VXxfdW>i zY4VX02c5;f7jDPY_7@Oa)CHH}cH<3y#}_!nng^W+h1e-RL*YFYOteC@h?BtJZ+?sE zy)P5^8Mregx{nQaw1NY-|3>{Z)|0`?zc?G2-acYiSU`tj#sSGfm7k86ZQ0SQgPevcklHxM9<~4yW zR796sisf1|!#{Z=e^)0;_8iUhL8g(;j$l=02FTPZ(dZV@s#aQ`DHkLM6=YsbE4iQ!b#*374l0Jw5;jD%J;vQayq=nD8-kHI~f9Ux|32SJUM`> zGp2UGK*4t?cRKi!2he`zI#j0f${I#f-jeT?u_C7S4WsA0)ryi-1L0(@%pa^&g5x=e z=KW9+Nn(=)1T&S8g_ug%dgk*~l2O-$r9#zEGBdQsweO%t*6F4c8JC36JtTizCyy+E4h%G(+ z5>y$%0txMuQ$e~wjFgN(xrAndHQo`Za+K*?gUVDTBV&Ap^}|{w#CIq{DRe}+l@(Ec zCCV6f_?dY_{+f{}6XGn!pL_up?}@>KijT^$w#Lb6iHW&^8RP~g6y=vZBXx~B9nI^i zGexaPjcd(%)zGw!DG_dDwh-7x6+ST#R^${iz_M$uM!da8SxgB_;Z0G%Y*HpvLjKw; zX=ir7i1O$-T|*TBoH$dlW+TLf5j5sep^DlDtkox;Kg{Q%EXWedJq@J@%VAcK)j3y1 zShM!CS#qax;D@RND%2t3W6kv+#Ky0F9<3YKDbV^XJ=^$s(Vtza8V72YY)577nnldI zHMA0PUo!F3j(ubV*CM@PiK<^|RM2(DuCbG7`W}Rg(xdYC>C~ z;1KJGLN&$cRxSZunjXcntykmpFJ7;dk>shY(DdK&3K_JDJ6R%D`e~6Qv67@Rwu+q9 z*|NG{r}4F8f{Dfzt0+cZMd$fvlX3Q`dzM46@r?ISxr;9gBTG2rmfiGOD*#c*3f)cc zF+PFZobY$-^}J8 z%n=h4;x2}cP!@SiVd!v;^Wwo0(N??-ygDr7gG^NKxDjSo{5T{?$|Qo5;8V!~D6O;F*I zuY!gd@+2j_8Rn=UWDa#*4E2auWoGYDddMW7t0=yuC(xLWky?vLimM~!$3fgu!dR>p z?L?!8z>6v$|MsLb&dU?ob)Zd!B)!a*Z2eTE7 zKCzP&e}XO>CT%=o(v+WUY`Az*`9inbTG& z_9_*oQKw;sc8{ipoBC`S4Tb7a%tUE)1fE+~ib$;|(`|4QbXc2>VzFi%1nX%ti;^s3~NIL0R}!!a{0A zyCRp0F7Y&vcP&3`&Dzv5!&#h}F2R-h&QhIfq*ts&qO13{_CP}1*sLz!hI9VoTSzTu zok5pV0+~jrGymE~{TgbS#nN5+*rF7ij)cnSLQw0Ltc70zmk|O!O(kM<3zw-sUvkx~ z2`y+{xAwKSa-0}n7{$I@Zop7CWy%_xIeN1e-7&OjQ6vZZPbZ^3_ z(~=;ZSP98S2oB#35b1~_x`2gWiPdIVddEf`AD9<@c_s)TM;3J$T_l?pr{<7PTgdiy zBc5IGx)g~n=s+Z$RzYCmv8PlJu%gkh^;%mTGMc)UwRINVD~K;`Rl!5@hhGg;y>5qj zq|u-Yf0q_~Y+Mbivkkfa0nAOzB1acnytogsj_m7FB(-FjihMek#GAU4M!iXCgdK8a zjoKm?*|iz7;dHm4$^hh(`Ufl>yb>$hjIA-;>{>C}G0Di%bGvUsJkfLAV|xq32c>RqJqTBJ3Dx zYC;*Dt|S$b6)aCJFnK(Eey$M1DpVV~_MIhwK> zygo(jWC|_IRw|456`roEyXtkNLWNAt-4N1qyN$I@DvBzt;e|?g<*HK1%~cq|^u*}C zmMrwh>{QAq?Ar~4l^DqT%SQ)w)FA(#7#u+N;>E975rYML>)LgE`2<7nN=C1pC{IkV zVw}_&v6j&S?QVh*)wF3#XmE@0($^BVl1969csLKUBNer{suVd!a~B!0MxWY?=(GD6 zy$G&ERFR#i6G4=2F?R4}Mz3B?3tnpoX3)qFF2sh9-Jn*e%9F>i{WG7$_~XyOO2!+@ z6k+38KyD@-0=uee54D0!Z1@B^ilj~StchdOn(*qvg~s5QJpWGc!6U^Aj!xt-HZn_V zS%|fyQ5YS@EP2lBIodXCLjG_+a)%En+7jzngk@J>6D~^xbxKkvf-R0-c%mX+o{?&j zZZ%RxFeav8Y0gkwtdtrwUb-i0Egd2C=ADu%w5VV-hNJvl)GZ?M;y$!?b=S+wKRK7Q zcOjPT!p<*#8m;TsBih=@Xc&c)?Vy`Ys>IvK@|1%N+M6J-^RCRaZcPP2eQh9DEGZr+ z?8B~wF14mk4Xkuen{wY^CWwS1PI<8gikY*)3?RSo5l8es4*J z43k_BIwc}of=6Pfs%xIxlMDGOJN zvl!a>G)52XMqA%fbgkZi%)%bN*ZzZw2!rn4@+J)2eK#kWuEW{)W~-`y1vhA5-7p%R z&f5N!a9f8cK1Xa=O}=9{wg%}Ur^+8Y(!UCeqw>%wj@|bYHD-bZO~mk3L$9_^MmF3G zvCiK^e@q6G?tHkM8%GqsBMZaB20W$UEt_5r~jc#WlR>Bv{6W>A=!#InoY zLOd04@Rz?*7PpW8u|+}bt`?+Z(GsX{Br4A2$ZZ(26Degmr9`O=t2KgHTL*==R3xcP z&Y(J7hC@6_x8zVz!CX3l4Xtss6i7r#E6kXMNN1~>9KTRzewfp))ij%)SBBl0fZdYP zd!zzQD5u8yk-u|41|Rqz7_tCFUMThZJVj)yQf6^Cwtn|Ew6cm5J|u1Bq>MWX-AfB&NE;C z62@=-0le`E6-CurMKjoIy)BuUmhMGJb}pPx!@GLWMT+wH2R?wA=MEy)o57~feFp8P zY@YXAyt4<1FD<|iw{FGQu~GEI<4C64)V*QiVk+VzOV^9GWf4ir#oYgHJz!wq>iZV#_6@_{)&lum)4x z_Of*CLVQ7wdT#XT-(h0qH%mcIF7yzMIvvTN3bPceK>PpJi(=3Nny zbSn}p$dGKQUlX&-t~RR)#F7I<8NCD^yke(vdf#4^aAh}M-{tS9-&^tC4`KU_pToXy z+|K8sx}a)Kh{h{;*V1#hs1xB%(?j>)g~`Wv(9F)f=Qn)(daVB7hZtcp^#LrEr1T1J zZSJ*lVyVVjhy)mkex9Whn=EinKDHe@KlfQI-Fl7M?-c~HnW0;C;+MbUY8?FToy;A+ zs&Nc7VZ=Of+e!G6s#+S5WBU)kgQq_I1@!uH74GJ-+O|%0HXm9Mqlvp|j%0`T>fr9^ zK;qo>XdwZW<>%tTA+<(1^6(>=-2N;hRgBnjvEjN;VbKMbFg--WrGy|XESoH1p|M4` z86(gC^vB4qScASZ&cdpT{~QDN-jC|GJ(RYoW1VW4!SSn- zhQds9&RBKn6M&GVK_Aayt(Hekbnw=tr>f z^o@v9_*iQO1*zeOrts9Q-$pc@!StS&kz$cF`s@pM`rmJXTP&h5G)A74!0e%ZJbl}( zssI|_!%~_hZFypv*S^JE5N&Kvmx7KiG<|fGMO=WrH+@Yhuj+KwiS#l4>@%2nl zS)mDikfmokO4q2A)hRVZBq2-5q&XC>%HOLkOYxZ66(s86?=0s4z5xbiOV)}L-&6b)h6(~CIaR#JNw~46+WBiU7IhB zq!NuR4!TsYnyBg>@G=Ib*cMq^k<}AMpCeYEf&dzfiGI-wOQ7hb+nA zkN7_){y&c3xC0 AQ~&?~ literal 0 HcmV?d00001 diff --git a/spec/mock_rails3_gem/public/javascripts/application.js b/spec/mock_rails3_gem/public/javascripts/application.js new file mode 100644 index 00000000..fe457769 --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/application.js @@ -0,0 +1,2 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults diff --git a/spec/mock_rails3_gem/public/javascripts/controls.js b/spec/mock_rails3_gem/public/javascripts/controls.js new file mode 100644 index 00000000..7392fb66 --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/controls.js @@ -0,0 +1,965 @@ +// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 + +// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { }; +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element); + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index--; + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++; + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); + return "
      " + ret.join('') + "
    "; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +}; + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML.unescapeHTML(); + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw('Server returned an invalid collection representation.'); + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}); \ No newline at end of file diff --git a/spec/mock_rails3_gem/public/javascripts/dragdrop.js b/spec/mock_rails3_gem/public/javascripts/dragdrop.js new file mode 100644 index 00000000..15c6dbca --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/dragdrop.js @@ -0,0 +1,974 @@ +// script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 + +// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +}; + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +}; + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = this.element.cumulativeOffset(); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this._originallyAbsolute) + Position.relativize(this.element); + delete this._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = this.element.cumulativeOffset(); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)); + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight; + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + element = $(element); + var s = Sortable.sortables[element.id]; + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + }; + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + }; + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.identify()] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = dropon.cumulativeOffset(); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + }; + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child); + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + }; + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +}; + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +}; + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +}; + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +}; \ No newline at end of file diff --git a/spec/mock_rails3_gem/public/javascripts/effects.js b/spec/mock_rails3_gem/public/javascripts/effects.js new file mode 100644 index 00000000..066ee590 --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/effects.js @@ -0,0 +1,1123 @@ +// script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 + +// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + .5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; + }, + pulse: function(pos, pulses) { + return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect, options) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + + return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, options || {})); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()); } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }); + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}); }}); }}); }}); }}); }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ); + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }, + oldOpacity = element.getInlineOpacity(), + transition = options.transition || Effect.Transitions.linear, + reverser = function(pos){ + return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); + }; + + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + }; + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ); + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ); + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '
    '; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { + results[property] = css[property]; + return results; + }); + if (!styles.opacity) styles.opacity = element.getOpacity(); + return styles; + }; +} + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element); + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + }; + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/spec/mock_rails3_gem/public/javascripts/prototype.js b/spec/mock_rails3_gem/public/javascripts/prototype.js new file mode 100644 index 00000000..9fe6e124 --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/prototype.js @@ -0,0 +1,4874 @@ +/* Prototype JavaScript framework, version 1.6.1 + * (c) 2005-2009 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.1', + + Browser: (function(){ + var ua = navigator.userAgent; + var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; + return { + IE: !!window.attachEvent && !isOpera, + Opera: isOpera, + WebKit: ua.indexOf('AppleWebKit/') > -1, + Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, + MobileSafari: /Apple.*Mobile.*Safari/.test(ua) + } + })(), + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: (function() { + var constructor = window.Element || window.HTMLElement; + return !!(constructor && constructor.prototype); + })(), + SpecificElementExtensions: (function() { + if (typeof window.HTMLDivElement !== 'undefined') + return true; + + var div = document.createElement('div'); + var form = document.createElement('form'); + var isSupported = false; + + if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { + isSupported = true; + } + + div = form = null; + + return isSupported; + })() + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +var Abstract = { }; + + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +/* Based on Alex Arnell's inheritance implementation. */ + +var Class = (function() { + function subclass() {}; + function create() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + return klass; + } + + function addMethods(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) { + if (source.toString != Object.prototype.toString) + properties.push("toString"); + if (source.valueOf != Object.prototype.valueOf) + properties.push("valueOf"); + } + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments); }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } + + return { + create: create, + Methods: { + addMethods: addMethods + } + }; +})(); +(function() { + + var _toString = Object.prototype.toString; + + function extend(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; + } + + function inspect(object) { + try { + if (isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + } + + function toJSON(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (isElement(object)) return; + + var results = []; + for (var property in object) { + var value = toJSON(object[property]); + if (!isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + } + + function toQueryString(object) { + return $H(object).toQueryString(); + } + + function toHTML(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + } + + function keys(object) { + var results = []; + for (var property in object) + results.push(property); + return results; + } + + function values(object) { + var results = []; + for (var property in object) + results.push(object[property]); + return results; + } + + function clone(object) { + return extend({ }, object); + } + + function isElement(object) { + return !!(object && object.nodeType == 1); + } + + function isArray(object) { + return _toString.call(object) == "[object Array]"; + } + + + function isHash(object) { + return object instanceof Hash; + } + + function isFunction(object) { + return typeof object === "function"; + } + + function isString(object) { + return _toString.call(object) == "[object String]"; + } + + function isNumber(object) { + return _toString.call(object) == "[object Number]"; + } + + function isUndefined(object) { + return typeof object === "undefined"; + } + + extend(Object, { + extend: extend, + inspect: inspect, + toJSON: toJSON, + toQueryString: toQueryString, + toHTML: toHTML, + keys: keys, + values: values, + clone: clone, + isElement: isElement, + isArray: isArray, + isHash: isHash, + isFunction: isFunction, + isString: isString, + isNumber: isNumber, + isUndefined: isUndefined + }); +})(); +Object.extend(Function.prototype, (function() { + var slice = Array.prototype.slice; + + function update(array, args) { + var arrayLength = array.length, length = args.length; + while (length--) array[arrayLength + length] = args[length]; + return array; + } + + function merge(array, args) { + array = slice.call(array, 0); + return update(array, args); + } + + function argumentNames() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] + .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + } + + function bind(context) { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = slice.call(arguments, 1); + return function() { + var a = merge(args, arguments); + return __method.apply(context, a); + } + } + + function bindAsEventListener(context) { + var __method = this, args = slice.call(arguments, 1); + return function(event) { + var a = update([event || window.event], args); + return __method.apply(context, a); + } + } + + function curry() { + if (!arguments.length) return this; + var __method = this, args = slice.call(arguments, 0); + return function() { + var a = merge(args, arguments); + return __method.apply(this, a); + } + } + + function delay(timeout) { + var __method = this, args = slice.call(arguments, 1); + timeout = timeout * 1000 + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + } + + function defer() { + var args = update([0.01], arguments); + return this.delay.apply(this, args); + } + + function wrap(wrapper) { + var __method = this; + return function() { + var a = update([__method.bind(this)], arguments); + return wrapper.apply(this, a); + } + } + + function methodize() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + var a = update([this], arguments); + return __method.apply(null, a); + }; + } + + return { + argumentNames: argumentNames, + bind: bind, + bindAsEventListener: bindAsEventListener, + curry: curry, + delay: delay, + defer: defer, + wrap: wrap, + methodize: methodize + } +})()); + + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + this.currentlyExecuting = false; + } catch(e) { + this.currentlyExecuting = false; + throw e; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, (function() { + + function prepareReplacement(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; + } + + function gsub(pattern, replacement) { + var result = '', source = this, match; + replacement = prepareReplacement(replacement); + + if (Object.isString(pattern)) + pattern = RegExp.escape(pattern); + + if (!(pattern.length || pattern.source)) { + replacement = replacement(''); + return replacement + source.split('').join(replacement) + replacement; + } + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + } + + function sub(pattern, replacement, count) { + replacement = prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + } + + function scan(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + } + + function truncate(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + } + + function strip() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + } + + function stripTags() { + return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, ''); + } + + function stripScripts() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + } + + function extractScripts() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + } + + function evalScripts() { + return this.extractScripts().map(function(script) { return eval(script) }); + } + + function escapeHTML() { + return this.replace(/&/g,'&').replace(//g,'>'); + } + + function unescapeHTML() { + return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); + } + + + function toQueryParams(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + } + + function toArray() { + return this.split(''); + } + + function succ() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + } + + function times(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + } + + function camelize() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + } + + function capitalize() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + } + + function underscore() { + return this.replace(/::/g, '/') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') + .replace(/([a-z\d])([A-Z])/g, '$1_$2') + .replace(/-/g, '_') + .toLowerCase(); + } + + function dasherize() { + return this.replace(/_/g, '-'); + } + + function inspect(useDoubleQuotes) { + var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) { + if (character in String.specialChar) { + return String.specialChar[character]; + } + return '\\u00' + character.charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + } + + function toJSON() { + return this.inspect(true); + } + + function unfilterJSON(filter) { + return this.replace(filter || Prototype.JSONFilter, '$1'); + } + + function isJSON() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + } + + function evalJSON(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + } + + function include(pattern) { + return this.indexOf(pattern) > -1; + } + + function startsWith(pattern) { + return this.indexOf(pattern) === 0; + } + + function endsWith(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + } + + function empty() { + return this == ''; + } + + function blank() { + return /^\s*$/.test(this); + } + + function interpolate(object, pattern) { + return new Template(this, pattern).evaluate(object); + } + + return { + gsub: gsub, + sub: sub, + scan: scan, + truncate: truncate, + strip: String.prototype.trim ? String.prototype.trim : strip, + stripTags: stripTags, + stripScripts: stripScripts, + extractScripts: extractScripts, + evalScripts: evalScripts, + escapeHTML: escapeHTML, + unescapeHTML: unescapeHTML, + toQueryParams: toQueryParams, + parseQuery: toQueryParams, + toArray: toArray, + succ: succ, + times: times, + camelize: camelize, + capitalize: capitalize, + underscore: underscore, + dasherize: dasherize, + inspect: inspect, + toJSON: toJSON, + unfilterJSON: unfilterJSON, + isJSON: isJSON, + evalJSON: evalJSON, + include: include, + startsWith: startsWith, + endsWith: endsWith, + empty: empty, + blank: blank, + interpolate: interpolate + }; +})()); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (object && Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return (match[1] + ''); + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = (function() { + function each(iterator, context) { + var index = 0; + try { + this._each(function(value) { + iterator.call(context, value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + } + + function eachSlice(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + } + + function all(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index); + if (!result) throw $break; + }); + return result; + } + + function any(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index)) + throw $break; + }); + return result; + } + + function collect(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index)); + }); + return results; + } + + function detect(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index)) { + result = value; + throw $break; + } + }); + return result; + } + + function findAll(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index)) + results.push(value); + }); + return results; + } + + function grep(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(RegExp.escape(filter)); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); + return results; + } + + function include(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + } + + function inGroupsOf(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + } + + function inject(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index); + }); + return memo; + } + + function invoke(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + } + + function max(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value >= result) + result = value; + }); + return result; + } + + function min(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value < result) + result = value; + }); + return result; + } + + function partition(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + } + + function pluck(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + } + + function reject(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index)) + results.push(value); + }); + return results; + } + + function sortBy(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + } + + function toArray() { + return this.map(); + } + + function zip() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + } + + function size() { + return this.toArray().length; + } + + function inspect() { + return '#'; + } + + + + + + + + + + return { + each: each, + eachSlice: eachSlice, + all: all, + every: all, + any: any, + some: any, + collect: collect, + map: collect, + detect: detect, + findAll: findAll, + select: findAll, + filter: findAll, + grep: grep, + include: include, + member: include, + inGroupsOf: inGroupsOf, + inject: inject, + invoke: invoke, + max: max, + min: min, + partition: partition, + pluck: pluck, + reject: reject, + sortBy: sortBy, + toArray: toArray, + entries: toArray, + zip: zip, + size: size, + inspect: inspect, + find: detect + }; +})(); +function $A(iterable) { + if (!iterable) return []; + if ('toArray' in Object(iterable)) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +Array.from = $A; + + +(function() { + var arrayProto = Array.prototype, + slice = arrayProto.slice, + _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available + + function each(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + } + if (!_each) _each = each; + + function clear() { + this.length = 0; + return this; + } + + function first() { + return this[0]; + } + + function last() { + return this[this.length - 1]; + } + + function compact() { + return this.select(function(value) { + return value != null; + }); + } + + function flatten() { + return this.inject([], function(array, value) { + if (Object.isArray(value)) + return array.concat(value.flatten()); + array.push(value); + return array; + }); + } + + function without() { + var values = slice.call(arguments, 0); + return this.select(function(value) { + return !values.include(value); + }); + } + + function reverse(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + } + + function uniq(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + } + + function intersect(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + } + + + function clone() { + return slice.call(this, 0); + } + + function size() { + return this.length; + } + + function inspect() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } + + function toJSON() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } + + function indexOf(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; + } + + function lastIndexOf(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; + } + + function concat() { + var array = slice.call(this, 0), item; + for (var i = 0, length = arguments.length; i < length; i++) { + item = arguments[i]; + if (Object.isArray(item) && !('callee' in item)) { + for (var j = 0, arrayLength = item.length; j < arrayLength; j++) + array.push(item[j]); + } else { + array.push(item); + } + } + return array; + } + + Object.extend(arrayProto, Enumerable); + + if (!arrayProto._reverse) + arrayProto._reverse = arrayProto.reverse; + + Object.extend(arrayProto, { + _each: _each, + clear: clear, + first: first, + last: last, + compact: compact, + flatten: flatten, + without: without, + reverse: reverse, + uniq: uniq, + intersect: intersect, + clone: clone, + toArray: clone, + size: size, + inspect: inspect, + toJSON: toJSON + }); + + var CONCAT_ARGUMENTS_BUGGY = (function() { + return [].concat(arguments)[0][0] !== 1; + })(1,2) + + if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; + + if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; + if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; +})(); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + function initialize(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + } + + function _each(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + + function set(key, value) { + return this._object[key] = value; + } + + function get(key) { + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + } + + function unset(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + } + + function toObject() { + return Object.clone(this._object); + } + + function keys() { + return this.pluck('key'); + } + + function values() { + return this.pluck('value'); + } + + function index(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + } + + function merge(object) { + return this.clone().update(object); + } + + function update(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + } + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + function toQueryString() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + } + + function inspect() { + return '#'; + } + + function toJSON() { + return Object.toJSON(this.toObject()); + } + + function clone() { + return new Hash(this); + } + + return { + initialize: initialize, + _each: _each, + set: set, + get: get, + unset: unset, + toObject: toObject, + toTemplateReplacements: toObject, + keys: keys, + values: values, + index: index, + merge: merge, + update: update, + toQueryString: toQueryString, + inspect: inspect, + toJSON: toJSON, + clone: clone + }; +})()); + +Hash.from = $H; +Object.extend(Number.prototype, (function() { + function toColorPart() { + return this.toPaddedString(2, 16); + } + + function succ() { + return this + 1; + } + + function times(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + } + + function toPaddedString(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + } + + function toJSON() { + return isFinite(this) ? this.toString() : 'null'; + } + + function abs() { + return Math.abs(this); + } + + function round() { + return Math.round(this); + } + + function ceil() { + return Math.ceil(this); + } + + function floor() { + return Math.floor(this); + } + + return { + toColorPart: toColorPart, + succ: succ, + times: times, + toPaddedString: toPaddedString, + toJSON: toJSON, + abs: abs, + round: round, + ceil: ceil, + floor: floor + }; +})()); + +function $R(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var ObjectRange = Class.create(Enumerable, (function() { + function initialize(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + } + + function _each(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + } + + function include(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } + + return { + initialize: initialize, + _each: _each, + include: include + }; +})()); + + + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null; } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + + + + + + + + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); + + + +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + + +(function(global) { + + var SETATTRIBUTE_IGNORES_NAME = (function(){ + var elForm = document.createElement("form"); + var elInput = document.createElement("input"); + var root = document.documentElement; + elInput.setAttribute("name", "test"); + elForm.appendChild(elInput); + root.appendChild(elForm); + var isBuggy = elForm.elements + ? (typeof elForm.elements.test == "undefined") + : null; + root.removeChild(elForm); + elForm = elInput = null; + return isBuggy; + })(); + + var element = global.Element; + global.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (SETATTRIBUTE_IGNORES_NAME && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(global.Element, element || { }); + if (element) global.Element.prototype = element.prototype; +})(this); + +Element.cache = { }; +Element.idCounter = 1; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; + }, + + show: function(element) { + element = $(element); + element.style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: (function(){ + + var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){ + var el = document.createElement("select"), + isBuggy = true; + el.innerHTML = ""; + if (el.options && el.options[0]) { + isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION"; + } + el = null; + return isBuggy; + })(); + + var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){ + try { + var el = document.createElement("table"); + if (el && el.tBodies) { + el.innerHTML = "test"; + var isBuggy = typeof el.tBodies[0] == "undefined"; + el = null; + return isBuggy; + } + } catch (e) { + return true; + } + })(); + + var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { + var s = document.createElement("script"), + isBuggy = false; + try { + s.appendChild(document.createTextNode("")); + isBuggy = !s.firstChild || + s.firstChild && s.firstChild.nodeType !== 3; + } catch (e) { + isBuggy = true; + } + s = null; + return isBuggy; + })(); + + function update(element, content) { + element = $(element); + + if (content && content.toElement) + content = content.toElement(); + + if (Object.isElement(content)) + return element.update().insert(content); + + content = Object.toHTML(content); + + var tagName = element.tagName.toUpperCase(); + + if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) { + element.text = content; + return element; + } + + if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { + if (tagName in Element._insertionTranslations.tags) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { + element.appendChild(node) + }); + } + else { + element.innerHTML = content.stripScripts(); + } + } + else { + element.innerHTML = content.stripScripts(); + } + + content.evalScripts.bind(content).defer(); + return element; + } + + return update; + })(), + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return Element.recursivelyCollect(element, 'parentNode'); + }, + + descendants: function(element) { + return Element.select(element, "*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return Element.recursivelyCollect(element, 'previousSibling'); + }, + + nextSiblings: function(element) { + return Element.recursivelyCollect(element, 'nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return Element.previousSiblings(element).reverse() + .concat(Element.nextSiblings(element)); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = Element.ancestors(element); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return Element.firstDescendant(element); + return Object.isNumber(expression) ? Element.descendants(element)[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = Element.previousSiblings(element); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = Element.nextSiblings(element); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + + select: function(element) { + var args = Array.prototype.slice.call(arguments, 1); + return Selector.findChildElements(element, args); + }, + + adjacent: function(element) { + var args = Array.prototype.slice.call(arguments, 1); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = Element.readAttribute(element, 'id'); + if (id) return id; + do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id)); + Element.writeAttribute(element, 'id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return Element.getDimensions(element).height; + }, + + getWidth: function(element) { + return Element.getDimensions(element).width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!Element.hasClassName(element, className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return Element[Element.hasClassName(element, className) ? + 'removeClassName' : 'addClassName'](element, className); + }, + + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Element.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = Element.getStyle(element, 'display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + if (Prototype.Browser.Opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (Element.getStyle(element, 'position') == 'absolute') return element; + + var offsets = Element.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (Element.getStyle(element, 'position') == 'relative') return element; + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + source = $(source); + var p = Element.viewportOffset(source); + + element = $(element); + var delta = [0, 0]; + var parent = null; + if (Element.getStyle(element, 'position') == 'absolute') { + parent = Element.getOffsetParent(element); + delta = Element.viewportOffset(parent); + } + + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + if (!Element.visible(element)) return null; + + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = (function(){ + + var classProp = 'className'; + var forProp = 'for'; + + var el = document.createElement('div'); + + el.setAttribute(classProp, 'x'); + + if (el.className !== 'x') { + el.setAttribute('class', 'x'); + if (el.className === 'x') { + classProp = 'class'; + } + } + el = null; + + el = document.createElement('label'); + el.setAttribute(forProp, 'x'); + if (el.htmlFor !== 'x') { + el.setAttribute('htmlFor', 'x'); + if (el.htmlFor === 'x') { + forProp = 'htmlFor'; + } + } + el = null; + + return { + read: { + names: { + 'class': classProp, + 'className': classProp, + 'for': forProp, + 'htmlFor': forProp + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute); + }, + _getAttr2: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: (function(){ + + var el = document.createElement('div'); + el.onclick = Prototype.emptyFunction; + var value = el.getAttribute('onclick'); + var f; + + if (String(value).indexOf('{') > -1) { + f = function(element, attribute) { + attribute = element.getAttribute(attribute); + if (!attribute) return null; + attribute = attribute.toString(); + attribute = attribute.split('{')[1]; + attribute = attribute.split('}')[0]; + return attribute.strip(); + }; + } + else if (value === '') { + f = function(element, attribute) { + attribute = element.getAttribute(attribute); + if (!attribute) return null; + return attribute.strip(); + }; + } + el = null; + return f; + })(), + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + } + })(); + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr2, + src: v._getAttr2, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); + + if (Prototype.BrowserFeatures.ElementExtensions) { + (function() { + function _descendants(element) { + var nodes = element.getElementsByTagName('*'), results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName !== "!") // Filter out comment nodes. + results.push(node); + return results; + } + + Element.Methods.down = function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? _descendants(element)[expression] : + Element.select(element, expression)[index || 0]; + } + })(); + } + +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if ('outerHTML' in document.documentElement) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['', '
    ', 1], + TBODY: ['', '
    ', 2], + TR: ['', '
    ', 3], + TD: ['
    ', '
    ', 4], + SELECT: ['', 1] + } +}; + +(function() { + var tags = Element._insertionTranslations.tags; + Object.extend(tags, { + THEAD: tags.TBODY, + TFOOT: tags.TBODY, + TH: tags.TD + }); +})(); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +(function(div) { + + if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = div['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; + } + + div = null; + +})(document.createElement('div')) + +Element.extend = (function() { + + function checkDeficiency(tagName) { + if (typeof window.Element != 'undefined') { + var proto = window.Element.prototype; + if (proto) { + var id = '_' + (Math.random()+'').slice(2); + var el = document.createElement(tagName); + proto[id] = 'x'; + var isBuggy = (el[id] !== 'x'); + delete proto[id]; + el = null; + return isBuggy; + } + } + return false; + } + + function extendElementWith(element, methods) { + for (var property in methods) { + var value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + } + + var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object'); + + if (Prototype.BrowserFeatures.SpecificElementExtensions) { + if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) { + return function(element) { + if (element && typeof element._extendedByPrototype == 'undefined') { + var t = element.tagName; + if (t && (/^(?:object|applet|embed)$/i.test(t))) { + extendElementWith(element, Element.Methods); + extendElementWith(element, Element.Methods.Simulated); + extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]); + } + } + return element; + } + } + return Prototype.K; + } + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || typeof element._extendedByPrototype != 'undefined' || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(); + + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + extendElementWith(element, methods); + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + var element = document.createElement(tagName); + var proto = element['__proto__'] || element.constructor.prototype; + element = null; + return proto; + } + + var elementPrototype = window.HTMLElement ? HTMLElement.prototype : + Element.prototype; + + if (F.ElementExtensions) { + copy(Element.Methods, elementPrototype); + copy(Element.Methods.Simulated, elementPrototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + + +document.viewport = { + + getDimensions: function() { + return { width: this.getWidth(), height: this.getHeight() }; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; + +(function(viewport) { + var B = Prototype.Browser, doc = document, element, property = {}; + + function getRootElement() { + if (B.WebKit && !doc.evaluate) + return document; + + if (B.Opera && window.parseFloat(window.opera.version()) < 9.5) + return document.body; + + return document.documentElement; + } + + function define(D) { + if (!element) element = getRootElement(); + + property[D] = 'client' + D; + + viewport['get' + D] = function() { return element[property[D]] }; + return viewport['get' + D](); + } + + viewport.getWidth = define.curry('Width'); + + viewport.getHeight = define.curry('Height'); +})(document.viewport); + + +Element.Storage = { + UID: 1 +}; + +Element.addMethods({ + getStorage: function(element) { + if (!(element = $(element))) return; + + var uid; + if (element === window) { + uid = 0; + } else { + if (typeof element._prototypeUID === "undefined") + element._prototypeUID = [Element.Storage.UID++]; + uid = element._prototypeUID[0]; + } + + if (!Element.Storage[uid]) + Element.Storage[uid] = $H(); + + return Element.Storage[uid]; + }, + + store: function(element, key, value) { + if (!(element = $(element))) return; + + if (arguments.length === 2) { + Element.getStorage(element).update(key); + } else { + Element.getStorage(element).set(key, value); + } + + return element; + }, + + retrieve: function(element, key, defaultValue) { + if (!(element = $(element))) return; + var hash = Element.getStorage(element), value = hash.get(key); + + if (Object.isUndefined(value)) { + hash.set(key, defaultValue); + value = defaultValue; + } + + return value; + }, + + clone: function(element, deep) { + if (!(element = $(element))) return; + var clone = element.cloneNode(deep); + clone._prototypeUID = void 0; + if (deep) { + var descendants = Element.select(clone, '*'), + i = descendants.length; + while (i--) { + descendants[i]._prototypeUID = void 0; + } + } + return Element.extend(clone); + } +}); +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + + }, + + shouldUseXPath: (function() { + + var IS_DESCENDANT_SELECTOR_BUGGY = (function(){ + var isBuggy = false; + if (document.evaluate && window.XPathResult) { + var el = document.createElement('div'); + el.innerHTML = '
    '; + + var xpath = ".//*[local-name()='ul' or local-name()='UL']" + + "//*[local-name()='li' or local-name()='LI']"; + + var result = document.evaluate(xpath, el, null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + + isBuggy = (result.snapshotLength !== 2); + el = null; + } + return isBuggy; + })(); + + return function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + if (IS_DESCENDANT_SELECTOR_BUGGY) return false; + + return true; + } + + })(), + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m, len = ps.length, name; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i"; + } +}); + +if (Prototype.BrowserFeatures.SelectorsAPI && + document.compatMode === 'BackCompat') { + Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){ + var div = document.createElement('div'), + span = document.createElement('span'); + + div.id = "prototype_test_id"; + span.className = 'Test'; + div.appendChild(span); + var isIgnored = (div.querySelector('#prototype_test_id .test') !== null); + div = span = null; + return isIgnored; + })(); +} + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v, len = p.length, name; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: [ + { name: 'laterSibling', re: /^\s*~\s*/ }, + { name: 'child', re: /^\s*>\s*/ }, + { name: 'adjacent', re: /^\s*\+\s*/ }, + { name: 'descendant', re: /^\s/ }, + + { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, + { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, + { name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ }, + { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ }, + { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, + { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } + ], + + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: (function(){ + + var PROPERTIES_ATTRIBUTES_MAP = (function(){ + var el = document.createElement('div'), + isBuggy = false, + propName = '_countedByPrototype', + value = 'x' + el[propName] = value; + isBuggy = (el.getAttribute(propName) === value); + el = null; + return isBuggy; + })(); + + return PROPERTIES_ATTRIBUTES_MAP ? + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } : + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = void 0; + return nodes; + } + })(), + + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + + if (root == document) { + if (!targetNode) return []; + if (!nodes) return [targetNode]; + } else { + if (!root.sourceIndex || root.sourceIndex < 1) { + var nodes = root.getElementsByTagName('*'); + for (var j = 0, node; node = nodes[j]; j++) { + if (node.id === id) return [node]; + } + } + } + + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + } + }); +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} + +var Form = { + reset: function(form) { + form = $(form); + form.reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + var elements = $(form).getElementsByTagName('*'), + element, + arr = [ ], + serializers = Form.Element.Serializers; + for (var i = 0; element = elements[i]; i++) { + arr.push(element); + } + return arr.inject([], function(elements, child) { + if (serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + }) + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return /^(?:input|select|textarea)$/i.test(element.tagName); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !(/^(?:button|reset|submit)$/i.test(element.type)))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; + +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +(function() { + + var Event = { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: {} + }; + + var docEl = document.documentElement; + var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl + && 'onmouseleave' in docEl; + + var _isButton; + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + _isButton = function(event, code) { + return event.button === buttonMap[code]; + }; + } else if (Prototype.Browser.WebKit) { + _isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + } else { + _isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + function isLeftClick(event) { return _isButton(event, 0) } + + function isMiddleClick(event) { return _isButton(event, 1) } + + function isRightClick(event) { return _isButton(event, 2) } + + function element(event) { + event = Event.extend(event); + + var node = event.target, type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + + if (node.nodeType == Node.TEXT_NODE) + node = node.parentNode; + + return Element.extend(node); + } + + function findElement(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + } + + function pointer(event) { + return { x: pointerX(event), y: pointerY(event) }; + } + + function pointerX(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0 }; + + return event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)); + } + + function pointerY(event) { + var docElement = document.documentElement, + body = document.body || { scrollTop: 0 }; + + return event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)); + } + + + function stop(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + + event.stopped = true; + } + + Event.Methods = { + isLeftClick: isLeftClick, + isMiddleClick: isMiddleClick, + isRightClick: isRightClick, + + element: element, + findElement: findElement, + + pointer: pointer, + pointerX: pointerX, + pointerY: pointerY, + + stop: stop + }; + + + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + function _relatedTarget(event) { + var element; + switch (event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } + + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return '[object Event]' } + }); + + Event.extend = function(event, element) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + + Object.extend(event, { + target: event.srcElement || element, + relatedTarget: _relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + + return Object.extend(event, methods); + }; + } else { + Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; + Object.extend(Event.prototype, methods); + Event.extend = Prototype.K; + } + + function _createResponder(element, eventName, handler) { + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) { + CACHE.push(element); + registry = Element.retrieve(element, 'prototype_event_registry', $H()); + } + + var respondersForEvent = registry.get(eventName); + if (Object.isUndefined(respondersForEvent)) { + respondersForEvent = []; + registry.set(eventName, respondersForEvent); + } + + if (respondersForEvent.pluck('handler').include(handler)) return false; + + var responder; + if (eventName.include(":")) { + responder = function(event) { + if (Object.isUndefined(event.eventName)) + return false; + + if (event.eventName !== eventName) + return false; + + Event.extend(event, element); + handler.call(element, event); + }; + } else { + if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED && + (eventName === "mouseenter" || eventName === "mouseleave")) { + if (eventName === "mouseenter" || eventName === "mouseleave") { + responder = function(event) { + Event.extend(event, element); + + var parent = event.relatedTarget; + while (parent && parent !== element) { + try { parent = parent.parentNode; } + catch(e) { parent = element; } + } + + if (parent === element) return; + + handler.call(element, event); + }; + } + } else { + responder = function(event) { + Event.extend(event, element); + handler.call(element, event); + }; + } + } + + responder.handler = handler; + respondersForEvent.push(responder); + return responder; + } + + function _destroyCache() { + for (var i = 0, length = CACHE.length; i < length; i++) { + Event.stopObserving(CACHE[i]); + CACHE[i] = null; + } + } + + var CACHE = []; + + if (Prototype.Browser.IE) + window.attachEvent('onunload', _destroyCache); + + if (Prototype.Browser.WebKit) + window.addEventListener('unload', Prototype.emptyFunction, false); + + + var _getDOMEventName = Prototype.K; + + if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { + _getDOMEventName = function(eventName) { + var translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; + return eventName in translations ? translations[eventName] : eventName; + }; + } + + function observe(element, eventName, handler) { + element = $(element); + + var responder = _createResponder(element, eventName, handler); + + if (!responder) return element; + + if (eventName.include(':')) { + if (element.addEventListener) + element.addEventListener("dataavailable", responder, false); + else { + element.attachEvent("ondataavailable", responder); + element.attachEvent("onfilterchange", responder); + } + } else { + var actualEventName = _getDOMEventName(eventName); + + if (element.addEventListener) + element.addEventListener(actualEventName, responder, false); + else + element.attachEvent("on" + actualEventName, responder); + } + + return element; + } + + function stopObserving(element, eventName, handler) { + element = $(element); + + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) return element; + + if (eventName && !handler) { + var responders = registry.get(eventName); + + if (Object.isUndefined(responders)) return element; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); + }); + return element; + } else if (!eventName) { + registry.each( function(pair) { + var eventName = pair.key, responders = pair.value; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); + }); + }); + return element; + } + + var responders = registry.get(eventName); + + if (!responders) return; + + var responder = responders.find( function(r) { return r.handler === handler; }); + if (!responder) return element; + + var actualEventName = _getDOMEventName(eventName); + + if (eventName.include(':')) { + if (element.removeEventListener) + element.removeEventListener("dataavailable", responder, false); + else { + element.detachEvent("ondataavailable", responder); + element.detachEvent("onfilterchange", responder); + } + } else { + if (element.removeEventListener) + element.removeEventListener(actualEventName, responder, false); + else + element.detachEvent('on' + actualEventName, responder); + } + + registry.set(eventName, responders.without(responder)); + + return element; + } + + function fire(element, eventName, memo, bubble) { + element = $(element); + + if (Object.isUndefined(bubble)) + bubble = true; + + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent('HTMLEvents'); + event.initEvent('dataavailable', true, true); + } else { + event = document.createEventObject(); + event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) + element.dispatchEvent(event); + else + element.fireEvent(event.eventType, event); + + return Event.extend(event); + } + + + Object.extend(Event, Event.Methods); + + Object.extend(Event, { + fire: fire, + observe: observe, + stopObserving: stopObserving + }); + + Element.addMethods({ + fire: fire, + + observe: observe, + + stopObserving: stopObserving + }); + + Object.extend(document, { + fire: fire.methodize(), + + observe: observe.methodize(), + + stopObserving: stopObserving.methodize(), + + loaded: false + }); + + if (window.Event) Object.extend(window.Event, Event); + else window.Event = Event; +})(); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearTimeout(timer); + document.loaded = true; + document.fire('dom:loaded'); + } + + function checkReadyState() { + if (document.readyState === 'complete') { + document.stopObserving('readystatechange', checkReadyState); + fireContentLoadedEvent(); + } + } + + function pollDoScroll() { + try { document.documentElement.doScroll('left'); } + catch(e) { + timer = pollDoScroll.defer(); + return; + } + fireContentLoadedEvent(); + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); + } else { + document.observe('readystatechange', checkReadyState); + if (window == top) + timer = pollDoScroll.defer(); + } + + Event.observe(window, 'load', fireContentLoadedEvent); +})(); + +Element.addMethods(); + +/*------------------------------- DEPRECATED -------------------------------*/ + +Hash.toQueryString = Object.toQueryString; + +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); + }, + + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, + + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, + + After: function(element, content) { + return Element.insert(element, {after:content}); + } +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +var Position = { + includeScrollOffsets: false, + + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = Element.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = Element.cumulativeScrollOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = Element.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + + cumulativeOffset: Element.Methods.cumulativeOffset, + + positionedOffset: Element.Methods.positionedOffset, + + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, + + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); + }, + + realOffset: Element.Methods.cumulativeScrollOffset, + + offsetParent: Element.Methods.getOffsetParent, + + page: Element.Methods.viewportOffset, + + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); + } + return elements; + }; + + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); + +/*--------------------------------------------------------------------------*/ diff --git a/spec/mock_rails3_gem/public/javascripts/rails.js b/spec/mock_rails3_gem/public/javascripts/rails.js new file mode 100644 index 00000000..c5fa02ae --- /dev/null +++ b/spec/mock_rails3_gem/public/javascripts/rails.js @@ -0,0 +1,118 @@ +document.observe("dom:loaded", function() { + function handleRemote(element) { + var method, url, params; + + if (element.tagName.toLowerCase() === 'form') { + method = element.readAttribute('method') || 'post'; + url = element.readAttribute('action'); + params = element.serialize(true); + } else { + method = element.readAttribute('data-method') || 'get'; + url = element.readAttribute('href'); + params = {}; + } + + var event = element.fire("ajax:before"); + if (event.stopped) return false; + + new Ajax.Request(url, { + method: method, + parameters: params, + asynchronous: true, + evalScripts: true, + + onLoading: function(request) { element.fire("ajax:loading", {request: request}); }, + onLoaded: function(request) { element.fire("ajax:loaded", {request: request}); }, + onInteractive: function(request) { element.fire("ajax:interactive", {request: request}); }, + onComplete: function(request) { element.fire("ajax:complete", {request: request}); }, + onSuccess: function(request) { element.fire("ajax:success", {request: request}); }, + onFailure: function(request) { element.fire("ajax:failure", {request: request}); } + }); + + element.fire("ajax:after"); + } + + function handleMethod(element) { + var method, url, token_name, token; + + method = element.readAttribute('data-method'); + url = element.readAttribute('href'); + csrf_param = $$('meta[name=csrf-param]').first(); + csrf_token = $$('meta[name=csrf-token]').first(); + + var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); + element.parentNode.appendChild(form); + + if (method != 'post') { + var field = new Element('input', { type: 'hidden', name: '_method', value: method }); + form.appendChild(field); + } + + if (csrf_param) { + var param = csrf_param.readAttribute('content'); + var token = csrf_token.readAttribute('content'); + var field = new Element('input', { type: 'hidden', name: param, value: token }); + form.appendChild(field); + } + + form.submit(); + } + + $(document.body).observe("click", function(event) { + var message = event.findElement().readAttribute('data-confirm'); + if (message && !confirm(message)) { + event.stop(); + return false; + } + + var element = event.findElement("a[data-remote]"); + if (element) { + handleRemote(element); + event.stop(); + return true; + } + + var element = event.findElement("a[data-method]"); + if (element) { + handleMethod(element); + event.stop(); + return true; + } + }); + + // TODO: I don't think submit bubbles in IE + $(document.body).observe("submit", function(event) { + var element = event.findElement(), + message = element.readAttribute('data-confirm'); + if (message && !confirm(message)) { + event.stop(); + return false; + } + + var inputs = element.select("input[type=submit][data-disable-with]"); + inputs.each(function(input) { + input.disabled = true; + input.writeAttribute('data-original-value', input.value); + input.value = input.readAttribute('data-disable-with'); + }); + + var element = event.findElement("form[data-remote]"); + if (element) { + handleRemote(element); + event.stop(); + } + }); + + $(document.body).observe("ajax:after", function(event) { + var element = event.findElement(); + + if (element.tagName.toLowerCase() === 'form') { + var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); + inputs.each(function(input) { + input.value = input.readAttribute('data-original-value'); + input.writeAttribute('data-original-value', null); + input.disabled = false; + }); + } + }); +}); \ No newline at end of file diff --git a/spec/mock_rails3_gem/public/robots.txt b/spec/mock_rails3_gem/public/robots.txt new file mode 100644 index 00000000..085187fa --- /dev/null +++ b/spec/mock_rails3_gem/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-Agent: * +# Disallow: / diff --git a/spec/mock_rails3_gem/public/stylesheets/.gitkeep b/spec/mock_rails3_gem/public/stylesheets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/spec/mock_rails3_gem/script/rails b/spec/mock_rails3_gem/script/rails new file mode 100755 index 00000000..febedced --- /dev/null +++ b/spec/mock_rails3_gem/script/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENV_PATH = File.expand_path('../../config/environment', __FILE__) +BOOT_PATH = File.expand_path('../../config/boot', __FILE__) +APP_PATH = File.expand_path('../../config/application', __FILE__) + +require BOOT_PATH +require 'rails/commands' diff --git a/spec/mock_rails3_gem/test/performance/browsing_test.rb b/spec/mock_rails3_gem/test/performance/browsing_test.rb new file mode 100644 index 00000000..867fc8c9 --- /dev/null +++ b/spec/mock_rails3_gem/test/performance/browsing_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' +require 'rails/performance_test_help' + +# Profiling results for each test method are written to tmp/performance. +class BrowsingTest < ActionDispatch::PerformanceTest + def test_homepage + get '/' + end +end diff --git a/spec/mock_rails3_gem/test/test_helper.rb b/spec/mock_rails3_gem/test/test_helper.rb new file mode 100644 index 00000000..8bf1192f --- /dev/null +++ b/spec/mock_rails3_gem/test/test_helper.rb @@ -0,0 +1,13 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path('../../config/environment', __FILE__) +require 'rails/test_help' + +class ActiveSupport::TestCase + # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. + # + # Note: You'll currently still have to declare fixtures explicitly in integration tests + # -- they do not yet inherit this setting + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/spec/mock_rails3_gem/vendor/plugins/.gitkeep b/spec/mock_rails3_gem/vendor/plugins/.gitkeep new file mode 100644 index 00000000..e69de29b From b0f677fe52265dfef814713819af7f08cb4f5925 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 13:37:35 -0700 Subject: [PATCH 029/677] Prepare Rails 3 test env. --- .gitignore | 1 + Rakefile | 8 ++++++++ spec/mock_rails3_gem/Gemfile | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 774e6fef..c9f002c6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ pkg test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 test/mock_app_plugin/vendor/plugins/sitemap_generator +test/mock_rails3_gem/vendor/gems/* diff --git a/Rakefile b/Rakefile index 7bdc7ec5..67c223a9 100644 --- a/Rakefile +++ b/Rakefile @@ -25,6 +25,7 @@ task :default => :test namespace :test do task :gem => ['test:prepare:gem', 'multi_spec'] task :plugin => ['test:prepare:plugin', 'multi_spec'] + task :rails3 => ['test:prepare:rails3', 'multi_spec'] task :multi_spec do Rake::Task['spec'].invoke @@ -44,6 +45,13 @@ namespace :test do rm_rf(local_path('spec/mock_app_plugin/public/sitemap*')) end + task :rails3 do + ENV["SITEMAP_RAILS"] = 'rails3' + prepare_path(local_path('spec/mock_rails3_gem/vendor/gems/sitemap_generator-1.2.3')) + cp(local_path('sitemap_generator.gemspec'), local_path('spec/mock_rails3_gem/vendor/gems/sitemap_generator-1.2.3/')) + rm_rf(local_path('spec/mock_rails3_gem/public/sitemap*')) + end + def local_path(path) File.join(File.dirname(__FILE__), path) end diff --git a/spec/mock_rails3_gem/Gemfile b/spec/mock_rails3_gem/Gemfile index 8ed08813..271f01fe 100644 --- a/spec/mock_rails3_gem/Gemfile +++ b/spec/mock_rails3_gem/Gemfile @@ -6,7 +6,7 @@ gem 'rails', '3.0.0.beta3' # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3-ruby', :require => 'sqlite3' -gem 'sitemap_generator', :require => false +gem 'sitemap_generator', :require => false, :path => 'vendor/gems/sitemap_generator-1.2.3' # Use unicorn as the web server # gem 'unicorn' From 3e616c5b9c73a917bdb59214d461d722182c7642 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 13:43:16 -0700 Subject: [PATCH 030/677] rails 3 compat Conflicts: lib/sitemap_generator.rb tasks/sitemap_generator_tasks.rake --- lib/sitemap_generator.rb | 1 + lib/sitemap_generator/helper.rb | 9 ++++----- lib/sitemap_generator/railtie.rb | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 lib/sitemap_generator/railtie.rb diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 6d94c99b..c86ba262 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -2,6 +2,7 @@ require 'sitemap_generator/link' require 'sitemap_generator/link_set' require 'sitemap_generator/helper' +require 'sitemap_generator/railtie' if defined?(Rails) module SitemapGenerator silence_warnings do diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb index 19bbc4e2..40e2e60c 100644 --- a/lib/sitemap_generator/helper.rb +++ b/lib/sitemap_generator/helper.rb @@ -1,9 +1,8 @@ require 'sitemap_generator' -require 'action_controller' module SitemapGenerator module Helper - include ActionController::UrlWriter + # include ActionController::UrlWriter def self.included(base) base.class_eval do @@ -17,15 +16,15 @@ def load_sitemap_rb sitemap_mapper_file = File.join(RAILS_ROOT, 'config/sitemap.rb') eval(open(sitemap_mapper_file).read) end - + def url_with_hostname(path) URI.join(SitemapGenerator::Sitemap.default_host, path).to_s end - + def w3c_date(date) date.utc.strftime("%Y-%m-%dT%H:%M:%S+00:00") end - + def ping_search_engines(sitemap_index) require 'open-uri' index_location = CGI.escape(url_with_hostname(sitemap_index)) diff --git a/lib/sitemap_generator/railtie.rb b/lib/sitemap_generator/railtie.rb new file mode 100644 index 00000000..3f762b35 --- /dev/null +++ b/lib/sitemap_generator/railtie.rb @@ -0,0 +1,9 @@ +module SitemapGenerator + class Railtie < Rails::Railtie + railtie_name :sitemap_generator + + rake_tasks do + load File.expand_path('../../../tasks/sitemap_generator_tasks.rake', __FILE__) + end + end +end \ No newline at end of file From 15def40efe4c31b332af487ae55630585f97f3d6 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 14:41:06 -0700 Subject: [PATCH 031/677] Get tests passing --- Rakefile | 2 -- lib/sitemap_generator.rb | 8 ++++---- lib/sitemap_generator/helper.rb | 6 +++--- lib/sitemap_generator/link_set.rb | 16 +++++++-------- lib/sitemap_generator/rails_helper.rb | 28 +++++++++++++++++++++++++++ lib/sitemap_generator/railtie.rb | 2 -- spec/mock_rails3_gem/Gemfile | 2 +- spec/mock_rails3_gem/Rakefile | 1 + spec/spec_helper.rb | 15 +++++++++++--- 9 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 lib/sitemap_generator/rails_helper.rb diff --git a/Rakefile b/Rakefile index 67c223a9..f3eb45ed 100644 --- a/Rakefile +++ b/Rakefile @@ -47,8 +47,6 @@ namespace :test do task :rails3 do ENV["SITEMAP_RAILS"] = 'rails3' - prepare_path(local_path('spec/mock_rails3_gem/vendor/gems/sitemap_generator-1.2.3')) - cp(local_path('sitemap_generator.gemspec'), local_path('spec/mock_rails3_gem/vendor/gems/sitemap_generator-1.2.3/')) rm_rf(local_path('spec/mock_rails3_gem/public/sitemap*')) end diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index c86ba262..c381735f 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -1,8 +1,8 @@ require 'sitemap_generator/mapper' require 'sitemap_generator/link' -require 'sitemap_generator/link_set' +require 'sitemap_generator/rails_helper' require 'sitemap_generator/helper' -require 'sitemap_generator/railtie' if defined?(Rails) +require 'sitemap_generator/link_set' module SitemapGenerator silence_warnings do @@ -20,5 +20,5 @@ class << self :sitemap_index => File.join(self.root, 'templates/sitemap_index.builder'), :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), :sitemap_sample => File.join(self.root, 'templates/sitemap.rb'), - } -end + } +end \ No newline at end of file diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb index 40e2e60c..6496df57 100644 --- a/lib/sitemap_generator/helper.rb +++ b/lib/sitemap_generator/helper.rb @@ -1,8 +1,8 @@ -require 'sitemap_generator' +require 'action_controller' unless SitemapGenerator::RailsHelper.rails3? module SitemapGenerator module Helper - # include ActionController::UrlWriter + include ActionController::UrlWriter unless SitemapGenerator::RailsHelper.rails3? def self.included(base) base.class_eval do @@ -13,7 +13,7 @@ def self.default_url_options(options = nil) end def load_sitemap_rb - sitemap_mapper_file = File.join(RAILS_ROOT, 'config/sitemap.rb') + sitemap_mapper_file = File.join(Rails.root, 'config/sitemap.rb') eval(open(sitemap_mapper_file).read) end diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index e6d9849e..56f39f48 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,5 +1,3 @@ -require File.dirname(__FILE__) + '/helper' - module SitemapGenerator class LinkSet include SitemapGenerator::Helper @@ -62,7 +60,7 @@ def write_sitemap(file = upcoming_file) buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) - filename = File.join(RAILS_ROOT, "public", file) + filename = File.join(Rails.root, "public", file) write_file(filename, buffer) show_progress("Sitemap", filename, buffer) if verbose links.clear @@ -74,7 +72,7 @@ def write_index buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) - filename = File.join(RAILS_ROOT, "public", index_file) + filename = File.join(Rails.root, "public", index_file) write_file(filename, buffer) show_progress("Sitemap Index", filename, buffer) if verbose links.clear @@ -144,24 +142,24 @@ def show_progress(title, filename, buffer) # Copy templates/sitemap.rb to config if not there yet. def install_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + if File.exist?(File.join(Rails.root, 'config/sitemap.rb')) puts "already exists: config/sitemap.rb, file not copied" else - FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) + FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(Rails.root, 'config/sitemap.rb')) puts "created: config/sitemap.rb" end end # Remove config/sitemap.rb if exists. def uninstall_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) + if File.exist?(File.join(Rails.root, 'config/sitemap.rb')) + File.rm(File.join(Rails.root, 'config/sitemap.rb')) end end # Clean sitemap files in output directory. def clean_files - FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + FileUtils.rm(Dir[File.join(Rails.root, 'public/sitemap*.xml.gz')]) end # Ping search engines passing sitemap location. diff --git a/lib/sitemap_generator/rails_helper.rb b/lib/sitemap_generator/rails_helper.rb new file mode 100644 index 00000000..695a7158 --- /dev/null +++ b/lib/sitemap_generator/rails_helper.rb @@ -0,0 +1,28 @@ +module SitemapGenerator + module RailsHelper + # Returns whether this environment is using ActionPack + # version 3.0.0 or greater. + # + # @return [Boolean] + def self.rails3? + # The ActionPack module is always loaded automatically in Rails >= 3 + return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) + + version = + if defined?(ActionPack::VERSION::MAJOR) + ActionPack::VERSION::MAJOR + else + # Rails 1.2 + ActionPack::VERSION::Major + end + + # 3.0.0.beta1 acts more like ActionPack 2 + # for purposes of this method + # (checking whether block helpers require = or -). + # This extra check can be removed when beta2 is out. + version >= 3 && + !(defined?(ActionPack::VERSION::TINY) && + ActionPack::VERSION::TINY == "0.beta") + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/railtie.rb b/lib/sitemap_generator/railtie.rb index 3f762b35..1027447d 100644 --- a/lib/sitemap_generator/railtie.rb +++ b/lib/sitemap_generator/railtie.rb @@ -1,7 +1,5 @@ module SitemapGenerator class Railtie < Rails::Railtie - railtie_name :sitemap_generator - rake_tasks do load File.expand_path('../../../tasks/sitemap_generator_tasks.rake', __FILE__) end diff --git a/spec/mock_rails3_gem/Gemfile b/spec/mock_rails3_gem/Gemfile index 271f01fe..dc241248 100644 --- a/spec/mock_rails3_gem/Gemfile +++ b/spec/mock_rails3_gem/Gemfile @@ -6,7 +6,7 @@ gem 'rails', '3.0.0.beta3' # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3-ruby', :require => 'sqlite3' -gem 'sitemap_generator', :require => false, :path => 'vendor/gems/sitemap_generator-1.2.3' +gem 'sitemap_generator', :path => '../../' # Use unicorn as the web server # gem 'unicorn' diff --git a/spec/mock_rails3_gem/Rakefile b/spec/mock_rails3_gem/Rakefile index 9cb20464..69a250d3 100644 --- a/spec/mock_rails3_gem/Rakefile +++ b/spec/mock_rails3_gem/Rakefile @@ -6,5 +6,6 @@ require File.expand_path('../config/application', __FILE__) require 'rake' require 'rake/testtask' require 'rake/rdoctask' +require 'sitemap_generator/railtie' Rails::Application.load_tasks diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2737419f..c724d315 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,14 +1,23 @@ ENV["RAILS_ENV"] ||= 'test' +ENV['BUNDLE_GEMFILE'] = File.join(File.dirname(__FILE__), 'mock_rails3_gem', 'Gemfile') -sitemap_rails = ENV["SITEMAP_RAILS"] ? "mock_app_#{ENV["SITEMAP_RAILS"]}" : 'mock_app_gem' +sitemap_rails = + case ENV["SITEMAP_RAILS"] + when 'rails3' + "mock_rails3_gem" + when 'gem', 'plugin' + "mock_app_#{ENV["SITEMAP_RAILS"]}" + else + "mock_app_gem" + end # Boot the environment -require File.join(File.dirname(__FILE__), sitemap_rails, 'config', 'boot') +#require File.join(File.dirname(__FILE__), sitemap_rails, 'config', 'boot') # Load the app's Rakefile so we know everything is being loaded correctly load(File.join(File.dirname(__FILE__), sitemap_rails, 'Rakefile')) -require 'ruby-debug' +#require 'ruby-debug' # debugger # Requires supporting files with custom matchers and macros, etc, From c71dc0a0b13b07dc5b06a3a25418f7e88af90079 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 15:21:29 -0700 Subject: [PATCH 032/677] Copy specs into rails3 app because we have to use RSpec 2 Get specs running --- spec/mock_rails3_gem/Gemfile | 7 +- spec/mock_rails3_gem/autotest/discover.rb | 2 + .../config/initializers/rspec_generator.rb | 6 ++ spec/mock_rails3_gem/config/sitemap.rb | 13 ++++ spec/mock_rails3_gem/db/schema.rb | 14 ++++ spec/mock_rails3_gem/lib/tasks/rspec.rake | 69 +++++++++++++++++ spec/mock_rails3_gem/spec/sitemap.file | 13 ++++ .../spec/sitemap_generator_spec.rb | 77 +++++++++++++++++++ spec/mock_rails3_gem/spec/spec_helper.rb | 28 +++++++ .../spec/support/file_macros.rb | 28 +++++++ 10 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 spec/mock_rails3_gem/autotest/discover.rb create mode 100644 spec/mock_rails3_gem/config/initializers/rspec_generator.rb create mode 100644 spec/mock_rails3_gem/config/sitemap.rb create mode 100644 spec/mock_rails3_gem/db/schema.rb create mode 100644 spec/mock_rails3_gem/lib/tasks/rspec.rake create mode 100644 spec/mock_rails3_gem/spec/sitemap.file create mode 100644 spec/mock_rails3_gem/spec/sitemap_generator_spec.rb create mode 100644 spec/mock_rails3_gem/spec/spec_helper.rb create mode 100644 spec/mock_rails3_gem/spec/support/file_macros.rb diff --git a/spec/mock_rails3_gem/Gemfile b/spec/mock_rails3_gem/Gemfile index dc241248..03267368 100644 --- a/spec/mock_rails3_gem/Gemfile +++ b/spec/mock_rails3_gem/Gemfile @@ -8,6 +8,10 @@ gem 'rails', '3.0.0.beta3' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'sitemap_generator', :path => '../../' +group :test do + gem "rspec-rails", ">= 2.0.0.beta.1" +end + # Use unicorn as the web server # gem 'unicorn' @@ -22,6 +26,3 @@ gem 'sitemap_generator', :path => '../../' # Bundle gems for certain environments: # gem 'rspec', :group => :test -# group :test do -# gem 'webrat' -# end diff --git a/spec/mock_rails3_gem/autotest/discover.rb b/spec/mock_rails3_gem/autotest/discover.rb new file mode 100644 index 00000000..f421dc5f --- /dev/null +++ b/spec/mock_rails3_gem/autotest/discover.rb @@ -0,0 +1,2 @@ +Autotest.add_discovery { "rails" } +Autotest.add_discovery { "rspec2" } diff --git a/spec/mock_rails3_gem/config/initializers/rspec_generator.rb b/spec/mock_rails3_gem/config/initializers/rspec_generator.rb new file mode 100644 index 00000000..39baf577 --- /dev/null +++ b/spec/mock_rails3_gem/config/initializers/rspec_generator.rb @@ -0,0 +1,6 @@ +MockRails3Gem::Application.configure do + config.generators do |g| + g.integration_tool :rspec + g.test_framework :rspec + end +end diff --git a/spec/mock_rails3_gem/config/sitemap.rb b/spec/mock_rails3_gem/config/sitemap.rb new file mode 100644 index 00000000..84681faf --- /dev/null +++ b/spec/mock_rails3_gem/config/sitemap.rb @@ -0,0 +1,13 @@ +SitemapGenerator::Sitemap.default_host = "http://www.example.com" +SitemapGenerator::Sitemap.yahoo_app_id = false + +SitemapGenerator::Sitemap.add_links do |sitemap| + sitemap.add contents_path, :priority => 0.7, :changefreq => 'daily' + + # add all individual articles + Content.find(:all).each do |c| + sitemap.add content_path(c), :lastmod => c.updated_at + end + + sitemap.add "/merchant_path", :host => "https://www.example.com" +end diff --git a/spec/mock_rails3_gem/db/schema.rb b/spec/mock_rails3_gem/db/schema.rb new file mode 100644 index 00000000..b81ae5a5 --- /dev/null +++ b/spec/mock_rails3_gem/db/schema.rb @@ -0,0 +1,14 @@ +# This file is auto-generated from the current state of the database. Instead of editing this file, +# please use the migrations feature of Active Record to incrementally modify your database, and +# then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your database schema. If you need +# to create the application database on another system, you should be using db:schema:load, not running +# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 0) do + +end diff --git a/spec/mock_rails3_gem/lib/tasks/rspec.rake b/spec/mock_rails3_gem/lib/tasks/rspec.rake new file mode 100644 index 00000000..f11f1b62 --- /dev/null +++ b/spec/mock_rails3_gem/lib/tasks/rspec.rake @@ -0,0 +1,69 @@ +begin + require 'rspec/core' + require 'rspec/core/rake_task' +rescue MissingSourceFile + module Rspec + module Core + class RakeTask + def initialize(name) + task name do + # if rspec-rails is a configured gem, this will output helpful material and exit ... + require File.expand_path(File.dirname(__FILE__) + "/../../config/environment") + + # ... otherwise, do this: + raise <<-MSG + +#{"*" * 80} +* You are trying to run an rspec rake task defined in +* #{__FILE__}, +* but rspec can not be found in vendor/gems, vendor/plugins or system gems. +#{"*" * 80} +MSG + end + end + end + end + end +end + +Rake.application.instance_variable_get('@tasks').delete('default') + +spec_prereq = File.exist?(File.join(Rails.root, 'config', 'database.yml')) ? "db:test:prepare" : :noop +task :noop do +end + +task :default => :spec +task :stats => "spec:statsetup" + +desc "Run all specs in spec directory (excluding plugin specs)" +Rspec::Core::RakeTask.new(:spec => spec_prereq) + +namespace :spec do + [:requests, :models, :controllers, :views, :helpers, :mailers, :lib].each do |sub| + desc "Run the code examples in spec/#{sub}" + Rspec::Core::RakeTask.new(sub => spec_prereq) do |t| + t.pattern = "./spec/#{sub}/**/*_spec.rb" + end + end + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Model\ specs spec/models) if File.exist?('spec/models') + ::STATS_DIRECTORIES << %w(View\ specs spec/views) if File.exist?('spec/views') + ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers) if File.exist?('spec/controllers') + ::STATS_DIRECTORIES << %w(Helper\ specs spec/helpers) if File.exist?('spec/helpers') + ::STATS_DIRECTORIES << %w(Library\ specs spec/lib) if File.exist?('spec/lib') + ::STATS_DIRECTORIES << %w(Mailer\ specs spec/mailers) if File.exist?('spec/mailers') + ::STATS_DIRECTORIES << %w(Routing\ specs spec/routing) if File.exist?('spec/routing') + ::STATS_DIRECTORIES << %w(Request\ specs spec/requests) if File.exist?('spec/requests') + ::CodeStatistics::TEST_TYPES << "Model specs" if File.exist?('spec/models') + ::CodeStatistics::TEST_TYPES << "View specs" if File.exist?('spec/views') + ::CodeStatistics::TEST_TYPES << "Controller specs" if File.exist?('spec/controllers') + ::CodeStatistics::TEST_TYPES << "Helper specs" if File.exist?('spec/helpers') + ::CodeStatistics::TEST_TYPES << "Library specs" if File.exist?('spec/lib') + ::CodeStatistics::TEST_TYPES << "Mailer specs" if File.exist?('spec/mailer') + ::CodeStatistics::TEST_TYPES << "Routing specs" if File.exist?('spec/routing') + ::CodeStatistics::TEST_TYPES << "Request specs" if File.exist?('spec/requests') + end +end + diff --git a/spec/mock_rails3_gem/spec/sitemap.file b/spec/mock_rails3_gem/spec/sitemap.file new file mode 100644 index 00000000..84681faf --- /dev/null +++ b/spec/mock_rails3_gem/spec/sitemap.file @@ -0,0 +1,13 @@ +SitemapGenerator::Sitemap.default_host = "http://www.example.com" +SitemapGenerator::Sitemap.yahoo_app_id = false + +SitemapGenerator::Sitemap.add_links do |sitemap| + sitemap.add contents_path, :priority => 0.7, :changefreq => 'daily' + + # add all individual articles + Content.find(:all).each do |c| + sitemap.add content_path(c), :lastmod => c.updated_at + end + + sitemap.add "/merchant_path", :host => "https://www.example.com" +end diff --git a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb new file mode 100644 index 00000000..e579097e --- /dev/null +++ b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe "SitemapGenerator" do + + context "clean task" do + before :all do + copy_sitemap_file_to_rails_app + FileUtils.touch(rails_path('/public/sitemap_index.xml.gz')) + Rake::Task['sitemap:clean'].invoke + end + + it "should delete the sitemaps" do + file_should_not_exist(rails_path('/public/sitemap_index.xml.gz')) + end + end + + context "fresh install" do + before :all do + delete_sitemap_file_from_rails_app + Rake::Task['sitemap:install'].invoke + end + + it "should create config/sitemap.rb" do + file_should_exist(rails_path('config/sitemap.rb')) + end + + it "should create config/sitemap.rb matching template" do + sitemap_template = SitemapGenerator.templates[:sitemap_sample] + files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) + end + + context "install multiple times" do + before :all do + copy_sitemap_file_to_rails_app + Rake::Task['sitemap:install'].invoke + end + + it "should not overwrite config/sitemap.rb" do + sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') + files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) + end + end + end + + context "generate sitemap" do + before :each do + Rake::Task['sitemap:refresh:no_ping'].invoke + end + + it "should create sitemaps" do + file_should_exist(rails_path('/public/sitemap_index.xml.gz')) + file_should_exist(rails_path('/public/sitemap1.xml.gz')) + end + + it "should have 14 links" do + SitemapGenerator::Sitemap.link_count.should == 14 + end + end + + protected + + # + # Helpers + # + + def rails_path(file) + File.join(RAILS_ROOT, file) + end + + def copy_sitemap_file_to_rails_app + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) + end + + def delete_sitemap_file_from_rails_app + FileUtils.remove(File.join(RAILS_ROOT, '/config/sitemap.rb')) rescue nil + end +end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/spec_helper.rb b/spec/mock_rails3_gem/spec/spec_helper.rb new file mode 100644 index 00000000..0a939a46 --- /dev/null +++ b/spec/mock_rails3_gem/spec/spec_helper.rb @@ -0,0 +1,28 @@ +# This file is copied to ~/spec when you run 'ruby script/generate rspec' +# from the project root directory. +ENV["RAILS_ENV"] ||= 'test' + +#require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) +load(File.join(File.dirname(__FILE__), '..', 'Rakefile')) +require 'rspec/rails' + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +Rspec.configure do |config| + config.include(FileMacros) + + # == Mock Framework + # + # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: + # + # config.mock_with :mocha + # config.mock_with :flexmock + # config.mock_with :rr + config.mock_with :rspec + + # If you'd prefer not to run each of your examples within a transaction, + # uncomment the following line. + # config.use_transactional_examples false +end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/support/file_macros.rb b/spec/mock_rails3_gem/spec/support/file_macros.rb new file mode 100644 index 00000000..db9a0ab7 --- /dev/null +++ b/spec/mock_rails3_gem/spec/support/file_macros.rb @@ -0,0 +1,28 @@ +module FileMacros + module ExampleMethods + + def files_should_be_identical(first, second) + identical_files?(first, second).should be(true) + end + + def files_should_not_be_identical(first, second) + identical_files?(first, second).should be(false) + end + + def file_should_exist(file) + File.exists?(file).should be(true) + end + + def file_should_not_exist(file) + File.exists?(file).should be(false) + end + + def identical_files?(first, second) + open(second, 'r').read.should == open(first, 'r').read + end + end + + def self.included(receiver) + receiver.send :include, ExampleMethods + end +end \ No newline at end of file From 43a87e4265d7c60051328b95e4ed99b42ec3cfc4 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 17:07:42 -0700 Subject: [PATCH 033/677] Fix railties inclusion. Include controllers, routes and db as in other mock apps. --- lib/sitemap_generator.rb | 2 + spec/mock_rails3_gem/Gemfile | 1 + spec/mock_rails3_gem/Rakefile | 1 - .../app/controllers/contents_controller.rb | 85 +++++++++++++++++++ spec/mock_rails3_gem/app/models/content.rb | 2 + spec/mock_rails3_gem/config/routes.rb | 2 + spec/mock_rails3_gem/db/schema.rb | 10 ++- .../spec/sitemap_generator_spec.rb | 6 +- spec/sitemap_generator_spec.rb | 6 +- 9 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 spec/mock_rails3_gem/app/controllers/contents_controller.rb create mode 100644 spec/mock_rails3_gem/app/models/content.rb diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index c381735f..3981df1a 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -4,6 +4,8 @@ require 'sitemap_generator/helper' require 'sitemap_generator/link_set' +require 'sitemap_generator/railtie' if SitemapGenerator::RailsHelper.rails3? + module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip diff --git a/spec/mock_rails3_gem/Gemfile b/spec/mock_rails3_gem/Gemfile index 03267368..8d66380b 100644 --- a/spec/mock_rails3_gem/Gemfile +++ b/spec/mock_rails3_gem/Gemfile @@ -10,6 +10,7 @@ gem 'sitemap_generator', :path => '../../' group :test do gem "rspec-rails", ">= 2.0.0.beta.1" + gem "ruby-debug19", :require => false end # Use unicorn as the web server diff --git a/spec/mock_rails3_gem/Rakefile b/spec/mock_rails3_gem/Rakefile index 69a250d3..9cb20464 100644 --- a/spec/mock_rails3_gem/Rakefile +++ b/spec/mock_rails3_gem/Rakefile @@ -6,6 +6,5 @@ require File.expand_path('../config/application', __FILE__) require 'rake' require 'rake/testtask' require 'rake/rdoctask' -require 'sitemap_generator/railtie' Rails::Application.load_tasks diff --git a/spec/mock_rails3_gem/app/controllers/contents_controller.rb b/spec/mock_rails3_gem/app/controllers/contents_controller.rb new file mode 100644 index 00000000..e3075438 --- /dev/null +++ b/spec/mock_rails3_gem/app/controllers/contents_controller.rb @@ -0,0 +1,85 @@ +class ContentsController < ApplicationController + # GET /contents + # GET /contents.xml + def index + @contents = Content.all + + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @contents } + end + end + + # GET /contents/1 + # GET /contents/1.xml + def show + @content = Content.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.xml { render :xml => @content } + end + end + + # GET /contents/new + # GET /contents/new.xml + def new + @content = Content.new + + respond_to do |format| + format.html # new.html.erb + format.xml { render :xml => @content } + end + end + + # GET /contents/1/edit + def edit + @content = Content.find(params[:id]) + end + + # POST /contents + # POST /contents.xml + def create + @content = Content.new(params[:content]) + + respond_to do |format| + if @content.save + flash[:notice] = 'Content was successfully created.' + format.html { redirect_to(@content) } + format.xml { render :xml => @content, :status => :created, :location => @content } + else + format.html { render :action => "new" } + format.xml { render :xml => @content.errors, :status => :unprocessable_entity } + end + end + end + + # PUT /contents/1 + # PUT /contents/1.xml + def update + @content = Content.find(params[:id]) + + respond_to do |format| + if @content.update_attributes(params[:content]) + flash[:notice] = 'Content was successfully updated.' + format.html { redirect_to(@content) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @content.errors, :status => :unprocessable_entity } + end + end + end + + # DELETE /contents/1 + # DELETE /contents/1.xml + def destroy + @content = Content.find(params[:id]) + @content.destroy + + respond_to do |format| + format.html { redirect_to(contents_url) } + format.xml { head :ok } + end + end +end diff --git a/spec/mock_rails3_gem/app/models/content.rb b/spec/mock_rails3_gem/app/models/content.rb new file mode 100644 index 00000000..b59924d8 --- /dev/null +++ b/spec/mock_rails3_gem/app/models/content.rb @@ -0,0 +1,2 @@ +class Content < ActiveRecord::Base +end diff --git a/spec/mock_rails3_gem/config/routes.rb b/spec/mock_rails3_gem/config/routes.rb index 4bb199ad..6b4b0cc7 100644 --- a/spec/mock_rails3_gem/config/routes.rb +++ b/spec/mock_rails3_gem/config/routes.rb @@ -1,4 +1,6 @@ MockRails3Gem::Application.routes.draw do |map| + resources :contents + # The priority is based upon order of creation: # first created -> highest priority. diff --git a/spec/mock_rails3_gem/db/schema.rb b/spec/mock_rails3_gem/db/schema.rb index b81ae5a5..8af0f80d 100644 --- a/spec/mock_rails3_gem/db/schema.rb +++ b/spec/mock_rails3_gem/db/schema.rb @@ -9,6 +9,12 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 0) do +ActiveRecord::Schema.define(:version => 20090826121911) do -end + create_table "contents", :force => true do |t| + t.string "title" + t.datetime "created_at" + t.datetime "updated_at" + end + +end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb index e579097e..f40cf0a7 100644 --- a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb +++ b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb @@ -64,14 +64,14 @@ # def rails_path(file) - File.join(RAILS_ROOT, file) + File.join(Rails.root, file) end def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(Rails.root, '/config/sitemap.rb')) end def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(RAILS_ROOT, '/config/sitemap.rb')) rescue nil + FileUtils.remove(File.join(Rails.root, '/config/sitemap.rb')) rescue nil end end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index e579097e..f40cf0a7 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -64,14 +64,14 @@ # def rails_path(file) - File.join(RAILS_ROOT, file) + File.join(Rails.root, file) end def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(RAILS_ROOT, '/config/sitemap.rb')) + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(Rails.root, '/config/sitemap.rb')) end def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(RAILS_ROOT, '/config/sitemap.rb')) rescue nil + FileUtils.remove(File.join(Rails.root, '/config/sitemap.rb')) rescue nil end end \ No newline at end of file From 8c784cf9cdfae720b37f116ce04ee2b6999fe63e Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 18:11:48 -0700 Subject: [PATCH 034/677] Fix UrlHelpers inclusion. Add database to Rails 3 app. --- .gitignore | 7 +-- lib/sitemap_generator/helper.rb | 6 +-- lib/sitemap_generator/link_set.rb | 2 + spec/mock_app_gem/db/data.yml | 49 ++++++++++++++++++ spec/mock_rails3_gem/.gitignore | 2 +- spec/mock_rails3_gem/db/data.yml | 49 ++++++++++++++++++ spec/mock_rails3_gem/db/development.sqlite3 | Bin 0 -> 10240 bytes .../migrate/20090826121911_create_contents.rb | 12 +++++ spec/mock_rails3_gem/db/schema.rb | 2 +- spec/mock_rails3_gem/db/test.sqlite3 | Bin 0 -> 10240 bytes .../spec/sitemap_generator_spec.rb | 6 +-- spec/sitemap_generator_spec.rb | 6 +-- tasks/sitemap_generator_tasks.rake | 12 +++++ 13 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 spec/mock_app_gem/db/data.yml create mode 100644 spec/mock_rails3_gem/db/data.yml create mode 100644 spec/mock_rails3_gem/db/development.sqlite3 create mode 100644 spec/mock_rails3_gem/db/migrate/20090826121911_create_contents.rb create mode 100644 spec/mock_rails3_gem/db/test.sqlite3 diff --git a/.gitignore b/.gitignore index c9f002c6..bf777173 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.swp pkg -test/mock_app_gem/vendor/gems/sitemap_generator-1.2.3 -test/mock_app_plugin/vendor/plugins/sitemap_generator -test/mock_rails3_gem/vendor/gems/* +spec/mock_app_gem/vendor/**/* +spec/mock_app_plugin/vendor/**/* +spec/mock_rails3_gem/vendor/**/* +tmp/**/* diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb index 6496df57..42bfbc92 100644 --- a/lib/sitemap_generator/helper.rb +++ b/lib/sitemap_generator/helper.rb @@ -1,9 +1,7 @@ -require 'action_controller' unless SitemapGenerator::RailsHelper.rails3? - module SitemapGenerator + # UrlHelpers are included by the rake tasks. This is not ideal, but should + # suffice until things are better organized. module Helper - include ActionController::UrlWriter unless SitemapGenerator::RailsHelper.rails3? - def self.included(base) base.class_eval do def self.default_url_options(options = nil) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 56f39f48..737eb717 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,3 +1,5 @@ +require 'builder' + module SitemapGenerator class LinkSet include SitemapGenerator::Helper diff --git a/spec/mock_app_gem/db/data.yml b/spec/mock_app_gem/db/data.yml new file mode 100644 index 00000000..bedb7849 --- /dev/null +++ b/spec/mock_app_gem/db/data.yml @@ -0,0 +1,49 @@ + +--- +contents: + columns: + - id + - title + - created_at + - updated_at + records: + - - "1" + - content 1...10 + - 2009-08-26 12:29:10 + - 2009-08-26 12:29:10 + - - "2" + - content 1 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "3" + - content 2 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "4" + - content 3 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "5" + - content 4 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "6" + - content 5 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "7" + - content 6 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "8" + - content 7 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "9" + - content 8 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "10" + - content 9 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 diff --git a/spec/mock_rails3_gem/.gitignore b/spec/mock_rails3_gem/.gitignore index af64fae5..dca0d5a7 100644 --- a/spec/mock_rails3_gem/.gitignore +++ b/spec/mock_rails3_gem/.gitignore @@ -1,4 +1,4 @@ .bundle -db/*.sqlite3 log/*.log tmp/**/* +public/sitemap* diff --git a/spec/mock_rails3_gem/db/data.yml b/spec/mock_rails3_gem/db/data.yml new file mode 100644 index 00000000..bedb7849 --- /dev/null +++ b/spec/mock_rails3_gem/db/data.yml @@ -0,0 +1,49 @@ + +--- +contents: + columns: + - id + - title + - created_at + - updated_at + records: + - - "1" + - content 1...10 + - 2009-08-26 12:29:10 + - 2009-08-26 12:29:10 + - - "2" + - content 1 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "3" + - content 2 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "4" + - content 3 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "5" + - content 4 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "6" + - content 5 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "7" + - content 6 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "8" + - content 7 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "9" + - content 8 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 + - - "10" + - content 9 + - 2009-08-26 12:29:56 + - 2009-08-26 12:29:56 diff --git a/spec/mock_rails3_gem/db/development.sqlite3 b/spec/mock_rails3_gem/db/development.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..89720a6b3f22030cd5a819de57bfbda12f3d699a GIT binary patch literal 10240 zcmeI2-;UBi6vk(0>8wN(H8ETfC*y_G{R1rpVG|Qk8$*^Yy9h?ENQKdD0!7-g@fPom zkK+^g96rH%u}|ROF3K#Ud+Tn_Nt@1mIp6fmuT3Xy`s%P5B%HpRL}NdpCA5JsMo%e4 z2*echd(|1HRfOvCOk~y~}$Un})KuoI-k){0fTV%b*BvNB?x zn4BQ7K>z_D00e*l5C8(#k-%Ne#Pafw7w`X>^mjzQkuRi29vCOaS7UDM8TW(;0R(^m z5C8&Km4K5;qx+@O`4@ItTRi=@i`8wrLM?mOuI-j93dy-4xvGj3h2(5VuE&ZLh2#*) zr7B0|G9*`7k)n{CwB&LWDGJHaC09w2qL3U-a@mR$h2*3pm!(KiNHv^B=AWxqP%k+Z9&<5k3;7`xdk~uHAi;FgrW-J)Fm6V>kZhXWY3Vio z0sT+)Z}3G^$hwGEX}^yRnR&^~y!q_mouBm+XX(o#n;S>3#Ey_s91#g2JpOD)l_zp5 zu|3{OmxoU{<<&glpZl_Q%L4=iAOHaf{1*Z%-}CEln{w4m(y6_V@z1CsKb~u4oXFDs7^up{{L~^9d?gR{eUD3wh?u z=b0`2x0LWU6-3?RzW&GNbZxTu!eouk!9hUbAfj;8?+2%qT77R%j(?xHoN+gtY`xBI*@SiO{feRo%Wd+}?}c#%4ry8Q0DQrJb^rPmJ0RIy^DKj9SH>^qay==g zgLQA_rjLLC1Rwwb2teQg384Q!pqGuSLI45~fB*zG0_*-S{U3rK1Oy-e0SG_<0{2qj usjLK@R_n0!rt`YpX&<)RkNKX`|1S7JKmY;|fB*y_a32MpR|8pW|MDl&p}Lg- literal 0 HcmV?d00001 diff --git a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb index f40cf0a7..a34301ea 100644 --- a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb +++ b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb @@ -64,14 +64,14 @@ # def rails_path(file) - File.join(Rails.root, file) + File.join(::Rails.root, file) end def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(Rails.root, '/config/sitemap.rb')) + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(::Rails.root, '/config/sitemap.rb')) end def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(Rails.root, '/config/sitemap.rb')) rescue nil + FileUtils.remove(File.join(::Rails.root, '/config/sitemap.rb')) rescue nil end end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index f40cf0a7..a34301ea 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -64,14 +64,14 @@ # def rails_path(file) - File.join(Rails.root, file) + File.join(::Rails.root, file) end def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(Rails.root, '/config/sitemap.rb')) + FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(::Rails.root, '/config/sitemap.rb')) end def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(Rails.root, '/config/sitemap.rb')) rescue nil + FileUtils.remove(File.join(::Rails.root, '/config/sitemap.rb')) rescue nil end end \ No newline at end of file diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index a679d47d..ffe961cd 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -21,6 +21,18 @@ namespace :sitemap do task 'refresh:no_ping' => ['sitemap:create'] task :create => [:environment] do + # TODO: Move away from auto-instantiating SitemapGenerator::Sitemap + # and move to a more natural Sitemap.new or similar. + if SitemapGenerator::RailsHelper.rails3? + SitemapGenerator::Sitemap.class_eval do + include Rails.application.routes.url_helpers + end + else + require 'action_controller' + SitemapGenerator::Sitemap.class_eval do + include ActionController::UrlWriter + end + end SitemapGenerator::Sitemap.create_files end end From 8cf78943c30e5deb068340d137df23eea5ea40a0 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 19:04:37 -0700 Subject: [PATCH 035/677] Version bump to 0.2.5 --- README.md | 105 +++++++++++++++++------------- Rakefile | 2 +- VERSION | 2 +- lib/sitemap_generator/link_set.rb | 1 + sitemap_generator.gemspec | 10 +-- 5 files changed, 69 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 55424875..ab46f3aa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ SitemapGenerator This plugin enables ['enterprise-class'][enterprise_class] Google Sitemaps to be easily generated for a Rails site as a rake task, using a simple 'Rails Routes'-like DSL. +**Now supporting Rails 3 as of version 0.2.5!** + Foreword ------- @@ -10,50 +12,22 @@ Unfortunately, Adam Salter passed away in 2009. Those who knew him know what an [Karl Varga](http://github.com/kjvarga) has taken over development of SitemapGenerator. The canonical repository is [http://github.com/kjvarga/sitemap_generator][canonical_repo] -Raison d'être -------- - -Most of the Sitemap plugins out there seem to try to recreate the Sitemap links by iterating the Rails routes. In some cases this is possible, but for a great deal of cases it isn't. +Installation +======= -a) There are probably quite a few routes in your routes file that don't need inclusion in the Sitemap. (AJAX routes I'm looking at you.) +**Rails 3:** -and +1. Add the gem to your Gemspec -b) How would you infer the correct series of links for the following route? + gem 'sitemap_generator' - map.zipcode 'location/:state/:city/:zipcode', :controller => 'zipcode', :action => 'index' +2. `$ rake sitemap:install` -Don't tell me it's trivial, because it isn't. It just looks trivial. - -So my idea is to have another file similar to 'routes.rb' called 'sitemap.rb', where you can define what goes into the Sitemap. - -Here's my solution: - - Zipcode.find(:all, :include => :city).each do |z| - sitemap.add zipcode_path(:state => z.city.state, :city => z.city, :zipcode => z) - end - -Easy hey? - -Other Sitemap settings for the link, like `lastmod`, `priority`, `changefreq` and `host` are entered automatically, although you can override them if you need to. - -Other "difficult" Sitemap issues, solved by this plugin: - -- Support for more than 50,000 urls (using a Sitemap Index file) -- Gzip of Sitemap files -- Variable priority of links -- Paging/sorting links (e.g. my_list?page=3) -- SSL host links (e.g. https:) -- Rails apps which are installed on a sub-path (e.g. example.com/blog_app/) - -Installation -======= - -**As a gem** +**Rails 2.x: As a gem** 1. Add the gem as a dependency in your config/environment.rb - config.gem 'sitemap_generator', :lib => false, :source => 'http://gemcutter.org' + config.gem 'sitemap_generator', :lib => false 2. `$ rake gems:install` @@ -67,11 +41,9 @@ Installation 4. `$ rake sitemap:install` -**As a plugin** - -1. Install plugin as normal +**Rails 2.x: As a plugin** - $ ./script/plugin install git://github.com/kjvarga/sitemap_generator.git +1. $ ./script/plugin install git://github.com/kjvarga/sitemap_generator.git ---- @@ -134,12 +106,55 @@ Example 'config/sitemap.rb' file = File.join(Rails.root, 'vendor/plugins/cadability_client/config/sitemap.rb') eval(open(file).read, binding, file) -Notes +Raison d'être +------- + +Most of the Sitemap plugins out there seem to try to recreate the Sitemap links by iterating the Rails routes. In some cases this is possible, but for a great deal of cases it isn't. + +a) There are probably quite a few routes in your routes file that don't need inclusion in the Sitemap. (AJAX routes I'm looking at you.) + +and + +b) How would you infer the correct series of links for the following route? + + map.zipcode 'location/:state/:city/:zipcode', :controller => 'zipcode', :action => 'index' + +Don't tell me it's trivial, because it isn't. It just looks trivial. + +So my idea is to have another file similar to 'routes.rb' called 'sitemap.rb', where you can define what goes into the Sitemap. + +Here's my solution: + + Zipcode.find(:all, :include => :city).each do |z| + sitemap.add zipcode_path(:state => z.city.state, :city => z.city, :zipcode => z) + end + +Easy hey? + +Other Sitemap settings for the link, like `lastmod`, `priority`, `changefreq` and `host` are entered automatically, although you can override them if you need to. + +Other "difficult" Sitemap issues, solved by this plugin: + +- Support for more than 50,000 urls (using a Sitemap Index file) +- Gzip of Sitemap files +- Variable priority of links +- Paging/sorting links (e.g. my_list?page=3) +- SSL host links (e.g. https:) +- Rails apps which are installed on a sub-path (e.g. example.com/blog_app/) + +Compatibility ======= -1) Tested/working on Rails 1.x.x <=> 2.x.x, no guarantees made for Rails 3.0. +Tested and working on: + +- **Rails** 3.0.0, sitemap_generator version >= 0.2.5 +- **Rails** 1.x - 2.3.5 sitemap_generator version < 0.2.5 +- **Ruby** 1.8.7, 1.9.1 + +Notes +======= -2) For large sitemaps it may be useful to split your generation into batches to avoid running out of memory. E.g.: +1) For large sitemaps it may be useful to split your generation into batches to avoid running out of memory. E.g.: # add movies Movie.find_in_batches(:batch_size => 1000) do |movies| @@ -148,7 +163,7 @@ Notes end end -3) New Capistrano deploys will remove your Sitemap files, unless you run `rake sitemap:refresh`. The way around this is to create a cap task: +2) New Capistrano deploys will remove your Sitemap files, unless you run `rake sitemap:refresh`. The way around this is to create a cap task: after "deploy:update_code", "deploy:copy_old_sitemap" @@ -158,7 +173,7 @@ Notes end end -4) If generation of your sitemap fails for some reason, the old sitemap will remain in public/. This ensures that robots will always find a valid sitemap. Running silently (`rake -s sitemap:refresh`) and with email forwarding setup you'll only get an email if your sitemap fails to build, and no notification when everything is fine - which will be most of the time. +3) If generation of your sitemap fails for some reason, the old sitemap will remain in public/. This ensures that robots will always find a valid sitemap. Running silently (`rake -s sitemap:refresh`) and with email forwarding setup you'll only get an email if your sitemap fails to build, and no notification when everything is fine - which will be most of the time. Known Bugs ======== diff --git a/Rakefile b/Rakefile index f3eb45ed..ee8895e5 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ begin Jeweler::Tasks.new do |gem| gem.name = "sitemap_generator" gem.summary = %Q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} - gem.description = %Q{Installs as a plugin or Gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} + gem.description = %Q{A Rails 3-compatible gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} gem.email = "kjvarga@gmail.com" gem.homepage = "http://github.com/kjvarga/sitemap_generator" gem.authors = ["Adam Salter", "Karl Varga"] diff --git a/VERSION b/VERSION index abd41058..3a4036fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.4 +0.2.5 diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 737eb717..6bc41224 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,4 +1,5 @@ require 'builder' +require 'action_view' module SitemapGenerator class LinkSet diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index c82b1b24..fd159df6 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,12 +5,12 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.2.4" + s.version = "0.2.5" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-04-01} - s.description = %q{Installs as a plugin or Gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} + s.date = %q{2010-04-19} + s.description = %q{A Rails 3-compatible gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ "README.md" @@ -25,6 +25,8 @@ Gem::Specification.new do |s| "lib/sitemap_generator/link.rb", "lib/sitemap_generator/link_set.rb", "lib/sitemap_generator/mapper.rb", + "lib/sitemap_generator/rails_helper.rb", + "lib/sitemap_generator/railtie.rb", "lib/sitemap_generator/tasks.rb", "rails/install.rb", "rails/uninstall.rb", @@ -36,7 +38,7 @@ Gem::Specification.new do |s| s.homepage = %q{http://github.com/kjvarga/sitemap_generator} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.5} + s.rubygems_version = %q{1.3.6} s.summary = %q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} if s.respond_to? :specification_version then From ed7634168310009d2fc8c7bf26f04b1bfe3246ba Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 19 Apr 2010 19:36:07 -0700 Subject: [PATCH 036/677] Explicitly require rspec 1.3.0 so we don't load 2.0 which is only for Rails 3 --- Rakefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Rakefile b/Rakefile index ee8895e5..8b36bd2f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,7 @@ require 'rake' require 'rake/rdoctask' +require 'rubygems' +gem 'rspec', '1.3.0' require 'spec/rake/spectask' begin From 0dd0a44a4dba50cf58098de96e2ec2b29ed99607 Mon Sep 17 00:00:00 2001 From: Alexandre Bini Date: Tue, 27 Apr 2010 14:59:51 -0300 Subject: [PATCH 037/677] adding images on sitemaps: http://www.google.com/support/webmasters/bin/answer.py?answer=178636 --- README.md | 27 ++++++++++++++--------- lib/sitemap_generator/link.rb | 15 ++++++++++--- lib/sitemap_generator/mapper.rb | 7 +++--- tasks/sitemap_generator_tasks.rake | 5 +++-- templates/sitemap_index.builder | 4 ++-- templates/xml_sitemap.builder | 35 +++++++++++++++++++++--------- 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ab46f3aa..06c77226 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Installation gem 'sitemap_generator' 2. `$ rake sitemap:install` - + **Rails 2.x: As a gem** 1. Add the gem as a dependency in your config/environment.rb @@ -56,7 +56,7 @@ Sitemaps with many urls (100,000+) take quite a long time to generate, so if you Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file. Sitemap: /sitemap_index.xml.gz - + The Sitemap URL in the robots file should be the complete URL to the Sitemap Index, such as http://www.example.org/sitemap_index.xml.gz @@ -75,12 +75,12 @@ Example 'config/sitemap.rb' # Usage: sitemap.add path, options # (default options are used if you don't specify them) # - # Defaults: :priority => 0.5, :changefreq => 'weekly', + # Defaults: :priority => 0.5, :changefreq => 'weekly', # :lastmod => Time.now, :host => default_host - + # Examples: - + # add '/articles' sitemap.add articles_path, :priority => 0.7, :changefreq => 'daily' @@ -91,25 +91,30 @@ Example 'config/sitemap.rb' # add merchant path sitemap.add '/purchase', :priority => 0.7, :host => "https://www.example.com" - + + # add all individual news with images + News.all.each do |n| + sitemap.add news_path(n), :lastmod => n.updated_at, :images=>n.images.collect{ |r| :loc=>r.image.url, :title=>r.image.name } + end + end # Including Sitemaps from Rails Engines. # - # These Sitemaps should be almost identical to a regular Sitemap file except + # These Sitemaps should be almost identical to a regular Sitemap file except # they needn't define their own SitemapGenerator::Sitemap.default_host since # they will undoubtedly share the host name of the application they belong to. # # As an example, say we have a Rails Engine in vendor/plugins/cadability_client # We can include its Sitemap here as follows: - # + # file = File.join(Rails.root, 'vendor/plugins/cadability_client/config/sitemap.rb') eval(open(file).read, binding, file) Raison d'être ------- -Most of the Sitemap plugins out there seem to try to recreate the Sitemap links by iterating the Rails routes. In some cases this is possible, but for a great deal of cases it isn't. +Most of the Sitemap plugins out there seem to try to recreate the Sitemap links by iterating the Rails routes. In some cases this is possible, but for a great deal of cases it isn't. a) There are probably quite a few routes in your routes file that don't need inclusion in the Sitemap. (AJAX routes I'm looking at you.) @@ -118,7 +123,7 @@ and b) How would you infer the correct series of links for the following route? map.zipcode 'location/:state/:city/:zipcode', :controller => 'zipcode', :action => 'index' - + Don't tell me it's trivial, because it isn't. It just looks trivial. So my idea is to have another file similar to 'routes.rb' called 'sitemap.rb', where you can define what goes into the Sitemap. @@ -206,3 +211,5 @@ Copyright (c) 2009 Adam @ [Codebright.net][cb], released under the MIT license [sitemap_generator_usage]:http://wiki.github.com/adamsalter/sitemap_generator/sitemapgenerator-usage "http://wiki.github.com/adamsalter/sitemap_generator/sitemapgenerator-usage" [boost_juice]:http://www.boostjuice.com.au/ "Mmmm, sweet, sweet Boost Juice." [cb]:http://codebright.net "http://codebright.net" +[sitemap_images]:http://www.google.com/support/webmasters/bin/answer.py?answer=178636 + diff --git a/lib/sitemap_generator/link.rb b/lib/sitemap_generator/link.rb index 8cad01e6..eaf83b86 100644 --- a/lib/sitemap_generator/link.rb +++ b/lib/sitemap_generator/link.rb @@ -1,9 +1,16 @@ - module SitemapGenerator class Link class << self def generate(path, options = {}) - options.assert_valid_keys(:priority, :changefreq, :lastmod, :host) + options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) + + unless options[:images].blank? + options[:images].each do |r| + r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) + r[:loc] = URI.join(Sitemap.default_host, r[:loc]).to_s unless r[:loc].blank? + end + end + options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host) { :path => path, @@ -11,9 +18,11 @@ def generate(path, options = {}) :changefreq => options[:changefreq], :lastmod => options[:lastmod], :host => options[:host], - :loc => URI.join(options[:host], path).to_s + :loc => URI.join(options[:host], path).to_s, + :images => options[:images] } end end end end + diff --git a/lib/sitemap_generator/mapper.rb b/lib/sitemap_generator/mapper.rb index 16cb1302..bbd35fe1 100644 --- a/lib/sitemap_generator/mapper.rb +++ b/lib/sitemap_generator/mapper.rb @@ -3,13 +3,14 @@ module SitemapGenerator # The object passed to the add_links block in config/sitemap.rb is a Generator instance. class Mapper attr_accessor :set - + def initialize(set) @set = set end - + def add(loc, options = {}) set.add_link Link.generate(loc, options) end end -end \ No newline at end of file +end + diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index ffe961cd..47599f64 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,5 +1,5 @@ require 'zlib' -require 'sitemap_generator' +#require 'sitemap_generator' namespace :sitemap do desc "Install a default config/sitemap.rb file" @@ -32,7 +32,8 @@ namespace :sitemap do SitemapGenerator::Sitemap.class_eval do include ActionController::UrlWriter end - end + end SitemapGenerator::Sitemap.create_files end end + diff --git a/templates/sitemap_index.builder b/templates/sitemap_index.builder index 8d45c502..39b584bc 100644 --- a/templates/sitemap_index.builder +++ b/templates/sitemap_index.builder @@ -1,4 +1,3 @@ - # # # @@ -19,4 +18,5 @@ xml.sitemapindex "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do xml.lastmod w3c_date(File.mtime(file)) end end -end \ No newline at end of file +end + diff --git a/templates/xml_sitemap.builder b/templates/xml_sitemap.builder index ec4fd25f..9ee16c09 100644 --- a/templates/xml_sitemap.builder +++ b/templates/xml_sitemap.builder @@ -1,15 +1,30 @@ xml.instruct! -xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", - "xsi:schemaLocation" => "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd", +xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", + "xsi:schemaLocation" => "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd", + "xmlns:image" => "http://www.google.com/schemas/sitemap-image/1.1", "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do - links.each do |link| - xml.url do - xml.loc link[:loc] - xml.lastmod w3c_date(link[:lastmod]) if link[:lastmod] - xml.changefreq link[:changefreq] if link[:changefreq] - xml.priority link[:priority] if link[:priority] - end - end + links.each do |link| + xml.url do + xml.loc link[:loc] + xml.lastmod w3c_date(link[:lastmod]) if link[:lastmod] + xml.changefreq link[:changefreq] if link[:changefreq] + xml.priority link[:priority] if link[:priority] + + unless link[:images].blank? + link[:images].each do |image| + xml.image:image do + xml.image :loc, image[:loc] if image[:loc] + xml.image :caption, image[:caption] if image[:caption] + xml.image :geo_location, image[:geo_location] if image[:geo_location] + xml.image :title, image[:title] if image[:title] + xml.image :license, image[:license] if image[:license] + end + end + end + + end + end end + From 89b2f145be25ae475bb1dd29feb742ce8a1c3a92 Mon Sep 17 00:00:00 2001 From: Alexandre Bini Date: Mon, 3 May 2010 09:42:35 -0300 Subject: [PATCH 038/677] fixing the sitemap limit size --- lib/sitemap_generator/link.rb | 21 +++++++++++-------- lib/sitemap_generator/link_set.rb | 9 +++++++- tasks/sitemap_generator_tasks.rake | 6 +++++- templates/xml_sitemap.builder | 33 +++++++++++++++++++----------- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/lib/sitemap_generator/link.rb b/lib/sitemap_generator/link.rb index eaf83b86..09235e16 100644 --- a/lib/sitemap_generator/link.rb +++ b/lib/sitemap_generator/link.rb @@ -3,14 +3,7 @@ class Link class << self def generate(path, options = {}) options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) - - unless options[:images].blank? - options[:images].each do |r| - r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) - r[:loc] = URI.join(Sitemap.default_host, r[:loc]).to_s unless r[:loc].blank? - end - end - + prepare_images options[:images] options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host) { :path => path, @@ -22,6 +15,18 @@ def generate(path, options = {}) :images => options[:images] } end + + def prepare_images(images) + unless images.blank? + images.delete_if{|key,value| key[:loc]==nil} + images.each do |r| + r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) + r[:loc] = URI.join(Sitemap.default_host, r[:loc]).to_s + end + end + images = images[0..1000] rescue [] + end + end end end diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 6bc41224..04d0f4fc 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -60,13 +60,19 @@ def write_pending # Write links to sitemap file. def write_sitemap(file = upcoming_file) + slice_index = 0 buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) filename = File.join(Rails.root, "public", file) write_file(filename, buffer) show_progress("Sitemap", filename, buffer) if verbose - links.clear + if slice_index==0 + links.clear + else + links.slice! slice_index, links.size + end + sitemaps.push filename end @@ -180,3 +186,4 @@ def create_files(verbose = true) end end end + diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index 47599f64..f2994bec 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,5 +1,9 @@ require 'zlib' -#require 'sitemap_generator' +begin + require 'sitemap_generator' +rescue LoadError, NameError + # Application should work without vlad +end namespace :sitemap do desc "Install a default config/sitemap.rb file" diff --git a/templates/xml_sitemap.builder b/templates/xml_sitemap.builder index 9ee16c09..fe02180e 100644 --- a/templates/xml_sitemap.builder +++ b/templates/xml_sitemap.builder @@ -4,25 +4,34 @@ xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns:image" => "http://www.google.com/schemas/sitemap-image/1.1", "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do - links.each do |link| - xml.url do - xml.loc link[:loc] - xml.lastmod w3c_date(link[:lastmod]) if link[:lastmod] - xml.changefreq link[:changefreq] if link[:changefreq] - xml.priority link[:priority] if link[:priority] + links.each_with_index do |link,index| + puts "#{index}/#{links.size}" + buffer_url = "" + url = Builder::XmlMarkup.new(:target=>buffer_url) + url.url do + url.loc link[:loc] + url.lastmod w3c_date(link[:lastmod]) if link[:lastmod] + url.changefreq link[:changefreq] if link[:changefreq] + url.priority link[:priority] if link[:priority] unless link[:images].blank? link[:images].each do |image| - xml.image:image do - xml.image :loc, image[:loc] if image[:loc] - xml.image :caption, image[:caption] if image[:caption] - xml.image :geo_location, image[:geo_location] if image[:geo_location] - xml.image :title, image[:title] if image[:title] - xml.image :license, image[:license] if image[:license] + url.image:image do + url.image :loc, image[:loc] + url.image :caption, image[:caption] if image[:caption] + url.image :geo_location, image[:geo_location] if image[:geo_location] + url.image :title, image[:title] if image[:title] + url.image :license, image[:license] if image[:license] end end end + end + if (buffer+buffer_url).size < 10.megabytes + xml << buffer_url + else + slice_index = index + break end end From 70e8bee4ecaff9bcdf78f3ae67268ce9e11c98f7 Mon Sep 17 00:00:00 2001 From: Alexandre Bini Date: Mon, 3 May 2010 13:43:08 -0300 Subject: [PATCH 039/677] fixing file size --- lib/sitemap_generator/link_set.rb | 2 ++ templates/xml_sitemap.builder | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 04d0f4fc..bb8c4aa5 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -184,6 +184,8 @@ def create_files(verbose = true) stop_time = Time.now puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose end + + end end diff --git a/templates/xml_sitemap.builder b/templates/xml_sitemap.builder index fe02180e..8d481b8f 100644 --- a/templates/xml_sitemap.builder +++ b/templates/xml_sitemap.builder @@ -34,6 +34,5 @@ xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", break end end - end From e73ebbb07d247ed56d1140e515cbbcea8d200c5a Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 6 May 2010 12:10:34 -0700 Subject: [PATCH 040/677] Improve image handling * Limit to 1000 images * Use the host specified, not the default --- lib/sitemap_generator.rb | 5 +++-- lib/sitemap_generator/link.rb | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 3981df1a..8589a9d8 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -10,9 +10,10 @@ module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip MAX_ENTRIES = 50_000 + MAX_IMAGES = 1_000 Sitemap = LinkSet.new end - + class << self attr_accessor :root, :templates end @@ -22,5 +23,5 @@ class << self :sitemap_index => File.join(self.root, 'templates/sitemap_index.builder'), :sitemap_xml => File.join(self.root, 'templates/xml_sitemap.builder'), :sitemap_sample => File.join(self.root, 'templates/sitemap.rb'), - } + } end \ No newline at end of file diff --git a/lib/sitemap_generator/link.rb b/lib/sitemap_generator/link.rb index 09235e16..0bd8d18f 100644 --- a/lib/sitemap_generator/link.rb +++ b/lib/sitemap_generator/link.rb @@ -3,8 +3,7 @@ class Link class << self def generate(path, options = {}) options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) - prepare_images options[:images] - options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host) + options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host, :images => []) { :path => path, :priority => options[:priority], @@ -12,21 +11,20 @@ def generate(path, options = {}) :lastmod => options[:lastmod], :host => options[:host], :loc => URI.join(options[:host], path).to_s, - :images => options[:images] + :images => prepare_images(options[:images], options[:host]) } end - def prepare_images(images) - unless images.blank? - images.delete_if{|key,value| key[:loc]==nil} - images.each do |r| - r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) - r[:loc] = URI.join(Sitemap.default_host, r[:loc]).to_s - end + # Maximum 1000 images. loc is required. + # ?? Does the image URL have to be on the same host? + def prepare_images(images, host) + images.delete_if { |key,value| key[:loc] == nil } + images.each do |r| + r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) + r[:loc] = URI.join(host, r[:loc]).to_s end - images = images[0..1000] rescue [] + images[0..(SitemapGenerator::MAX_IMAGES-1)] end - end end end From f3dfa24fe9a139c90cc9ae039be7ba0092d215da Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 6 May 2010 12:35:23 -0700 Subject: [PATCH 041/677] Revert API changes except for the template handling. --- lib/sitemap_generator.rb | 4 +- lib/sitemap_generator/link_set.rb | 182 +++++++++++++++++++++- lib/sitemap_generator/link_set/builder.rb | 29 ---- lib/sitemap_generator/link_set/writer.rb | 157 ------------------- lib/sitemap_generator/mapper.rb | 15 ++ 5 files changed, 196 insertions(+), 191 deletions(-) delete mode 100644 lib/sitemap_generator/link_set/builder.rb delete mode 100644 lib/sitemap_generator/link_set/writer.rb create mode 100644 lib/sitemap_generator/mapper.rb diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 74780d7f..e3f686bd 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -1,6 +1,6 @@ +require 'sitemap_generator/mapper' require 'sitemap_generator/link' require 'sitemap_generator/link_set' -require 'sitemap_generator/link_set/builder' require 'sitemap_generator/helper' require 'sitemap_generator/templates' @@ -8,7 +8,7 @@ module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip MAX_ENTRIES = 50_000 - Sitemap = SitemapGenerator::LinkSet::Builder.new + Sitemap = LinkSet.new end class << self diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 2792ca7c..e6d9849e 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,5 +1,181 @@ +require File.dirname(__FILE__) + '/helper' + module SitemapGenerator - module LinkSet - + class LinkSet + include SitemapGenerator::Helper + include ActionView::Helpers::NumberHelper + + attr_accessor :default_host, :yahoo_app_id, :links + attr_accessor :sitemaps + attr_accessor :max_entries + attr_accessor :link_count + + alias :sitemap_files :sitemaps + + # Create new link set instance. + def initialize + self.links = [] + self.sitemaps = [] + self.max_entries = SitemapGenerator::MAX_ENTRIES + self.link_count = 0 + end + + # Add default links to sitemap files. + def add_default_links + links.push Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + links.push Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + self.link_count += 2 + end + + # Add links to sitemap files passing a block. + def add_links + raise ArgumentError, "Default hostname not set" if default_host.blank? + add_default_links if first_link? + yield Mapper.new(self) + end + + # Add links from mapper to sitemap files. + def add_link(link) + write_upcoming if enough_links? + links.push link + self.link_count += 1 + end + + # Write links to sitemap file. + def write + write_pending + end + + # Write links to upcoming sitemap file. + def write_upcoming + write_sitemap(upcoming_file) + end + + # Write pending links to sitemap, write index file if needed. + def write_pending + write_upcoming + write_index + end + + # Write links to sitemap file. + def write_sitemap(file = upcoming_file) + buffer = "" + xml = Builder::XmlMarkup.new(:target => buffer) + eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) + filename = File.join(RAILS_ROOT, "public", file) + write_file(filename, buffer) + show_progress("Sitemap", filename, buffer) if verbose + links.clear + sitemaps.push filename + end + + # Write sitemap links to sitemap index file. + def write_index + buffer = "" + xml = Builder::XmlMarkup.new(:target => buffer) + eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) + filename = File.join(RAILS_ROOT, "public", index_file) + write_file(filename, buffer) + show_progress("Sitemap Index", filename, buffer) if verbose + links.clear + sitemaps.clear + end + + # Return sitemap or sitemap index main name. + def index_file + "sitemap_index.xml.gz" + end + + # Return upcoming sitemap name with index. + def upcoming_file + "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? + end + + # Return upcoming sitemap index, first is 1. + def upcoming_index + sitemaps.length + 1 unless enough_sitemaps? + end + + # Return true if upcoming is first sitemap. + def first_sitemap? + sitemaps.empty? + end + + # Return true if sitemap index needed. + def multiple_sitemaps? + !first_sitemap? + end + + # Return true if more sitemaps can be added. + def more_sitemaps? + sitemaps.length < max_entries + end + + # Return true if no sitemaps can be added. + def enough_sitemaps? + !more_sitemaps? + end + + # Return true if this is the first link added. + def first_link? + links.empty? && first_sitemap? + end + + # Return true if more links can be added. + def more_links? + links.length < max_entries + end + + # Return true if no further links can be added. + def enough_links? + !more_links? + end + + # Commit buffer to gzipped file. + def write_file(name, buffer) + Zlib::GzipWriter.open(name) { |gz| gz.write buffer } + end + + # Report progress line. + def show_progress(title, filename, buffer) + puts "+ #{filename}" + puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes + end + + # Copy templates/sitemap.rb to config if not there yet. + def install_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "already exists: config/sitemap.rb, file not copied" + else + FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) + puts "created: config/sitemap.rb" + end + end + + # Remove config/sitemap.rb if exists. + def uninstall_sitemap_rb + if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) + File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) + end + end + + # Clean sitemap files in output directory. + def clean_files + FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + end + + # Ping search engines passing sitemap location. + def ping_search_engines + super index_file + end + + # Create sitemap files in output directory. + def create_files(verbose = true) + start_time = Time.now + load_sitemap_rb + write + stop_time = Time.now + puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + end end -end \ No newline at end of file +end diff --git a/lib/sitemap_generator/link_set/builder.rb b/lib/sitemap_generator/link_set/builder.rb deleted file mode 100644 index ad2a5d4c..00000000 --- a/lib/sitemap_generator/link_set/builder.rb +++ /dev/null @@ -1,29 +0,0 @@ -module SitemapGenerator - module LinkSet - # The object passed to SitemapGenerator::Sitemap.add_links block in - # config/sitemap.rb is a SetBuilder instance. - class Builder - attr_accessor :host, :default_host - - # Add links to sitemap files. - # - # Pass a block which takes as its argument a LinkSet::Builder instance. - # - # Pass optional host list of host symbols (or a single symbol) - # to add the links to sitemap files for those hosts. - def add_links(host) - @host = host.is_a?(Array) ? host : [host || default_host].compact! - raise ArgumentError, "Default hostname not set" if @host.empty? - - set = LinkSet.new - add_default_links if first_link? - yield Mapper.new(self) - end - - # Clean sitemap files from output directory. - #def clean_files - # FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) - #end - end - end -end \ No newline at end of file diff --git a/lib/sitemap_generator/link_set/writer.rb b/lib/sitemap_generator/link_set/writer.rb deleted file mode 100644 index 4b444ae8..00000000 --- a/lib/sitemap_generator/link_set/writer.rb +++ /dev/null @@ -1,157 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -module SitemapGenerator - module LinkSet - module Writer - include SitemapGenerator::Helper - include ActionView::Helpers::NumberHelper - - attr_accessor :default_host, :yahoo_app_id, :links - attr_accessor :sitemaps, :max_entries, :link_count - attr_accessor :host - - alias :sitemap_files :sitemaps - - # Create new link set instance. - # - # Optional host is the host symbol. Defines the subdirectory - # in which to place sitemap files. If nil, no subdirectory is used. - def initialize(host) - self.host = host.to_s - self.links = [] - self.sitemaps = [] - self.max_entries = SitemapGenerator::MAX_ENTRIES - self.link_count = 0 - end - - # Add default links to sitemap files. - def add_default_links - add_link Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - add_link Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - end - - # Add links from mapper to sitemap files. - def add_link(link) - write_upcoming if enough_links? - links.push link - self.link_count += 1 - end - - # Write links to sitemap file. - def write - write_pending - end - - # Write links to upcoming sitemap file. - def write_upcoming - write_sitemap(upcoming_file) - end - - # Write pending links to sitemap, write index file if needed. - def write_pending - write_upcoming - write_index - end - - # Write links to sitemap file. - def write_sitemap(file = upcoming_file) - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_xml, binding) - filename = File.join(RAILS_ROOT, "tmp", file) - write_file(filename, buffer) - show_progress("Sitemap", filename, buffer) if verbose - links.clear - sitemaps.push filename - end - - # Write sitemap links to sitemap index file. - def write_index - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_index, binding) - filename = File.join(RAILS_ROOT, "public", index_file) - write_file(filename, buffer) - show_progress("Sitemap Index", filename, buffer) if verbose - links.clear - sitemaps.clear - end - - # Return sitemap or sitemap index main name. - def index_file - "sitemap_index.xml.gz" - end - - # Return upcoming sitemap name with index. - def upcoming_file - "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? - end - - # Return upcoming sitemap index, first is 1. - def upcoming_index - sitemaps.length + 1 unless enough_sitemaps? - end - - # Return true if upcoming is first sitemap. - def first_sitemap? - sitemaps.empty? - end - - # Return true if sitemap index needed. - def multiple_sitemaps? - !first_sitemap? - end - - # Return true if more sitemaps can be added. - def more_sitemaps? - sitemaps.length < max_entries - end - - # Return true if no sitemaps can be added. - def enough_sitemaps? - !more_sitemaps? - end - - # Return true if this is the first link added. - def first_link? - links.empty? && first_sitemap? - end - - # Return true if more links can be added. - def more_links? - links.length < max_entries - end - - # Return true if no further links can be added. - def enough_links? - !more_links? - end - - # Commit buffer to gzipped file. - def write_file(name, buffer) - Zlib::GzipWriter.open(name) { |gz| gz.write buffer } - end - - # Report progress line. - def show_progress(title, filename, buffer) - puts "+ #{filename}" - puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes - end - - - # Ping search engines passing sitemap location. - def ping_search_engines - super index_file - end - - # Create sitemap files in output directory. - def create_files(verbose = true) - start_time = Time.now - load_sitemap_rb - write - stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose - end - end - end -end diff --git a/lib/sitemap_generator/mapper.rb b/lib/sitemap_generator/mapper.rb new file mode 100644 index 00000000..16cb1302 --- /dev/null +++ b/lib/sitemap_generator/mapper.rb @@ -0,0 +1,15 @@ +module SitemapGenerator + # Generator instances are used to build links. + # The object passed to the add_links block in config/sitemap.rb is a Generator instance. + class Mapper + attr_accessor :set + + def initialize(set) + @set = set + end + + def add(loc, options = {}) + set.add_link Link.generate(loc, options) + end + end +end \ No newline at end of file From e9413f8b735b3fc11c68d5a9db2afbf2e33c6997 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 6 May 2010 13:12:15 -0700 Subject: [PATCH 042/677] Fix templates. Move more stuff to Utilities. --- lib/sitemap_generator.rb | 3 ++- lib/sitemap_generator/link_set.rb | 26 ++------------------------ lib/sitemap_generator/templates.rb | 21 +++++++++++++-------- lib/sitemap_generator/utilities.rb | 10 ++++++++-- rails/install.rb | 2 +- rails/uninstall.rb | 2 +- sitemap_generator.gemspec | 4 +++- tasks/sitemap_generator_tasks.rake | 4 ++-- 8 files changed, 32 insertions(+), 40 deletions(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index e3f686bd..8488d1e4 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -3,6 +3,7 @@ require 'sitemap_generator/link_set' require 'sitemap_generator/helper' require 'sitemap_generator/templates' +require 'sitemap_generator/utilities' module SitemapGenerator silence_warnings do @@ -10,7 +11,7 @@ module SitemapGenerator MAX_ENTRIES = 50_000 Sitemap = LinkSet.new end - + class << self attr_accessor :root, :templates end diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index e6d9849e..b6fd4e81 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -61,7 +61,7 @@ def write_pending def write_sitemap(file = upcoming_file) buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_xml]), binding) + eval(SitemapGenerator.templates.sitemap_xml, binding) filename = File.join(RAILS_ROOT, "public", file) write_file(filename, buffer) show_progress("Sitemap", filename, buffer) if verbose @@ -73,7 +73,7 @@ def write_sitemap(file = upcoming_file) def write_index buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(File.read(SitemapGenerator.templates[:sitemap_index]), binding) + eval(SitemapGenerator.templates.sitemap_index, binding) filename = File.join(RAILS_ROOT, "public", index_file) write_file(filename, buffer) show_progress("Sitemap Index", filename, buffer) if verbose @@ -142,28 +142,6 @@ def show_progress(title, filename, buffer) puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes end - # Copy templates/sitemap.rb to config if not there yet. - def install_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "already exists: config/sitemap.rb, file not copied" - else - FileUtils.cp(SitemapGenerator.templates[:sitemap_sample], File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "created: config/sitemap.rb" - end - end - - # Remove config/sitemap.rb if exists. - def uninstall_sitemap_rb - if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) - end - end - - # Clean sitemap files in output directory. - def clean_files - FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) - end - # Ping search engines passing sitemap location. def ping_search_engines super index_file diff --git a/lib/sitemap_generator/templates.rb b/lib/sitemap_generator/templates.rb index 20939f12..fbdd6f97 100644 --- a/lib/sitemap_generator/templates.rb +++ b/lib/sitemap_generator/templates.rb @@ -11,7 +11,8 @@ class Templates :sitemap_xml => 'xml_sitemap.builder', :sitemap_sample => 'sitemap.rb', } - + + # Dynamically define accessors for each key defined in FILES attr_accessor *FILES.keys FILES.keys.each do |name| eval <<-END @@ -20,19 +21,23 @@ class Templates end END end - + def initialize(root = SitemapGenerator.root) @root = root end - - def template_path(file) - File.join(@root, 'templates', file) + + # Return the full path to a template. + # + # file template symbol e.g. :sitemap_index + def template_path(template) + File.join(@root, 'templates', self.class::FILES[template]) end - + protected - + + # Read the template file and return its contents. def read_template(template) - File.read(template_path(self.class::FILES[template])) + File.read(template_path(template)) end end end \ No newline at end of file diff --git a/lib/sitemap_generator/utilities.rb b/lib/sitemap_generator/utilities.rb index e19fe179..423b16e2 100644 --- a/lib/sitemap_generator/utilities.rb +++ b/lib/sitemap_generator/utilities.rb @@ -1,5 +1,6 @@ module SitemapGenerator module Utilities + extend self # Copy templates/sitemap.rb to config if not there yet. def install_sitemap_rb @@ -7,7 +8,7 @@ def install_sitemap_rb puts "already exists: config/sitemap.rb, file not copied" else FileUtils.cp( - SitemapGenerator.templates.template_path(:sitemap_sample), + SitemapGenerator.templates.template_path(:sitemap_sample), File.join(RAILS_ROOT, 'config/sitemap.rb')) puts "created: config/sitemap.rb" end @@ -18,6 +19,11 @@ def uninstall_sitemap_rb if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) File.rm(File.join(RAILS_ROOT, 'config/sitemap.rb')) end - end + end + + # Clean sitemap files in output directory. + def clean_files + FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) + end end end \ No newline at end of file diff --git a/rails/install.rb b/rails/install.rb index 9d2abd61..30db915d 100644 --- a/rails/install.rb +++ b/rails/install.rb @@ -1,2 +1,2 @@ # Install hook code here -SitemapGenerator::Sitemap.install_sitemap_rb +SitemapGenerator::Utilities.install_sitemap_rb diff --git a/rails/uninstall.rb b/rails/uninstall.rb index 2715ac46..db822434 100644 --- a/rails/uninstall.rb +++ b/rails/uninstall.rb @@ -1,2 +1,2 @@ # Uninstall hook code here -SitemapGenerator::Sitemap.uninstall_sitemap_rb +SitemapGenerator::Utilities.uninstall_sitemap_rb diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index c82b1b24..7e13c641 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-04-01} + s.date = %q{2010-05-06} s.description = %q{Installs as a plugin or Gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ @@ -26,6 +26,8 @@ Gem::Specification.new do |s| "lib/sitemap_generator/link_set.rb", "lib/sitemap_generator/mapper.rb", "lib/sitemap_generator/tasks.rb", + "lib/sitemap_generator/templates.rb", + "lib/sitemap_generator/utilities.rb", "rails/install.rb", "rails/uninstall.rb", "tasks/sitemap_generator_tasks.rake", diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index a679d47d..603341da 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -4,12 +4,12 @@ require 'sitemap_generator' namespace :sitemap do desc "Install a default config/sitemap.rb file" task :install do - SitemapGenerator::Sitemap.install_sitemap_rb + SitemapGenerator::Utilities.install_sitemap_rb end desc "Delete all Sitemap files in public/ directory" task :clean do - SitemapGenerator::Sitemap.clean_files + SitemapGenerator::Utilities.clean_files end desc "Create Sitemap XML files in public/ directory (rake -s for no output)" From 79ae09289816579dfb581323133cd99d4fbfce69 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 6 May 2010 14:10:09 -0700 Subject: [PATCH 043/677] Set encoding on builder files to prevent problems on 1.9.1. Try to get tests passing...still broken. --- Rakefile | 14 +++++++------- sitemap_generator.gemspec | 4 +++- spec/sitemap_generator_spec.rb | 24 ++++++++++++------------ spec/support/file_macros.rb | 16 +++++++++------- templates/sitemap_index.builder | 1 + templates/xml_sitemap.builder | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Rakefile b/Rakefile index 8b36bd2f..d0baeac5 100644 --- a/Rakefile +++ b/Rakefile @@ -28,19 +28,19 @@ namespace :test do task :gem => ['test:prepare:gem', 'multi_spec'] task :plugin => ['test:prepare:plugin', 'multi_spec'] task :rails3 => ['test:prepare:rails3', 'multi_spec'] - + task :multi_spec do Rake::Task['spec'].invoke Rake::Task['spec'].reenable end - + namespace :prepare do task :gem do ENV["SITEMAP_RAILS"] = 'gem' prepare_path(local_path('spec/mock_app_gem/vendor/gems/sitemap_generator-1.2.3')) rm_rf(local_path('spec/mock_app_gem/public/sitemap*')) end - + task :plugin do ENV["SITEMAP_RAILS"] = 'plugin' prepare_path(local_path('spec/mock_app_plugin/vendor/plugins/sitemap_generator-1.2.3')) @@ -51,11 +51,11 @@ namespace :test do ENV["SITEMAP_RAILS"] = 'rails3' rm_rf(local_path('spec/mock_rails3_gem/public/sitemap*')) end - + def local_path(path) File.join(File.dirname(__FILE__), path) end - + def prepare_path(path) rm_rf path mkdir_p path @@ -64,8 +64,8 @@ namespace :test do end end -desc "Run all tests both as a plugin and gem" -task :test => ['test:plugin', 'test:gem'] +desc "Run tests as a gem install" +task :test => ['test:gem'] Spec::Rake::SpecTask.new(:spec) do |spec| spec.libs << 'lib' << 'spec' diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index fd159df6..2490cff7 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-04-19} + s.date = %q{2010-05-06} s.description = %q{A Rails 3-compatible gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ @@ -28,6 +28,8 @@ Gem::Specification.new do |s| "lib/sitemap_generator/rails_helper.rb", "lib/sitemap_generator/railtie.rb", "lib/sitemap_generator/tasks.rb", + "lib/sitemap_generator/templates.rb", + "lib/sitemap_generator/utilities.rb", "rails/install.rb", "rails/uninstall.rb", "tasks/sitemap_generator_tasks.rake", diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index d75c8cdb..858704c1 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -15,7 +15,7 @@ end context "fresh install" do - before :all do + before :each do delete_sitemap_file_from_rails_app Rake::Task['sitemap:install'].invoke end @@ -28,19 +28,19 @@ sitemap_template = SitemapGenerator.templates.template_path(:sitemap_sample) files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) end + end - context "install multiple times" do - before :all do - copy_sitemap_file_to_rails_app - Rake::Task['sitemap:install'].invoke - end - - it "should not overwrite config/sitemap.rb" do - sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') - files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) - end + context "install multiple times" do + before :each do + copy_sitemap_file_to_rails_app + Rake::Task['sitemap:install'].invoke + end + + it "should not overwrite config/sitemap.rb" do + sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') + files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) end - end + end context "generate sitemap" do before :each do diff --git a/spec/support/file_macros.rb b/spec/support/file_macros.rb index db9a0ab7..26e7ec2c 100644 --- a/spec/support/file_macros.rb +++ b/spec/support/file_macros.rb @@ -1,6 +1,6 @@ module FileMacros module ExampleMethods - + def files_should_be_identical(first, second) identical_files?(first, second).should be(true) end @@ -8,7 +8,7 @@ def files_should_be_identical(first, second) def files_should_not_be_identical(first, second) identical_files?(first, second).should be(false) end - + def file_should_exist(file) File.exists?(file).should be(true) end @@ -16,13 +16,15 @@ def file_should_exist(file) def file_should_not_exist(file) File.exists?(file).should be(false) end - + def identical_files?(first, second) + file_should_exist(first) + file_should_exist(second) open(second, 'r').read.should == open(first, 'r').read end - end - + end + def self.included(receiver) - receiver.send :include, ExampleMethods - end + receiver.send :include, ExampleMethods + end end \ No newline at end of file diff --git a/templates/sitemap_index.builder b/templates/sitemap_index.builder index 39b584bc..5dcfe7c2 100644 --- a/templates/sitemap_index.builder +++ b/templates/sitemap_index.builder @@ -1,3 +1,4 @@ +# encoding: utf-8 # # # diff --git a/templates/xml_sitemap.builder b/templates/xml_sitemap.builder index 8d481b8f..61a8bf15 100644 --- a/templates/xml_sitemap.builder +++ b/templates/xml_sitemap.builder @@ -1,3 +1,4 @@ +# encoding: utf-8 xml.instruct! xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation" => "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd", @@ -5,7 +6,6 @@ xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do links.each_with_index do |link,index| - puts "#{index}/#{links.size}" buffer_url = "" url = Builder::XmlMarkup.new(:target=>buffer_url) url.url do From b7750d3451c803b5e8c056940e742cfb5e0472e4 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 6 May 2010 14:12:40 -0700 Subject: [PATCH 044/677] Whitespace cleanup --- lib/sitemap_generator/link_set.rb | 4 +-- spec/sitemap_generator/templates_spec.rb | 6 ++-- spec/sitemap_generator_spec.rb | 40 ++++++++++++------------ spec/spec_helper.rb | 2 +- templates/sitemap.rb | 12 +++---- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 213374af..70ab3ed1 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -63,7 +63,7 @@ def write_sitemap(file = upcoming_file) slice_index = 0 buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_xml, binding) + eval(SitemapGenerator.templates.sitemap_xml, binding) filename = File.join(Rails.root, "public", file) write_file(filename, buffer) show_progress("Sitemap", filename, buffer) if verbose @@ -80,7 +80,7 @@ def write_sitemap(file = upcoming_file) def write_index buffer = "" xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_index, binding) + eval(SitemapGenerator.templates.sitemap_index, binding) filename = File.join(Rails.root, "public", index_file) write_file(filename, buffer) show_progress("Sitemap Index", filename, buffer) if verbose diff --git a/spec/sitemap_generator/templates_spec.rb b/spec/sitemap_generator/templates_spec.rb index 94daa0cf..7dc5de08 100644 --- a/spec/sitemap_generator/templates_spec.rb +++ b/spec/sitemap_generator/templates_spec.rb @@ -8,17 +8,17 @@ SitemapGenerator.templates.send(name).should == File.read(File.join(SitemapGenerator.root, 'templates', file)) end end - + describe "templates" do before :each do SitemapGenerator.templates.sitemap_xml = nil File.stub!(:read).and_return('read file') end - + it "should only be read once" do File.should_receive(:read).once SitemapGenerator.templates.sitemap_xml SitemapGenerator.templates.sitemap_xml - end + end end end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index 858704c1..6b1542ca 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -1,76 +1,76 @@ require 'spec_helper' describe "SitemapGenerator" do - + context "clean task" do before :all do copy_sitemap_file_to_rails_app FileUtils.touch(rails_path('/public/sitemap_index.xml.gz')) Rake::Task['sitemap:clean'].invoke end - + it "should delete the sitemaps" do file_should_not_exist(rails_path('/public/sitemap_index.xml.gz')) end end - + context "fresh install" do before :each do delete_sitemap_file_from_rails_app Rake::Task['sitemap:install'].invoke end - + it "should create config/sitemap.rb" do file_should_exist(rails_path('config/sitemap.rb')) end - + it "should create config/sitemap.rb matching template" do sitemap_template = SitemapGenerator.templates.template_path(:sitemap_sample) files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) end - end - + end + context "install multiple times" do before :each do copy_sitemap_file_to_rails_app Rake::Task['sitemap:install'].invoke - end - + end + it "should not overwrite config/sitemap.rb" do sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) end end - + context "generate sitemap" do before :each do Rake::Task['sitemap:refresh:no_ping'].invoke - end - - it "should create sitemaps" do + end + + it "should create sitemaps" do file_should_exist(rails_path('/public/sitemap_index.xml.gz')) file_should_exist(rails_path('/public/sitemap1.xml.gz')) end - + it "should have 14 links" do SitemapGenerator::Sitemap.link_count.should == 14 - end + end end - + protected - + # # Helpers # - + def rails_path(file) File.join(::Rails.root, file) end - + def copy_sitemap_file_to_rails_app FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(::Rails.root, '/config/sitemap.rb')) end - + def delete_sitemap_file_from_rails_app FileUtils.remove(File.join(::Rails.root, '/config/sitemap.rb')) rescue nil end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c724d315..b475de1c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ ENV["RAILS_ENV"] ||= 'test' ENV['BUNDLE_GEMFILE'] = File.join(File.dirname(__FILE__), 'mock_rails3_gem', 'Gemfile') -sitemap_rails = +sitemap_rails = case ENV["SITEMAP_RAILS"] when 'rails3' "mock_rails3_gem" diff --git a/templates/sitemap.rb b/templates/sitemap.rb index ecccb8f2..bae40abc 100644 --- a/templates/sitemap.rb +++ b/templates/sitemap.rb @@ -10,12 +10,12 @@ # Usage: sitemap.add path, options # (default options are used if you don't specify) # - # Defaults: :priority => 0.5, :changefreq => 'weekly', + # Defaults: :priority => 0.5, :changefreq => 'weekly', # :lastmod => Time.now, :host => default_host - + # Examples: - + # add '/articles' sitemap.add articles_path, :priority => 0.7, :changefreq => 'daily' @@ -26,17 +26,17 @@ # add merchant path sitemap.add '/purchase', :priority => 0.7, :host => "https://www.example.com" - + end # Including Sitemaps from Rails Engines. # -# These Sitemaps should be almost identical to a regular Sitemap file except +# These Sitemaps should be almost identical to a regular Sitemap file except # they needn't define their own SitemapGenerator::Sitemap.default_host since # they will undoubtedly share the host name of the application they belong to. # # As an example, say we have a Rails Engine in vendor/plugins/cadability_client # We can include its Sitemap here as follows: -# +# # file = File.join(Rails.root, 'vendor/plugins/cadability_client/config/sitemap.rb') # eval(open(file).read, binding, file) \ No newline at end of file From 39fbb15e578ef5a9f8f56229b56918965ad2f476 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 11 May 2010 12:40:02 -0700 Subject: [PATCH 045/677] Updated readme --- README.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 06c77226..4bc30f1b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,18 @@ SitemapGenerator This plugin enables ['enterprise-class'][enterprise_class] Google Sitemaps to be easily generated for a Rails site as a rake task, using a simple 'Rails Routes'-like DSL. -**Now supporting Rails 3 as of version 0.2.5!** +Features +------- + +- v0.2.6: **Support ['image sitemaps'][sitemap_images]**! +- v0.2.5: **Support Rails 3**! + +- Adheres to the ['Sitemap protocol specification'][sitemap_protocol] +- Handles millions of links +- Automatic Gzip of Sitemap files +- Set the priority of links, change frequency etc +- You control which links are included +- You set the host name, so it doesn't matter if your application is in a subdirectory Foreword ------- @@ -51,9 +62,14 @@ Installation creates a config/sitemap.rb file which will contain your l You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. -Sitemaps with many urls (100,000+) take quite a long time to generate, so if you need to refresh your Sitemaps regularly you can set the rake task up as a cron job. Most cron agents will only send you an email if there is output from the cron task. +To keep your Sitemaps up-to-date, setup a cron job. Pass the -s option to the rake task to silence all but the most important output. If you're using Whenever, then your schedule would look something like: + + # config/schedule.rb + every 1.day, :at => '5:00 am' do + rake "-s sitemap:refresh" + end -Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file. +Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file: Sitemap: /sitemap_index.xml.gz @@ -138,22 +154,13 @@ Easy hey? Other Sitemap settings for the link, like `lastmod`, `priority`, `changefreq` and `host` are entered automatically, although you can override them if you need to. -Other "difficult" Sitemap issues, solved by this plugin: - -- Support for more than 50,000 urls (using a Sitemap Index file) -- Gzip of Sitemap files -- Variable priority of links -- Paging/sorting links (e.g. my_list?page=3) -- SSL host links (e.g. https:) -- Rails apps which are installed on a sub-path (e.g. example.com/blog_app/) - Compatibility ======= Tested and working on: - **Rails** 3.0.0, sitemap_generator version >= 0.2.5 -- **Rails** 1.x - 2.3.5 sitemap_generator version < 0.2.5 +- **Rails** 1.x - 2.3.5 - **Ruby** 1.8.7, 1.9.1 Notes @@ -187,21 +194,26 @@ Known Bugs - There's no check on the size of a URL which [isn't supposed to exceed 2,048 bytes][sitemaps_xml]. - Currently only supports one Sitemap Index file, which can contain 50,000 Sitemap files which can each contain 50,000 urls, so it _only_ supports up to 2,500,000,000 (2.5 billion) urls. I personally have no need of support for more urls, but plugin could be improved to support this. -Wishlist +Wishlist & Coming Soon ======== +- Support for generating sitemaps for sites with multiple domains. Sitemaps are generated into subdirectories and we use a Rack middleware to rewrite requests for sitemaps to the correct subdirectory based on the request host. +- I want to refactor the code because it has grown a lot. Part of this refactoring will include implementing some more checks to make sure we adhere to standards as well as making sure that the sitemaps are being generated as efficiently as possible. + +I'd like to simplify adding links to a sitemap. Right now it's all or nothing. I'd like to break it up so you can add batches. - Auto coverage testing. Generate a report of broken URLs by checking the status codes of each page in the sitemap. Thanks (in no particular order) ======== +- [Alexadre Bini](http://github.com/alexandrebini) for image sitemaps - [Dan Pickett](http://github.com/dpickett) - [Rob Biedenharn](http://github.com/rab) - [Richie Vos](http://github.com/jerryvos) - [Adrian Mugnolo](http://github.com/xymbol) -Copyright (c) 2009 Adam @ [Codebright.net][cb], released under the MIT license +Copyright (c) 2009 Karl Varga released under the MIT license [canonical_repo]:http://github.com/kjvarga/sitemap_generator [enterprise_class]:https://twitter.com/dhh/status/1631034662 "I use enterprise in the same sense the Phusion guys do - i.e. Enterprise Ruby. Please don't look down on my use of the word 'enterprise' to represent being a cut above. It doesn't mean you ever have to work for a company the size of IBM. Or constantly fight inertia, writing crappy software, adhering to change management practices and spending hours in meetings... Not that there's anything wrong with that - Wait, what?" @@ -212,4 +224,4 @@ Copyright (c) 2009 Adam @ [Codebright.net][cb], released under the MIT license [boost_juice]:http://www.boostjuice.com.au/ "Mmmm, sweet, sweet Boost Juice." [cb]:http://codebright.net "http://codebright.net" [sitemap_images]:http://www.google.com/support/webmasters/bin/answer.py?answer=178636 - +[sitemap_protocol]:http://sitemaps.org/protocol.php \ No newline at end of file From c2fb20a72d2e9a867c1b947419c7d31d38108828 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 17 May 2010 12:11:48 -0700 Subject: [PATCH 046/677] Update Rakefile, README for image sitemap support and make the description consistent --- README.md | 6 ++++-- Rakefile | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4bc30f1b..e8ef3186 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SitemapGenerator ================ -This plugin enables ['enterprise-class'][enterprise_class] Google Sitemaps to be easily generated for a Rails site as a rake task, using a simple 'Rails Routes'-like DSL. +A Rails 3-compatible gem/plugin to generate ['enterprise-class'][enterprise_class] Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the ['Sitemap protocol specification'][sitemap_protocol]. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links. Features ------- @@ -12,6 +12,8 @@ Features - Adheres to the ['Sitemap protocol specification'][sitemap_protocol] - Handles millions of links - Automatic Gzip of Sitemap files +- Automatic ping of search engines to notify them of new sitemaps: Google, Yahoo, Bing, Ask, SitemapWriter +- Won't clobber your old sitemaps if the new one fails to generate - Set the priority of links, change frequency etc - You control which links are included - You set the host name, so it doesn't matter if your application is in a subdirectory @@ -60,7 +62,7 @@ Installation Installation creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. If you want to create this file manually run rake sitemap:install. -You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping all the ['major'][sitemap_engines] search engines. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. +You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping these ['major search engines'][sitemap_engines]: Google, Yahoo, Bing, Ask, SitemapWriter. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. To keep your Sitemaps up-to-date, setup a cron job. Pass the -s option to the rake task to silence all but the most important output. If you're using Whenever, then your schedule would look something like: diff --git a/Rakefile b/Rakefile index d0baeac5..729bbdc3 100644 --- a/Rakefile +++ b/Rakefile @@ -8,8 +8,8 @@ begin require 'jeweler' Jeweler::Tasks.new do |gem| gem.name = "sitemap_generator" - gem.summary = %Q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} - gem.description = %Q{A Rails 3-compatible gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} + gem.summary = %Q{Easily generate enterprise class Sitemaps for your Rails site using a familiar Rails Routes-like DSL} + gem.description = %Q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} gem.email = "kjvarga@gmail.com" gem.homepage = "http://github.com/kjvarga/sitemap_generator" gem.authors = ["Adam Salter", "Karl Varga"] From 125b1669b2ed3dba3be55003d7e20b4e325ee1e2 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 17 May 2010 12:12:15 -0700 Subject: [PATCH 047/677] Version bump to 0.2.6 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3a4036fb..53a75d67 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.5 +0.2.6 From 1e8b0c65c8a3f9a83405079b90c319dc124926a5 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 17 May 2010 12:12:32 -0700 Subject: [PATCH 048/677] Regenerated gemspec for version 0.2.6 --- sitemap_generator.gemspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 2490cff7..3dfec4d5 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,12 +5,12 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.2.5" + s.version = "0.2.6" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-06} - s.description = %q{A Rails 3-compatible gem to easily generate enterprise class Sitemaps readable by all search engines. Automatically ping search engines to notify them of new sitemaps, including Google, Yahoo and Bing. Provides rake tasks to easily manage your sitemaps. Won't clobber your old sitemaps if the new one fails to generate. Setup a cron schedule and never worry about your sitemaps again.} + s.date = %q{2010-05-17} + s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ "README.md" @@ -41,7 +41,7 @@ Gem::Specification.new do |s| s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.rubygems_version = %q{1.3.6} - s.summary = %q{Easily generate enterprise class Sitemaps for your Rails site using a simple 'Rails Routes'-like DSL and a single Rake task} + s.summary = %q{Easily generate enterprise class Sitemaps for your Rails site using a familiar Rails Routes-like DSL} if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION From aec74d100a4842336995e78b98a2238d82a9d3af Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 20 May 2010 14:55:55 -0700 Subject: [PATCH 049/677] Fix the tests. Some of the problem was that spec tests in the mock apps were being included! --- Rakefile | 44 +++++++---- spec/mock_rails3_gem/spec/sitemap.file | 13 ---- .../spec/sitemap_generator_spec.rb | 77 ------------------- spec/mock_rails3_gem/spec/spec_helper.rb | 28 ------- .../spec/support/file_macros.rb | 28 ------- spec/sitemap_generator_spec.rb | 10 +-- spec/spec_helper.rb | 27 +++++-- 7 files changed, 55 insertions(+), 172 deletions(-) delete mode 100644 spec/mock_rails3_gem/spec/sitemap.file delete mode 100644 spec/mock_rails3_gem/spec/sitemap_generator_spec.rb delete mode 100644 spec/mock_rails3_gem/spec/spec_helper.rb delete mode 100644 spec/mock_rails3_gem/spec/support/file_macros.rb diff --git a/Rakefile b/Rakefile index 729bbdc3..22668146 100644 --- a/Rakefile +++ b/Rakefile @@ -22,6 +22,28 @@ rescue LoadError puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" end +# +# Helper methods +# +module Helpers + extend self + + # Return a full local path to path fragment path + def local_path(path) + File.join(File.dirname(__FILE__), path) + end + + # Copy all of the local files into path after completely cleaning it + def prepare_path(path) + rm_rf path + mkdir_p path + cp_r(FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}"], path) + end +end + +# +# Tasks +# task :default => :test namespace :test do @@ -37,29 +59,19 @@ namespace :test do namespace :prepare do task :gem do ENV["SITEMAP_RAILS"] = 'gem' - prepare_path(local_path('spec/mock_app_gem/vendor/gems/sitemap_generator-1.2.3')) - rm_rf(local_path('spec/mock_app_gem/public/sitemap*')) + Helpers.prepare_path(Helpers.local_path('spec/mock_app_gem/vendor/gems/sitemap_generator-1.2.3')) + rm_rf(Helpers.local_path('spec/mock_app_gem/public/sitemap*')) end task :plugin do ENV["SITEMAP_RAILS"] = 'plugin' - prepare_path(local_path('spec/mock_app_plugin/vendor/plugins/sitemap_generator-1.2.3')) - rm_rf(local_path('spec/mock_app_plugin/public/sitemap*')) + Helpers.prepare_path(Helpers.local_path('spec/mock_app_plugin/vendor/plugins/sitemap_generator-1.2.3')) + rm_rf(Helpers.local_path('spec/mock_app_plugin/public/sitemap*')) end task :rails3 do ENV["SITEMAP_RAILS"] = 'rails3' - rm_rf(local_path('spec/mock_rails3_gem/public/sitemap*')) - end - - def local_path(path) - File.join(File.dirname(__FILE__), path) - end - - def prepare_path(path) - rm_rf path - mkdir_p path - cp_r(FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}"], path) + rm_rf(Helpers.local_path('spec/mock_rails3_gem/public/sitemap*')) end end end @@ -86,4 +98,4 @@ Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.options << '--line-numbers' << '--inline-source' rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('lib/**/*.rb') -end +end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/sitemap.file b/spec/mock_rails3_gem/spec/sitemap.file deleted file mode 100644 index 84681faf..00000000 --- a/spec/mock_rails3_gem/spec/sitemap.file +++ /dev/null @@ -1,13 +0,0 @@ -SitemapGenerator::Sitemap.default_host = "http://www.example.com" -SitemapGenerator::Sitemap.yahoo_app_id = false - -SitemapGenerator::Sitemap.add_links do |sitemap| - sitemap.add contents_path, :priority => 0.7, :changefreq => 'daily' - - # add all individual articles - Content.find(:all).each do |c| - sitemap.add content_path(c), :lastmod => c.updated_at - end - - sitemap.add "/merchant_path", :host => "https://www.example.com" -end diff --git a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb b/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb deleted file mode 100644 index a34301ea..00000000 --- a/spec/mock_rails3_gem/spec/sitemap_generator_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe "SitemapGenerator" do - - context "clean task" do - before :all do - copy_sitemap_file_to_rails_app - FileUtils.touch(rails_path('/public/sitemap_index.xml.gz')) - Rake::Task['sitemap:clean'].invoke - end - - it "should delete the sitemaps" do - file_should_not_exist(rails_path('/public/sitemap_index.xml.gz')) - end - end - - context "fresh install" do - before :all do - delete_sitemap_file_from_rails_app - Rake::Task['sitemap:install'].invoke - end - - it "should create config/sitemap.rb" do - file_should_exist(rails_path('config/sitemap.rb')) - end - - it "should create config/sitemap.rb matching template" do - sitemap_template = SitemapGenerator.templates[:sitemap_sample] - files_should_be_identical(rails_path('config/sitemap.rb'), sitemap_template) - end - - context "install multiple times" do - before :all do - copy_sitemap_file_to_rails_app - Rake::Task['sitemap:install'].invoke - end - - it "should not overwrite config/sitemap.rb" do - sitemap_file = File.join(File.dirname(__FILE__), '/sitemap.file') - files_should_be_identical(sitemap_file, rails_path('/config/sitemap.rb')) - end - end - end - - context "generate sitemap" do - before :each do - Rake::Task['sitemap:refresh:no_ping'].invoke - end - - it "should create sitemaps" do - file_should_exist(rails_path('/public/sitemap_index.xml.gz')) - file_should_exist(rails_path('/public/sitemap1.xml.gz')) - end - - it "should have 14 links" do - SitemapGenerator::Sitemap.link_count.should == 14 - end - end - - protected - - # - # Helpers - # - - def rails_path(file) - File.join(::Rails.root, file) - end - - def copy_sitemap_file_to_rails_app - FileUtils.cp(File.join(File.dirname(__FILE__), '/sitemap.file'), File.join(::Rails.root, '/config/sitemap.rb')) - end - - def delete_sitemap_file_from_rails_app - FileUtils.remove(File.join(::Rails.root, '/config/sitemap.rb')) rescue nil - end -end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/spec_helper.rb b/spec/mock_rails3_gem/spec/spec_helper.rb deleted file mode 100644 index 0a939a46..00000000 --- a/spec/mock_rails3_gem/spec/spec_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# This file is copied to ~/spec when you run 'ruby script/generate rspec' -# from the project root directory. -ENV["RAILS_ENV"] ||= 'test' - -#require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) -load(File.join(File.dirname(__FILE__), '..', 'Rakefile')) -require 'rspec/rails' - -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} - -Rspec.configure do |config| - config.include(FileMacros) - - # == Mock Framework - # - # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: - # - # config.mock_with :mocha - # config.mock_with :flexmock - # config.mock_with :rr - config.mock_with :rspec - - # If you'd prefer not to run each of your examples within a transaction, - # uncomment the following line. - # config.use_transactional_examples false -end \ No newline at end of file diff --git a/spec/mock_rails3_gem/spec/support/file_macros.rb b/spec/mock_rails3_gem/spec/support/file_macros.rb deleted file mode 100644 index db9a0ab7..00000000 --- a/spec/mock_rails3_gem/spec/support/file_macros.rb +++ /dev/null @@ -1,28 +0,0 @@ -module FileMacros - module ExampleMethods - - def files_should_be_identical(first, second) - identical_files?(first, second).should be(true) - end - - def files_should_not_be_identical(first, second) - identical_files?(first, second).should be(false) - end - - def file_should_exist(file) - File.exists?(file).should be(true) - end - - def file_should_not_exist(file) - File.exists?(file).should be(false) - end - - def identical_files?(first, second) - open(second, 'r').read.should == open(first, 'r').read - end - end - - def self.included(receiver) - receiver.send :include, ExampleMethods - end -end \ No newline at end of file diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index 6b1542ca..6c32540f 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -3,10 +3,10 @@ describe "SitemapGenerator" do context "clean task" do - before :all do + before :each do copy_sitemap_file_to_rails_app FileUtils.touch(rails_path('/public/sitemap_index.xml.gz')) - Rake::Task['sitemap:clean'].invoke + Helpers.invoke_task('sitemap:clean') end it "should delete the sitemaps" do @@ -17,7 +17,7 @@ context "fresh install" do before :each do delete_sitemap_file_from_rails_app - Rake::Task['sitemap:install'].invoke + Helpers.invoke_task('sitemap:install') end it "should create config/sitemap.rb" do @@ -33,7 +33,7 @@ context "install multiple times" do before :each do copy_sitemap_file_to_rails_app - Rake::Task['sitemap:install'].invoke + Helpers.invoke_task('sitemap:install') end it "should not overwrite config/sitemap.rb" do @@ -44,7 +44,7 @@ context "generate sitemap" do before :each do - Rake::Task['sitemap:refresh:no_ping'].invoke + Helpers.invoke_task('sitemap:refresh:no_ping') end it "should create sitemaps" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b475de1c..a0148199 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,14 +11,19 @@ "mock_app_gem" end -# Boot the environment -#require File.join(File.dirname(__FILE__), sitemap_rails, 'config', 'boot') - # Load the app's Rakefile so we know everything is being loaded correctly load(File.join(File.dirname(__FILE__), sitemap_rails, 'Rakefile')) -#require 'ruby-debug' -# debugger +require 'rubygems' +begin + case RUBY_VERSION + when '1.9.1' + require 'ruby-debug19' + else + require 'ruby-debug' + end +rescue Exception => e +end # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. @@ -26,4 +31,16 @@ Spec::Runner.configure do |config| config.include(FileMacros) +end + +module Helpers + extend self + + # Invoke and then re-enable the task so it can be called multiple times + # + # task task symbol/string + def invoke_task(task) + Rake::Task[task.to_s].invoke + Rake::Task[task.to_s].reenable + end end \ No newline at end of file From d785618b59aebf8dda01467dca0174b69941f36a Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 20 May 2010 14:58:04 -0700 Subject: [PATCH 050/677] Rewrite sitemap writing to be more efficient and modular. Respect file size & link limits. Never output text in a rake task unless in verbose mode. --- README.md | 1 - lib/sitemap_generator.rb | 14 +- lib/sitemap_generator/builder.rb | 9 + lib/sitemap_generator/builder/helper.rb | 10 + lib/sitemap_generator/builder/sitemap_file.rb | 114 ++++++++ .../builder/sitemap_index_file.rb | 24 ++ lib/sitemap_generator/helper.rb | 55 ---- lib/sitemap_generator/interpreter.rb | 28 ++ lib/sitemap_generator/link.rb | 51 ++-- lib/sitemap_generator/link_set.rb | 257 +++++++++--------- lib/sitemap_generator/rails_helper.rb | 28 -- lib/sitemap_generator/templates.rb | 4 +- lib/sitemap_generator/utilities.rb | 31 ++- sitemap_generator.gemspec | 13 +- spec/sitemap_generator/templates_spec.rb | 6 +- spec/spec_helper.rb | 1 + tasks/sitemap_generator_tasks.rake | 18 +- templates/sitemap_index.builder | 23 -- templates/xml_sitemap.builder | 38 --- 19 files changed, 395 insertions(+), 330 deletions(-) create mode 100644 lib/sitemap_generator/builder.rb create mode 100644 lib/sitemap_generator/builder/helper.rb create mode 100644 lib/sitemap_generator/builder/sitemap_file.rb create mode 100644 lib/sitemap_generator/builder/sitemap_index_file.rb delete mode 100644 lib/sitemap_generator/helper.rb create mode 100644 lib/sitemap_generator/interpreter.rb delete mode 100644 lib/sitemap_generator/rails_helper.rb delete mode 100644 templates/sitemap_index.builder delete mode 100644 templates/xml_sitemap.builder diff --git a/README.md b/README.md index e8ef3186..c8f04a02 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,6 @@ Notes Known Bugs ======== -- Sitemaps.org [states][sitemaps_org] that no Sitemap XML file should be more than 10Mb uncompressed. The plugin will warn you about this, but does nothing to avoid it (like move some URLs into a later file). - There's no check on the size of a URL which [isn't supposed to exceed 2,048 bytes][sitemaps_xml]. - Currently only supports one Sitemap Index file, which can contain 50,000 Sitemap files which can each contain 50,000 urls, so it _only_ supports up to 2,500,000,000 (2.5 billion) urls. I personally have no need of support for more urls, but plugin could be improved to support this. diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index 737919b5..cc5d4da0 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -1,19 +1,19 @@ +require 'sitemap_generator/builder' require 'sitemap_generator/mapper' require 'sitemap_generator/link' -require 'sitemap_generator/rails_helper' -require 'sitemap_generator/helper' require 'sitemap_generator/link_set' -require 'sitemap_generator/helper' require 'sitemap_generator/templates' require 'sitemap_generator/utilities' - -require 'sitemap_generator/railtie' if SitemapGenerator::RailsHelper.rails3? +require 'sitemap_generator/railtie' if SitemapGenerator::Utilities.rails3? module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip - MAX_ENTRIES = 50_000 - MAX_IMAGES = 1_000 + MAX_SITEMAP_FILES = 50_000 # max sitemap links per index file + MAX_SITEMAP_LINKS = 50_000 # max links per sitemap + MAX_SITEMAP_IMAGES = 1_000 # max images per url + MAX_SITEMAP_FILESIZE = 10.megabytes # bytes + Sitemap = LinkSet.new end diff --git a/lib/sitemap_generator/builder.rb b/lib/sitemap_generator/builder.rb new file mode 100644 index 00000000..d8f032f4 --- /dev/null +++ b/lib/sitemap_generator/builder.rb @@ -0,0 +1,9 @@ +require 'sitemap_generator/builder/helper' +require 'sitemap_generator/builder/sitemap_file' +require 'sitemap_generator/builder/sitemap_index_file' + +module SitemapGenerator + module Builder + + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/builder/helper.rb b/lib/sitemap_generator/builder/helper.rb new file mode 100644 index 00000000..6332069e --- /dev/null +++ b/lib/sitemap_generator/builder/helper.rb @@ -0,0 +1,10 @@ +module SitemapGenerator + module Builder + module Helper + + def w3c_date(date) + date.utc.strftime("%Y-%m-%dT%H:%M:%S+00:00") + end + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb new file mode 100644 index 00000000..90ef10cd --- /dev/null +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -0,0 +1,114 @@ +require 'sitemap_generator/builder/helper' +require 'builder' +require 'zlib' + +module SitemapGenerator + module Builder + class SitemapFile + include SitemapGenerator::Builder::Helper + + attr_accessor :sitemap_path, :public_path, :filesize, :link_count, :hostname + + # public_path full path of the directory to write sitemaps in. + # Usually your Rails public/ directory. + # + # sitemap_path relative path including filename of the sitemap + # file relative to public_path + # + # hostname hostname including protocol to use in all links + # e.g. http://en.google.ca + def initialize(public_path, sitemap_path, hostname) + self.sitemap_path = sitemap_path + self.public_path = public_path + self.hostname = hostname + self.link_count = 0 + + @xml_content = '' # XML urlset content + @xml_wrapper_start = %q[] + @xml_wrapper_end = %q[] + self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize + end + + def lastmod + File.mtime(self.full_path) rescue nil + end + + def empty? + self.link_count == 0 + end + + def full_url + URI.join(self.hostname, self.sitemap_path).to_s + end + + def full_path + @full_path ||= File.join(self.public_path, self.sitemap_path) + end + + # Return a boolean indicating whether the sitemap file can fit another link + # of bytes bytes in size. + def file_can_fit?(bytes) + (self.filesize + bytes) < SitemapGenerator::MAX_SITEMAP_FILESIZE && self.link_count < SitemapGenerator::MAX_SITEMAP_LINKS + end + + # Add a link to the sitemap file and return a boolean indicating whether the + # link was added. + # + # If a link cannot be added, the file is too large or the link limit has been reached. + def add_link(link) + xml = build_xml(::Builder::XmlMarkup.new, link) + unless file_can_fit?(xml.bytesize) + self.finalize! + return false + end + + @xml_content << xml + self.filesize += xml.bytesize + self.link_count += 1 + true + end + alias_method :<<, :add_link + + # Return XML as a String + def build_xml(builder, link) + builder.url do + builder.loc link[:loc] + builder.lastmod w3c_date(link[:lastmod]) if link[:lastmod] + builder.changefreq link[:changefreq] if link[:changefreq] + builder.priority link[:priority] if link[:priority] + + unless link[:images].blank? + link[:images].each do |image| + builder.image:image do + builder.image :loc, image[:loc] + builder.image :caption, image[:caption] if image[:caption] + builder.image :geo_location, image[:geo_location] if image[:geo_location] + builder.image :title, image[:title] if image[:title] + builder.image :license, image[:license] if image[:license] + end + end + end + end + builder << '' + end + + # Insert the content into the XML "wrapper" and write and close the file. + # + # All the xml content in the instance is cleared, but attributes like + # filesize are still available. + def finalize! + return if self.frozen? + + open(self.full_path, 'w') do |file| + gz = Zlib::GzipWriter.new(file) + gz.write @xml_wrapper_start + gz.write @xml_content + gz.write @xml_wrapper_end + gz.close + end + @xml_content = @xml_wrapper_start = @xml_wrapper_end = '' + self.freeze + end + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/builder/sitemap_index_file.rb b/lib/sitemap_generator/builder/sitemap_index_file.rb new file mode 100644 index 00000000..06b7704c --- /dev/null +++ b/lib/sitemap_generator/builder/sitemap_index_file.rb @@ -0,0 +1,24 @@ +module SitemapGenerator + module Builder + class SitemapIndexFile < SitemapFile + + def initialize(*args) + super(*args) + + @ml_content = '' # XML urlset content + @xml_wrapper_start = %q[] + @xml_wrapper_end = %q[] + self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize + end + + # Return XML as a String + def build_xml(builder, link) + builder.url do + builder.loc link[:loc] + builder.lastmod w3c_date(link[:lastmod]) if link[:lastmod] + end + builder << '' + end + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/helper.rb b/lib/sitemap_generator/helper.rb deleted file mode 100644 index 42bfbc92..00000000 --- a/lib/sitemap_generator/helper.rb +++ /dev/null @@ -1,55 +0,0 @@ -module SitemapGenerator - # UrlHelpers are included by the rake tasks. This is not ideal, but should - # suffice until things are better organized. - module Helper - def self.included(base) - base.class_eval do - def self.default_url_options(options = nil) - { :host => SitemapGenerator::Sitemap.default_host } - end - end - end - - def load_sitemap_rb - sitemap_mapper_file = File.join(Rails.root, 'config/sitemap.rb') - eval(open(sitemap_mapper_file).read) - end - - def url_with_hostname(path) - URI.join(SitemapGenerator::Sitemap.default_host, path).to_s - end - - def w3c_date(date) - date.utc.strftime("%Y-%m-%dT%H:%M:%S+00:00") - end - - def ping_search_engines(sitemap_index) - require 'open-uri' - index_location = CGI.escape(url_with_hostname(sitemap_index)) - # engines list from http://en.wikipedia.org/wiki/Sitemap_index - yahoo_app_id = SitemapGenerator::Sitemap.yahoo_app_id - {:google => "http://www.google.com/webmasters/sitemaps/ping?sitemap=#{index_location}", - :yahoo => "http://search.yahooapis.com/SiteExplorerService/V1/ping?sitemap=#{index_location}&appid=#{yahoo_app_id}", - :ask => "http://submissions.ask.com/ping?sitemap=#{index_location}", - :bing => "http://www.bing.com/webmaster/ping.aspx?siteMap=#{index_location}", - :sitemap_writer => "http://www.sitemapwriter.com/notify.php?crawler=all&url=#{index_location}"}.each do |engine, link| - begin - unless SitemapGenerator::Sitemap.yahoo_app_id == false - open(link) - puts "Successful ping of #{engine.to_s.titleize}" if verbose - end - rescue Timeout::Error, StandardError => e - puts "Ping failed for #{engine.to_s.titleize}: #{e.inspect}" if verbose - puts <<-END if engine == :yahoo && verbose -Yahoo requires an 'AppID' for more than one ping per "timeframe", you can either: - - remove yahoo from the ping list (config/sitemap.rb): - SitemapGenerator::Sitemap.yahoo_app_id = false - - or add your Yahoo AppID to the generator (config/sitemap.rb): - SitemapGenerator::Sitemap.yahoo_app_id = "my_app_id" -For more information: http://developer.yahoo.com/search/siteexplorer/V1/updateNotification.html - END - end - end - end - end -end diff --git a/lib/sitemap_generator/interpreter.rb b/lib/sitemap_generator/interpreter.rb new file mode 100644 index 00000000..8ff0122a --- /dev/null +++ b/lib/sitemap_generator/interpreter.rb @@ -0,0 +1,28 @@ +module SitemapGenerator + + # Evaluate a sitemap config file within the context of a class that includes the + # Rails URL helpers. + class Interpreter + + if SitemapGenerator::Utilities.rails3? + include ::Rails.application.routes.url_helpers + else + require 'action_controller' + include ActionController::UrlWriter + end + + def initialize(sitemap_config_file=nil) + sitemap_config_file ||= File.join(::Rails.root, 'config/sitemap.rb') + eval(open(sitemap_config_file).read) + end + + # KJV do we need this? We should be using path_* helpers. + # def self.default_url_options(options = nil) + # { :host => SitemapGenerator::Sitemap.default_host } + # end + + def self.run + new + end + end +end \ No newline at end of file diff --git a/lib/sitemap_generator/link.rb b/lib/sitemap_generator/link.rb index 0bd8d18f..80fcac49 100644 --- a/lib/sitemap_generator/link.rb +++ b/lib/sitemap_generator/link.rb @@ -1,30 +1,35 @@ module SitemapGenerator - class Link - class << self - def generate(path, options = {}) - options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) - options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host, :images => []) - { - :path => path, - :priority => options[:priority], - :changefreq => options[:changefreq], - :lastmod => options[:lastmod], - :host => options[:host], - :loc => URI.join(options[:host], path).to_s, - :images => prepare_images(options[:images], options[:host]) - } + module Link + extend self + + # Return a Hash of options suitable to pass to a SitemapGenerator::Builder::SitemapFile instance. + def generate(path, options = {}) + if path.is_a?(SitemapGenerator::Builder::SitemapFile) + options.reverse_merge!(:host => path.hostname, :lastmod => path.lastmod) + path = path.sitemap_path end - # Maximum 1000 images. loc is required. - # ?? Does the image URL have to be on the same host? - def prepare_images(images, host) - images.delete_if { |key,value| key[:loc] == nil } - images.each do |r| - r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) - r[:loc] = URI.join(host, r[:loc]).to_s - end - images[0..(SitemapGenerator::MAX_IMAGES-1)] + options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) + options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host, :images => []) + { + :path => path, + :priority => options[:priority], + :changefreq => options[:changefreq], + :lastmod => options[:lastmod], + :host => options[:host], + :loc => URI.join(options[:host], path).to_s, + :images => prepare_images(options[:images], options[:host]) + } + end + + # Return an Array of image option Hashes suitable to be parsed by SitemapGenerator::Builder::SitemapFile + def prepare_images(images, host) + images.delete_if { |key,value| key[:loc] == nil } + images.each do |r| + r.assert_valid_keys(:loc, :caption, :geo_location, :title, :license) + r[:loc] = URI.join(host, r[:loc]).to_s end + images[0..(SitemapGenerator::MAX_SITEMAP_IMAGES-1)] end end end diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 70ab3ed1..b16a6575 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -1,166 +1,173 @@ require 'builder' require 'action_view' +# A LinkSet provisions a bunch of links to sitemap files. It also writes the index file +# which lists all the sitemap files written. module SitemapGenerator class LinkSet - include SitemapGenerator::Helper - include ActionView::Helpers::NumberHelper - - attr_accessor :default_host, :yahoo_app_id, :links - attr_accessor :sitemaps - attr_accessor :max_entries - attr_accessor :link_count - - alias :sitemap_files :sitemaps - - # Create new link set instance. - def initialize - self.links = [] - self.sitemaps = [] - self.max_entries = SitemapGenerator::MAX_ENTRIES - self.link_count = 0 - end + include ActionView::Helpers::NumberHelper # for number_with_delimiter - # Add default links to sitemap files. - def add_default_links - links.push Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - links.push Link.generate("/#{index_file}", :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) - self.link_count += 2 - end + attr_accessor :default_host, :public_path, :sitemaps_path + attr_accessor :sitemap, :sitemaps, :sitemap_index + attr_accessor :verbose, :yahoo_app_id - # Add links to sitemap files passing a block. - def add_links - raise ArgumentError, "Default hostname not set" if default_host.blank? - add_default_links if first_link? - yield Mapper.new(self) - end + # Evaluate the sitemap config file and write all sitemaps. + # + # This should be refactored so that we can have multiple instances + # of LinkSet. + def create + require 'sitemap_generator/interpreter' - # Add links from mapper to sitemap files. - def add_link(link) - write_upcoming if enough_links? - links.push link - self.link_count += 1 - end + start_time = Time.now + SitemapGenerator::Interpreter.run + finalize! + end_time = Time.now - # Write links to sitemap file. - def write - write_pending + puts "\nSitemap stats: #{number_with_delimiter(self.link_count)} links / #{self.sitemaps.size} files / " + ("%dm%02ds" % (end_time - start_time).divmod(60)) if verbose end - # Write links to upcoming sitemap file. - def write_upcoming - write_sitemap(upcoming_file) + # public_path (optional) full path to the directory to write sitemaps in. + # Defaults to your Rails public/ directory. + # + # sitemaps_path (optional) path fragment within public to write sitemaps + # to e.g. 'en/'. Sitemaps are written to public_path + sitemaps_path + # + # default_host hostname including protocol to use in all sitemap links + # e.g. http://en.google.ca + def initialize(public_path = nil, sitemaps_path = nil, default_host = nil) + public_path = File.join(::Rails.root, 'public/') if public_path.nil? + self.default_host = default_host + self.public_path = public_path + self.sitemaps_path = sitemaps_path + + # Completed sitemaps + self.sitemaps = [] end - # Write pending links to sitemap, write index file if needed. - def write_pending - write_upcoming - write_index + def link_count + self.sitemaps.map(&:link_count).inject(:+) end - # Write links to sitemap file. - def write_sitemap(file = upcoming_file) - slice_index = 0 - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_xml, binding) - filename = File.join(Rails.root, "public", file) - write_file(filename, buffer) - show_progress("Sitemap", filename, buffer) if verbose - if slice_index==0 - links.clear - else - links.slice! slice_index, links.size - end + # Called within the user's eval'ed sitemap config file. Add links to sitemap files + # passing a block. + # + # TODO: Refactor. The call chain is confusing and convoluted here. + def add_links + raise ArgumentError, "Default hostname not set" if default_host.blank? - sitemaps.push filename - end + # I'd rather have these calls in create but we have to wait + # for default_host to be set by the user's sitemap config + new_sitemap + add_default_links - # Write sitemap links to sitemap index file. - def write_index - buffer = "" - xml = Builder::XmlMarkup.new(:target => buffer) - eval(SitemapGenerator.templates.sitemap_index, binding) - filename = File.join(Rails.root, "public", index_file) - write_file(filename, buffer) - show_progress("Sitemap Index", filename, buffer) if verbose - links.clear - sitemaps.clear + yield Mapper.new(self) end - # Return sitemap or sitemap index main name. - def index_file - "sitemap_index.xml.gz" + # Called from Mapper. + # + # Add a link to the current sitemap. + def add_link(link) + unless self.sitemap << link + new_sitemap + self.sitemap << link + end end - # Return upcoming sitemap name with index. - def upcoming_file - "sitemap#{upcoming_index}.xml.gz" unless enough_sitemaps? - end + # Add the current sitemap to the sitemaps Array and + # start a new sitemap. + # + # If the current sitemap is nil or empty it is not added. + def new_sitemap + unless self.sitemap_index + self.sitemap_index = SitemapGenerator::Builder::SitemapIndexFile.new(public_path, sitemap_index_path, default_host) + end - # Return upcoming sitemap index, first is 1. - def upcoming_index - sitemaps.length + 1 unless enough_sitemaps? - end + unless self.sitemap + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) + end - # Return true if upcoming is first sitemap. - def first_sitemap? - sitemaps.empty? - end + # Mark the sitemap as complete and add it to the sitemap index + unless self.sitemap.empty? + self.sitemap.finalize! + self.sitemap_index << Link.generate(self.sitemap) + self.sitemaps << self.sitemap + show_progress(self.sitemap) if verbose - # Return true if sitemap index needed. - def multiple_sitemaps? - !first_sitemap? + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) + end end - # Return true if more sitemaps can be added. - def more_sitemaps? - sitemaps.length < max_entries + # Report progress line. + def show_progress(sitemap) + uncompressed_size = number_to_human_size(sitemap.filesize) + compressed_size = number_to_human_size(File.size?(sitemap.full_path)) + puts "+ #{sitemap.sitemap_path} #{sitemap.link_count} links / #{uncompressed_size} / #{compressed_size} gzipped" end - # Return true if no sitemaps can be added. - def enough_sitemaps? - !more_sitemaps? + # Finalize all sitemap files + def finalize! + new_sitemap + self.sitemap_index.finalize! end - # Return true if this is the first link added. - def first_link? - links.empty? && first_sitemap? - end + # Ping search engines. + # + # @see http://en.wikipedia.org/wiki/Sitemap_index + def ping_search_engines + require 'open-uri' + + sitemap_index_url = CGI.escape(self.sitemap_index.full_url) + search_engines = { + :google => "http://www.google.com/webmasters/sitemaps/ping?sitemap=#{sitemap_index_url}", + :yahoo => "http://search.yahooapis.com/SiteExplorerService/V1/ping?sitemap=#{sitemap_index_url}&appid=#{yahoo_app_id}", + :ask => "http://submissions.ask.com/ping?sitemap=#{sitemap_index_url}", + :bing => "http://www.bing.com/webmaster/ping.aspx?siteMap=#{sitemap_index_url}", + :sitemap_writer => "http://www.sitemapwriter.com/notify.php?crawler=all&url=#{sitemap_index_url}" + } + + puts "\n" if verbose + search_engines.each do |engine, link| + next if engine == :yahoo && !self.yahoo_app_id + begin + open(link) + puts "Successful ping of #{engine.to_s.titleize}" if verbose + rescue Timeout::Error, StandardError => e + puts "Ping failed for #{engine.to_s.titleize}: #{e.inspect} (URL #{link})" if verbose + end + end - # Return true if more links can be added. - def more_links? - links.length < max_entries - end + if !self.yahoo_app_id && verbose + puts "\n" + puts <<-END.gsub(/^\s+/, '') + To ping Yahoo you require a Yahoo AppID. Add it to your config/sitemap.rb with: - # Return true if no further links can be added. - def enough_links? - !more_links? - end + SitemapGenerator::Sitemap.yahoo_app_id = "my_app_id" - # Commit buffer to gzipped file. - def write_file(name, buffer) - Zlib::GzipWriter.open(name) { |gz| gz.write buffer } + For more information see http://developer.yahoo.com/search/siteexplorer/V1/updateNotification.html + END + end end - # Report progress line. - def show_progress(title, filename, buffer) - puts "+ #{filename}" - puts "** #{title} too big! The uncompressed size exceeds 10Mb" if buffer.size > 10.megabytes + protected + + def add_default_links + self.sitemap << Link.generate('/', :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) + self.sitemap << Link.generate(self.sitemap_index, :lastmod => Time.now, :changefreq => 'always', :priority => 1.0) end - # Ping search engines passing sitemap location. - def ping_search_engines - super index_file + # Return the current sitemap filename with index. + # + # The index depends on the length of the sitemaps array. + def new_sitemap_path + File.join(self.sitemaps_path || '', "sitemap#{self.sitemaps.length + 1}.xml.gz") end - # Create sitemap files in output directory. - def create_files(verbose = true) - start_time = Time.now - load_sitemap_rb - write - stop_time = Time.now - puts "Sitemap stats: #{number_with_delimiter(SitemapGenerator::Sitemap.link_count)} links, " + ("%dm%02ds" % (stop_time - start_time).divmod(60)) if verbose + # Return the current sitemap index filename. + # + # At the moment we only support one index file which can link to + # up to 50,000 sitemap files. + def sitemap_index_path + File.join(self.sitemaps_path || '', 'sitemap_index.xml.gz') end end -end +end \ No newline at end of file diff --git a/lib/sitemap_generator/rails_helper.rb b/lib/sitemap_generator/rails_helper.rb deleted file mode 100644 index 695a7158..00000000 --- a/lib/sitemap_generator/rails_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -module SitemapGenerator - module RailsHelper - # Returns whether this environment is using ActionPack - # version 3.0.0 or greater. - # - # @return [Boolean] - def self.rails3? - # The ActionPack module is always loaded automatically in Rails >= 3 - return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) - - version = - if defined?(ActionPack::VERSION::MAJOR) - ActionPack::VERSION::MAJOR - else - # Rails 1.2 - ActionPack::VERSION::Major - end - - # 3.0.0.beta1 acts more like ActionPack 2 - # for purposes of this method - # (checking whether block helpers require = or -). - # This extra check can be removed when beta2 is out. - version >= 3 && - !(defined?(ActionPack::VERSION::TINY) && - ActionPack::VERSION::TINY == "0.beta") - end - end -end \ No newline at end of file diff --git a/lib/sitemap_generator/templates.rb b/lib/sitemap_generator/templates.rb index fbdd6f97..e14ef34b 100644 --- a/lib/sitemap_generator/templates.rb +++ b/lib/sitemap_generator/templates.rb @@ -7,8 +7,6 @@ module SitemapGenerator # Define an accessor method for each template file. class Templates FILES = { - :sitemap_index => 'sitemap_index.builder', - :sitemap_xml => 'xml_sitemap.builder', :sitemap_sample => 'sitemap.rb', } @@ -28,7 +26,7 @@ def initialize(root = SitemapGenerator.root) # Return the full path to a template. # - # file template symbol e.g. :sitemap_index + # file template symbol e.g. :sitemap_sample def template_path(template) File.join(@root, 'templates', self.class::FILES[template]) end diff --git a/lib/sitemap_generator/utilities.rb b/lib/sitemap_generator/utilities.rb index 423b16e2..63fbc510 100644 --- a/lib/sitemap_generator/utilities.rb +++ b/lib/sitemap_generator/utilities.rb @@ -3,14 +3,14 @@ module Utilities extend self # Copy templates/sitemap.rb to config if not there yet. - def install_sitemap_rb + def install_sitemap_rb(verbose=false) if File.exist?(File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "already exists: config/sitemap.rb, file not copied" + puts "already exists: config/sitemap.rb, file not copied" if verbose else FileUtils.cp( SitemapGenerator.templates.template_path(:sitemap_sample), File.join(RAILS_ROOT, 'config/sitemap.rb')) - puts "created: config/sitemap.rb" + puts "created: config/sitemap.rb" if verbose end end @@ -25,5 +25,30 @@ def uninstall_sitemap_rb def clean_files FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) end + + # Returns whether this environment is using ActionPack + # version 3.0.0 or greater. + # + # @return [Boolean] + def self.rails3? + # The ActionPack module is always loaded automatically in Rails >= 3 + return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) + + version = + if defined?(ActionPack::VERSION::MAJOR) + ActionPack::VERSION::MAJOR + else + # Rails 1.2 + ActionPack::VERSION::Major + end + + # 3.0.0.beta1 acts more like ActionPack 2 + # for purposes of this method + # (checking whether block helpers require = or -). + # This extra check can be removed when beta2 is out. + version >= 3 && + !(defined?(ActionPack::VERSION::TINY) && + ActionPack::VERSION::TINY == "0.beta") + end end end \ No newline at end of file diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 3dfec4d5..88fbfcff 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-17} + s.date = %q{2010-05-21} s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ @@ -21,11 +21,14 @@ Gem::Specification.new do |s| "Rakefile", "VERSION", "lib/sitemap_generator.rb", - "lib/sitemap_generator/helper.rb", + "lib/sitemap_generator/builder.rb", + "lib/sitemap_generator/builder/helper.rb", + "lib/sitemap_generator/builder/sitemap_file.rb", + "lib/sitemap_generator/builder/sitemap_index_file.rb", + "lib/sitemap_generator/interpreter.rb", "lib/sitemap_generator/link.rb", "lib/sitemap_generator/link_set.rb", "lib/sitemap_generator/mapper.rb", - "lib/sitemap_generator/rails_helper.rb", "lib/sitemap_generator/railtie.rb", "lib/sitemap_generator/tasks.rb", "lib/sitemap_generator/templates.rb", @@ -33,9 +36,7 @@ Gem::Specification.new do |s| "rails/install.rb", "rails/uninstall.rb", "tasks/sitemap_generator_tasks.rake", - "templates/sitemap.rb", - "templates/sitemap_index.builder", - "templates/xml_sitemap.builder" + "templates/sitemap.rb" ] s.homepage = %q{http://github.com/kjvarga/sitemap_generator} s.rdoc_options = ["--charset=UTF-8"] diff --git a/spec/sitemap_generator/templates_spec.rb b/spec/sitemap_generator/templates_spec.rb index 7dc5de08..94345dbe 100644 --- a/spec/sitemap_generator/templates_spec.rb +++ b/spec/sitemap_generator/templates_spec.rb @@ -11,14 +11,14 @@ describe "templates" do before :each do - SitemapGenerator.templates.sitemap_xml = nil + SitemapGenerator.templates.sitemap_sample = nil File.stub!(:read).and_return('read file') end it "should only be read once" do File.should_receive(:read).once - SitemapGenerator.templates.sitemap_xml - SitemapGenerator.templates.sitemap_xml + SitemapGenerator.templates.sitemap_sample + SitemapGenerator.templates.sitemap_sample end end end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0148199..c8240283 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -40,6 +40,7 @@ module Helpers # # task task symbol/string def invoke_task(task) + Rake.send(:verbose, false) Rake::Task[task.to_s].invoke Rake::Task[task.to_s].reenable end diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index e03730eb..8e1a61fa 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,4 +1,3 @@ -require 'zlib' begin require 'sitemap_generator' rescue LoadError, NameError @@ -8,7 +7,7 @@ end namespace :sitemap do desc "Install a default config/sitemap.rb file" task :install do - SitemapGenerator::Utilities.install_sitemap_rb + SitemapGenerator::Utilities.install_sitemap_rb(verbose) end desc "Delete all Sitemap files in public/ directory" @@ -25,19 +24,8 @@ namespace :sitemap do task 'refresh:no_ping' => ['sitemap:create'] task :create => [:environment] do - # TODO: Move away from auto-instantiating SitemapGenerator::Sitemap - # and move to a more natural Sitemap.new or similar. - if SitemapGenerator::RailsHelper.rails3? - SitemapGenerator::Sitemap.class_eval do - include Rails.application.routes.url_helpers - end - else - require 'action_controller' - SitemapGenerator::Sitemap.class_eval do - include ActionController::UrlWriter - end - end - SitemapGenerator::Sitemap.create_files + SitemapGenerator::Sitemap.verbose = verbose + SitemapGenerator::Sitemap.create end end diff --git a/templates/sitemap_index.builder b/templates/sitemap_index.builder deleted file mode 100644 index 5dcfe7c2..00000000 --- a/templates/sitemap_index.builder +++ /dev/null @@ -1,23 +0,0 @@ -# encoding: utf-8 -# -# -# -# http://www.example.com/sitemap1.xml.gz -# 2004-10-01T18:23:17+00:00 -# -# -# http://www.example.com/sitemap2.xml.gz -# 2005-01-01 -# -# - -xml.instruct! -xml.sitemapindex "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do - sitemap_files.each do |file| - xml.sitemap do - xml.loc url_with_hostname(File.basename(file)) - xml.lastmod w3c_date(File.mtime(file)) - end - end -end - diff --git a/templates/xml_sitemap.builder b/templates/xml_sitemap.builder deleted file mode 100644 index 61a8bf15..00000000 --- a/templates/xml_sitemap.builder +++ /dev/null @@ -1,38 +0,0 @@ -# encoding: utf-8 -xml.instruct! -xml.urlset "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", - "xsi:schemaLocation" => "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd", - "xmlns:image" => "http://www.google.com/schemas/sitemap-image/1.1", - "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do - - links.each_with_index do |link,index| - buffer_url = "" - url = Builder::XmlMarkup.new(:target=>buffer_url) - url.url do - url.loc link[:loc] - url.lastmod w3c_date(link[:lastmod]) if link[:lastmod] - url.changefreq link[:changefreq] if link[:changefreq] - url.priority link[:priority] if link[:priority] - - unless link[:images].blank? - link[:images].each do |image| - url.image:image do - url.image :loc, image[:loc] - url.image :caption, image[:caption] if image[:caption] - url.image :geo_location, image[:geo_location] if image[:geo_location] - url.image :title, image[:title] if image[:title] - url.image :license, image[:license] if image[:license] - end - end - end - end - - if (buffer+buffer_url).size < 10.megabytes - xml << buffer_url - else - slice_index = index - break - end - end -end - From ebdf4886a920f25910327e7963dd74d99e04f8e9 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 21 May 2010 02:38:38 -0700 Subject: [PATCH 051/677] Version bump to 0.3.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 53a75d67..0d91a54c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.6 +0.3.0 From 22dcb61705be75ac4c2a623af54db0d3c7385e46 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 21 May 2010 02:39:42 -0700 Subject: [PATCH 052/677] Regenerated gemspec for version 0.3.0 --- sitemap_generator.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 88fbfcff..89653d80 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.2.6" + s.version = "0.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] From a8563d33058f230d4b44d41c6dd817b7cb5f1d16 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 21 May 2010 13:43:46 -0700 Subject: [PATCH 053/677] Partial fixes for Rails 3 --- lib/sitemap_generator.rb | 2 ++ lib/sitemap_generator/link_set.rb | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/sitemap_generator.rb b/lib/sitemap_generator.rb index cc5d4da0..a06dec87 100644 --- a/lib/sitemap_generator.rb +++ b/lib/sitemap_generator.rb @@ -6,6 +6,8 @@ require 'sitemap_generator/utilities' require 'sitemap_generator/railtie' if SitemapGenerator::Utilities.rails3? +require 'active_support/core_ext/numeric' + module SitemapGenerator silence_warnings do VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index b16a6575..7ad74688 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -18,6 +18,8 @@ class LinkSet def create require 'sitemap_generator/interpreter' + self.public_path = File.join(::Rails.root, 'public/') if self.public_path.nil? + start_time = Time.now SitemapGenerator::Interpreter.run finalize! @@ -35,7 +37,6 @@ def create # default_host hostname including protocol to use in all sitemap links # e.g. http://en.google.ca def initialize(public_path = nil, sitemaps_path = nil, default_host = nil) - public_path = File.join(::Rails.root, 'public/') if public_path.nil? self.default_host = default_host self.public_path = public_path self.sitemaps_path = sitemaps_path From 770375cc58f7864581cea83011dbe8b735d21de5 Mon Sep 17 00:00:00 2001 From: Jason Weathered Date: Sat, 22 May 2010 21:31:17 +1000 Subject: [PATCH 054/677] Add schema validation tests. --- Rakefile | 2 + spec/sitemap_generator_spec.rb | 8 +++ spec/spec_helper.rb | 1 + spec/support/siteindex.xsd | 73 +++++++++++++++++++++ spec/support/sitemap.xsd | 115 +++++++++++++++++++++++++++++++++ spec/support/xml_macros.rb | 22 +++++++ 6 files changed, 221 insertions(+) create mode 100644 spec/support/siteindex.xsd create mode 100644 spec/support/sitemap.xsd create mode 100644 spec/support/xml_macros.rb diff --git a/Rakefile b/Rakefile index 22668146..b6e1bf65 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,7 @@ require 'rake/rdoctask' require 'rubygems' gem 'rspec', '1.3.0' require 'spec/rake/spectask' +gem 'nokogiri' begin require 'jeweler' @@ -16,6 +17,7 @@ begin gem.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] gem.test_files = [] gem.add_development_dependency "rspec" + gem.add_development_dependency "nokogiri" end Jeweler::GemcutterTasks.new rescue LoadError diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index 6c32540f..ba84738e 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -55,6 +55,14 @@ it "should have 14 links" do SitemapGenerator::Sitemap.link_count.should == 14 end + + it "index XML should validate" do + gzipped_xml_file_should_validate_against_schema rails_path('/public/sitemap_index.xml.gz'), 'siteindex' + end + + it "sitemap XML should validate" do + gzipped_xml_file_should_validate_against_schema rails_path('/public/sitemap1.xml.gz'), 'sitemap' + end end protected diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c8240283..b4260f54 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,7 @@ Spec::Runner.configure do |config| config.include(FileMacros) + config.include(XmlMacros) end module Helpers diff --git a/spec/support/siteindex.xsd b/spec/support/siteindex.xsd new file mode 100644 index 00000000..efc41636 --- /dev/null +++ b/spec/support/siteindex.xsd @@ -0,0 +1,73 @@ + + + + + XML Schema for Sitemap index files. + Last Modifed 2009-04-08 + + + + + + + Container for a set of up to 50,000 sitemap URLs. + This is the root element of the XML file. + + + + + + + + + + + + + Container for the data needed to describe a sitemap. + + + + + + + + + + + + REQUIRED: The location URI of a sitemap. + The URI must conform to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt). + + + + + + + + + + + + OPTIONAL: The date the document was last modified. The date must conform + to the W3C DATETIME format (http://www.w3.org/TR/NOTE-datetime). + Example: 2005-05-10 + Lastmod may also contain a timestamp. + Example: 2005-05-10T17:33:30+08:00 + + + + + + + + + + + + + + diff --git a/spec/support/sitemap.xsd b/spec/support/sitemap.xsd new file mode 100644 index 00000000..17efb326 --- /dev/null +++ b/spec/support/sitemap.xsd @@ -0,0 +1,115 @@ + + + + + XML Schema for Sitemap files. + Last Modifed 2008-03-26 + + + + + + + Container for a set of up to 50,000 document elements. + This is the root element of the XML file. + + + + + + + + + + + + + Container for the data needed to describe a document to crawl. + + + + + + + + + + + + + + + REQUIRED: The location URI of a document. + The URI must conform to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt). + + + + + + + + + + + + OPTIONAL: The date the document was last modified. The date must conform + to the W3C DATETIME format (http://www.w3.org/TR/NOTE-datetime). + Example: 2005-05-10 + Lastmod may also contain a timestamp. + Example: 2005-05-10T17:33:30+08:00 + + + + + + + + + + + + + + + + OPTIONAL: Indicates how frequently the content at a particular URL is + likely to change. The value "always" should be used to describe + documents that change each time they are accessed. The value "never" + should be used to describe archived URLs. Please note that web + crawlers may not necessarily crawl pages marked "always" more often. + Consider this element as a friendly suggestion and not a command. + + + + + + + + + + + + + + + + + OPTIONAL: The priority of a particular URL relative to other pages + on the same site. The value for this element is a number between + 0.0 and 1.0 where 0.0 identifies the lowest priority page(s). + The default priority of a page is 0.5. Priority is used to select + between pages on your site. Setting a priority of 1.0 for all URLs + will not help you, as the relative priority of pages on your site + is what will be considered. + + + + + + + + + diff --git a/spec/support/xml_macros.rb b/spec/support/xml_macros.rb new file mode 100644 index 00000000..f637933a --- /dev/null +++ b/spec/support/xml_macros.rb @@ -0,0 +1,22 @@ +require 'nokogiri' + +module XmlMacros + + def gzipped_xml_file_should_validate_against_schema(xml_gz_filename, schema_name) + Zlib::GzipReader.open(xml_gz_filename) do |xml_file| + xml_data_should_validate_against_schema xml_file.read, schema_name + end + end + + def xml_data_should_validate_against_schema(xml_data, schema_name) + + schema_file = File.join(File.dirname(__FILE__), "#{schema_name}.xsd") + schema = Nokogiri::XML::Schema File.read(schema_file) + + doc = Nokogiri::XML(xml_data) + + schema.validate(doc).should == [] + + end + +end From c661bc158e9135af3f3a8004bb7a6d299feda0e7 Mon Sep 17 00:00:00 2001 From: Jason Weathered Date: Sat, 22 May 2010 21:33:21 +1000 Subject: [PATCH 055/677] Sitemap index should contain entries. Fixes regression by d785618b. --- lib/sitemap_generator/builder/sitemap_index_file.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sitemap_generator/builder/sitemap_index_file.rb b/lib/sitemap_generator/builder/sitemap_index_file.rb index 06b7704c..c1cb7757 100644 --- a/lib/sitemap_generator/builder/sitemap_index_file.rb +++ b/lib/sitemap_generator/builder/sitemap_index_file.rb @@ -13,7 +13,7 @@ def initialize(*args) # Return XML as a String def build_xml(builder, link) - builder.url do + builder.sitemap do builder.loc link[:loc] builder.lastmod w3c_date(link[:lastmod]) if link[:lastmod] end From baefa3dce82e12d9812485d27c5a83cd0afd94d2 Mon Sep 17 00:00:00 2001 From: Jason Weathered Date: Sat, 22 May 2010 22:18:49 +1000 Subject: [PATCH 056/677] Fix XML schema and namespace attributes. Reformat onto multiple lines for readability and cleaner future diffs. sitemapindex: * typo in @xml_content initialization. * add schema sitemap: * fix schema path (s/siteindex.xsd/sitemap.xsd/) --- lib/sitemap_generator/builder/sitemap_file.rb | 12 +++++++++++- lib/sitemap_generator/builder/sitemap_index_file.rb | 13 +++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index 90ef10cd..9eb1e9f0 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -24,7 +24,17 @@ def initialize(public_path, sitemap_path, hostname) self.link_count = 0 @xml_content = '' # XML urlset content - @xml_wrapper_start = %q[] + @xml_wrapper_start = <<-HTML + + + HTML + @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>') @xml_wrapper_end = %q[] self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize end diff --git a/lib/sitemap_generator/builder/sitemap_index_file.rb b/lib/sitemap_generator/builder/sitemap_index_file.rb index c1cb7757..b3829429 100644 --- a/lib/sitemap_generator/builder/sitemap_index_file.rb +++ b/lib/sitemap_generator/builder/sitemap_index_file.rb @@ -5,8 +5,17 @@ class SitemapIndexFile < SitemapFile def initialize(*args) super(*args) - @ml_content = '' # XML urlset content - @xml_wrapper_start = %q[] + @xml_content = '' # XML urlset content + @xml_wrapper_start = <<-HTML + + + HTML + @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>') @xml_wrapper_end = %q[] self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize end From 24039d95ddc74420f844a2f68c8614ed26de9c64 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Sat, 22 May 2010 21:24:27 -0700 Subject: [PATCH 057/677] Version bump to 0.3.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0d91a54c..9e11b32f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.3.1 From e91a1df3550c4975a17b1957c42a0481cfcd98d3 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Sat, 22 May 2010 21:24:42 -0700 Subject: [PATCH 058/677] Regenerated gemspec for version 0.3.1 --- sitemap_generator.gemspec | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 89653d80..3441e421 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.3.0" + s.version = "0.3.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-21} + s.date = %q{2010-05-22} s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ @@ -50,11 +50,14 @@ Gem::Specification.new do |s| if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, [">= 0"]) else s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) end else s.add_dependency(%q, [">= 0"]) + s.add_dependency(%q, [">= 0"]) end end From 88b5dc5a845d018bd31252d982c5908519271d47 Mon Sep 17 00:00:00 2001 From: Jason Weathered Date: Mon, 24 May 2010 07:06:59 +0800 Subject: [PATCH 059/677] Add whitespace tests. --- spec/sitemap_generator_spec.rb | 8 ++++++++ spec/support/xml_macros.rb | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/spec/sitemap_generator_spec.rb b/spec/sitemap_generator_spec.rb index ba84738e..0dc46de0 100644 --- a/spec/sitemap_generator_spec.rb +++ b/spec/sitemap_generator_spec.rb @@ -63,6 +63,14 @@ it "sitemap XML should validate" do gzipped_xml_file_should_validate_against_schema rails_path('/public/sitemap1.xml.gz'), 'sitemap' end + + it "index XML should not have excess whitespace" do + gzipped_xml_file_should_have_minimal_whitespace rails_path('/public/sitemap_index.xml.gz') + end + + it "sitemap XML should not have excess whitespace" do + gzipped_xml_file_should_have_minimal_whitespace rails_path('/public/sitemap1.xml.gz') + end end protected diff --git a/spec/support/xml_macros.rb b/spec/support/xml_macros.rb index f637933a..21636da5 100644 --- a/spec/support/xml_macros.rb +++ b/spec/support/xml_macros.rb @@ -19,4 +19,18 @@ def xml_data_should_validate_against_schema(xml_data, schema_name) end + def gzipped_xml_file_should_have_minimal_whitespace(xml_gz_filename) + Zlib::GzipReader.open(xml_gz_filename) do |xml_file| + xml_data_should_have_minimal_whitespace xml_file.read + end + end + + def xml_data_should_have_minimal_whitespace(xml_data) + xml_data.should_not match /^\s/ + xml_data.should_not match /\s$/ + xml_data.should_not match /\s\s+/ + xml_data.should_not match /\s[<>]/ + xml_data.should_not match /[<>]\s/ + end + end From 481c3d2bcf65966cb2006c8fc54b45b89c742e3a Mon Sep 17 00:00:00 2001 From: Jason Weathered Date: Mon, 24 May 2010 07:08:34 +0800 Subject: [PATCH 060/677] Strip leading whitespace from XML declaration. --- lib/sitemap_generator/builder/sitemap_file.rb | 2 +- lib/sitemap_generator/builder/sitemap_index_file.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index 9eb1e9f0..17d2cd2d 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -34,7 +34,7 @@ def initialize(public_path, sitemap_path, hostname) xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" > HTML - @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>') + @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip! @xml_wrapper_end = %q[] self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize end diff --git a/lib/sitemap_generator/builder/sitemap_index_file.rb b/lib/sitemap_generator/builder/sitemap_index_file.rb index b3829429..409741a5 100644 --- a/lib/sitemap_generator/builder/sitemap_index_file.rb +++ b/lib/sitemap_generator/builder/sitemap_index_file.rb @@ -15,7 +15,7 @@ def initialize(*args) xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" > HTML - @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>') + @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip! @xml_wrapper_end = %q[] self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize end From 5bdf264e5e3d2d7a29becb19e40ca0b70c4d7577 Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Sun, 23 May 2010 13:12:36 +0200 Subject: [PATCH 061/677] Fixed method link_count for Ruby v1.8 --- lib/sitemap_generator/link_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 7ad74688..38e7dc5a 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -46,7 +46,7 @@ def initialize(public_path = nil, sitemaps_path = nil, default_host = nil) end def link_count - self.sitemaps.map(&:link_count).inject(:+) + self.sitemaps.inject(0) { |link_count_sum, sitemap| link_count_sum + sitemap.link_count } end # Called within the user's eval'ed sitemap config file. Add links to sitemap files From 96053df816934d476b555401f5c66e503ef2d04d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 24 May 2010 21:19:11 -0700 Subject: [PATCH 062/677] Add a task to release a new version --- Rakefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Rakefile b/Rakefile index b6e1bf65..ec6f712d 100644 --- a/Rakefile +++ b/Rakefile @@ -78,6 +78,14 @@ namespace :test do end end +desc "Release a new patch version" +task :release_new_version do + Rake::Task['version:bump:patch'].invoke + Rake::Task['github:release'].invoke + Rake::Task['git:release'].invoke + Rake::Task['gemcutter:release'].invoke +end + desc "Run tests as a gem install" task :test => ['test:gem'] From 69a693fff299967fc953f0bd1e5bb50785c7eb89 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 24 May 2010 21:19:21 -0700 Subject: [PATCH 063/677] Version bump to 0.3.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9e11b32f..d15723fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.1 +0.3.2 From 367fd192090a17d2eac1ffe8e85aee9883972468 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Mon, 24 May 2010 21:19:21 -0700 Subject: [PATCH 064/677] Regenerated gemspec for version 0.3.2 --- sitemap_generator.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 3441e421..3ac205d9 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.3.1" + s.version = "0.3.2" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-22} + s.date = %q{2010-05-24} s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ From 56b3b2c3dc93e092430f1af523d0f867cc2f56e9 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 25 May 2010 12:25:36 -0700 Subject: [PATCH 065/677] Update contributors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8f04a02..ec7b1270 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ Thanks (in no particular order) - [Rob Biedenharn](http://github.com/rab) - [Richie Vos](http://github.com/jerryvos) - [Adrian Mugnolo](http://github.com/xymbol) - +- [Jason Weathered](http://github.com/jasoncodes) Copyright (c) 2009 Karl Varga released under the MIT license From ed80f70b0cee2b2b46e829a99e1eca9c90eee22d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 28 May 2010 11:47:57 -0700 Subject: [PATCH 066/677] Open the file with write binary flags --- Rakefile | 3 +++ lib/sitemap_generator/builder/sitemap_file.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index ec6f712d..a31f91d8 100644 --- a/Rakefile +++ b/Rakefile @@ -49,6 +49,9 @@ end task :default => :test namespace :test do + #desc "Test as a gem, plugin and Rails 3 gem" + #task :all => ['test:gem', 'test:plugin'] + task :gem => ['test:prepare:gem', 'multi_spec'] task :plugin => ['test:prepare:plugin', 'multi_spec'] task :rails3 => ['test:prepare:rails3', 'multi_spec'] diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index 17d2cd2d..be9be60c 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -109,7 +109,7 @@ def build_xml(builder, link) def finalize! return if self.frozen? - open(self.full_path, 'w') do |file| + open(self.full_path, 'wb') do |file| gz = Zlib::GzipWriter.new(file) gz.write @xml_wrapper_start gz.write @xml_content From f01b0d948c2467255dd4ae7794e6779e4bc90c9d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 28 May 2010 11:48:44 -0700 Subject: [PATCH 067/677] Version bump to 0.3.3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d15723fb..1c09c74e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.2 +0.3.3 From fa14c196d574aa9a47d6a01598f6e29d9b10ad2f Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Fri, 28 May 2010 11:48:45 -0700 Subject: [PATCH 068/677] Regenerated gemspec for version 0.3.3 --- sitemap_generator.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 3ac205d9..ec8721b0 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.3.2" + s.version = "0.3.3" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-24} + s.date = %q{2010-05-28} s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ From 0b0c27416664f0fbf385d31596da44802210c0fa Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 20 Jul 2010 18:15:53 -0700 Subject: [PATCH 069/677] Simplify the Rails 3 detection --- lib/sitemap_generator/utilities.rb | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/lib/sitemap_generator/utilities.rb b/lib/sitemap_generator/utilities.rb index 63fbc510..bfa4e627 100644 --- a/lib/sitemap_generator/utilities.rb +++ b/lib/sitemap_generator/utilities.rb @@ -26,29 +26,11 @@ def clean_files FileUtils.rm(Dir[File.join(RAILS_ROOT, 'public/sitemap*.xml.gz')]) end - # Returns whether this environment is using ActionPack - # version 3.0.0 or greater. + # Returns a boolean indicating whether this environment is Rails 3 # # @return [Boolean] def self.rails3? - # The ActionPack module is always loaded automatically in Rails >= 3 - return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) - - version = - if defined?(ActionPack::VERSION::MAJOR) - ActionPack::VERSION::MAJOR - else - # Rails 1.2 - ActionPack::VERSION::Major - end - - # 3.0.0.beta1 acts more like ActionPack 2 - # for purposes of this method - # (checking whether block helpers require = or -). - # This extra check can be removed when beta2 is out. - version >= 3 && - !(defined?(ActionPack::VERSION::TINY) && - ActionPack::VERSION::TINY == "0.beta") + Rails.version.to_f >= 3 end end end \ No newline at end of file From 7593239859aacb43da6207e9b4c806688d09c699 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 20 Jul 2010 18:16:31 -0700 Subject: [PATCH 070/677] Ruby <= 1.8.6 compatibility: use string.length instead of string.bytesize --- lib/sitemap_generator/builder/sitemap_file.rb | 11 ++++++++--- lib/sitemap_generator/builder/sitemap_index_file.rb | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index be9be60c..2a196c72 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -36,7 +36,7 @@ def initialize(public_path, sitemap_path, hostname) HTML @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip! @xml_wrapper_end = %q[] - self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize + self.filesize = bytesize(@xml_wrapper_start) + bytesize(@xml_wrapper_end) end def lastmod @@ -67,13 +67,13 @@ def file_can_fit?(bytes) # If a link cannot be added, the file is too large or the link limit has been reached. def add_link(link) xml = build_xml(::Builder::XmlMarkup.new, link) - unless file_can_fit?(xml.bytesize) + unless file_can_fit?(bytesize(xml)) self.finalize! return false end @xml_content << xml - self.filesize += xml.bytesize + self.filesize += bytesize(xml) self.link_count += 1 true end @@ -119,6 +119,11 @@ def finalize! @xml_content = @xml_wrapper_start = @xml_wrapper_end = '' self.freeze end + + # Return the bytesize length of the string + def bytesize(string) + string.respond_to?(:bytesize) ? string.bytesize : string.length + end end end end \ No newline at end of file diff --git a/lib/sitemap_generator/builder/sitemap_index_file.rb b/lib/sitemap_generator/builder/sitemap_index_file.rb index 409741a5..a5df9d09 100644 --- a/lib/sitemap_generator/builder/sitemap_index_file.rb +++ b/lib/sitemap_generator/builder/sitemap_index_file.rb @@ -17,7 +17,7 @@ def initialize(*args) HTML @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip! @xml_wrapper_end = %q[] - self.filesize = @xml_wrapper_start.bytesize + @xml_wrapper_end.bytesize + self.filesize = bytesize(@xml_wrapper_start) + bytesize(@xml_wrapper_end) end # Return XML as a String From c6e454e71411517a97bc27e5aa31953fd8a552f6 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 20 Jul 2010 18:40:29 -0700 Subject: [PATCH 071/677] Update gem description & add better release task to Rakefile --- README.md | 15 +++++++-------- Rakefile | 28 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ec7b1270..d4b01da5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ SitemapGenerator ================ -A Rails 3-compatible gem/plugin to generate ['enterprise-class'][enterprise_class] Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the ['Sitemap protocol specification'][sitemap_protocol]. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links. +SitemapGenerator is a Rails gem that makes it easy to generate ['enterprise-class'][enterprise_class] Sitemaps readable by all search engines. Generated Sitemaps adhere to the ['Sitemap protocol specification'][sitemap_protocol]. When you generate new Sitemaps, SitemapGenerator can automatically ping the major search engines (including Google, Yahoo and Bing) to notify them. SitemapGenerator includes rake tasks to easily manage your sitemaps. Features ------- -- v0.2.6: **Support ['image sitemaps'][sitemap_images]**! -- v0.2.5: **Support Rails 3**! +- v0.2.6: ['Google Image Sitemap'][sitemap_images] support +- v0.2.5: Rails 3 support (beta) - Adheres to the ['Sitemap protocol specification'][sitemap_protocol] - Handles millions of links - Automatic Gzip of Sitemap files - Automatic ping of search engines to notify them of new sitemaps: Google, Yahoo, Bing, Ask, SitemapWriter -- Won't clobber your old sitemaps if the new one fails to generate -- Set the priority of links, change frequency etc -- You control which links are included -- You set the host name, so it doesn't matter if your application is in a subdirectory +- Leaves your old sitemaps in place if a new one fails to generate +- Allows you to set the hostname for the links in your Sitemap Foreword ------- @@ -163,7 +161,7 @@ Tested and working on: - **Rails** 3.0.0, sitemap_generator version >= 0.2.5 - **Rails** 1.x - 2.3.5 -- **Ruby** 1.8.7, 1.9.1 +- **Ruby** 1.8.6, 1.8.7, 1.9.1 Notes ======= @@ -213,6 +211,7 @@ Thanks (in no particular order) - [Richie Vos](http://github.com/jerryvos) - [Adrian Mugnolo](http://github.com/xymbol) - [Jason Weathered](http://github.com/jasoncodes) +- [Andy Stewart](http://github.com/airblade) Copyright (c) 2009 Karl Varga released under the MIT license diff --git a/Rakefile b/Rakefile index a31f91d8..41c90e7c 100644 --- a/Rakefile +++ b/Rakefile @@ -10,10 +10,10 @@ begin Jeweler::Tasks.new do |gem| gem.name = "sitemap_generator" gem.summary = %Q{Easily generate enterprise class Sitemaps for your Rails site using a familiar Rails Routes-like DSL} - gem.description = %Q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} + gem.description = %Q{SitemapGenerator is a Rails gem that makes it easy to generate enterprise-class Sitemaps readable by all search engines. Generated Sitemaps adhere to the Sitemap protocol specification. When you generate new Sitemaps, SitemapGenerator can automatically ping the major search engines (including Google, Yahoo and Bing) to notify them. SitemapGenerator includes rake tasks to easily manage your sitemaps.} gem.email = "kjvarga@gmail.com" gem.homepage = "http://github.com/kjvarga/sitemap_generator" - gem.authors = ["Adam Salter", "Karl Varga"] + gem.authors = ["Karl Varga", "Adam Salter"] gem.files = FileList["[A-Z]*", "{bin,lib,rails,templates,tasks}/**/*"] gem.test_files = [] gem.add_development_dependency "rspec" @@ -81,14 +81,6 @@ namespace :test do end end -desc "Release a new patch version" -task :release_new_version do - Rake::Task['version:bump:patch'].invoke - Rake::Task['github:release'].invoke - Rake::Task['git:release'].invoke - Rake::Task['gemcutter:release'].invoke -end - desc "Run tests as a gem install" task :test => ['test:gem'] @@ -111,4 +103,20 @@ Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.options << '--line-numbers' << '--inline-source' rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('lib/**/*.rb') +end + +namespace :release do + + desc "Release a new patch version" + task :patch do + Rake::Task['version:bump:patch'].invoke + Rake::Task['release:current'].invoke + end + + desc "Release the current version (e.g. after a version bump). This rebuilds the gemspec, pushes the updated code, tags it and releases to RubyGems" + task :current do + Rake::Task['github:release'].invoke + Rake::Task['git:release'].invoke + Rake::Task['gemcutter:release'].invoke + end end \ No newline at end of file From 6ee4291f6428833faf27703457a3382abcfa4a02 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 20 Jul 2010 18:40:56 -0700 Subject: [PATCH 072/677] Version bump to 1.0.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1c09c74e..3eefcb9d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +1.0.0 From 0295282a1edf2a6d68e6558507b348a687ae7704 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 20 Jul 2010 18:41:12 -0700 Subject: [PATCH 073/677] Regenerated gemspec for version 1.0.0 --- sitemap_generator.gemspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index ec8721b0..5e907f29 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,12 +5,12 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "0.3.3" + s.version = "1.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Adam Salter", "Karl Varga"] - s.date = %q{2010-05-28} - s.description = %q{A Rails 3-compatible gem/plugin to generate enterprise-class Sitemaps using a familiar Rails Routes-like DSL. Sitemaps are readable by all search engines and adhere to the Sitemap protocol specification. Automatically pings search engines to notify them of new sitemaps (including Google, Yahoo and Bing). Provides rake tasks to easily manage your sitemaps. Supports image sitemaps and handles millions of links.} + s.authors = ["Karl Varga", "Adam Salter"] + s.date = %q{2010-07-20} + s.description = %q{SitemapGenerator is a Rails gem that makes it easy to generate enterprise-class Sitemaps readable by all search engines. Generated Sitemaps adhere to the Sitemap protocol specification. When you generate new Sitemaps, SitemapGenerator can automatically ping the major search engines (including Google, Yahoo and Bing) to notify them. SitemapGenerator includes rake tasks to easily manage your sitemaps.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ "README.md" From 5ae33dedd259c55fd1f7b30b4b40aff94951134d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 21 Jul 2010 10:43:57 -0700 Subject: [PATCH 074/677] Fix bug when installed as a plugin, we need to load the environment for all rake tasks --- tasks/sitemap_generator_tasks.rake | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tasks/sitemap_generator_tasks.rake b/tasks/sitemap_generator_tasks.rake index 8e1a61fa..d2bd0f8f 100644 --- a/tasks/sitemap_generator_tasks.rake +++ b/tasks/sitemap_generator_tasks.rake @@ -1,17 +1,26 @@ -begin +environment = begin + + # Try to require the library. If we are installed as a gem, this should work. + # We don't need to load the environment. require 'sitemap_generator' -rescue LoadError, NameError - # Application should work without vlad + [] + +rescue LoadError + + # We must be installed as a plugin. Make sure the environment is loaded + # when running all rake tasks. + [:environment] + end namespace :sitemap do desc "Install a default config/sitemap.rb file" - task :install do + task :install => environment do SitemapGenerator::Utilities.install_sitemap_rb(verbose) end desc "Delete all Sitemap files in public/ directory" - task :clean do + task :clean => environment do SitemapGenerator::Utilities.clean_files end @@ -27,5 +36,4 @@ namespace :sitemap do SitemapGenerator::Sitemap.verbose = verbose SitemapGenerator::Sitemap.create end -end - +end \ No newline at end of file From b08c4b9e29e4c90550ec6745dce13154ae23631a Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 21 Jul 2010 10:44:36 -0700 Subject: [PATCH 075/677] Version bump to 1.0.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3eefcb9d..7dea76ed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 From 5b9ea75c941089fd823b5741b0f583372eedc2a5 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 21 Jul 2010 10:44:37 -0700 Subject: [PATCH 076/677] Regenerated gemspec for version 1.0.1 --- sitemap_generator.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sitemap_generator.gemspec b/sitemap_generator.gemspec index 5e907f29..59fabcb5 100644 --- a/sitemap_generator.gemspec +++ b/sitemap_generator.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{sitemap_generator} - s.version = "1.0.0" + s.version = "1.0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Karl Varga", "Adam Salter"] - s.date = %q{2010-07-20} + s.date = %q{2010-07-21} s.description = %q{SitemapGenerator is a Rails gem that makes it easy to generate enterprise-class Sitemaps readable by all search engines. Generated Sitemaps adhere to the Sitemap protocol specification. When you generate new Sitemaps, SitemapGenerator can automatically ping the major search engines (including Google, Yahoo and Bing) to notify them. SitemapGenerator includes rake tasks to easily manage your sitemaps.} s.email = %q{kjvarga@gmail.com} s.extra_rdoc_files = [ From a26e94ba4145d5fba67f1a5b6e55aeb8e23c1d8c Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Wed, 21 Jul 2010 10:44:37 -0700 Subject: [PATCH 077/677] removing first space on xml start. ' ' it's allow firefox xml view merging jason merging kjvarga/sitemap_generator add sitemap name suport for example: SitemapGenerator::Sitemap.add_links :name=>neighborhood do |sitemap| ... end adjusting README --- README.md | 9 ++++++- lib/sitemap_generator/link_set.rb | 40 +++++++++++++++++++++---------- lib/sitemap_generator/mapper.rb | 10 ++++---- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d4b01da5..6bf7865e 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,13 @@ Example 'config/sitemap.rb' end end + + # Create a new sitemap with specific name + SitemapGenerator::Sitemap.add_links :name=>"neighborhoods" do |sitemap| + Neighborhood.all.each do |r| + sitemap.add neighborhood_path(r.permalink) + end + end # Including Sitemaps from Rails Engines. # @@ -224,4 +231,4 @@ Copyright (c) 2009 Karl Varga released under the MIT license [boost_juice]:http://www.boostjuice.com.au/ "Mmmm, sweet, sweet Boost Juice." [cb]:http://codebright.net "http://codebright.net" [sitemap_images]:http://www.google.com/support/webmasters/bin/answer.py?answer=178636 -[sitemap_protocol]:http://sitemaps.org/protocol.php \ No newline at end of file +[sitemap_protocol]:http://sitemaps.org/protocol.php diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 38e7dc5a..36a57bcf 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -9,7 +9,7 @@ class LinkSet attr_accessor :default_host, :public_path, :sitemaps_path attr_accessor :sitemap, :sitemaps, :sitemap_index - attr_accessor :verbose, :yahoo_app_id + attr_accessor :verbose, :yahoo_app_id, :named_sitemaps # Evaluate the sitemap config file and write all sitemaps. # @@ -43,6 +43,7 @@ def initialize(public_path = nil, sitemaps_path = nil, default_host = nil) # Completed sitemaps self.sitemaps = [] + self.named_sitemaps = [] end def link_count @@ -53,23 +54,23 @@ def link_count # passing a block. # # TODO: Refactor. The call chain is confusing and convoluted here. - def add_links + def add_links(options={}) raise ArgumentError, "Default hostname not set" if default_host.blank? # I'd rather have these calls in create but we have to wait # for default_host to be set by the user's sitemap config - new_sitemap + new_sitemap(options) add_default_links - yield Mapper.new(self) + yield Mapper.new(self, options) end # Called from Mapper. # # Add a link to the current sitemap. - def add_link(link) + def add_link(link, options={}) unless self.sitemap << link - new_sitemap + new_sitemap options self.sitemap << link end end @@ -78,13 +79,13 @@ def add_link(link) # start a new sitemap. # # If the current sitemap is nil or empty it is not added. - def new_sitemap + def new_sitemap(options={}) unless self.sitemap_index self.sitemap_index = SitemapGenerator::Builder::SitemapIndexFile.new(public_path, sitemap_index_path, default_host) end unless self.sitemap - self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path(options), default_host) end # Mark the sitemap as complete and add it to the sitemap index @@ -94,7 +95,7 @@ def new_sitemap self.sitemaps << self.sitemap show_progress(self.sitemap) if verbose - self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path(options), default_host) end end @@ -159,8 +160,23 @@ def add_default_links # Return the current sitemap filename with index. # # The index depends on the length of the sitemaps array. - def new_sitemap_path - File.join(self.sitemaps_path || '', "sitemap#{self.sitemaps.length + 1}.xml.gz") + def new_sitemap_path(options={}) + base = self.sitemaps_path || '' + + if options[:name] + count = 0 + sitemap_name = File.join(base, "sitemap-#{options[:name]}.xml.gz") + sitemaps.each do |s| + if s.sitemap_path == sitemap_name + count = count+1 + sitemap_name = File.join(base, "sitemap-#{options[:name]}-#{count}.xml.gz") + end + end + named_sitemaps << sitemap + else + sitemap_name = File.join(base, "sitemap-#{self.sitemaps.length - named_sitemaps.length + 1}.xml.gz") + end + sitemap_name end # Return the current sitemap index filename. @@ -171,4 +187,4 @@ def sitemap_index_path File.join(self.sitemaps_path || '', 'sitemap_index.xml.gz') end end -end \ No newline at end of file +end diff --git a/lib/sitemap_generator/mapper.rb b/lib/sitemap_generator/mapper.rb index bbd35fe1..1145edc5 100644 --- a/lib/sitemap_generator/mapper.rb +++ b/lib/sitemap_generator/mapper.rb @@ -2,15 +2,15 @@ module SitemapGenerator # Generator instances are used to build links. # The object passed to the add_links block in config/sitemap.rb is a Generator instance. class Mapper - attr_accessor :set + attr_accessor :set, :options - def initialize(set) + def initialize(set, options={}) @set = set + @options = options end - def add(loc, options = {}) - set.add_link Link.generate(loc, options) + def add(loc, link = {}) + set.add_link Link.generate(loc, link), options end end end - From 74a17fa9f3b3fd7b7d76b045e3b5d457f8cf20a3 Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 16 Sep 2010 11:38:44 -0700 Subject: [PATCH 078/677] Revert "removing first space on xml start. ' ' it's allow firefox xml view" This reverts commit a26e94ba4145d5fba67f1a5b6e55aeb8e23c1d8c. --- README.md | 9 +------ lib/sitemap_generator/link_set.rb | 40 ++++++++++--------------------- lib/sitemap_generator/mapper.rb | 10 ++++---- 3 files changed, 18 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 6bf7865e..d4b01da5 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,6 @@ Example 'config/sitemap.rb' end end - - # Create a new sitemap with specific name - SitemapGenerator::Sitemap.add_links :name=>"neighborhoods" do |sitemap| - Neighborhood.all.each do |r| - sitemap.add neighborhood_path(r.permalink) - end - end # Including Sitemaps from Rails Engines. # @@ -231,4 +224,4 @@ Copyright (c) 2009 Karl Varga released under the MIT license [boost_juice]:http://www.boostjuice.com.au/ "Mmmm, sweet, sweet Boost Juice." [cb]:http://codebright.net "http://codebright.net" [sitemap_images]:http://www.google.com/support/webmasters/bin/answer.py?answer=178636 -[sitemap_protocol]:http://sitemaps.org/protocol.php +[sitemap_protocol]:http://sitemaps.org/protocol.php \ No newline at end of file diff --git a/lib/sitemap_generator/link_set.rb b/lib/sitemap_generator/link_set.rb index 36a57bcf..38e7dc5a 100644 --- a/lib/sitemap_generator/link_set.rb +++ b/lib/sitemap_generator/link_set.rb @@ -9,7 +9,7 @@ class LinkSet attr_accessor :default_host, :public_path, :sitemaps_path attr_accessor :sitemap, :sitemaps, :sitemap_index - attr_accessor :verbose, :yahoo_app_id, :named_sitemaps + attr_accessor :verbose, :yahoo_app_id # Evaluate the sitemap config file and write all sitemaps. # @@ -43,7 +43,6 @@ def initialize(public_path = nil, sitemaps_path = nil, default_host = nil) # Completed sitemaps self.sitemaps = [] - self.named_sitemaps = [] end def link_count @@ -54,23 +53,23 @@ def link_count # passing a block. # # TODO: Refactor. The call chain is confusing and convoluted here. - def add_links(options={}) + def add_links raise ArgumentError, "Default hostname not set" if default_host.blank? # I'd rather have these calls in create but we have to wait # for default_host to be set by the user's sitemap config - new_sitemap(options) + new_sitemap add_default_links - yield Mapper.new(self, options) + yield Mapper.new(self) end # Called from Mapper. # # Add a link to the current sitemap. - def add_link(link, options={}) + def add_link(link) unless self.sitemap << link - new_sitemap options + new_sitemap self.sitemap << link end end @@ -79,13 +78,13 @@ def add_link(link, options={}) # start a new sitemap. # # If the current sitemap is nil or empty it is not added. - def new_sitemap(options={}) + def new_sitemap unless self.sitemap_index self.sitemap_index = SitemapGenerator::Builder::SitemapIndexFile.new(public_path, sitemap_index_path, default_host) end unless self.sitemap - self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path(options), default_host) + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) end # Mark the sitemap as complete and add it to the sitemap index @@ -95,7 +94,7 @@ def new_sitemap(options={}) self.sitemaps << self.sitemap show_progress(self.sitemap) if verbose - self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path(options), default_host) + self.sitemap = SitemapGenerator::Builder::SitemapFile.new(public_path, new_sitemap_path, default_host) end end @@ -160,23 +159,8 @@ def add_default_links # Return the current sitemap filename with index. # # The index depends on the length of the sitemaps array. - def new_sitemap_path(options={}) - base = self.sitemaps_path || '' - - if options[:name] - count = 0 - sitemap_name = File.join(base, "sitemap-#{options[:name]}.xml.gz") - sitemaps.each do |s| - if s.sitemap_path == sitemap_name - count = count+1 - sitemap_name = File.join(base, "sitemap-#{options[:name]}-#{count}.xml.gz") - end - end - named_sitemaps << sitemap - else - sitemap_name = File.join(base, "sitemap-#{self.sitemaps.length - named_sitemaps.length + 1}.xml.gz") - end - sitemap_name + def new_sitemap_path + File.join(self.sitemaps_path || '', "sitemap#{self.sitemaps.length + 1}.xml.gz") end # Return the current sitemap index filename. @@ -187,4 +171,4 @@ def sitemap_index_path File.join(self.sitemaps_path || '', 'sitemap_index.xml.gz') end end -end +end \ No newline at end of file diff --git a/lib/sitemap_generator/mapper.rb b/lib/sitemap_generator/mapper.rb index 1145edc5..bbd35fe1 100644 --- a/lib/sitemap_generator/mapper.rb +++ b/lib/sitemap_generator/mapper.rb @@ -2,15 +2,15 @@ module SitemapGenerator # Generator instances are used to build links. # The object passed to the add_links block in config/sitemap.rb is a Generator instance. class Mapper - attr_accessor :set, :options + attr_accessor :set - def initialize(set, options={}) + def initialize(set) @set = set - @options = options end - def add(loc, link = {}) - set.add_link Link.generate(loc, link), options + def add(loc, options = {}) + set.add_link Link.generate(loc, options) end end end + From a16e9d19cf3524d360b808b2ff530f767920f66c Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Mon, 13 Sep 2010 11:53:57 -0700 Subject: [PATCH 079/677] added missing sqlite3-ruby dev dependency --- Rakefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Rakefile b/Rakefile index 41c90e7c..30fbf3ed 100644 --- a/Rakefile +++ b/Rakefile @@ -18,6 +18,7 @@ begin gem.test_files = [] gem.add_development_dependency "rspec" gem.add_development_dependency "nokogiri" + gem.add_development_dependency "sqlite3-ruby" end Jeweler::GemcutterTasks.new rescue LoadError From 2599784da93d0ec58d51b74db05d5ad360fd97e3 Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Mon, 13 Sep 2010 17:12:45 -0700 Subject: [PATCH 080/677] add support for most video sitemap elements --- lib/sitemap_generator/builder/sitemap_file.rb | 28 ++++++++- lib/sitemap_generator/link.rb | 5 +- spec/sitemap_generator/video_sitemap_spec.rb | 58 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 spec/sitemap_generator/video_sitemap_spec.rb diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index 2a196c72..d5c1e02d 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -32,6 +32,7 @@ def initialize(public_path, sitemap_path, hostname) xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" > HTML @xml_wrapper_start.gsub!(/\s+/, ' ').gsub!(/ *> */, '>').strip! @@ -98,6 +99,31 @@ def build_xml(builder, link) end end end + + unless link[:video].blank? + video = link[:video] + builder.video :video do + # required elements + builder.video :thumbnail_loc, video[:thumbnail_loc] + builder.video :title, video[:title] + builder.video :description, video[:description] + + builder.video :content_loc, video[:content_loc] if video[:content_loc] + if video[:player_loc] + builder.video :player_loc, video[:player_loc], :allow_embed => (video[:allow_embed] ? 'yes' : 'no'), :autoplay => video[:autoplay] + end + + builder.video :rating, video[:rating] if video[:rating] + builder.video :view_count, video[:view_count] if video[:view_count] + builder.video :publication_date, video[:publication_date] if video[:publication_date] + builder.video :expiration_date, video[:expiration_date] if video[:expiration_date] + builder.video :duration, video[:duration] if video[:duration] + builder.video :family_friendly, (video[:family_friendly] ? 'yes' : 'no') if video[:family_friendly] + builder.video :duration, video[:duration] if video[:duration] + video[:tags].each {|tag| builder.video :tag, tag } if video[:tags] + video[:categories].each {|category| builder.video :category, category} if video[:categories] + end + end end builder << '' end @@ -126,4 +152,4 @@ def bytesize(string) end end end -end \ No newline at end of file +end diff --git a/lib/sitemap_generator/link.rb b/lib/sitemap_generator/link.rb index 80fcac49..7a44af32 100644 --- a/lib/sitemap_generator/link.rb +++ b/lib/sitemap_generator/link.rb @@ -9,7 +9,7 @@ def generate(path, options = {}) path = path.sitemap_path end - options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images) + options.assert_valid_keys(:priority, :changefreq, :lastmod, :host, :images, :video) options.reverse_merge!(:priority => 0.5, :changefreq => 'weekly', :lastmod => Time.now, :host => Sitemap.default_host, :images => []) { :path => path, @@ -18,7 +18,8 @@ def generate(path, options = {}) :lastmod => options[:lastmod], :host => options[:host], :loc => URI.join(options[:host], path).to_s, - :images => prepare_images(options[:images], options[:host]) + :images => prepare_images(options[:images], options[:host]), + :video => options[:video] } end diff --git a/spec/sitemap_generator/video_sitemap_spec.rb b/spec/sitemap_generator/video_sitemap_spec.rb new file mode 100644 index 00000000..4c94ea7f --- /dev/null +++ b/spec/sitemap_generator/video_sitemap_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe "SitemapGenerator" do + + it "should add the video sitemap element" do + loc = 'http://www.example.com/cool_video.html' + thumbnail_loc = 'http://www.example.com/video1_thumbnail.png' + title = 'Cool Video' + content_loc = 'http://www.example.com/cool_video.mpg' + player_loc = 'http://www.example.com/cool_video_player.swf' + allow_embed = true + autoplay = 'id=123' + description = 'An new perspective in cool video technology' + tags = %w{tag1 tag2 tag3} + categories = %w{cat1 cat2 cat3} + + sitemap_generator = SitemapGenerator::Builder::SitemapFile.new('./public', '', 'example.com') + video_link = { + :loc => loc, + :video => { + :thumbnail_loc => thumbnail_loc, + :title => title, + :content_loc => content_loc, + :player_loc => player_loc, + :description => description, + :allow_embed => allow_embed, + :autoplay => autoplay, + :tags => tags, + :categories => categories + } + } + + # generate the video sitemap xml fragment + video_xml_fragment = sitemap_generator.build_xml(::Builder::XmlMarkup.new, video_link) + + # validate the xml generated + video_xml_fragment.should_not be_nil + xmldoc = Nokogiri::XML.parse("#{video_xml_fragment}") + + url = xmldoc.at_xpath("//url") + url.should_not be_nil + url.at_xpath("loc").text.should == loc + + video = url.at_xpath("video:video") + video.should_not be_nil + video.at_xpath("video:thumbnail_loc").text.should == thumbnail_loc + video.at_xpath("video:title").text.should == title + video.at_xpath("video:content_loc").text.should == content_loc + video.xpath("video:tag").size.should == 3 + video.xpath("video:category").size.should == 3 + + player_loc_node = video.at_xpath("video:player_loc") + player_loc_node.should_not be_nil + player_loc_node.text.should == player_loc + player_loc_node.attribute('allow_embed').text.should == (allow_embed ? 'yes' : 'no') + player_loc_node.attribute('autoplay').text.should == autoplay + end +end From 9218e96ede1feba74fb170b4eda3e9200e4097da Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Tue, 17 Aug 2010 23:08:47 -0700 Subject: [PATCH 081/677] Upgrade the test apps to 2.3.8 --- spec/mock_app_gem/config/environment.rb | 2 +- spec/mock_app_plugin/config/environment.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/mock_app_gem/config/environment.rb b/spec/mock_app_gem/config/environment.rb index 1f6f5cfa..5970f2ee 100644 --- a/spec/mock_app_gem/config/environment.rb +++ b/spec/mock_app_gem/config/environment.rb @@ -1,7 +1,7 @@ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.3.8' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') diff --git a/spec/mock_app_plugin/config/environment.rb b/spec/mock_app_plugin/config/environment.rb index 029a58b3..2d8f9302 100644 --- a/spec/mock_app_plugin/config/environment.rb +++ b/spec/mock_app_plugin/config/environment.rb @@ -1,7 +1,7 @@ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.3.8' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') From 0bcded73a1a112309c401665753ccb1cc1a5909d Mon Sep 17 00:00:00 2001 From: Karl Varga Date: Thu, 16 Sep 2010 15:37:11 -0700 Subject: [PATCH 082/677] Move schemas to spec/support/schemas * Validate the video element against the Video XSD * Video element can only have on category * Reorder video elements so they validate * Update README --- README.md | 119 +++++--- lib/sitemap_generator/builder/sitemap_file.rb | 18 +- spec/sitemap_generator/video_sitemap_spec.rb | 22 +- spec/support/{ => schemas}/siteindex.xsd | 0 spec/support/schemas/sitemap-video.xsd | 289 ++++++++++++++++++ spec/support/{ => schemas}/sitemap.xsd | 0 spec/support/xml_macros.rb | 45 ++- 7 files changed, 420 insertions(+), 73 deletions(-) rename spec/support/{ => schemas}/siteindex.xsd (100%) create mode 100644 spec/support/schemas/sitemap-video.xsd rename spec/support/{ => schemas}/sitemap.xsd (100%) diff --git a/README.md b/README.md index d4b01da5..e71e3121 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,37 @@ SitemapGenerator ================ -SitemapGenerator is a Rails gem that makes it easy to generate ['enterprise-class'][enterprise_class] Sitemaps readable by all search engines. Generated Sitemaps adhere to the ['Sitemap protocol specification'][sitemap_protocol]. When you generate new Sitemaps, SitemapGenerator can automatically ping the major search engines (including Google, Yahoo and Bing) to notify them. SitemapGenerator includes rake tasks to easily manage your sitemaps. +SitemapGenerator generates Sitemaps for your Rails application. The Sitemaps adhere to the [Sitemap 0.9 protocol][sitemap_protocol] specification. You specify the contents of your Sitemap using a configuration file, à la Rails Routes. A set of rake tasks is included to help you manage your Sitemaps. Features ------- -- v0.2.6: ['Google Image Sitemap'][sitemap_images] support -- v0.2.5: Rails 3 support (beta) - -- Adheres to the ['Sitemap protocol specification'][sitemap_protocol] +- Supports [Video sitemaps][sitemap_video] and [Image sitemaps][sitemap_images] +- Rails3 compatible (beta) +- Adheres to the [Sitemap 0.9 protocol][sitemap_protocol] - Handles millions of links -- Automatic Gzip of Sitemap files -- Automatic ping of search engines to notify them of new sitemaps: Google, Yahoo, Bing, Ask, SitemapWriter -- Leaves your old sitemaps in place if a new one fails to generate -- Allows you to set the hostname for the links in your Sitemap +- Compresses Sitemaps using GZip +- Notifies Search Engines (Google, Yahoo, Bing, Ask, SitemapWriter) of new sitemaps +- Ensures your old Sitemaps stay in place if the new Sitemap fails to generate +- You set the hostname (and protocol) of the links in your Sitemap + +Changelog +------- + +- v1.1.0: [Video sitemap][sitemap_video] support +- v0.2.6: [Image Sitemap][sitemap_images] support +- v0.2.5: Rails 3 support (beta) Foreword ------- -Unfortunately, Adam Salter passed away in 2009. Those who knew him know what an amazing guy he was, and what an excellent Rails programmer he was. His passing is a great loss to the Rails community. +Adam Salter first created SitemapGenerator while we were working together in Sydney, Australia. Unfortunately, he passed away in 2009. Since then I have taken over development of SitemapGenerator. -[Karl Varga](http://github.com/kjvarga) has taken over development of SitemapGenerator. The canonical repository is [http://github.com/kjvarga/sitemap_generator][canonical_repo] +Those who knew him know what an amazing guy he was, and what an excellent Rails programmer he was. His passing is a great loss to the Rails community. -Installation +The canonical repository is now: [http://github.com/kjvarga/sitemap_generator][canonical_repo] + +Install ======= **Rails 3:** @@ -56,31 +64,55 @@ Installation 1. $ ./script/plugin install git://github.com/kjvarga/sitemap_generator.git ----- +Usage +====== + +rake sitemap:install creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. + +Once you have configured your sitemap in config/sitemap.rb run rake sitemap:refresh as needed to create/rebuild your Sitemap files. Sitemaps are generated into the public/ folder and are named sitemap_index.xml.gz, sitemap1.xml.gz, sitemap2.xml.gz, etc. + +Using rake sitemap:refresh will notify major search engines to let them know that a new Sitemap is available (Google, Yahoo, Bing, Ask, SitemapWriter). To generate new Sitemaps without notifying search engines (for example when running in a local environment) use rake sitemap:refresh:no_ping. + +To ping Yahoo you will need to set your Yahoo AppID in config/sitemap.rb. For example: SitemapGenerator::Sitemap.yahoo_app_id = "my_app_id" -Installation creates a config/sitemap.rb file which will contain your logic for generating the Sitemap files. If you want to create this file manually run rake sitemap:install. +To disable all non-essential output (only errors will be displayed) run the rake tasks with the -s option. For example rake -s sitemap:refresh. -You can run rake sitemap:refresh as needed to create Sitemap files. This will also ping these ['major search engines'][sitemap_engines]: Google, Yahoo, Bing, Ask, SitemapWriter. If you want to disable all non-essential output run the rake task with rake -s sitemap:refresh. +Cron +----- -To keep your Sitemaps up-to-date, setup a cron job. Pass the -s option to the rake task to silence all but the most important output. If you're using Whenever, then your schedule would look something like: +To keep your Sitemaps up-to-date, setup a cron job. Make sure to pass the -s option to silence rake. That way you will only get email when the sitemap build fails. + +If you're using Whenever, your schedule would look something like the following: # config/schedule.rb every 1.day, :at => '5:00 am' do rake "-s sitemap:refresh" end -Optionally, you can add the following to your public/robots.txt file, so that robots can find the sitemap file: +Robots.txt +---------- + +You should add the Sitemap index file to public/robots.txt to help search engines find your Sitemaps. The URL should be the complete URL to the Sitemap index file. For example: + + Sitemap: http://www.example.org/sitemap_index.xml.gz - Sitemap: /sitemap_index.xml.gz +Image and Video Sitemaps +----------- -The Sitemap URL in the robots file should be the complete URL to the Sitemap Index, such as http://www.example.org/sitemap_index.xml.gz +Images can be added to a sitemap URL by passing an :images array to add(). Each item in the array must be a Hash containing tags defined by the [Image Sitemap][image_tags] specification. For example: + sitemap.add('/index.html', :images => [{ :loc => 'http://www.example.com/image.png', :title => 'Image' }]) -Example 'config/sitemap.rb' -========== +A video can be added to a sitemap URL by passing a :video Hash to add(). The Hash can contain tags defined by the [Video Sitemap specification][video_tags]. To associate more than one tag with a video, pass the tags as an array with the key :tags. + + sitemap.add('/index.html', :video => { :thumbnail_loc => 'http://www.example.com/video1_thumbnail.png', :title => 'Title', :description => 'Description', :content_loc => 'http://www.example.com/cool_video.mpg', :tags => %w[one two three], :category => 'Category' }) + +Example config/sitemap.rb +--------- # Set the host name for URL creation SitemapGenerator::Sitemap.default_host = "http://www.example.com" + SitemapGenerator::Sitemap.yahoo_app_id = nil # Set to your Yahoo AppID to ping Yahoo SitemapGenerator::Sitemap.add_links do |sitemap| # Put links creation logic here. @@ -94,25 +126,21 @@ Example 'config/sitemap.rb' # Defaults: :priority => 0.5, :changefreq => 'weekly', # :lastmod => Time.now, :host => default_host - - # Examples: - # add '/articles' sitemap.add articles_path, :priority => 0.7, :changefreq => 'daily' - # add all individual articles - Article.find(:all).each do |a| + # add all articles + Article.all.each do |a| sitemap.add article_path(a), :lastmod => a.updated_at end - # add merchant path - sitemap.add '/purchase', :priority => 0.7, :host => "https://www.example.com" - - # add all individual news with images - News.all.each do |n| - sitemap.add news_path(n), :lastmod => n.updated_at, :images=>n.images.collect{ |r| :loc=>r.image.url, :title=>r.image.name } + # add news page with images + News.all.each do |news| + images = news.images.collect do |image| + { :loc => image.url, :title => image.name } + end + sitemap.add news_path(news), :images => images end - end # Including Sitemaps from Rails Engines. @@ -159,9 +187,9 @@ Compatibility Tested and working on: -- **Rails** 3.0.0, sitemap_generator version >= 0.2.5 -- **Rails** 1.x - 2.3.5 -- **Ruby** 1.8.6, 1.8.7, 1.9.1 +- **Rails** 3.0.0 +- **Rails** 1.x - 2.3.8 +- **Ruby** 1.8.6, 1.8.7, 1.8.7 Enterprise Edition, 1.9.1 Notes ======= @@ -185,8 +213,6 @@ Notes end end -3) If generation of your sitemap fails for some reason, the old sitemap will remain in public/. This ensures that robots will always find a valid sitemap. Running silently (`rake -s sitemap:refresh`) and with email forwarding setup you'll only get an email if your sitemap fails to build, and no notification when everything is fine - which will be most of the time. - Known Bugs ======== @@ -196,15 +222,16 @@ Known Bugs Wishlist & Coming Soon ======== -- Support for generating sitemaps for sites with multiple domains. Sitemaps are generated into subdirectories and we use a Rack middleware to rewrite requests for sitemaps to the correct subdirectory based on the request host. -- I want to refactor the code because it has grown a lot. Part of this refactoring will include implementing some more checks to make sure we adhere to standards as well as making sure that the sitemaps are being generated as efficiently as possible. - -I'd like to simplify adding links to a sitemap. Right now it's all or nothing. I'd like to break it up so you can add batches. +- Ultimately I'd like to make this gem framework agnostic. It is better suited to being run as a command-line tool as opposed to Ruby-specific Rake tasks. +- Add rake tasks/options to validate the generated sitemaps. +- Support News, Mobile, Geo and other types of sitemaps +- Support for generating sitemaps for sites with multiple domains. Sitemaps can be generated into subdirectories and we can use Rack middleware to rewrite requests for sitemaps to the correct subdirectory based on the request host. - Auto coverage testing. Generate a report of broken URLs by checking the status codes of each page in the sitemap. Thanks (in no particular order) ======== +- [Alex Soto](http://github.com/apsoto) for video sitemaps - [Alexadre Bini](http://github.com/alexandrebini) for image sitemaps - [Dan Pickett](http://github.com/dpickett) - [Rob Biedenharn](http://github.com/rab) @@ -217,11 +244,11 @@ Copyright (c) 2009 Karl Varga released under the MIT license [canonical_repo]:http://github.com/kjvarga/sitemap_generator [enterprise_class]:https://twitter.com/dhh/status/1631034662 "I use enterprise in the same sense the Phusion guys do - i.e. Enterprise Ruby. Please don't look down on my use of the word 'enterprise' to represent being a cut above. It doesn't mean you ever have to work for a company the size of IBM. Or constantly fight inertia, writing crappy software, adhering to change management practices and spending hours in meetings... Not that there's anything wrong with that - Wait, what?" -[sitemap_engines]:http://en.wikipedia.org/wiki/Sitemap_index "http://en.wikipedia.org/wiki/Sitemap_index" [sitemaps_org]:http://www.sitemaps.org/protocol.php "http://www.sitemaps.org/protocol.php" [sitemaps_xml]:http://www.sitemaps.org/protocol.php#xmlTagDefinitions "XML Tag Definitions" [sitemap_generator_usage]:http://wiki.github.com/adamsalter/sitemap_generator/sitemapgenerator-usage "http://wiki.github.com/adamsalter/sitemap_generator/sitemapgenerator-usage" -[boost_juice]:http://www.boostjuice.com.au/ "Mmmm, sweet, sweet Boost Juice." -[cb]:http://codebright.net "http://codebright.net" [sitemap_images]:http://www.google.com/support/webmasters/bin/answer.py?answer=178636 -[sitemap_protocol]:http://sitemaps.org/protocol.php \ No newline at end of file +[sitemap_video]:http://www.google.com/support/webmasters/bin/topic.py?topic=10079 +[sitemap_protocol]:http://sitemaps.org/protocol.php +[video_tags]:http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=80472#4 +[image_tags]:http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=178636 \ No newline at end of file diff --git a/lib/sitemap_generator/builder/sitemap_file.rb b/lib/sitemap_generator/builder/sitemap_file.rb index d5c1e02d..4fedb005 100644 --- a/lib/sitemap_generator/builder/sitemap_file.rb +++ b/lib/sitemap_generator/builder/sitemap_file.rb @@ -4,6 +4,14 @@ module SitemapGenerator module Builder + # + # General Usage: + # + # sitemap = SitemapFile.new('public/', 'sitemap.xml', 'http://example.com') + # <- creates a new sitemap file in directory public/ + # sitemap.add_link({ ... }) <- add a link to the sitemap + # sitemap.finalize! <- write and close the sitemap file + # class SitemapFile include SitemapGenerator::Builder::Helper @@ -104,14 +112,13 @@ def build_xml(builder, link) video = link[:video] builder.video :video do # required elements - builder.video :thumbnail_loc, video[:thumbnail_loc] - builder.video :title, video[:title] - builder.video :description, video[:description] - builder.video :content_loc, video[:content_loc] if video[:content_loc] if video[:player_loc] builder.video :player_loc, video[:player_loc], :allow_embed => (video[:allow_embed] ? 'yes' : 'no'), :autoplay => video[:autoplay] end + builder.video :thumbnail_loc, video[:thumbnail_loc] + builder.video :title, video[:title] + builder.video :description, video[:description] builder.video :rating, video[:rating] if video[:rating] builder.video :view_count, video[:view_count] if video[:view_count] @@ -121,7 +128,8 @@ def build_xml(builder, link) builder.video :family_friendly, (video[:family_friendly] ? 'yes' : 'no') if video[:family_friendly] builder.video :duration, video[:duration] if video[:duration] video[:tags].each {|tag| builder.video :tag, tag } if video[:tags] - video[:categories].each {|category| builder.video :category, category} if video[:categories] + builder.video :tag, video[:tag] if video[:tag] + builder.video :category, video[:category] if video[:category] end end end diff --git a/spec/sitemap_generator/video_sitemap_spec.rb b/spec/sitemap_generator/video_sitemap_spec.rb index 4c94ea7f..f58387b8 100644 --- a/spec/sitemap_generator/video_sitemap_spec.rb +++ b/spec/sitemap_generator/video_sitemap_spec.rb @@ -12,9 +12,9 @@ autoplay = 'id=123' description = 'An new perspective in cool video technology' tags = %w{tag1 tag2 tag3} - categories = %w{cat1 cat2 cat3} - - sitemap_generator = SitemapGenerator::Builder::SitemapFile.new('./public', '', 'example.com') + category = 'cat1' + + sitemap_generator = SitemapGenerator::Builder::SitemapFile.new(File.join(::Rails.root, '/public/'), 'sitemap.xml.gz', 'http://example.com') video_link = { :loc => loc, :video => { @@ -26,7 +26,7 @@ :allow_embed => allow_embed, :autoplay => autoplay, :tags => tags, - :categories => categories + :category => category } } @@ -34,10 +34,12 @@ video_xml_fragment = sitemap_generator.build_xml(::Builder::XmlMarkup.new, video_link) # validate the xml generated - video_xml_fragment.should_not be_nil - xmldoc = Nokogiri::XML.parse("#{video_xml_fragment}") - - url = xmldoc.at_xpath("//url") + #video_xml_fragment.should_not be_nil + doc = Nokogiri::XML.parse("#{video_xml_fragment}") + + + # Check that the options were parsed correctly + url = doc.at_xpath("//url") url.should_not be_nil url.at_xpath("loc").text.should == loc @@ -47,8 +49,10 @@ video.at_xpath("video:title").text.should == title video.at_xpath("video:content_loc").text.should == content_loc video.xpath("video:tag").size.should == 3 - video.xpath("video:category").size.should == 3 + video.xpath("video:category").size.should == 1 + xml_fragment_should_validate_against_schema(video, 'http://www.google.com/schemas/sitemap-video/1.1', 'sitemap-video') + player_loc_node = video.at_xpath("video:player_loc") player_loc_node.should_not be_nil player_loc_node.text.should == player_loc diff --git a/spec/support/siteindex.xsd b/spec/support/schemas/siteindex.xsd similarity index 100% rename from spec/support/siteindex.xsd rename to spec/support/schemas/siteindex.xsd diff --git a/spec/support/schemas/sitemap-video.xsd b/spec/support/schemas/sitemap-video.xsd new file mode 100644 index 00000000..d401bdca --- /dev/null +++ b/spec/support/schemas/sitemap-video.xsd @@ -0,0 +1,289 @@ + + + + + + XML Schema for the Video Sitemap extension. This schema defines the + Video-specific elements only; the core Sitemap elements are defined + separately. + + Help Center documentation for the Video Sitemap extension: + + http://www.google.com/support/webmasters/bin/topic.py?topic=10079 + + Copyright 2010 Google Inc. All Rights Reserved. + + + + + + + A value that can be yes or no. Permitted cases are all-lowercase (yes/no), + all-uppercase (YES/NO) or starting with capital (Yes/No). + + + + + + + + + + + + + + + + Space-separated country codes in ISO 3166 format. + + Country codes: + http://www.iso.org/iso/english_country_names_and_code_elements + + + + + + + + + + + + + + At least one of <video:player_loc> and + <video:content_loc> is required. + + This should be a .mpg, .mpeg, .mp4, .m4v, .mov, .wmv, .asf, .avi, + .ra, .ram, .rm, .flv, or other video file format, and can be omitted + if <video:player_loc> is specified. However, because Google + needs to be able to check that the Flash object is actually a player + for video (as opposed to some other use of Flash, e.g. games and + animations), it's helpful to provide both. + + + + + + + At least one of <video:player_loc> and + <video:content_loc> is required. + + A URL pointing to a Flash player for a specific video. In general, + this is the information in the src element of an <embed> tag + and should not be the same as the content of the <loc> tag. + ​Since each video is uniquely identified by its content URL (the + location of the actual video file) or, if a content URL is not + present, a player URL (a URL pointing to a player for the video), + you must include either the <video:player_loc> or + <video:content_loc> tags. If these tags are omitted and we + can't find this information, we'll be unable to index your video. + + + + + + + + + Attribute allow_embed specifies whether Google can embed the + video in search results. Allowed values are "Yes" or "No". + The default value is "Yes". + + + + + + + User-defined string that Google may append (if appropriate) + to the flashvars parameter to enable autoplay of the video. + + + + + + + + + + + A URL pointing to the URL for the video thumbnail image file. We can + accept most image sizes/types but recommend your thumbs are at least + 120x90 pixels in .jpg, .png, or. gif formats. + + + + + + + The title of the video. + + + + + + + + + + + + The description of the video. + + + + + + + + + + + + The rating of the video. + + + + + + + + + + + + + The number of times the video has been viewed. + + + + + + + The date the video was first published, in W3C format. Acceptable + values are complete date (YYYY-MM-DD) and complete date plus hours, + minutes and seconds, and timezone (YYYY-MM-DDThh:mm:ss+TZD). + For example, 2007-07-16T19:20:30+08:00. + + + + + + + + + + + + + + + + + The date after which the video will no longer be available, in + W3C format. Acceptable values are complete date (YYYY-MM-DD) and + complete date plus hours, minutes and seconds, and timezone + (YYYY-MM-DDThh:mm:ss+TZD). For example, 2007-07-16T19:20:30+08:00. + Don't supply this information if your video does not expire. + + + + + + + + + + + + + + + + + A tag associated with the video. Tags are generally very short + descriptions of key concepts associated with a video or piece of + content. A single video could have several tags, although it might + belong to only one category. For example, a video about grilling + food may belong in the Grilling category, but could be tagged + "steak", "meat", "summer", and "outdoor". Create a new + <video:tag> element for each tag associated with a video. + + + + + + + The video's category. For example, cooking. In general, categories + are broad groupings of content by subject. Usually a video will + belong to a single category. For example, a site about cooking could + have categories for Broiling, Baking, and Grilling. + + + + + + + + + + + + Whether the video is suitable for viewing by children. No if the + video should be available only to users with SafeSearch turned off. + + + + + + + The duration of the video in seconds. + + + + + + + + + + + + A list of countries where the video may or may not be played. + If there is no <video:restriction> tag, it is assumed that + the video can be played in all territories. + + + + + + + + + Attribute "relationship" specifies whether the video is + restricted or permitted for the specified countries. + + + + + + + + + + + + + + + + + + diff --git a/spec/support/sitemap.xsd b/spec/support/schemas/sitemap.xsd similarity index 100% rename from spec/support/sitemap.xsd rename to spec/support/schemas/sitemap.xsd diff --git a/spec/support/xml_macros.rb b/spec/support/xml_macros.rb index 21636da5..3d551e66 100644 --- a/spec/support/xml_macros.rb +++ b/spec/support/xml_macros.rb @@ -1,30 +1,49 @@ require 'nokogiri' module XmlMacros - + def gzipped_xml_file_should_validate_against_schema(xml_gz_filename, schema_name) Zlib::GzipReader.open(xml_gz_filename) do |xml_file| - xml_data_should_validate_against_schema xml_file.read, schema_name + xml_data_should_validate_against_schema(xml_file.read, schema_name) end end - - def xml_data_should_validate_against_schema(xml_data, schema_name) - - schema_file = File.join(File.dirname(__FILE__), "#{schema_name}.xsd") + + def xml_data_should_validate_against_schema(xml, schema_name) + xml = xml.is_a?(String) ? xml : xml.to_s + doc = Nokogiri::XML(xml) + schema_file = File.join(File.dirname(__FILE__), 'schemas', "#{schema_name}.xsd") schema = Nokogiri::XML::Schema File.read(schema_file) - - doc = Nokogiri::XML(xml_data) - schema.validate(doc).should == [] - end - + + # Validate a fragment of XML against a schema. Builds a document with a root + # node for you so the fragment can be validated. + # + # Unfortunately Nokogiri doesn't support validating + # documents with multiple namespaces. So we have to extract the element + # and create a new document from it. If the xmlns isn't set on the element + # we get an error like: + # + # Element 'video': No matching global declaration available for the validation root. + # + # xmlns the XML namespace of the root element. + # xml_fragment XML string + # + # Example: + # xml_fragment_should_validate('