diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a412f4cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +log/* +tmp/* +TAGS +*~ +.#* +schema/schema.rb +schema/*_structure.sql +schema/*.sqlite3 +schema/*.sqlite +schema/*.db +*.sqlite +*.sqlite3 +*.db +src/* +.hgignore +.hg/* +.svn/* \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..a760c99c --- /dev/null +++ b/Rakefile @@ -0,0 +1,35 @@ +require 'rubygems' +require 'rake/rdoctask' + +require 'merb-core' +require 'merb-core/tasks/merb' + +include FileUtils + +# Load the basic runtime dependencies; this will include +# any plugins and therefore plugin rake tasks. +init_env = ENV['MERB_ENV'] || 'rake' +Merb.load_dependencies(:environment => init_env) + +# Get Merb plugins and dependencies +Merb::Plugins.rakefiles.each { |r| require r } + +# Load any app level custom rakefile extensions from lib/tasks +tasks_path = File.join(File.dirname(__FILE__), "lib", "tasks") +rake_files = Dir["#{tasks_path}/*.rake"] +rake_files.each{|rake_file| load rake_file } + +desc "Start runner environment" +task :merb_env do + Merb.start_environment(:environment => init_env, :adapter => 'runner') +end + +require 'spec/rake/spectask' +require 'merb-core/test/tasks/spectasks' +desc 'Default: run spec examples' +task :default => 'spec' + +############################################################################## +# ADD YOUR CUSTOM TASKS IN /lib/tasks +# NAME YOUR RAKE FILES file_name.rake +############################################################################## diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 00000000..5ce39a01 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,2 @@ +class Application < Merb::Controller +end \ No newline at end of file diff --git a/app/controllers/exceptions.rb b/app/controllers/exceptions.rb new file mode 100644 index 00000000..4fdb5665 --- /dev/null +++ b/app/controllers/exceptions.rb @@ -0,0 +1,13 @@ +class Exceptions < Merb::Controller + + # handle NotFound exceptions (404) + def not_found + render :format => :html + end + + # handle NotAcceptable exceptions (406) + def not_acceptable + render :format => :html + end + +end \ No newline at end of file diff --git a/app/helpers/global_helpers.rb b/app/helpers/global_helpers.rb new file mode 100644 index 00000000..9c9e5aa1 --- /dev/null +++ b/app/helpers/global_helpers.rb @@ -0,0 +1,5 @@ +module Merb + module GlobalHelpers + # helpers defined here available to all views. + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..ae84896c --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,17 @@ +# This is a default user class used to activate merb-auth. Feel free to change from a User to +# Some other class, or to remove it altogether. If removed, merb-auth may not work by default. +# +# Don't forget that by default the salted_user mixin is used from merb-more +# You'll need to setup your db as per the salted_user mixin, and you'll need +# To use :password, and :password_confirmation when creating a user +# +# see merb/merb-auth/setup.rb to see how to disable the salted_user mixin +# +# You will need to setup your database and create a user. +class User + include DataMapper::Resource + + property :id, Serial + property :login, String + +end diff --git a/app/views/exceptions/not_acceptable.html.erb b/app/views/exceptions/not_acceptable.html.erb new file mode 100644 index 00000000..a7b7752a --- /dev/null +++ b/app/views/exceptions/not_acceptable.html.erb @@ -0,0 +1,63 @@ +
+
+ + +

pocket rocket web framework

+
+
+ +
+

Exception:

+

<%= request.exceptions.first.message %>

+
+ +
+

Why am I seeing this page?

+

Merb couldn't find an appropriate content_type to return, + based on what you said was available via provides() and + what the client requested.

+ +

How to add a mime-type

+

+      Merb.add_mime_type :pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip"
+    
+

What this means is:

+ + +

You can then do:

+

+      class Foo < Application
+        provides :pdf
+      end
+    
+ +

Where can I find help?

+

If you have any questions or if you can't figure something out, please take a + look at our project page, + feel free to come chat at irc.freenode.net, channel #merb, + or post to merb mailing list + on Google Groups.

+ +

What if I've found a bug?

+

If you want to file a bug or make your own contribution to Merb, + feel free to register and create a ticket at our + project development page + on Lighthouse.

+ +

How do I edit this page?

+

You can change what people see when this happens by editing app/views/exceptions/not_acceptable.html.erb.

+ +
+ + +
diff --git a/app/views/exceptions/not_found.html.erb b/app/views/exceptions/not_found.html.erb new file mode 100644 index 00000000..42b41a89 --- /dev/null +++ b/app/views/exceptions/not_found.html.erb @@ -0,0 +1,47 @@ +
+
+ + +

pocket rocket web framework

+
+
+ +
+

Exception:

+

<%= request.exceptions.first.message %>

+
+ +
+

Welcome to Merb!

+

Merb is a light-weight MVC framework written in Ruby. We hope you enjoy it.

+ +

Where can I find help?

+

If you have any questions or if you can't figure something out, please take a + look at our project page, + feel free to come chat at irc.freenode.net, channel #merb, + or post to merb mailing list + on Google Groups.

+ +

What if I've found a bug?

+

If you want to file a bug or make your own contribution to Merb, + feel free to register and create a ticket at our + project development page + on Lighthouse.

+ +

How do I edit this page?

+

You're seeing this page because you need to edit the following files: +

+

+
+ + +
diff --git a/app/views/layout/application.html.erb b/app/views/layout/application.html.erb new file mode 100644 index 00000000..4637ef2d --- /dev/null +++ b/app/views/layout/application.html.erb @@ -0,0 +1,12 @@ + + + + Fresh Merb App + + + + + <%#= message[:notice] %> + <%= catch_content :for_layout %> + + \ No newline at end of file diff --git a/autotest/discover.rb b/autotest/discover.rb new file mode 100644 index 00000000..8df3b0f3 --- /dev/null +++ b/autotest/discover.rb @@ -0,0 +1,2 @@ +Autotest.add_discovery { "merb" } +Autotest.add_discovery { "rspec" } \ No newline at end of file diff --git a/autotest/merb.rb b/autotest/merb.rb new file mode 100644 index 00000000..f78df157 --- /dev/null +++ b/autotest/merb.rb @@ -0,0 +1,152 @@ +# Adapted from Autotest::Rails +require 'autotest' + +class Autotest::Merb < Autotest + + # +model_tests_dir+:: the directory to find model-centric tests + # +controller_tests_dir+:: the directory to find controller-centric tests + # +view_tests_dir+:: the directory to find view-centric tests + # +fixtures_dir+:: the directory to find fixtures in + attr_accessor :model_tests_dir, :controller_tests_dir, :view_tests_dir, :fixtures_dir + + def initialize + super + + initialize_test_layout + + # Ignore any happenings in these directories + add_exception %r%^\./(?:doc|log|public|tmp|\.git|\.hg|\.svn|framework|gems|schema|\.DS_Store|autotest|bin|.*\.sqlite3)% + # Ignore SCM directories and custom Autotest mappings + %w[.svn .hg .git .autotest].each { |exception| add_exception(exception) } + + + # Ignore any mappings that Autotest may have already set up + clear_mappings + + # Any changes to a file in the root of the 'lib' directory will run any + # model test with a corresponding name. + add_mapping %r%^lib\/.*\.rb% do |filename, _| + files_matching Regexp.new(["^#{model_test_for(filename)}$"]) + end + + # Any changes to a fixture will run corresponding view, controller and + # model tests + add_mapping %r%^#{fixtures_dir}/(.*)s.yml% do |_, m| + [ + model_test_for(m[1]), + controller_test_for(m[1]), + view_test_for(m[1]) + ] + end + + # Any change to a test will cause it to be run + add_mapping %r%^test/(unit|models|integration|controllers|views|functional)/.*rb$% do |filename, _| + filename + end + + # Any change to a model will cause it's corresponding test to be run + add_mapping %r%^app/models/(.*)\.rb$% do |_, m| + model_test_for(m[1]) + end + + # Any change to the global helper will result in all view and controller + # tests being run + add_mapping %r%^app/helpers/global_helpers.rb% do + files_matching %r%^test/(views|functional|controllers)/.*_test\.rb$% + end + + # Any change to a helper will run it's corresponding view and controller + # tests, unless the helper is the global helper. Changes to the global + # helper run all view and controller tests. + add_mapping %r%^app/helpers/(.*)_helper(s)?.rb% do |_, m| + if m[1] == "global" then + files_matching %r%^test/(views|functional|controllers)/.*_test\.rb$% + else + [ + view_test_for(m[1]), + controller_test_for(m[1]) + ] + end + end + + # Changes to views result in their corresponding view and controller test + # being run + add_mapping %r%^app/views/(.*)/% do |_, m| + [ + view_test_for(m[1]), + controller_test_for(m[1]) + ] + end + + # Changes to a controller result in its corresponding test being run. If + # the controller is the exception or application controller, all + # controller tests are run. + add_mapping %r%^app/controllers/(.*)\.rb$% do |_, m| + if ["application", "exception"].include?(m[1]) + files_matching %r%^test/(controllers|views|functional)/.*_test\.rb$% + else + controller_test_for(m[1]) + end + end + + # If a change is made to the router, run all controller and view tests + add_mapping %r%^config/router.rb$% do # FIX + files_matching %r%^test/(controllers|views|functional)/.*_test\.rb$% + end + + # If any of the major files governing the environment are altered, run + # everything + add_mapping %r%^test/test_helper.rb|config/(init|rack|environments/test.rb|database.yml)% do # FIX + files_matching %r%^test/(unit|models|controllers|views|functional)/.*_test\.rb$% + end + end + +private + + # Determines the paths we can expect tests or specs to reside, as well as + # corresponding fixtures. + def initialize_test_layout + self.model_tests_dir = "test/unit" + self.controller_tests_dir = "test/functional" + self.view_tests_dir = "test/views" + self.fixtures_dir = "test/fixtures" + end + + # Given a filename and the test type, this method will return the + # corresponding test's or spec's name. + # + # ==== Arguments + # +filename+:: the file name of the model, view, or controller + # +kind_of_test+:: the type of test we that we should run + # + # ==== Returns + # String:: the name of the corresponding test or spec + # + # ==== Example + # + # > test_for("user", :model) + # => "user_test.rb" + # > test_for("login", :controller) + # => "login_controller_test.rb" + # > test_for("form", :view) + # => "form_view_spec.rb" # If you're running a RSpec-like suite + def test_for(filename, kind_of_test) + name = [filename] + name << kind_of_test.to_s if kind_of_test == :view + name << "test" + return name.join("_") + ".rb" + end + + def model_test_for(filename) + [model_tests_dir, test_for(filename, :model)].join("/") + end + + def controller_test_for(filename) + [controller_tests_dir, test_for(filename, :controller)].join("/") + end + + def view_test_for(filename) + [view_tests_dir, test_for(filename, :view)].join("/") + end + +end \ No newline at end of file diff --git a/autotest/merb_rspec.rb b/autotest/merb_rspec.rb new file mode 100644 index 00000000..536665d8 --- /dev/null +++ b/autotest/merb_rspec.rb @@ -0,0 +1,165 @@ +# Adapted from Autotest::Rails, RSpec's autotest class, as well as merb-core's. +require 'autotest' + +class RspecCommandError < StandardError; end + +# This class maps your application's structure so Autotest can understand what +# specs to run when files change. +# +# Fixtures are _not_ covered by this class. If you change a fixture file, you +# will have to run your spec suite manually, or, better yet, provide your own +# Autotest map explaining how your fixtures are set up. +class Autotest::MerbRspec < Autotest + def initialize + super + + # Ignore any happenings in these directories + add_exception %r%^\./(?:doc|log|public|tmp|\.git|\.hg|\.svn|framework|gems|schema|\.DS_Store|autotest|bin|.*\.sqlite3|.*\.thor)% + # Ignore SCM directories and custom Autotest mappings + %w[.svn .hg .git .autotest].each { |exception| add_exception(exception) } + + # Ignore any mappings that Autotest may have already set up + clear_mappings + + # Anything in /lib could have a spec anywhere, if at all. So, look for + # files with roughly the same name as the file in /lib + add_mapping %r%^lib\/(.*)\.rb% do |_, m| + files_matching %r%^spec\/#{m[1]}% + end + + add_mapping %r%^spec/(spec_helper|shared/.*)\.rb$% do + all_specs + end + + # Changing a spec will cause it to run itself + add_mapping %r%^spec/.*\.rb$% do |filename, _| + filename + end + + # Any change to a model will cause it's corresponding test to be run + add_mapping %r%^app/models/(.*)\.rb$% do |_, m| + spec_for(m[1], 'model') + end + + # Any change to global_helpers will result in all view and controller + # tests being run + add_mapping %r%^app/helpers/global_helpers\.rb% do + files_matching %r%^spec/(views|controllers|helpers|requests)/.*_spec\.rb$% + end + + # Any change to a helper will cause its spec to be run + add_mapping %r%^app/helpers/((.*)_helper(s)?)\.rb% do |_, m| + spec_for(m[1], 'helper') + end + + # Changes to a view cause its spec to be run + add_mapping %r%^app/views/(.*)/% do |_, m| + spec_for(m[1], 'view') + end + + # Changes to a controller result in its corresponding spec being run. If + # the controller is the exception or application controller, all + # controller specs are run. + add_mapping %r%^app/controllers/(.*)\.rb$% do |_, m| + if ["application", "exception"].include?(m[1]) + files_matching %r%^spec/controllers/.*_spec\.rb$% + else + spec_for(m[1], 'controller') + end + end + + # If a change is made to the router, run controller, view and helper specs + add_mapping %r%^config/router.rb$% do + files_matching %r%^spec/(controllers|views|helpers)/.*_spec\.rb$% + end + + # If any of the major files governing the environment are altered, run + # everything + add_mapping %r%^config/(init|rack|environments/test).*\.rb|database\.yml% do + all_specs + end + end + + def failed_results(results) + results.scan(/^\d+\)\n(?:\e\[\d*m)?(?:.*?Error in )?'([^\n]*)'(?: FAILED)?(?:\e\[\d*m)?\n(.*?)\n\n/m) + end + + def handle_results(results) + @failures = failed_results(results) + @files_to_test = consolidate_failures(@failures) + @files_to_test.empty? && !$TESTING ? hook(:green) : hook(:red) + @tainted = !@files_to_test.empty? + end + + def consolidate_failures(failed) + filters = Hash.new { |h,k| h[k] = [] } + failed.each do |spec, failed_trace| + if f = test_files_for(failed).find { |f| f =~ /spec\// } + filters[f] << spec + break + end + end + filters + end + + def make_test_cmd(specs_to_runs) + [ + ruby, + "-S", + spec_command, + add_options_if_present, + files_to_test.keys.flatten.join(' ') + ].join(' ') + end + + def add_options_if_present + File.exist?("spec/spec.opts") ? "-O spec/spec.opts " : "" + end + + # Finds the proper spec command to use. Precendence is set in the + # lazily-evaluated method spec_commands. Alias + Override that in + # ~/.autotest to provide a different spec command then the default + # paths provided. + def spec_command(separator=File::ALT_SEPARATOR) + unless defined?(@spec_command) + @spec_command = spec_commands.find { |cmd| File.exists?(cmd) } + + raise RspecCommandError, "No spec command could be found" unless @spec_command + + @spec_command.gsub!(File::SEPARATOR, separator) if separator + end + @spec_command + end + + # Autotest will look for spec commands in the following + # locations, in this order: + # + # * default spec bin/loader installed in Rubygems + # * any spec command found in PATH + def spec_commands + [File.join(Config::CONFIG['bindir'], 'spec'), 'spec'] + end + +private + + # Runs +files_matching+ for all specs + def all_specs + files_matching %r%^spec/.*_spec\.rb$% + end + + # Generates a path to some spec given its kind and the match from a mapping + # + # ==== Arguments + # match:: the match from a mapping + # kind:: the kind of spec that the match represents + # + # ==== Returns + # String + # + # ==== Example + # > spec_for('post', :view') + # => "spec/views/post_spec.rb" + def spec_for(match, kind) + File.join("spec", kind + 's', "#{match}_spec.rb") + end +end diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..ba3a4f82 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,33 @@ +--- +# This is a sample database file for the DataMapper ORM +development: &defaults + # These are the settings for repository :default + adapter: sqlite3 + database: sample_development.db + + # Add more repositories + # repositories: + # repo1: + # adapter: sqlite3 + # database: sample_1_development.db + # repo2: + # ... + +test: + <<: *defaults + database: sample_test.db + + # repositories: + # repo1: + # database: sample_1_test.db + +production: + <<: *defaults + database: production.db + + # repositories: + # repo1: + # database: sample_production.db + +rake: + <<: *defaults \ No newline at end of file diff --git a/config/dependencies.rb b/config/dependencies.rb new file mode 100644 index 00000000..04f15370 --- /dev/null +++ b/config/dependencies.rb @@ -0,0 +1,23 @@ +# dependencies are generated using a strict version, don't forget to edit the dependency versions when upgrading. +merb_gems_version = "1.0.3" +dm_gems_version = "0.9.7" + +# For more information about each component, please read http://wiki.merbivore.com/faqs/merb_components +dependency "merb-action-args", merb_gems_version +dependency "merb-assets", merb_gems_version +dependency "merb-cache", merb_gems_version +dependency "merb-helpers", merb_gems_version +dependency "merb-mailer", merb_gems_version +dependency "merb-slices", merb_gems_version +dependency "merb-auth-core", merb_gems_version +dependency "merb-auth-more", merb_gems_version +dependency "merb-auth-slice-password", merb_gems_version +dependency "merb-param-protection", merb_gems_version +dependency "merb-exceptions", merb_gems_version + +dependency "dm-core", dm_gems_version +dependency "dm-aggregates", dm_gems_version +dependency "dm-migrations", dm_gems_version +dependency "dm-timestamps", dm_gems_version +dependency "dm-types", dm_gems_version +dependency "dm-validations", dm_gems_version diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..d6d4ab38 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,15 @@ +Merb.logger.info("Loaded DEVELOPMENT Environment...") +Merb::Config.use { |c| + c[:exception_details] = true + c[:reload_templates] = true + c[:reload_classes] = true + c[:reload_time] = 0.5 + c[:ignore_tampered_cookies] = true + c[:log_auto_flush ] = true + c[:log_level] = :debug + + c[:log_stream] = STDOUT + c[:log_file] = nil + # Or redirect logging into a file: + # c[:log_file] = Merb.root / "log" / "development.log" +} diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..17a2d4b1 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,10 @@ +Merb.logger.info("Loaded PRODUCTION Environment...") +Merb::Config.use { |c| + c[:exception_details] = false + c[:reload_classes] = false + c[:log_level] = :error + + c[:log_file] = Merb.root / "log" / "production.log" + # or redirect logger using IO handle + # c[:log_stream] = STDOUT +} diff --git a/config/environments/rake.rb b/config/environments/rake.rb new file mode 100644 index 00000000..5e4b9a77 --- /dev/null +++ b/config/environments/rake.rb @@ -0,0 +1,11 @@ +Merb.logger.info("Loaded RAKE Environment...") +Merb::Config.use { |c| + c[:exception_details] = true + c[:reload_classes] = false + c[:log_auto_flush ] = true + + c[:log_stream] = STDOUT + c[:log_file] = nil + # Or redirect logging into a file: + # c[:log_file] = Merb.root / "log" / "development.log" +} diff --git a/config/environments/staging.rb b/config/environments/staging.rb new file mode 100644 index 00000000..f5b50589 --- /dev/null +++ b/config/environments/staging.rb @@ -0,0 +1,10 @@ +Merb.logger.info("Loaded STAGING Environment...") +Merb::Config.use { |c| + c[:exception_details] = false + c[:reload_classes] = false + c[:log_level] = :error + + c[:log_file] = Merb.root / "log" / "staging.log" + # or redirect logger using IO handle + # c[:log_stream] = STDOUT +} diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..671ec76e --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,12 @@ +Merb.logger.info("Loaded TEST Environment...") +Merb::Config.use { |c| + c[:testing] = true + c[:exception_details] = true + c[:log_auto_flush ] = true + # log less in testing environment + c[:log_level] = :error + + #c[:log_file] = Merb.root / "log" / "test.log" + # or redirect logger using IO handle + c[:log_stream] = STDOUT +} diff --git a/config/init.rb b/config/init.rb new file mode 100644 index 00000000..472a32b4 --- /dev/null +++ b/config/init.rb @@ -0,0 +1,24 @@ +# Go to http://wiki.merbivore.com/pages/init-rb + +require 'config/dependencies.rb' + +use_orm :datamapper +use_test :rspec +use_template_engine :erb + +Merb::Config.use do |c| + c[:use_mutex] = false + c[:session_store] = 'cookie' # can also be 'memory', 'memcache', 'container', 'datamapper + + # cookie session store configuration + c[:session_secret_key] = 'cccf61526b3a7beffe6b120432ae00c1640a60fe' # required for cookie session store + c[:session_id_key] = '_one_click_session_id' # cookie session id key, defaults to "_session_id" +end + +Merb::BootLoader.before_app_loads do + # This will get executed after dependencies have been loaded but before your app's classes have loaded. +end + +Merb::BootLoader.after_app_loads do + # This will get executed after your app's classes have been loaded. +end diff --git a/config/rack.rb b/config/rack.rb new file mode 100644 index 00000000..494c6873 --- /dev/null +++ b/config/rack.rb @@ -0,0 +1,11 @@ +# use PathPrefix Middleware if :path_prefix is set in Merb::Config +if prefix = ::Merb::Config[:path_prefix] + use Merb::Rack::PathPrefix, prefix +end + +# comment this out if you are running merb behind a load balancer +# that serves static files +use Merb::Rack::Static, Merb.dir_for(:public) + +# this is our main merb application +run Merb::Rack::Application.new \ No newline at end of file diff --git a/config/router.rb b/config/router.rb new file mode 100644 index 00000000..b83c9afa --- /dev/null +++ b/config/router.rb @@ -0,0 +1,44 @@ +# Merb::Router is the request routing mapper for the merb framework. +# +# You can route a specific URL to a controller / action pair: +# +# match("/contact"). +# to(:controller => "info", :action => "contact") +# +# You can define placeholder parts of the url with the :symbol notation. These +# placeholders will be available in the params hash of your controllers. For example: +# +# match("/books/:book_id/:action"). +# to(:controller => "books") +# +# Or, use placeholders in the "to" results for more complicated routing, e.g.: +# +# match("/admin/:module/:controller/:action/:id"). +# to(:controller => ":module/:controller") +# +# You can specify conditions on the placeholder by passing a hash as the second +# argument of "match" +# +# match("/registration/:course_name", :course_name => /^[a-z]{3,5}-\d{5}$/). +# to(:controller => "registration") +# +# You can also use regular expressions, deferred routes, and many other options. +# See merb/specs/merb/router.rb for a fairly complete usage sample. + +Merb.logger.info("Compiling routes...") +Merb::Router.prepare do + # RESTful routes + # resources :posts + + # Adds the required routes for merb-auth using the password slice + slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "") + + # This is the default route for /:controller/:action/:id + # This is fine for most cases. If you're heavily using resource-based + # routes, you may want to comment/remove this line to prevent + # clients from calling your create or destroy actions with a GET + default_routes + + # Change this for your home page to be available at / + # match('/').to(:controller => 'whatever', :action =>'index') +end \ No newline at end of file diff --git a/doc/rdoc/generators/merb_generator.rb b/doc/rdoc/generators/merb_generator.rb new file mode 100644 index 00000000..544c273a --- /dev/null +++ b/doc/rdoc/generators/merb_generator.rb @@ -0,0 +1,1362 @@ + +# # We're responsible for generating all the HTML files +# from the object tree defined in code_objects.rb. We +# generate: +# +# [files] an html file for each input file given. These +# input files appear as objects of class +# TopLevel +# +# [classes] an html file for each class or module encountered. +# These classes are not grouped by file: if a file +# contains four classes, we'll generate an html +# file for the file itself, and four html files +# for the individual classes. +# +# Method descriptions appear in whatever entity (file, class, +# or module) that contains them. +# +# We generate files in a structure below a specified subdirectory, +# normally +doc+. +# +# opdir +# | +# |___ files +# | |__ per file summaries +# | +# |___ classes +# |__ per class/module descriptions +# +# HTML is generated using the Template class. +# + +require 'ftools' + +require 'rdoc/options' +require 'rdoc/template' +require 'rdoc/markup/simple_markup' +require 'rdoc/markup/simple_markup/to_html' +require 'cgi' + +module Generators + + # Name of sub-direcories that hold file and class/module descriptions + + FILE_DIR = "files" + CLASS_DIR = "classes" + CSS_NAME = "stylesheet.css" + + + ## + # Build a hash of all items that can be cross-referenced. + # This is used when we output required and included names: + # if the names appear in this hash, we can generate + # an html cross reference to the appropriate description. + # We also use this when parsing comment blocks: any decorated + # words matching an entry in this list are hyperlinked. + + class AllReferences + @@refs = {} + + def AllReferences::reset + @@refs = {} + end + + def AllReferences.add(name, html_class) + @@refs[name] = html_class + end + + def AllReferences.[](name) + @@refs[name] + end + + def AllReferences.keys + @@refs.keys + end + end + + + ## + # Subclass of the SM::ToHtml class that supports looking + # up words in the AllReferences list. Those that are + # found (like AllReferences in this comment) will + # be hyperlinked + + class HyperlinkHtml < SM::ToHtml + # We need to record the html path of our caller so we can generate + # correct relative paths for any hyperlinks that we find + def initialize(from_path, context) + super() + @from_path = from_path + + @parent_name = context.parent_name + @parent_name += "::" if @parent_name + @context = context + end + + # We're invoked when any text matches the CROSSREF pattern + # (defined in MarkUp). If we fine the corresponding reference, + # generate a hyperlink. If the name we're looking for contains + # no punctuation, we look for it up the module/class chain. For + # example, HyperlinkHtml is found, even without the Generators:: + # prefix, because we look for it in module Generators first. + + def handle_special_CROSSREF(special) + name = special.text + if name[0,1] == '#' + lookup = name[1..-1] + name = lookup unless Options.instance.show_hash + else + lookup = name + end + + if /([A-Z].*)[.\#](.*)/ =~ lookup + container = $1 + method = $2 + ref = @context.find_symbol(container, method) + else + ref = @context.find_symbol(lookup) + end + + if ref and ref.document_self + "#{name}" + else + name #it does not need to be a link + end + end + + + # Generate a hyperlink for url, labeled with text. Handle the + # special cases for img: and link: described under handle_special_HYPEDLINK + def gen_url(url, text) + if url =~ /([A-Za-z]+):(.*)/ + type = $1 + path = $2 + else + type = "http" + path = url + url = "http://#{url}" + end + + if type == "link" + url = path + end + + if (type == "http" || type == "link") && url =~ /\.(gif|png|jpg|jpeg|bmp)$/ + "" + elsif (type == "http" || type == "link") + "#{text}" + else + "#{text.sub(%r{^#{type}:/*}, '')}" + + end + end + + # And we're invoked with a potential external hyperlink mailto: + # just gets inserted. http: links are checked to see if they + # reference an image. If so, that image gets inserted using an + # tag. Otherwise a conventional is used. We also + # support a special type of hyperlink, link:, which is a reference + # to a local file whose path is relative to the --op directory. + + def handle_special_HYPERLINK(special) + url = special.text + gen_url(url, url) + end + + # HEre's a hypedlink where the label is different to the URL + #

/, '') + res.sub!(/<\/p>$/, '') + end + res + end + + + def style_url(path, css_name=nil) + css_name ||= CSS_NAME + end + + # Build a webcvs URL with the given 'url' argument. URLs with a '%s' in them + # get the file's path sprintfed into them; otherwise they're just catenated + # together. + + def cvs_url(url, full_path) + if /%s/ =~ url + return sprintf( url, full_path ) + else + return url + full_path + end + end + end + + + ##################################################################### + # + # A Context is built by the parser to represent a container: contexts + # hold classes, modules, methods, require lists and include lists. + # ClassModule and TopLevel are the context objects we process here + # + class ContextUser + + include MarkUp + + attr_reader :context + + def initialize(context, options) + @context = context + @options = options + + end + + # convenience method to build a hyperlink # Where's the DRY in this?? Put this in the template where it belongs + def href(link, cls, name) + %{"#{name}"} + end + + # Create a list of HtmlMethod objects for each method + # in the corresponding context object. If the @options.show_all + # variable is set (corresponding to the --all option, + # we include all methods, otherwise just the public ones. + + def collect_methods + list = @context.method_list + unless @options.show_all + list = list.find_all {|m| m.visibility == :public || m.visibility == :protected || m.force_documentation } + end + @methods = list.collect {|m| HtmlMethod.new(m, self, @options) } + end + + # Build a summary list of all the methods in this context + def build_method_summary_list(path_prefix="") + collect_methods unless @methods + meths = @methods.sort + res = [] + meths.each do |meth| + res << { + "name" => CGI.escapeHTML(meth.name), + "aref" => meth.aref, + "href" => meth.path + } + end + res + end + + + # Build a list of aliases for which we couldn't find a + # corresponding method + def build_alias_summary_list(section) + values = [] + @context.aliases.each do |al| + next unless al.section == section + res = { + 'old_name' => al.old_name, + 'new_name' => al.new_name, + } + if al.comment && !al.comment.empty? + res['desc'] = markup(al.comment, true) + end + values << res + end + values + end + + # Build a list of constants + def build_constants_summary_list(section) + values = [] + @context.constants.each do |co| + next unless co.section == section + res = { + 'name' => co.name, + 'value' => CGI.escapeHTML(co.value) + } + res['desc'] = markup(co.comment, true) if co.comment && !co.comment.empty? + values << res + end + values + end + + def build_requires_list(context) + potentially_referenced_list(context.requires) {|fn| [fn + ".rb"] } + end + + def build_include_list(context) + potentially_referenced_list(context.includes) + end + + # Build a list from an array of Htmlxxx items. Look up each + # in the AllReferences hash: if we find a corresponding entry, + # we generate a hyperlink to it, otherwise just output the name. + # However, some names potentially need massaging. For example, + # you may require a Ruby file without the .rb extension, + # but the file names we know about may have it. To deal with + # this, we pass in a block which performs the massaging, + # returning an array of alternative names to match + + def potentially_referenced_list(array) + res = [] + array.each do |i| + ref = AllReferences[i.name] + # if !ref + # container = @context.parent + # while !ref && container + # name = container.name + "::" + i.name + # ref = AllReferences[name] + # container = container.parent + # end + # end + + ref = @context.find_symbol(i.name) + ref = ref.viewer if ref + + if !ref && block_given? + possibles = yield(i.name) + while !ref and !possibles.empty? + ref = AllReferences[possibles.shift] + end + end + h_name = CGI.escapeHTML(i.name) + if ref and ref.document_self + path = ref.path + res << { "name" => h_name, "href" => path } + else + res << { "name" => h_name, "href" => "" } + end + end + res + end + + # Build an array of arrays of method details. The outer array has up + # to six entries, public, private, and protected for both class + # methods, the other for instance methods. The inner arrays contain + # a hash for each method + + def build_method_detail_list(section) + outer = [] + + methods = @methods.sort + for singleton in [true, false] + for vis in [ :public, :protected, :private ] + res = [] + methods.each do |m| + if m.section == section and + m.document_self and + m.visibility == vis and + m.singleton == singleton + row = {} + if m.call_seq + row["callseq"] = m.call_seq.gsub(/->/, '→') + else + row["name"] = CGI.escapeHTML(m.name) + row["params"] = m.params + end + desc = m.description.strip + row["m_desc"] = desc unless desc.empty? + row["aref"] = m.aref + row["href"] = m.path + row["m_seq"] = m.seq + row["visibility"] = m.visibility.to_s + + alias_names = [] + m.aliases.each do |other| + if other.viewer # won't be if the alias is private + alias_names << { + 'name' => other.name, + 'href' => other.viewer.path, + 'aref' => other.viewer.aref + } + end + end + unless alias_names.empty? + row["aka"] = alias_names + end + + #if @options.inline_source + code = m.source_code + row["sourcecode"] = code if code + #else + # code = m.src_url + #if code + # row["codeurl"] = code + # row["imgurl"] = m.img_url + #end + #end + res << row + end + end + if res.size > 0 + outer << { + "type" => vis.to_s.capitalize, + "category" => singleton ? "Class" : "Instance", + "methods" => res + } + end + end + end + outer + end + + # Build the structured list of classes and modules contained + # in this context. + + def build_class_list(level, from, section, infile=nil) + res = "" + prefix = "  ::" * level; + + from.modules.sort.each do |mod| + next unless mod.section == section + next if infile && !mod.defined_in?(infile) + if mod.document_self + res << + prefix << + "Module " << + href(mod.viewer.path, "link", mod.full_name) << + "
\n" << + build_class_list(level + 1, mod, section, infile) + end + end + + from.classes.sort.each do |cls| + next unless cls.section == section + next if infile && !cls.defined_in?(infile) + if cls.document_self + res << + prefix << + "Class " << + href(cls.viewer.path, "link", cls.full_name) << + "
\n" << + build_class_list(level + 1, cls, section, infile) + end + end + + res + end + + def document_self + @context.document_self + end + + def diagram_reference(diagram) + res = diagram.gsub(/((?:src|href)=")(.*?)"/) { + $1 + $2 + '"' + } + res + end + + + # Find a symbol in ourselves or our parent + def find_symbol(symbol, method=nil) + res = @context.find_symbol(symbol, method) + if res + res = res.viewer + end + res + end + + # create table of contents if we contain sections + + def add_table_of_sections + toc = [] + @context.sections.each do |section| + if section.title + toc << { + 'secname' => section.title, + 'href' => section.sequence + } + end + end + + @values['toc'] = toc unless toc.empty? + end + + + end + + ##################################################################### + # + # Wrap a ClassModule context + + class HtmlClass < ContextUser + + @@c_seq = "C00000000" + + attr_reader :path + + def initialize(context, html_file, prefix, options) + super(context, options) + @@c_seq = @@c_seq.succ + @c_seq = @@c_seq + @html_file = html_file + @is_module = context.is_module? + @values = {} + + context.viewer = self + + @path = http_url(context.full_name, prefix) + + collect_methods + + AllReferences.add(name, self) + end + + # return the relative file name to store this class in, + # which is also its url + def http_url(full_name, prefix) + path = full_name.dup + if path['<<'] + path.gsub!(/<<\s*(\w*)/) { "from-#$1" } + end + File.join(prefix, path.split("::")) + ".html" + end + + def seq + @c_seq + end + + def aref + @c_seq + end + + def scope + a = @context.full_name.split("::") + if a.length > 1 + a.pop + a.join("::") + else + "" + end + end + + def name + @context.full_name.gsub("#{scope}::", '') + end + + def full_name + @context.full_name + end + + def parent_name + @context.parent.full_name + end + + def write_on(f) + value_hash + template = TemplatePage.new(RDoc::Page::BODY, + RDoc::Page::CLASS_PAGE, + RDoc::Page::METHOD_LIST) + template.write_html_on(f, @values) + end + + def value_hash + class_attribute_values + add_table_of_sections + + @values["charset"] = @options.charset + @values["style_url"] = style_url(path, @options.css) + + # Convert README to html + unless File.exist?('files/README.html') + File.open('files/README.html', 'w') do |file| + file << markup(File.read(File.expand_path(@options.main_page))) + end + end + + d = markup(@context.comment) + @values["description"] = d unless d.empty? + + ml = build_method_summary_list + @values["methods"] = ml unless ml.empty? + + il = build_include_list(@context) + @values["includes"] = il unless il.empty? + + @values["sections"] = @context.sections.map do |section| + + secdata = { + "sectitle" => section.title, + "secsequence" => section.sequence, + "seccomment" => markup(section.comment) + } + + al = build_alias_summary_list(section) + secdata["aliases"] = al unless al.empty? + + co = build_constants_summary_list(section) + secdata["constants"] = co unless co.empty? + + al = build_attribute_list(section) + secdata["attributes"] = al unless al.empty? + + cl = build_class_list(0, @context, section) + secdata["classlist"] = cl unless cl.empty? + + mdl = build_method_detail_list(section) + secdata["method_list"] = mdl unless mdl.empty? + + secdata + end + + @values + end + + def build_attribute_list(section) + atts = @context.attributes.sort + res = [] + atts.each do |att| + next unless att.section == section + if att.visibility == :public || att.visibility == :protected || @options.show_all + entry = { + "name" => CGI.escapeHTML(att.name), + "rw" => att.rw, + "a_desc" => markup(att.comment, true) + } + unless att.visibility == :public || att.visibility == :protected + entry["rw"] << "-" + end + res << entry + end + end + res + end + + def class_attribute_values + h_name = CGI.escapeHTML(name) + + @values["classmod"] = @is_module ? "Module" : "Class" + @values["title"] = "#{@values['classmod']}: #{h_name}" + + c = @context + c = c.parent while c and !c.diagram + if c && c.diagram + @values["diagram"] = diagram_reference(c.diagram) + end + + @values["full_name"] = h_name + @values["class_seq"] = seq + parent_class = @context.superclass + + if parent_class + @values["parent"] = CGI.escapeHTML(parent_class) + + if parent_name + lookup = parent_name + "::" + parent_class + else + lookup = parent_class + end + + parent_url = AllReferences[lookup] || AllReferences[parent_class] + + if parent_url and parent_url.document_self + @values["par_url"] = parent_url.path + end + end + + files = [] + @context.in_files.each do |f| + res = {} + full_path = CGI.escapeHTML(f.file_absolute_name) + + res["full_path"] = full_path + res["full_path_url"] = f.viewer.path if f.document_self + + if @options.webcvs + res["cvsurl"] = cvs_url( @options.webcvs, full_path ) + end + + files << res + end + + @values['infiles'] = files + end + + def <=>(other) + self.name <=> other.name + end + + end + + ##################################################################### + # + # Handles the mapping of a file's information to HTML. In reality, + # a file corresponds to a +TopLevel+ object, containing modules, + # classes, and top-level methods. In theory it _could_ contain + # attributes and aliases, but we ignore these for now. + + class HtmlFile < ContextUser + + @@f_seq = "F00000000" + + attr_reader :path + attr_reader :name + + def initialize(context, options, file_dir) + super(context, options) + @@f_seq = @@f_seq.succ + @f_seq = @@f_seq + @values = {} + + @path = http_url(file_dir) + @source_file_path = File.expand_path(@context.file_relative_name).gsub("\/doc\/", "/") + @name = @context.file_relative_name + + collect_methods + AllReferences.add(name, self) + context.viewer = self + end + + def http_url(file_dir) + File.join(file_dir, @context.file_relative_name.tr('.', '_')) + ".html" + end + + def filename_to_label + @context.file_relative_name.gsub(/%|\/|\?|\#/) {|s| '%' + ("%x" % s[0]) } + end + + def seq + @f_seq + end + + def aref + @f_seq + end + + def name + full_path = @context.file_absolute_name + short_name = File.basename(full_path) + end + + def full_name + @context.file_absolute_name + end + + def scope + @context.file_relative_name.gsub(/\/#{name}$/, '') + end + + def parent_name + nil + end + + def full_file_source + ret_str = "" + File.open(@source_file_path, 'r') do |f| + while(!f.eof?) do + ret_str += f.readline() + end + end + ret_str + rescue + "file not found -#{@source_file_path}-" + #@source_file_path + end + + def value_hash + file_attribute_values + add_table_of_sections + + @values["charset"] = @options.charset + @values["href"] = path + @values["style_url"] = style_url(path, @options.css) + @values["file_seq"] = seq + + #pulling in the source for this file + #@values["source_code"] = @context.token_stream + + @values["file_source_code"] = CGI.escapeHTML(full_file_source) + + if @context.comment + d = markup(@context.comment) + @values["description"] = d if d.size > 0 + end + + ml = build_method_summary_list + @values["methods"] = ml unless ml.empty? + + il = build_include_list(@context) + @values["includes"] = il unless il.empty? + + rl = build_requires_list(@context) + @values["requires"] = rl unless rl.empty? + + + file_context = @context + + @values["sections"] = @context.sections.map do |section| + + secdata = { + "sectitle" => section.title, + "secsequence" => section.sequence, + "seccomment" => markup(section.comment) + } + + cl = build_class_list(0, @context, section, file_context) + @values["classlist"] = cl unless cl.empty? + + mdl = build_method_detail_list(section) + secdata["method_list"] = mdl unless mdl.empty? + + al = build_alias_summary_list(section) + secdata["aliases"] = al unless al.empty? + + co = build_constants_summary_list(section) + @values["constants"] = co unless co.empty? + + secdata + end + + @values + end + + def write_on(f) + value_hash + template = TemplatePage.new(RDoc::Page::SRC_BODY,RDoc::Page::FILE_PAGE, RDoc::Page::METHOD_LIST) + template.write_html_on(f, @values) + end + + def file_attribute_values + full_path = @context.file_absolute_name + short_name = File.basename(full_path) + + @values["title"] = CGI.escapeHTML("File: #{short_name}") + + if @context.diagram + @values["diagram"] = diagram_reference(@context.diagram) + end + + @values["short_name"] = CGI.escapeHTML(short_name) + @values["full_path"] = CGI.escapeHTML(full_path) + @values["dtm_modified"] = @context.file_stat.mtime.to_s + + if @options.webcvs + @values["cvsurl"] = cvs_url( @options.webcvs, @values["full_path"] ) + end + end + + def <=>(other) + self.name <=> other.name + end + end + + ##################################################################### + + class HtmlMethod + include MarkUp + + attr_reader :context + attr_reader :src_url + attr_reader :img_url + attr_reader :source_code + + @@m_seq = "M000000" + + @@all_methods = [] + + def HtmlMethod::reset + @@all_methods = [] + end + + def initialize(context, html_class, options) + @context = context + @html_class = html_class + @options = options + @@m_seq = @@m_seq.succ + @m_seq = @@m_seq + @@all_methods << self + + context.viewer = self + + if (ts = @context.token_stream) + @source_code = markup_code(ts) + #unless @options.inline_source + # @src_url = create_source_code_file(@source_code) + # @img_url = MERBGenerator.gen_url(path, 'source.png') + #end + end + AllReferences.add(name, self) + end + + def seq + @m_seq + end + + def aref + @m_seq + end + + def scope + @html_class.full_name + end + + # return a reference to outselves to be used as an href= + # the form depends on whether we're all in one file + # or in multiple files + + def name + @context.name + end + + def section + @context.section + end + + def parent_name + if @context.parent.parent + @context.parent.parent.full_name + else + nil + end + end + + def path + @html_class.path + end + + def description + markup(@context.comment) + end + + def visibility + @context.visibility + end + + def singleton + @context.singleton + end + + def call_seq + cs = @context.call_seq + if cs + cs.gsub(/\n/, "
\n") + else + nil + end + end + + def params + # params coming from a call-seq in 'C' will start with the + # method name + p = @context.params + if p !~ /^\w/ + p = @context.params.gsub(/\s*\#.*/, '') + p = p.tr("\n", " ").squeeze(" ") + p = "(" + p + ")" unless p[0] == ?( + + if (block = @context.block_params) + # If this method has explicit block parameters, remove any + # explicit &block + + p.sub!(/,?\s*&\w+/, '') + + block.gsub!(/\s*\#.*/, '') + block = block.tr("\n", " ").squeeze(" ") + if block[0] == ?( + block.sub!(/^\(/, '').sub!(/\)/, '') + end + p << " {|#{block.strip}| ...}" + end + end + CGI.escapeHTML(p) + end + + def create_source_code_file(code_body) + meth_path = @html_class.path.sub(/\.html$/, '.src') + File.makedirs(meth_path) + file_path = File.join(meth_path, seq) + ".html" + + template = TemplatePage.new(RDoc::Page::SRC_PAGE) + File.open(file_path, "w") do |f| + values = { + 'title' => CGI.escapeHTML(name), + 'code' => code_body, + 'style_url' => style_url(file_path, @options.css), + 'charset' => @options.charset + } + template.write_html_on(f, values) + end + file_path + end + + def HtmlMethod.all_methods + @@all_methods + end + + def <=>(other) + @context <=> other.context + end + + ## + # Given a sequence of source tokens, mark up the source code + # to make it look purty. + + + def markup_code(tokens) + src = "" + tokens.each do |t| + next unless t + # p t.class + # style = STYLE_MAP[t.class] + style = case t + when RubyToken::TkCONSTANT then "ruby-constant" + when RubyToken::TkKW then "ruby-keyword kw" + when RubyToken::TkIVAR then "ruby-ivar" + when RubyToken::TkOp then "ruby-operator" + when RubyToken::TkId then "ruby-identifier" + when RubyToken::TkNode then "ruby-node" + when RubyToken::TkCOMMENT then "ruby-comment cmt" + when RubyToken::TkREGEXP then "ruby-regexp re" + when RubyToken::TkSTRING then "ruby-value str" + when RubyToken::TkVal then "ruby-value" + else + nil + end + + text = CGI.escapeHTML(t.text) + + if style + src << "#{text}" + else + src << text + end + end + + add_line_numbers(src) + src + end + + # we rely on the fact that the first line of a source code + # listing has + # # File xxxxx, line dddd + + def add_line_numbers(src) + if src =~ /\A.*, line (\d+)/ + first = $1.to_i - 1 + last = first + src.count("\n") + size = last.to_s.length + real_fmt = "%#{size}d: " + fmt = " " * (size+2) + src.gsub!(/^/) do + res = sprintf(fmt, first) + first += 1 + fmt = real_fmt + res + end + end + end + + def document_self + @context.document_self + end + + def aliases + @context.aliases + end + + def find_symbol(symbol, method=nil) + res = @context.parent.find_symbol(symbol, method) + if res + res = res.viewer + end + res + end + end + + ##################################################################### + + class MERBGenerator + + include MarkUp + + # Generators may need to return specific subclasses depending + # on the options they are passed. Because of this + # we create them using a factory + + def MERBGenerator.for(options) + AllReferences::reset + HtmlMethod::reset + + MERBGenerator.new(options) + + end + + class < "helvetica"} #this is not used anywhere but the template function demands a hash of values + template.write_html_on(f, values) + end + end + + def write_javascript + #Argh... I couldn't figure out how to copy these from the template dir so they were copied into + # the template file "ajax.rb" and processed similarlly to the style sheets. Not exactly a good thing to do with + # external library code. Not very DRY. + + File.open("api_grease.js", "w") do |f| + f << RDoc::Page::API_GREASE_JS + end + + File.open("prototype.js", "w") do |f| + f << RDoc::Page::PROTOTYPE_JS + end + + rescue LoadError + $stderr.puts "Could not find AJAX template" + exit 99 + end + + ## + # See the comments at the top for a description of the + # directory structure + + def gen_sub_directories + File.makedirs(FILE_DIR, CLASS_DIR) + rescue + $stderr.puts $!.message + exit 1 + end + + ## + # Generate: + # + # * a list of HtmlFile objects for each TopLevel object. + # * a list of HtmlClass objects for each first level + # class or module in the TopLevel objects + # * a complete list of all hyperlinkable terms (file, + # class, module, and method names) + + def build_indices + + @toplevels.each do |toplevel| + @files << HtmlFile.new(toplevel, @options, FILE_DIR) + end + + RDoc::TopLevel.all_classes_and_modules.each do |cls| + build_class_list(cls, @files[0], CLASS_DIR) + end + end + + def build_class_list(from, html_file, class_dir) + @classes << HtmlClass.new(from, html_file, class_dir, @options) + from.each_classmodule do |mod| + build_class_list(mod, html_file, class_dir) + end + end + + ## + # Generate all the HTML + # + def generate_html + # the individual descriptions for files and classes + gen_into(@files) + gen_into(@classes) + # and the index files + gen_file_index + gen_class_index + gen_method_index + gen_main_index + + # this method is defined in the template file + write_extra_pages if defined? write_extra_pages + end + + def gen_into(list) + list.each do |item| + if item.document_self + op_file = item.path + File.makedirs(File.dirname(op_file)) + File.open(op_file, "w") { |file| item.write_on(file) } + end + end + + end + + def gen_file_index + gen_an_index(@files, 'Files', + RDoc::Page::FILE_INDEX, + "fr_file_index.html") + end + + def gen_class_index + gen_an_index(@classes, 'Classes', + RDoc::Page::CLASS_INDEX, + "fr_class_index.html") + end + + def gen_method_index + gen_an_index(HtmlMethod.all_methods, 'Methods', + RDoc::Page::METHOD_INDEX, + "fr_method_index.html") + end + + + def gen_an_index(collection, title, template, filename) + template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template) + res = [] + collection.sort.each do |f| + if f.document_self + res << { "href" => f.path, "name" => f.name, "scope" => f.scope, "seq_id" => f.seq } + end + end + + values = { + "entries" => res, + 'list_title' => CGI.escapeHTML(title), + 'index_url' => main_url, + 'charset' => @options.charset, + 'style_url' => style_url('', @options.css), + } + + File.open(filename, "w") do |f| + template.write_html_on(f, values) + end + end + + # The main index page is mostly a template frameset, but includes + # the initial page. If the --main option was given, + # we use this as our main page, otherwise we use the + # first file specified on the command line. + + def gen_main_index + template = TemplatePage.new(RDoc::Page::INDEX) + File.open("index.html", "w") do |f| + tStr = "" + #File.open(main_url, 'r') do |g| + # tStr = markup(g) + #end + values = { + "initial_page" => tStr, + 'title' => CGI.escapeHTML(@options.title), + 'charset' => @options.charset, + 'content' => File.read('files/README.html') + } + + values['inline_source'] = true + template.write_html_on(f, values) + end + end + + # return the url of the main page + def main_url + "files/README.html" + end + + + end + +end diff --git a/doc/rdoc/generators/template/merb/api_grease.js b/doc/rdoc/generators/template/merb/api_grease.js new file mode 100644 index 00000000..3df710e8 --- /dev/null +++ b/doc/rdoc/generators/template/merb/api_grease.js @@ -0,0 +1,640 @@ + +function setupPage(){ + hookUpActiveSearch(); + hookUpTabs(); + suppressPostbacks(); + var url_params = getUrlParams(); + if (url_params != null){ + loadUrlParams(url_params); + }else{ + loadDefaults(); + } + resizeDivs(); + window.onresize = function(){ resizeDivs(); }; +} + +function getUrlParams(){ + var window_location = window.location.href + var param_pos = window_location.search(/\?/) + if (param_pos > 0){ + return(window_location.slice(param_pos, window_location.length)); + }else{ + return(null); + } +} + +function loadUrlParams(url_param){ + //get the tabs + var t = getTabs(); + // now find our variables + var s_params = /(\?)(a=.+?)(&)(name=.*)/; + var results = url_param.match(s_params); + url_anchor = results[2].replace(/a=/,''); + + if (url_anchor.match(/M.+/)){//load the methods tab and scroller content + setActiveTabAndLoadContent(t[0]); + }else{ + if(url_anchor.match(/C.+/)){ //load the classes tab and scroller content + setActiveTabAndLoadContent(t[1]); + }else{ + if (url_anchor.match(/F.+/)){//load the files tab + setActiveTabAndLoadContent(t[2]); + }else{ + // default to loading the methods + setActiveTabAndLoadContent(t[0]); + } + } + } + paramLoadOfContentAnchor(url_anchor + "_link"); +} + +function updateUrlParams(anchor_id, name){ + //Also setting the page title + //window.document.title = name + " method - MerbBrain.com "; + + //updating the window location + var current_href = window.location.href; + //var m_name = name.replace("?","?"); + var rep_str = ".html?a=" + anchor_id + "&name=" + name; + var new_href = current_href.replace(/\.html.*/, rep_str); + if (new_href != current_href){ + window.location.href = new_href; + } +} + +//does as it says... +function hookUpActiveSearch(){ + + var s_field = $('searchForm').getInputs('text')[0]; + //var s_field = document.forms[0].searchText; + Event.observe(s_field, 'keydown', function(event) { + var el = Event.element(event); + var key = event.which || event.keyCode; + + switch (key) { + case Event.KEY_RETURN: + forceLoadOfContentAnchor(getCurrentAnchor()); + Event.stop(event); + break; + + case Event.KEY_UP: + scrollListToElementOffset(getCurrentAnchor(),-1); + break; + + case Event.KEY_DOWN: + scrollListToElementOffset(getCurrentAnchor(),1); + break; + + default: + break; + } + + }); + + Event.observe(s_field, 'keyup', function(event) { + var el = Event.element(event); + var key = event.which || event.keyCode; + switch (key) { + case Event.KEY_RETURN: + Event.stop(event); + break; + + case Event.KEY_UP: + break; + + case Event.KEY_DOWN: + break; + + default: + scrollToName(el.value); + setSavedSearch(getCurrentTab(), el.value); + break; + } + + }); + + Event.observe(s_field, 'keypress', function(event){ + var el = Event.element(event); + var key = event.which || event.keyCode; + switch (key) { + case Event.KEY_RETURN: + Event.stop(event); + break; + + default: + break; + } + + }); + + //Event.observe(document, 'keypress', function(event){ + // var key = event.which || event.keyCode; + // if (key == Event.KEY_TAB){ + // cycleNextTab(); + // Event.stop(event); + // } + //}); +} + +function hookUpTabs(){ + + var tabs = getTabs(); + for(x=0; x < tabs.length; x++) + { + Event.observe(tabs[x], 'click', function(event){ + var el = Event.element(event); + setActiveTabAndLoadContent(el); + }); + //tabs[x].onclick = function (){ return setActiveTabAndLoadContent(this);}; //the prototype guys say this is bad.. + } + +} + +function suppressPostbacks(){ + Event.observe('searchForm', 'submit', function(event){ + Event.stop(event); + }); +} + +function loadDefaults(){ + var t = getTabs(); + setActiveTabAndLoadContent(t[0]); //default loading of the first tab +} + +function resizeDivs(){ + var inner_height = 700; + if (window.innerHeight){ + inner_height = window.innerHeight; //all browsers except IE use this to determine the space available inside a window. Thank you Microsoft!! + }else{ + if(document.documentElement.clientHeight > 0){ //IE uses this in 'strict' mode + inner_height = document.documentElement.clientHeight; + }else{ + inner_height = document.body.clientHeight; //IE uses this in 'quirks' mode + } + } + $('rdocContent').style.height = (inner_height - 92) + "px";//Thankfully all browsers can agree on how to set the height of a div + $('listScroller').style.height = (inner_height - 88) + "px"; +} + +//The main function for handling clicks on the tabs +function setActiveTabAndLoadContent(current_tab){ + changeLoadingStatus("on"); + var tab_string = String(current_tab.innerHTML).strip(); //thank you ProtoType! + switch (tab_string){ + case "classes": + setCurrentTab("classes"); + loadScrollerContent('fr_class_index.html'); + setSearchFieldValue(getSavedSearch("classes")); + scrollToName(getSavedSearch("classes")); + setSearchFocus(); + break; + + case "files": + setCurrentTab("files"); + loadScrollerContent('fr_file_index.html'); + setSearchFieldValue(getSavedSearch("files")); + scrollToName(getSavedSearch("files")); + setSearchFocus(); + break; + + case "methods": + setCurrentTab("methods"); + loadScrollerContent('fr_method_index.html'); + setSearchFieldValue(getSavedSearch("methods")); + scrollToName(getSavedSearch("methods")); + setSearchFocus(); + break; + + default: + break; + } + changeLoadingStatus("off"); +} + +function cycleNextTab(){ + var currentT = getCurrentTab(); + var tabs = getTabs(); + if (currentT == "methods"){ + setActiveTabAndLoadContent(tabs[1]); + setSearchFocus(); + }else{ + if (currentT == "classes"){ + setActiveTabAndLoadContent(tabs[2]); + setSearchFocus(); + }else{ + if (currentT == "files"){ + setActiveTabAndLoadContent(tabs[0]); + setSearchFocus(); + } + } + } +} + +function getTabs(){ + return($('groupType').getElementsByTagName('li')); +} + +var Active_Tab = ""; +function getCurrentTab(){ + return Active_Tab; +} + +function setCurrentTab(tab_name){ + var tabs = getTabs(); + for(x=0; x < tabs.length; x++) + { + if(tabs[x].innerHTML.strip() == tab_name) //W00t!!! String.prototype.strip! + { + tabs[x].className = "activeLi"; + Active_Tab = tab_name; + } + else + { + tabs[x].className = ""; + } + } +} + +//These globals should not be used globally (hence the getters and setters) +var File_Search = ""; +var Method_Search = ""; +var Class_Search = ""; +function setSavedSearch(tab_name, s_val){ + switch(tab_name){ + case "methods": + Method_Search = s_val; + break; + case "files": + File_Search = s_val; + break; + case "classes": + Class_Search = s_val; + break; + } +} + +function getSavedSearch(tab_name){ + switch(tab_name){ + case "methods": + return (Method_Search); + break; + case "files": + return (File_Search); + break; + case "classes": + return (Class_Search); + break; + } +} + +//These globals handle the history stack + + +function setListScrollerContent(s){ + + $('listScroller').innerHTML = s; +} + +function setMainContent(s){ + + $('rdocContent').innerHTML = s; +} + +function setSearchFieldValue(s){ + + document.forms[0].searchText.value = s; +} + +function getSearchFieldValue(){ + + return Form.Element.getValue('searchText'); +} + +function setSearchFocus(){ + + document.forms[0].searchText.focus(); +} + +var Anchor_ID_Of_Current = null; // holds the last highlighted anchor tag in the scroll lsit +function getCurrentAnchor(){ + return(Anchor_ID_Of_Current); +} + +function setCurrentAnchor(a_id){ + Anchor_ID_Of_Current = a_id; +} + +//var Index_Of_Current = 0; //holds the last highlighted index +//function getCurrentIndex(){ +// return (Index_Of_Current); +//} + +//function setCurrentIndex(new_i){ +// Index_Of_Current = new_i; +//} + +function loadScrollerContent(url){ + + var scrollerHtml = new Ajax.Request(url, { + asynchronous: false, + method: 'get', + onComplete: function(method_data) { + setListScrollerContent(method_data.responseText); + } + }); + +} + +//called primarily from the links inside the scroller list +//loads the main page div then jumps to the anchor/element with id +function loadContent(url, anchor_id){ + + var mainHtml = new Ajax.Request(url, { + method: 'get', + onLoading: changeLoadingStatus("on"), + onSuccess: function(method_data) { + setMainContent(method_data.responseText);}, + onComplete: function(request) { + changeLoadingStatus("off"); + new jumpToAnchor(anchor_id); + } + }); +} + +//An alternative function that also will stuff the index history for methods, files, classes +function loadIndexContent(url, anchor_id, name, scope) +{ + if (From_URL_Param == true){ + var mainHtml = new Ajax.Request(url, { + method: 'get', + onLoading: changeLoadingStatus("on"), + onSuccess: function(method_data) { + setMainContent(method_data.responseText);}, + onComplete: function(request) { + changeLoadingStatus("off"); + updateBrowserBar(name, anchor_id, scope); + new jumpToAnchor(anchor_id);} + }); + From_URL_Param = false; + }else{ + updateUrlParams(anchor_id, name); + } + +} + +function updateBrowserBar(name, anchor_id, scope){ + if (getCurrentTab() == "methods"){ + $('browserBarInfo').update("class/module: " + scope + "  method: " + name + " "); + }else{ if(getCurrentTab() == "classes"){ + $('browserBarInfo').update("class/module: " + scope + "::" + name + " "); + }else{ + $('browserBarInfo').update("file: " + scope + "/" + name + " "); + } + } +} + + +// Force loads the contents of the index of the current scroller list. It does this by +// pulling the onclick method out and executing it manually. +function forceLoadOfContent(index_to_load){ + var scroller = $('listScroller'); + var a_array = scroller.getElementsByTagName('a'); + if ((index_to_load >= 0) && (index_to_load < a_array.length)){ + var load_element = a_array[index_to_load]; + var el_text = load_element.innerHTML.strip(); + setSearchFieldValue(el_text); + setSavedSearch(getCurrentTab(), el_text); + eval("new " + load_element.onclick); + } +} + +function forceLoadOfContentAnchor(anchor_id){ + + var load_element = $(anchor_id); + if (load_element != null){ + var el_text = load_element.innerHTML.strip(); + setSearchFieldValue(el_text); + scrollToAnchor(anchor_id); + setSavedSearch(getCurrentTab(), el_text); + eval("new " + load_element.onclick); + } +} + +var From_URL_Param = false; +function paramLoadOfContentAnchor(anchor_id){ + From_URL_Param = true; + forceLoadOfContentAnchor(anchor_id); +} + +//this handles the up/down keystrokes to move the selection of items in the list +function scrollListToElementOffset(anchor_id, offset){ + var scroller = $('listScroller'); + var a_array = scroller.getElementsByTagName('a'); + var current_index = findIndexOfAnchor(a_array, anchor_id); + if ((current_index >= 0) && (current_index < a_array.length)){ + scrollListToAnchor(a_array[current_index + offset].id); + setListActiveAnchor(a_array[current_index + offset].id); + } +} + +function findIndexOfAnchor(a_array, anchor_id){ + var found=false; + var counter = 0; + while(!found && counter < a_array.length){ + if (a_array[counter].id == anchor_id){ + found = true; + }else{ + counter +=1; + } + } + return(counter); +} + +function scrollToName(searcher_name){ + + var scroller = $('listScroller'); + var a_array = scroller.getElementsByTagName('a'); + + if (!searcher_name.match(new RegExp(/\s+/))){ //if searcher name is blank + + var searcher_pattern = new RegExp("^"+searcher_name, "i"); //the "i" is for case INsensitive + var found_index = -1; + + var found = false; + var x = 0; + while(!found && x < a_array.length){ + if(a_array[x].innerHTML.match(searcher_pattern)){ + found = true; + found_index = x; + } + else{ + x++; + } + } + + // // an attempt at binary searching... have not given up on this yet... + //found_index = binSearcher(searcher_pattern, a_array, 0, a_array.length); + + if ((found_index >= 0) && (found_index < a_array.length)){ + + scrollListToAnchor(a_array[found_index].id);//scroll to the item + setListActiveAnchor(a_array[found_index].id);//highlight the item + } + }else{ //since searcher name is blank + //scrollListToIndex(a_array, 0);//scroll to the item + //setListActiveItem(a_array, 0);//highlight the item + } +} + +function scrollToAnchor(anchor_id){ + var scroller = $('listScroller'); + if ($(anchor_id) != null){ + scrollListToAnchor(anchor_id); + setListActiveAnchor(anchor_id); + } +} + +function getY(element){ + + var y = 0; + for( var e = element; e; e = e.offsetParent)//iterate the offset Parents + { + y += e.offsetTop; //add up the offsetTop values + } + //for( e = element.parentNode; e && e != document.body; e = e.parentNode) + // if (e.scrollTop) y -= e.scrollTop; //subtract scrollbar values + return y; +} + +//function setListActiveItem(item_array, active_index){ +// +// item_array[getCurrentIndex()].className = ""; +// setCurrentIndex(active_index); +// item_array[getCurrentIndex()].className = "activeA"; //setting the active class name +//} + +function setListActiveAnchor(active_anchor){ + if ((getCurrentAnchor() != null) && ($(getCurrentAnchor()) != null)){ + $(getCurrentAnchor()).className = ""; + } + setCurrentAnchor(active_anchor); + $(getCurrentAnchor()).className = "activeA"; + +} + +//handles the scrolling of the list and setting of the current index +//function scrollListToIndex(a_array, scroll_index){ +// if (scroll_index > 0){ +// var scroller = $('listScroller'); +// scroller.scrollTop = getY(a_array[scroll_index]) - 120; //the -120 is what keeps it from going to the top... +// } +//} + +function scrollListToAnchor(scroll2_anchor){ + var scroller = $('listScroller'); + scroller.scrollTop = getY($(scroll2_anchor)) - 120; +} + +function jumpToAnchor(anchor_id){ + + var contentScroller = $('rdocContent'); + var a_div = $(anchor_id); + contentScroller.scrollTop = getY(a_div) - 80; //80 is the offset to adjust scroll point + var a_title = $(anchor_id + "_title"); + a_title.style.backgroundColor = "#222"; + a_title.style.color = "#FFF"; + a_title.style.padding = "3px"; + // a_title.style.borderBottom = "2px solid #ccc"; + + //other attempts + //a_div.className = "activeMethod"; //setting the active class name + //a_div.style.backgroundColor = "#ffc"; + //var titles = a_div.getElementsByClassName("title"); + //titles[0].className = "activeTitle"; + +} + +function jumpToTop(){ + $('rdocContent').scrollTop = 0; +} + +function changeLoadingStatus(status){ + if (status == "on"){ + $('loadingStatus').show(); + } + else{ + $('loadingStatus').hide(); + } +} + +//************* Misc functions (mostly from the old rdocs) *********************** +//snagged code from the old templating system +function toggleSource( id ){ + + var elem + var link + + if( document.getElementById ) + { + elem = document.getElementById( id ) + link = document.getElementById( "l_" + id ) + } + else if ( document.all ) + { + elem = eval( "document.all." + id ) + link = eval( "document.all.l_" + id ) + } + else + return false; + + if( elem.style.display == "block" ) + { + elem.style.display = "none" + link.innerHTML = "show source" + } + else + { + elem.style.display = "block" + link.innerHTML = "hide source" + } +} + +function openCode( url ){ + window.open( url, "SOURCE_CODE", "width=400,height=400,scrollbars=yes" ) +} + +//this function handles the ajax calling and afterits loaded the jumping to the anchor... +function jsHref(url){ + //alert(url); + var mainHtml = new Ajax.Request(url, { + method: 'get', + onSuccess: function(method_data) { + setMainContent(method_data.responseText);} + }); +} + +//function comparePatterns(string, regexp){ +// var direction = 0; +// +// +// return (direction) +//} + +////returns the index of the element +//function binSearcher(regexp_pattern, list, start_index, stop_index){ +// //divide the list in half +// var split_point = 0; +// split_point = parseInt((stop_index - start_index)/2); +// direction = comparePatterns(list[split_point].innerHTML, regexp_pattern); +// if(direction < 0) +// return (binSearcher(regexp_pattern, list, start_index, split_point)); +// else +// if(direction > 0) +// return (binSearcher(regexp_pattern, list, split_point, stop_index)); +// else +// return(split_point); +// +//} + + + diff --git a/doc/rdoc/generators/template/merb/index.html.erb b/doc/rdoc/generators/template/merb/index.html.erb new file mode 100644 index 00000000..5c8e7986 --- /dev/null +++ b/doc/rdoc/generators/template/merb/index.html.erb @@ -0,0 +1,37 @@ + + + + Documentation + + + + + + + + +

+ + +
+ + diff --git a/doc/rdoc/generators/template/merb/merb.css b/doc/rdoc/generators/template/merb/merb.css new file mode 100644 index 00000000..e4f5531b --- /dev/null +++ b/doc/rdoc/generators/template/merb/merb.css @@ -0,0 +1,252 @@ +html, body, div, span, applet, object, iframe,h1, h2, h3, h4, h5, h6, p, blockquote, pre,a, abbr, acronym, address, big, cite, code,del, dfn, em, font, img, ins, kbd, q, s, samp,small, strike, strong, sub, sup, tt, var,dl, dt, dd, ol, ul, li,fieldset, form, label, legend,table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; } + +/* GENERAL RULES */ + +body { + background: #000 url(../img/body.gif) repeat-x bottom center; + color: #000; + font: normal 12px "Lucida Grande", "Arial", sans-serif; + line-height: 1; +} +ul {list-style-type: none;} +#content_full ul.revisions{list-style-type: disc;} +#content_full ul.revisions li{margin-left: 15px;padding: 3px 0;} +li a {display: block;} +#content_full ul.revisions li a{display: inline;} +strong {font-weight: bold;} +table {border-collapse: separate;border-spacing: 0; } +caption, th, td {text-align: left;font-weight: normal; } +.invisible {display: none;} +.full_width {width:100%;} + +/* LAYOUT */ + +.wrap_to_center, #foot { + margin: 0 auto; + display: block; + width: 800px; +} + +#content {width: 100%;} + +#content_top { + background: #fff url(../img/content_top.gif) no-repeat top center; + float:left; + margin:25px 0px; + width:100%; +} +#content_bottom { + background: url(../img/content_bottom.gif) no-repeat bottom center; + width:100%; + float:left; +} +#content_main { + float:left; + margin: 10px 20px 20px 20px; + width:506px; +} +#content p { + line-height:17px; +} +#content_full {margin: 10px 20px 20px 20px;} + +/* HEADER & NAVIGATION */ + +#header { + background: #1db900 url(../img/header_waves.gif) repeat-x top center; + height:74px; + width: 100%; +} +#waves { + background: url(../img/header_waves.gif)no-repeat top left; + height:74px; + width:980px; +} +#header img {margin-top:8px; float:left;} +#header a {color:#fff; text-decoration:none;} +#header a:hover {color:#000;} +ul#nav {float:right;display:block;width:43.3em;margin-top:25px;} +ul#nav li {display:block;float:left;} +ul#nav li a {display:block;float:left;margin:0px 5px;padding:6px 9px 31px 9px;} +ul#nav li a:hover {background:url(../img/header_hover.gif) repeat-x bottom center;} +ul#nav li a#active {background:url(../img/header_arrow.gif)no-repeat bottom center;} +ul#nav li.last a {margin-right:0;} + +/* TEXT FORMATTING */ + +h1 { + border-bottom:2px solid #ccc; + color:#000; + font:bold 28px "Arial" sans-serif; + letter-spacing:1px; + margin:20px 0px; + text-align:left; + width:100%; +} +h1.home { + border:0; + color:#fff; + font-size:36px; + margin:20px 0px; + text-align:center; +} +h2 { + color:#7aad00; + font:bold 22px "Lucida Grande" sans-serif; + margin:10px 0px; +} +h3 { + font:bold 16px "Lucida Grande"; + margin:10px 0px; +} +#content a {color:#d7ff00;} +#content a:hover {background:#d7ff00;color:#000;} +#content_main ul {margin:10px 0px;} +#content_main ul li { + background: url(../img/li.gif) no-repeat left center; + padding: 4px 4px 4px 16px; + font-weight:bold; +} +p {margin-bottom:12px;} +#content_main a,#content_full a {color:#11b716;font-weight:bold;} +#content_main a:hover,#content_full a:hover {background:#22d716;} +pre { + background:#222; + color:#fff; + font:12px "Courier" serif; + line-height:18px; + padding: 12px; + margin-bottom: 10px; +} +code { + font:bold 12px "Courier" serif; +} +pre code {font-weight:normal;} + +/* SIDEBAR FOR CONTENT */ + +#content_sidebar { + float: left; + margin: 20px 20px 15px 10px; + width: 224px; +} +.sidebar_top { + background:#868686 url(../img/sidebar_top.gif) no-repeat top center; + margin-bottom:12px; + width:224px; +} +dl.sidebar_bottom { + background: url(../img/sidebar_bottom.gif) no-repeat bottom center; + padding:12px; +} +dl.sidebar_bottom dt { + color:#fff; + font:bold 14px "Lucida Grande" sans-serif; + margin-bottom:6px; +} +dl.sidebar_bottom dd {padding:3px 0px;} +#content_sidebar p {padding:10px 0px;} +p#rss a { + background: url(../img/rss.gif) no-repeat left center; + color:#000; + font:bold 14px "Lucida Grande"; + padding: 8px 6px 8px 34px; + text-decoration:none; +} +p#rss a:hover { + background: url(../img/rss.gif) no-repeat left center; + background-color:#fff; + text-decoration:underline; +} + +/* FOOTER */ + +#footer {background:#444; clear:both;} +#footer p {padding:12px; color:#999; margin:0; text-align:center;} + +/* FEATURES PAGE */ +.feature { + background-repeat:no-repeat; + background-position:top left; + border-bottom:2px solid #ccc; + padding-left:150px; +} +div#speed {background-image: url(../img/feature_speed.gif);} +div#light {background-image: url(../img/feature_light.gif);} +div#power {background-image: url(../img/feature_power.gif);} + +.quicklinks_top { + background:#868686 url(../img/quicklinks_top.gif) no-repeat top center; + float:right; + margin-bottom:12px; + width:169px; +} +ul.quicklinks_bottom { + background: url(../img/quicklinks_bottom.gif) no-repeat bottom center; + padding:12px; +} +ul.quicklinks_bottom li { + display:block; + padding:3px 0px; +} +#content_full ul.quicklinks_bottom li a{ + color:#d7ff00; + display:inline; +} +#content_full ul.quicklinks_bottom li a:hover { + background:#d7ff00; + color:#000; +} + +/* DOCUMENTATION PAGE */ +.sub-framework { + border-bottom:2px solid #ccc; + margin-bottom: 20px; + padding-bottom: 10px; +} + + +/* ICONS FOR HOMEPAGE */ + +#icons_top { + background: url(../img/icons_top.gif) no-repeat top center; + float:left; + width:800px; +} +#icons_bottom { + background: url(../img/icons_bottom.gif) no-repeat bottom center; + float:left; + width:800px; +} +#icons_top dl { + color:#fff; + float:left; + width: 224px; + padding: 15px 20px; +} +#icons_top dt { + background-repeat:no-repeat; + background-position:center 2.5em; + color:#35d726; + font:bold 18px 'Lucida Grande' sans-serif; + padding: 6px 6px 150px 6px; + text-align:center; +} +#icons_top dd { + font: 11px "Lucida Grande"; + line-height:18px; + text-align:center; +} +dl#speed, dl#light {border-right:1px solid #444;} +dl#light, dl#power {border-left:1px solid #000;} +dl#speed dt {background-image: url(../img/icon_speed.gif);} +dl#light dt {background-image: url(../img/icon_light.gif);} +dl#power dt {background-image: url(../img/icon_power.gif);} \ No newline at end of file diff --git a/doc/rdoc/generators/template/merb/merb.rb b/doc/rdoc/generators/template/merb/merb.rb new file mode 100644 index 00000000..36a77ed3 --- /dev/null +++ b/doc/rdoc/generators/template/merb/merb.rb @@ -0,0 +1,351 @@ +module RDoc +module Page + +STYLE = File.read(File.join(File.dirname(__FILE__), 'merb_doc_styles.css')) +FONTS = "" + +################################################################### + +CLASS_PAGE = < + +HTML + +################################################################### + +METHOD_LIST = < +IF:diagram +
+ %diagram% +
+ENDIF:diagram + +IF:description +
%description%
+ENDIF:description + +IF:requires +
Required Files
+
    +START:requires +
  • %name%
  • +END:requires +
+ENDIF:requires + +IF:toc +
Contents
+ +ENDIF:toc + +IF:methods +
Methods
+
    +START:methods +
  • %name%
  • +END:methods +
+ENDIF:methods + +IF:includes +
Included Modules
+
    +START:includes +
  • %name%
  • +END:includes +
+ENDIF:includes + +START:sections +IF:sectitle + +IF:seccomment +
+%seccomment% +
+ENDIF:seccomment +ENDIF:sectitle + +IF:classlist +
Classes and Modules
+ %classlist% +ENDIF:classlist + +IF:constants +
Constants
+ +START:constants + + + + + +IF:desc + + + + +ENDIF:desc +END:constants +
%name%=%value%
 %desc%
+ENDIF:constants + +IF:attributes +
Attributes
+ +START:attributes + + + + + +END:attributes +
+IF:rw +[%rw%] +ENDIF:rw + %name%%a_desc%
+ENDIF:attributes + +IF:method_list +START:method_list +IF:methods +
%type% %category% methods
+START:methods +
+
+IF:callseq + %callseq% +ENDIF:callseq +IFNOT:callseq + %name%%params% +ENDIF:callseq +IF:codeurl +[ source ] +ENDIF:codeurl +
+IF:m_desc +
+ %m_desc% +
+ENDIF:m_desc +IF:aka +
+ This method is also aliased as +START:aka + %name% +END:aka +
+ENDIF:aka +IF:sourcecode +
+ +
+
+%sourcecode%
+
+
+
+ENDIF:sourcecode +
+END:methods +ENDIF:methods +END:method_list +ENDIF:method_list +END:sections + +HTML + + + + +BODY = < + +
+ #{METHOD_LIST} +
+ +ENDBODY + + + +SRC_BODY = < + +
+

Source Code

+
%file_source_code%
+
+ENDSRCBODY + + +###################### File Page ########################## +FILE_PAGE = < +

%short_name%

+ + + + + + + + + +
Path:%full_path% +IF:cvsurl +  (CVS) +ENDIF:cvsurl +
Last Update:%dtm_modified%
+ +HTML + + +#### This is not used but kept for historical purposes +########################## Source code ########################## +# Separate page onlye + +SRC_PAGE = < +%title% + + + + +
%code%
+ + +HTML + +########################### source page body ################### + +SCR_CODE_BODY = < + %source_code% + + +HTML + +########################## Index ################################ + +FR_INDEX_BODY = < +START:entries +
  • %name%%scope%
  • +END:entries + +HTML + +CLASS_INDEX = FILE_INDEX +METHOD_INDEX = FILE_INDEX + +INDEX = < + + + + + + + + Merb | %title% API Documentation + + + + + +
      +
    • methods
    • +
    • classes
    • +
    • files
    • + +
    +
    +
    +
    + +
    +
    +
    + Loading via ajax... this could take a sec. +
    +
    +
    +    %title% README +
    +
    + %content% +
    +
    +Documentation for %title% usage tips + + + + + + +HTML + +API_GREASE_JS = File.read(File.join(File.dirname(__FILE__), 'api_grease.js')) + +PROTOTYPE_JS = File.read(File.join(File.dirname(__FILE__), 'prototype.js')) +end +end + diff --git a/doc/rdoc/generators/template/merb/merb_doc_styles.css b/doc/rdoc/generators/template/merb/merb_doc_styles.css new file mode 100644 index 00000000..cb01bb73 --- /dev/null +++ b/doc/rdoc/generators/template/merb/merb_doc_styles.css @@ -0,0 +1,492 @@ +html, body { + padding:10px 0px 0px 5px; + margin: 0px; + font-family: "Lucida Grande", "Lucida Sans Unicode", sans-serif; + font-size: 14px; +} + +body { + background: #000000 url(http://merbivore.com/img/header_waves.gif) repeat-x scroll center top; +} + +td, p { + background: #FFF; + color: #000; + margin: 0px; + font-size: small; + line-height: 17px; + margin-bottom 12px; +} + +#floater { + position: absolute; + top: 5px; + right: 5px; +} + +#floater strong { + color: white; +} + +#floater a { + color: black; +} + +#floater a:hover { + background-color: transparent; +} + + +/*holds the whole searching/drill down stuff */ +#listFrame{ + float:left; + padding: 2px; + width: 350px; + background-color: #868686; + border: 1px solid #999; + border-right: none; +} + +#browserBar{ + height: 25px; + padding:11px 0px 0px 0px; + margin:0px; + background-color: #868686; + border-top: 1px solid #999; + color: white; +} + +#browserBar a{ + text-decoration: none; +} + +.button{ + text-decoration: none; + padding:3px 8px 3px 8px; + border: 1px solid #66a; + background-color: #ccf; + color: #66a; +} + +.buttonInactive{ + text-decoration: none; + padding:3px 8px 3px 8px; + border: 1px solid #999; + background-color: #ccc; + color: #999; +} + +.miniButton{ + text-decoration: none; + padding:3px 2px 3px 2px; + border: 1px solid #66a; + background-color: #ccf; + color: #66a; +} + +.miniButtonInactive{ + text-decoration: none; + padding:3px 2px 3px 2px; + border: 1px solid #999; + background-color: #ccc; + color: #999; +} + +#blowOutListBox{ + position: absolute; + top: 63px; + left: 399px; + border: 1px solid #999; + padding: 0px; + margin: 0px; + z-index: 1000; + background-color: #ccf; + color: #66a; +} + +#blowOutListBox ul{ + list-style-type:none; + padding: 0px; + margin: 0px; +} + +#blowOutListBox ul li{ + padding: 3px; + margin: 0px; + line-height: 1.1em; + +} + +#blowOutListBox ul li a{ + text-decoration: none; + padding: 3px; +} +#blowOutListBox ul li a:hover{ + background-color: #ddf; +} + +/*holds the content for browsing etc... also is the target of method/class/file name clicks */ +#rdocContent{ + height: 600px; + background-color: #fff; + border: 1px solid #999; + border-left: 0px; + padding:5px; + overflow: auto; +} + +/*the grouping for methods,files,class,all... i.e. the tabs */ +ul#groupType{ + list-style-type: none; + padding: 0px; + padding-left: 5px; + margin: 0px; +} + +ul#groupType li{ + color: white; + display:inline; + padding: 5px 5px 0px 5px; + cursor: pointer; +} + +ul#groupType li#loadingStatus{ + margin: 3px; + border: 0px; + padding: 3px 3px 6px 3px; + color: #666; +} + +ul#groupType li.activeLi{ + border: 1px solid #999; + border-bottom: 0px; + background-color: #868686; + font-weight: bold; + padding-bottom: 1px; +} + +#listSearch{ + height: 25px; + padding: 3px; +} + +#listSearch input[type=text]{ + width: 340px; + font-size: 1.2em; +} + +#listScroller{ + width: 342px; + height: 700px; + margin: 3px; + background-color: #fcfcfc; + border: 1px solid #999; + overflow: auto; +} + +#listScroller ul{ + width: 500px; + padding:0px; + margin:0px; + list-style: none; +} + +#listScroller li{ + padding: 0px; + margin: 0px; + display: block; + line-height: 1.1em; +} + +a, h1 a, h2 a, .sectiontitle a, #listScroller a{ + color: #11B716; + font-weight: bold; + text-decoration: none; + padding: 0px 1px 1px 1px; + margin: 3px; + font-weight: bold; +} + +a:hover, h1 a:hover, h2 a:hover, .sectiontitle a:hover, #listScroller a:hover{ + background-color: #22D716; + color: black; +} + +#browserBar a, .banner a { + color: #D7FF00; +} + +#browserBar a:hover, .banner a:hover { + background-color: #D7FF00; + color: #000; +} + +#listScroller a.activeA { + background-color: #22D716; + color: black ; + border: 1px solid #ccc; + padding: 0px 1px 1px 1px; +} + +#listScroller small{ + color: #999; +} + +.activeTitle{ + font-family: monospace; + font-size: large; + border-bottom: 1px dashed black; + margin-bottom: 0.3em; + padding-bottom: 0.1em; + background-color: #ffc; +} + +.activeMethod{ + margin-left: 1em; + margin-right: 1em; + margin-bottom: 1em; +} + + +.activeMethod .title { + font-family: monospace; + font-size: large; + border-bottom: 1px dashed black; + margin-bottom: 0.3em; + padding-bottom: 0.1em; + background-color: #ffa; +} + +.activeMethod .description, .activeMethod .sourcecode { + margin-left: 1em; +} + +.activeMethod .sourcecode p.source-link { + text-indent: 0em; + margin-top: 0.5em; +} + +.activeMethod .aka { + margin-top: 0.3em; + margin-left: 1em; + font-style: italic; + text-indent: 2em; +} + +#content { + margin: 0.5em; +} + +#description p { + margin-bottom: 0.5em; +} + +.sectiontitle { + color: black; + font-size: 28px; + margin: 20px 0px; + border-bottom: 2px solid #CCCCCC; + +/* margin-top: 1em; + margin-bottom: 1em; + padding: 0.5em; + padding-left: 2em; + background: #005; + color: #FFF; + font-weight: bold; + border: 1px dotted black;*/ +} + +.attr-rw { + padding-left: 1em; + padding-right: 1em; + text-align: center; + color: #7AAD00; +} + +.attr-name { + font-weight: bold; +} + +.attr-desc { +} + +.attr-value { + font-family: monospace; +} + +.file-title-prefix { + font-size: large; +} + +.file-title { + font-size: large; + font-weight: bold; + background: #005; + color: #FFF; +} + +.banner { + background: #888; + color: #FFF; +/* border: 1px solid black;*/ + padding: 1em; +} + +.banner td { + background: transparent; + color: #FFF; +} + +.dyn-source { + display: none; + color: #000; + border: 0px; + border-left: 1px dotted #CCC; + border-top: 1px dotted #CCC; + margin: 0em; + padding: 0em; +} + +.dyn-source .cmt { + color: #7AAD00; + font-style: italic; +} + +.dyn-source .kw { + color: #11B716; + font-weight: bold; +} + +.method { + margin-left: 1em; + margin-right: 1em; + margin-bottom: 2em; +} + +.description pre, .description td { + font-family:"Courier",serif; +} +pre, .description pre { + font-size: 12px; + line-height: 18px; + color: white; + padding: 12px; + background: #222; + overflow: auto; +} + +h2.title, .method .title { + color: #7AAD00; + font-size: 22px; + margin: 10px 0px; + +/* font-size: large; + border-bottom: 1px dashed black; + margin: 0.3em; + padding: 0.2em; +*/} + +.method .description, .method .sourcecode { + margin-left: 1em; +} + +.description p, .sourcecode p { + margin-bottom: 0.5em; +} + +.method .sourcecode p.source-link { + text-indent: 0em; + margin-top: 0.5em; +} + +.method .aka { + margin-top: 0.3em; + margin-left: 1em; + font-style: italic; + text-indent: 2em; +} + +h1 { + padding: 1em; + font-size: x-large; +} + +h2 { + padding: 0.5em 1em 0.5em 1em; + font-size: large; +} + + +h1, h2, h3, h4, h5, h6 { + color: white; + background-color: #868686; +} + +h3, h4, h5, h6 { + padding: 0.2em 1em 0.2em 1em; + font-weight: bold; +} + +h4 { + margin-bottom: 2px; +} + +.sourcecode > pre { + padding: 0px; + margin: 0px; + border: 1px dotted black; + background: #FFE; +} + +/* ============= */ +/* = home page = */ +/* ============= */ + +body#home { + margin: 0; + padding: 0; +} + +#content { + margin: 0 auto; + width: 800px; +} + +#content h1.home { + background-color: transparent; + border:0pt none; + color:#FFFFFF; + font-size:36px; + margin:20px 0px; + padding: 0; + text-align:center; +} + +#content #documentation-links h2.title { + background-color: transparent; +} + +#documentation-links { + background-color : white; + margin-bottom: 20px; + padding-bottom: 10px; +} + +#documentation-links p { + margin: 22px; +} + +body#home #footer { + background:#444444 none repeat scroll 0%; + clear:both; +} + +#footer p { + background-color: transparent; + color:#999999; + margin:0 auto; + padding:12px; + text-align:center; + width: 800px; +} \ No newline at end of file diff --git a/doc/rdoc/generators/template/merb/prototype.js b/doc/rdoc/generators/template/merb/prototype.js new file mode 100644 index 00000000..50582217 --- /dev/null +++ b/doc/rdoc/generators/template/merb/prototype.js @@ -0,0 +1,2515 @@ +/* Prototype JavaScript framework, version 1.5.0 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0', + BrowserFeatures: { + XPath: !!document.evaluate + }, + + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + emptyFunction: function() {}, + K: function(x) { return x } +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +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 === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + 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); + } +}); + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +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; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + 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); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(this); + } finally { + this.currentlyExecuting = false; + } + } + } +} +String.interpret = function(value){ + return value == null ? '' : String(value); +} + +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 = count === undefined ? 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 this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : 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 div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('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 name = decodeURIComponent(pair[0]); + var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + + if (hash[name] !== undefined) { + if (hash[name].constructor != Array) + hash[name] = [hash[name]]; + if (value) hash[name].push(value); + } + else hash[name] = 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); + }, + + 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.replace(/\\/g, '\\\\'); + if (useDoubleQuotes) + return '"' + escapedString.replace(/"/g, '\\"') + '"'; + else + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + String.interpret(object[match[3]]); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = false; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push((iterator || Prototype.K)(value, index)); + }); + return results; + }, + + detect: function(iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(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) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.map(function(value, index) { + return {value: value, criteria: iterator(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 (typeof args.last() == 'function') + 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, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0, length = iterable.length; i < length; i++) + results.push(iterable[i]); + return results; + } +} + +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(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0, length = this.length; i < length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function() { + return this.inject([], function(array, value) { + return array.include(value) ? array : array.concat([value]); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string){ + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if(window.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(arguments[i].constructor == Array) { + for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(obj) { + Object.extend(this, obj || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + + if (pair.value && pair.value.constructor == Array) { + var values = pair.value.compact(); + if (values.length < 2) pair.value = values.reduce(); + else { + key = encodeURIComponent(pair.key); + values.each(function(value) { + value = value != undefined ? encodeURIComponent(value) : ''; + parts.push(key + '=' + encodeURIComponent(value)); + }); + return; + } + } + if (pair.value == undefined) pair[1] = ''; + parts.push(pair.map(encodeURIComponent).join('=')); + }); + + return parts.join('&'); + } +}); + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (value && value == Hash.prototype[key]) continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject(this, function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + + toQueryString: function() { + return Hash.toQueryString(this); + }, + + inspect: function() { + return '#'; + } +}); + +function $H(object) { + if (object && object.constructor == Hash) return object; + return new Hash(object); +}; +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + 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 (typeof responder[callback] == 'function') { + 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 = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '' + } + Object.extend(this.options, options || {}); + + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = this.options.parameters; + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + params = Hash.toQueryString(params); + if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + + // when GET, append parameters to URL + if (this.method == 'get' && params) + this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; + + try { + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) + setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + var body = this.method == 'post' ? (this.options.postBody || params) : null; + + this.transport.send(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 (typeof extras.push == 'function') + 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() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + this.transport.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.getHeader('Content-type') || 'text/javascript').strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalJSON: function() { + try { + var json = this.getHeader('X-JSON'); + return json ? eval('(' + json + ')') : null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, param) { + this.updateContent(); + onComplete(transport, param); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.container[this.success() ? 'success' : 'failure']; + var response = this.transport.responseText; + + if (!this.options.evalScripts) response = response.stripScripts(); + + if (receiver = $(receiver)) { + if (this.options.insertion) + new this.options.insertion(receiver, response); + else + receiver.update(response); + } + + if (this.success()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(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(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + 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 (typeof element == 'string') + 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(query.snapshotItem(i)); + return results; + }; +} + +document.getElementsByClassName = function(className, parentElement) { + if (Prototype.BrowserFeatures.XPath) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } else { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child; + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + if (Element.hasClassName(child, className)) + elements.push(Element.extend(child)); + } + return elements; + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element || _nativeExtensions || element.nodeType == 3) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Object.clone(Element.Methods), cache = Element.extend.cache; + + if (element.tagName == 'FORM') + Object.extend(methods, Form.Methods); + if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) + Object.extend(methods, Form.Element.Methods); + + Object.extend(methods, Element.Methods.Simulated); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +}; + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +}; + +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).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + 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 $A($(element).getElementsByTagName('*')); + }, + + 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 (typeof selector == 'string') + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + return Selector.findElement($(element).ancestors(), expression, index); + }, + + down: function(element, expression, index) { + return Selector.findElement($(element).descendants(), expression, index); + }, + + previous: function(element, expression, index) { + return Selector.findElement($(element).previousSiblings(), expression, index); + }, + + next: function(element, expression, index) { + return Selector.findElement($(element).nextSiblings(), expression, index); + }, + + getElementsBySelector: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + getElementsByClassName: function(element, className) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (document.all && !window.opera) { + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + if(attribute) return attribute.nodeValue; + } + return element.getAttribute(name); + }, + + 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; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).add(className); + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); + return element; + }, + + observe: function() { + Event.observe.apply(Event, arguments); + return $A(arguments).first(); + }, + + stopObserving: function() { + Event.stopObserving.apply(Event, arguments); + return $A(arguments).first(); + }, + + // 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.match(/^\s*$/); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + if (['float','cssFloat'].include(style)) + style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); + style = style.camelize(); + var value = element.style[style]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } else if (element.currentStyle) { + value = element.currentStyle[style]; + } + } + + if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) + value = element['offset'+style.capitalize()] + 'px'; + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + if(style == 'opacity') { + if(value) return parseFloat(value); + if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) { + var value = style[name]; + if(name == 'opacity') { + if (value == 1) { + value = (/Gecko/.test(navigator.userAgent) && + !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else if(value == '') { + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')'; + } + } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; + element.style[name.camelize()] = 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 (window.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.style.overflow || 'auto'; + if ((Element.getStyle(element, 'overflow') || 'visible') != '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; + } +}; + +Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); + +Element._attributeTranslations = {}; + +Element._attributeTranslations.names = { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" +}; + +Element._attributeTranslations.values = { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; + } +}; + +Object.extend(Element._attributeTranslations.values, { + href: Element._attributeTranslations.values._getAttr, + src: Element._attributeTranslations.values._getAttr, + disabled: Element._attributeTranslations.values._flag, + checked: Element._attributeTranslations.values._flag, + readonly: Element._attributeTranslations.values._flag, + multiple: Element._attributeTranslations.values._flag +}); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations; + attribute = t.names[attribute] || attribute; + return $(element).getAttributeNode(attribute).specified; + } +}; + +// IE is missing .innerHTML support for TABLE-related elements +if (document.all && !window.opera){ + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + var div = document.createElement('div'); + switch (tagName) { + case 'THEAD': + case 'TBODY': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 2; + break; + case 'TR': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 3; + break; + case 'TD': + div.innerHTML = '
    ' + html.stripScripts() + '
    '; + depth = 4; + } + $A(element.childNodes).each(function(node){ + element.removeChild(node) + }); + depth.times(function(){ div = div.firstChild }); + + $A(div.childNodes).each( + function(node){ element.appendChild(node) }); + } else { + element.innerHTML = html.stripScripts(); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + } +}; + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { + var className = 'HTML' + tag + 'Element'; + if(window[className]) return; + var klass = window[className] = {}; + klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; + }); + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + if (typeof HTMLElement != 'undefined') { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + copy(Form.Methods, HTMLFormElement.prototype); + [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { + copy(Form.Element.Methods, klass.prototype); + }); + _nativeExtensions = true; + } +} + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +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); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.readAttribute("id") == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0, length = clause.length; i < length; i++) + conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + element = $(element); \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0, length = scope.length; i < length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +Object.extend(Selector, { + matchElements: function(elements, expression) { + var selector = new Selector(expression); + return elements.select(selector.match.bind(selector)).map(Element.extend); + }, + + findElement: function(elements, expression, index) { + if (typeof expression == 'number') index = expression, expression = false; + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + return expressions.map(function(expression) { + return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.inject([], function(elements, result) { + return elements.concat(selector.findElements(result || element)); + }); + }); + }).flatten(); + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != undefined) { + if (result[key]) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return getHash ? data : Hash.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); + }, + + 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().each(function(element) { + element.blur(); + element.disabled = 'true'; + }); + return form; + }, + + enable: function(form) { + form = $(form); + form.getElements().each(function(element) { + element.disabled = ''; + }); + return form; + }, + + findFirstElement: function(form) { + return $(form).getElements().find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + } +} + +Object.extend(Form, Form.Methods); + +/*--------------------------------------------------------------------------*/ + +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 Hash.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select && ( element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type) ) ) + element.select(); + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.blur(); + element.disabled = false; + return element; + } +} + +Object.extend(Form.Element, Form.Element.Methods); +var Field = Form.Element; +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + default: + return Form.Element.Serializers.textarea(element); + } + }, + + inputSelector: function(element) { + return element.checked ? element.value : null; + }, + + textarea: function(element) { + return element.value; + }, + + select: function(element) { + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + 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 = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + 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.bind(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(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +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, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0, length = Event.observers.length; i < length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + Event._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + try { + element.detachEvent('on' + name, observer); + } catch (e) {} + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [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=='BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: 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; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.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 = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.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); + }, + + // within must be called directly before + 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; + }, + + page: 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) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + 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 = Position.page(source); + + // find coordinate system to use + target = $(target); + 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(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // 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) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.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'; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + 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; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.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 [valueL, valueT]; + } +} + +Element.addMethods(); \ No newline at end of file diff --git a/merb/merb-auth/setup.rb b/merb/merb-auth/setup.rb new file mode 100644 index 00000000..612f01d5 --- /dev/null +++ b/merb/merb-auth/setup.rb @@ -0,0 +1,44 @@ +# This file is specifically setup for use with the merb-auth plugin. +# This file should be used to setup and configure your authentication stack. +# It is not required and may safely be deleted. +# +# To change the parameter names for the password or login field you may set either of these two options +# +# Merb::Plugins.config[:"merb-auth"][:login_param] = :email +# Merb::Plugins.config[:"merb-auth"][:password_param] = :my_password_field_name + +begin + # Sets the default class ofr authentication. This is primarily used for + # Plugins and the default strategies + Merb::Authentication.user_class = User + + + # Mixin the salted user mixin + require 'merb-auth-more/mixins/salted_user' + Merb::Authentication.user_class.class_eval{ include Merb::Authentication::Mixins::SaltedUser } + + # Setup the session serialization + class Merb::Authentication + + def fetch_user(session_user_id) + Merb::Authentication.user_class.get(session_user_id) + end + + def store_user(user) + user.nil? ? user : user.id + end + end + +rescue + Merb.logger.error <<-TEXT + + You need to setup some kind of user class with merb-auth. + Merb::Authentication.user_class = User + + If you want to fully customize your authentication you should use merb-core directly. + + See merb/merb-auth/setup.rb and strategies.rb to customize your setup + + TEXT +end + diff --git a/merb/merb-auth/strategies.rb b/merb/merb-auth/strategies.rb new file mode 100644 index 00000000..fd6f20a0 --- /dev/null +++ b/merb/merb-auth/strategies.rb @@ -0,0 +1,11 @@ +# This file is specifically for you to define your strategies +# +# You should declare you strategies directly and/or use +# Merb::Authentication.activate!(:label_of_strategy) +# +# To load and set the order of strategy processing + +Merb::Slices::config[:"merb-auth-slice-password"][:no_default_strategies] = true + +Merb::Authentication.activate!(:default_password_form) +Merb::Authentication.activate!(:default_basic_auth) \ No newline at end of file diff --git a/merb/session/session.rb b/merb/session/session.rb new file mode 100644 index 00000000..5d053163 --- /dev/null +++ b/merb/session/session.rb @@ -0,0 +1,9 @@ +module Merb + module Session + + # The Merb::Session module gets mixed into Merb::SessionContainer to allow + # app-level functionality; it will be included and methods will be available + # through request.session as instance methods. + + end +end \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 00000000..455e706f --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,17 @@ +# Sets the default handler for FastCGI scripts +AddHandler fastcgi-script .fcgi + +# If Apache2 is used together with mod_fcgid, +# uncomment the line below and comment in the line +# above to set the correct script handler +#AddHandler fcgid-script .fcgi + +RewriteEngine On + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ merb.fcgi [QSA,L] + + +ErrorDocument 500 "

    Application Error

    Merb could not be reached" diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..c908d63b Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/merb.jpg b/public/images/merb.jpg new file mode 100644 index 00000000..a19dcf40 Binary files /dev/null and b/public/images/merb.jpg differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js new file mode 100644 index 00000000..246a8be4 --- /dev/null +++ b/public/javascripts/application.js @@ -0,0 +1 @@ +// Common JavaScript code across your application goes here. \ No newline at end of file diff --git a/public/javascripts/jquery.js b/public/javascripts/jquery.js new file mode 100644 index 00000000..82b98e1d --- /dev/null +++ b/public/javascripts/jquery.js @@ -0,0 +1,32 @@ +/* + * jQuery 1.2.6 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ + * $Rev: 5685 $ + */ +(function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else +return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else +return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else +selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else +this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else +return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else +jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else +jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
    "]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div
    ","
    "]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else +ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&¬xml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&¬xml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&¬xml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else +while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return im[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else +for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
    ").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else +xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else +jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else +for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else +s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else +e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})(); \ No newline at end of file diff --git a/public/merb.fcgi b/public/merb.fcgi new file mode 100755 index 00000000..9804e0f3 --- /dev/null +++ b/public/merb.fcgi @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'merb-core' + +# this is Merb.root, change this if you have some funky setup. +merb_root = File.expand_path(File.dirname(__FILE__) / '../') + +# If the fcgi process runs as apache, make sure +# we have an inlinedir set for Rubyinline action-args to work +unless ENV["INLINEDIR"] || ENV["HOME"] + tmpdir = merb_root / "tmp" + unless File.directory?(tmpdir) + Dir.mkdir(tmpdir) + end + ENV["INLINEDIR"] = tmpdir +end + +# start merb with the fcgi adapter, add options or change the log dir here +Merb.start(:adapter => 'fcgi', + :merb_root => merb_root, + :log_file => merb_root /'log'/'merb.log') \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..f85a11b3 --- /dev/null +++ b/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: / \ No newline at end of file diff --git a/public/stylesheets/master.css b/public/stylesheets/master.css new file mode 100644 index 00000000..c4fa6760 --- /dev/null +++ b/public/stylesheets/master.css @@ -0,0 +1,119 @@ +body { + font-family: Arial, Verdana, sans-serif; + font-size: 12px; + background-color: #fff; +} +* { + margin: 0px; + padding: 0px; + text-decoration: none; +} +html { + height: 100%; + margin-bottom: 1px; +} +#container { + width: 80%; + text-align: left; + background-color: #fff; + margin-right: auto; + margin-left: auto; +} +#header-container { + width: 100%; + padding-top: 15px; +} +#header-container h1, #header-container h2 { + margin-left: 6px; + margin-bottom: 6px; +} +.spacer { + width: 100%; + height: 15px; +} +hr { + border: 0px; + color: #ccc; + background-color: #cdcdcd; + height: 1px; + width: 100%; + text-align: left; +} +h1 { + font-size: 28px; + color: #c55; + background-color: #fff; + font-family: Arial, Verdana, sans-serif; + font-weight: 300; +} +h2 { + font-size: 15px; + color: #999; + font-family: Arial, Verdana, sans-serif; + font-weight: 300; + background-color: #fff; +} +h3 { + color: #4d9b12; + font-size: 15px; + text-align: left; + font-weight: 300; + padding: 5px; + margin-top: 5px; +} + +#left-container { + float: left; + width: 250px; + background-color: #FFFFFF; + color: black; +} + +#left-container h3 { + color: #c55; +} + +#main-container { + margin: 5px 5px 5px 260px; + padding: 15px; + border-left: 1px solid silver; + min-height: 400px; +} +p { + color: #000; + background-color: #fff; + line-height: 20px; + padding: 5px; +} +a { + color: #4d9b12; + background-color: #fff; + text-decoration: none; +} +a:hover { + color: #4d9b12; + background-color: #fff; + text-decoration: underline; +} +#footer-container { + clear: both; + font-size: 12px; + font-family: Verdana, Arial, sans-serif; +} +.right { + float: right; + font-size: 100%; + margin-top: 5px; + color: #999; + background-color: #fff; +} +.left { + float: left; + font-size: 100%; + margin-top: 5px; + color: #999; + background-color: #fff; +} +#main-container ul { + margin-left: 3.0em; +} \ No newline at end of file diff --git a/spec/spec.opts b/spec/spec.opts new file mode 100644 index 00000000..e69de29b diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..32c65b55 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,20 @@ +require "rubygems" + +# Add the local gems dir if found within the app root; any dependencies loaded +# hereafter will try to load from the local gems before loading system gems. +if (local_gem_dir = File.join(File.dirname(__FILE__), '..', 'gems')) && $BUNDLE.nil? + $BUNDLE = true; Gem.clear_paths; Gem.path.unshift(local_gem_dir) +end + +require "merb-core" +require "spec" # Satisfies Autotest and anyone else not using the Rake tasks + +# this loads all plugins required in your init file so don't add them +# here again, Merb will do it for you +Merb.start_environment(:testing => true, :adapter => 'runner', :environment => ENV['MERB_ENV'] || 'test') + +Spec::Runner.configure do |config| + config.include(Merb::Test::ViewHelper) + config.include(Merb::Test::RouteHelper) + config.include(Merb::Test::ControllerHelper) +end diff --git a/tasks/doc.thor b/tasks/doc.thor new file mode 100644 index 00000000..77e32227 --- /dev/null +++ b/tasks/doc.thor @@ -0,0 +1,149 @@ +$: << File.join("doc") +require 'rubygems' +require 'rdoc/rdoc' +require 'fileutils' +require 'erb' + +module Merb + + class GemNotFoundException < Exception + end + + module DocMethods + def setup_gem_path + if File.directory?(gems_dir = File.join(File.dirname(__FILE__), 'gems')) + $BUNDLE = true; Gem.clear_paths; Gem.path.unshift(gems_dir) + end + end + + def get_more + libs = [] + more_library = find_library("merb-more") + File.open("#{more_library}/lib/merb-more.rb").read.each_line do |line| + if line['require'] + libs << line.gsub("require '", '').gsub("'\n", '') + end + end + return libs + end + + def generate_documentation(file_list, destination, arguments = []) + output_dir = File.join("/../doc", "rdoc", destination) + FileUtils.rm_rf(output_dir) + + arguments += [ + "--fmt", "merb", + "--op", output_dir + ] + RDoc::RDoc.new.document(arguments + file_list) + AdvancedDoc.new.index + end + + def find_library(directory_snippet) + gem_dir = nil + Gem.path.find do |path| + dir = Dir.glob("#{path}/gems/#{directory_snippet}*") + dir.empty? ? false : gem_dir = dir.last + end + raise GemNotFoundException if gem_dir.nil? + return gem_dir + end + + def get_file_list(directory_snippet) + gem_dir = find_library(directory_snippet) + files = Dir.glob("#{gem_dir}/**/lib/**/*.rb") + files += ["#{gem_dir}/README"] if File.exists?("#{gem_dir}/README") + return files + end + end + + class AdvancedDoc < Thor + + group 'core' + include DocMethods + + def initialize + super + setup_gem_path + end + + desc 'index', "Regenerate the index file for your framework documentation" + def index + @directories = Dir.entries(File.join(File.dirname(__FILE__) + "/../", "doc", "rdoc")) + @directories.delete(".") + @directories.delete("..") + @directories.delete("generators") + @directories.delete("index.html") + index_template = File.read(File.join("doc", "rdoc", "generators", "template", "merb", "index.html.erb")) + + File.open(File.join("doc", "rdoc", "index.html"), "w") do |file| + file.write(ERB.new(index_template).result(binding)) + end + end + + desc 'plugins', 'Generate the rdoc for each merb-plugins seperatly' + def plugins + libs = ["merb_activerecord", "merb_builder", "merb_jquery", "merb_laszlo", "merb_parts", "merb_screw_unit", "merb_sequel", "merb_stories", "merb_test_unit"] + + libs.each do |lib| + options[:gem] = lib + gem + end + end + + desc 'more', 'Generate the rdoc for each merb-more gem seperatly' + def more + libs = get_more + libs.each do |lib| + options[:gem] = lib + gem + end + end + + desc 'core', 'Generate the rdoc for merb-core' + def core + options[:gem] = "merb-core" + gem + end + + desc 'gem', 'Generate the rdoc for a specific gem' + method_options "--gem" => :required + def gem + file_list = get_file_list(options[:gem]) + readme = File.join(find_library("merb-core"), "README") + generate_documentation(file_list, options[:gem], ["-m", readme]) + rescue GemNotFoundException + puts "Can not find the gem in the gem path #{options[:gem]}" + end + + end + + class Doc < Thor + + include DocMethods + + def initialize + super + setup_gem_path + + end + + desc 'stack', 'Generate the rdoc for merb-core, merb-more merged together' + def stack + libs = ["merb"] + + file_list = [] + libs.each do |gem_name| + begin + file_list += get_file_list(gem_name) + rescue GemNotFoundException + puts "Could not find #{gem_name} in #{Gem.path}. Continuing with out it." + end + end + readme = File.join(find_library("merb"), "README") + generate_documentation(file_list, "stack", ["-m", readme]) + end + + end + +end \ No newline at end of file diff --git a/tasks/merb.thor b/tasks/merb.thor new file mode 100644 index 00000000..6f7f60f8 --- /dev/null +++ b/tasks/merb.thor @@ -0,0 +1,2020 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'thor' +require 'fileutils' +require 'yaml' + +# Important - don't change this line or its position +MERB_THOR_VERSION = '0.2.1' + +############################################################################## + +module ColorfulMessages + + # red + def error(*messages) + puts messages.map { |msg| "\033[1;31m#{msg}\033[0m" } + end + + # yellow + def warning(*messages) + puts messages.map { |msg| "\033[1;33m#{msg}\033[0m" } + end + + # green + def success(*messages) + puts messages.map { |msg| "\033[1;32m#{msg}\033[0m" } + end + + alias_method :message, :success + + # magenta + def note(*messages) + puts messages.map { |msg| "\033[1;35m#{msg}\033[0m" } + end + + # blue + def info(*messages) + puts messages.map { |msg| "\033[1;34m#{msg}\033[0m" } + end + +end + +############################################################################## + +require 'rubygems/dependency_installer' +require 'rubygems/uninstaller' +require 'rubygems/dependency' + +module GemManagement + + include ColorfulMessages + + # Install a gem - looks remotely and local gem cache; + # won't process rdoc or ri options. + def install_gem(gem, options = {}) + refresh = options.delete(:refresh) || [] + from_cache = (options.key?(:cache) && options.delete(:cache)) + if from_cache + install_gem_from_cache(gem, options) + else + version = options.delete(:version) + Gem.configuration.update_sources = false + + # Limit source index to install dir + update_source_index(options[:install_dir]) if options[:install_dir] + + installer = Gem::DependencyInstaller.new(options.merge(:user_install => false)) + + # Force-refresh certain gems by excluding them from the current index + if !options[:ignore_dependencies] && refresh.respond_to?(:include?) && !refresh.empty? + source_index = installer.instance_variable_get(:@source_index) + source_index.gems.each do |name, spec| + source_index.gems.delete(name) if refresh.include?(spec.name) + end + end + + exception = nil + begin + installer.install gem, version + rescue Gem::InstallError => e + exception = e + rescue Gem::GemNotFoundException => e + if from_cache && gem_file = find_gem_in_cache(gem, version) + puts "Located #{gem} in gem cache..." + installer.install gem_file + else + exception = e + end + rescue => e + exception = e + end + if installer.installed_gems.empty? && exception + error "Failed to install gem '#{gem} (#{version || 'any version'})' (#{exception.message})" + end + ensure_bin_wrapper_for_installed_gems(installer.installed_gems, options) + installer.installed_gems.each do |spec| + success "Successfully installed #{spec.full_name}" + end + return !installer.installed_gems.empty? + end + end + + # Install a gem - looks in the system's gem cache instead of remotely; + # won't process rdoc or ri options. + def install_gem_from_cache(gem, options = {}) + version = options.delete(:version) + Gem.configuration.update_sources = false + installer = Gem::DependencyInstaller.new(options.merge(:user_install => false)) + exception = nil + begin + if gem_file = find_gem_in_cache(gem, version) + puts "Located #{gem} in gem cache..." + installer.install gem_file + else + raise Gem::InstallError, "Unknown gem #{gem}" + end + rescue Gem::InstallError => e + exception = e + end + if installer.installed_gems.empty? && exception + error "Failed to install gem '#{gem}' (#{e.message})" + end + ensure_bin_wrapper_for_installed_gems(installer.installed_gems, options) + installer.installed_gems.each do |spec| + success "Successfully installed #{spec.full_name}" + end + end + + # Install a gem from source - builds and packages it first then installs. + # + # Examples: + # install_gem_from_source(source_dir, :install_dir => ...) + # install_gem_from_source(source_dir, gem_name) + # install_gem_from_source(source_dir, :skip => [...]) + def install_gem_from_source(source_dir, *args) + installed_gems = [] + opts = args.last.is_a?(Hash) ? args.pop : {} + Dir.chdir(source_dir) do + gem_name = args[0] || File.basename(source_dir) + gem_pkg_dir = File.join(source_dir, 'pkg') + gem_pkg_glob = File.join(gem_pkg_dir, "#{gem_name}-*.gem") + skip_gems = opts.delete(:skip) || [] + + # Cleanup what's already there + clobber(source_dir) + FileUtils.mkdir_p(gem_pkg_dir) unless File.directory?(gem_pkg_dir) + + # Recursively process all gem packages within the source dir + skip_gems << gem_name + packages = package_all(source_dir, skip_gems) + + if packages.length == 1 + # The are no subpackages for the main package + refresh = [gem_name] + else + # Gather all packages into the top-level pkg directory + packages.each do |pkg| + FileUtils.copy_entry(pkg, File.join(gem_pkg_dir, File.basename(pkg))) + end + + # Finally package the main gem - without clobbering the already copied pkgs + package(source_dir, false) + + # Gather subgems to refresh during installation of the main gem + refresh = packages.map do |pkg| + File.basename(pkg, '.gem')[/^(.*?)-([\d\.]+)$/, 1] rescue nil + end.compact + + # Install subgems explicitly even if ignore_dependencies is set + if opts[:ignore_dependencies] + refresh.each do |name| + gem_pkg = Dir[File.join(gem_pkg_dir, "#{name}-*.gem")][0] + install_pkg(gem_pkg, opts) + end + end + end + + ensure_bin_wrapper_for(opts[:install_dir], opts[:bin_dir], *installed_gems) + + # Finally install the main gem + if install_pkg(Dir[gem_pkg_glob][0], opts.merge(:refresh => refresh)) + installed_gems = refresh + else + installed_gems = [] + end + end + installed_gems + end + + def install_pkg(gem_pkg, opts = {}) + if (gem_pkg && File.exists?(gem_pkg)) + # Needs to be executed from the directory that contains all packages + Dir.chdir(File.dirname(gem_pkg)) { install_gem(gem_pkg, opts) } + else + false + end + end + + # Uninstall a gem. + def uninstall_gem(gem, options = {}) + if options[:version] && !options[:version].is_a?(Gem::Requirement) + options[:version] = Gem::Requirement.new ["= #{options[:version]}"] + end + update_source_index(options[:install_dir]) if options[:install_dir] + Gem::Uninstaller.new(gem, options).uninstall rescue nil + end + + def clobber(source_dir) + Dir.chdir(source_dir) do + system "#{Gem.ruby} -S rake -s clobber" unless File.exists?('Thorfile') + end + end + + def package(source_dir, clobber = true) + Dir.chdir(source_dir) do + if File.exists?('Thorfile') + thor ":package" + elsif File.exists?('Rakefile') + rake "clobber" if clobber + rake "package" + end + end + Dir[File.join(source_dir, 'pkg/*.gem')] + end + + def package_all(source_dir, skip = [], packages = []) + if Dir[File.join(source_dir, '{Rakefile,Thorfile}')][0] + name = File.basename(source_dir) + Dir[File.join(source_dir, '*', '{Rakefile,Thorfile}')].each do |taskfile| + package_all(File.dirname(taskfile), skip, packages) + end + packages.push(*package(source_dir)) unless skip.include?(name) + end + packages.uniq + end + + def rake(cmd) + cmd << " >/dev/null" if $SILENT && !Gem.win_platform? + system "#{Gem.ruby} -S #{which('rake')} -s #{cmd} >/dev/null" + end + + def thor(cmd) + cmd << " >/dev/null" if $SILENT && !Gem.win_platform? + system "#{Gem.ruby} -S #{which('thor')} #{cmd}" + end + + # Use the local bin/* executables if available. + def which(executable) + if File.executable?(exec = File.join(Dir.pwd, 'bin', executable)) + exec + else + executable + end + end + + # Partition gems into system, local and missing gems + def partition_dependencies(dependencies, gem_dir) + system_specs, local_specs, missing_deps = [], [], [] + if gem_dir && File.directory?(gem_dir) + gem_dir = File.expand_path(gem_dir) + ::Gem.clear_paths; ::Gem.path.unshift(gem_dir) + ::Gem.source_index.refresh! + dependencies.each do |dep| + gemspecs = ::Gem.source_index.search(dep) + local = gemspecs.reverse.find { |s| s.loaded_from.index(gem_dir) == 0 } + if local + local_specs << local + elsif gemspecs.last + system_specs << gemspecs.last + else + missing_deps << dep + end + end + ::Gem.clear_paths + else + dependencies.each do |dep| + gemspecs = ::Gem.source_index.search(dep) + if gemspecs.last + system_specs << gemspecs.last + else + missing_deps << dep + end + end + end + [system_specs, local_specs, missing_deps] + end + + # Create a modified executable wrapper in the specified bin directory. + def ensure_bin_wrapper_for(gem_dir, bin_dir, *gems) + options = gems.last.is_a?(Hash) ? gems.last : {} + options[:no_minigems] ||= [] + if bin_dir && File.directory?(bin_dir) + gems.each do |gem| + if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{gem}-*.gemspec")].last + spec = Gem::Specification.load(gemspec_path) + enable_minigems = !options[:no_minigems].include?(spec.name) + spec.executables.each do |exec| + executable = File.join(bin_dir, exec) + message "Writing executable wrapper #{executable}" + File.open(executable, 'w', 0755) do |f| + f.write(executable_wrapper(spec, exec, enable_minigems)) + end + end + end + end + end + end + + def ensure_bin_wrapper_for_installed_gems(gemspecs, options) + if options[:install_dir] && options[:bin_dir] + gems = gemspecs.map { |spec| spec.name } + ensure_bin_wrapper_for(options[:install_dir], options[:bin_dir], *gems) + end + end + + private + + def executable_wrapper(spec, bin_file_name, minigems = true) + requirements = ['minigems', 'rubygems'] + requirements.reverse! unless minigems + try_req, then_req = requirements + <<-TEXT +#!/usr/bin/env ruby +# +# This file was generated by Merb's GemManagement +# +# The application '#{spec.name}' is installed as part of a gem, and +# this file is here to facilitate running it. + +begin + require '#{try_req}' +rescue LoadError + require '#{then_req}' +end + +# use gems dir if ../gems exists - eg. only for ./bin/#{bin_file_name} +if File.directory?(gems_dir = File.join(File.dirname(__FILE__), '..', 'gems')) + $BUNDLE = true; Gem.clear_paths; Gem.path.replace([File.expand_path(gems_dir)]) + ENV["PATH"] = "\#{File.dirname(__FILE__)}:\#{gems_dir}/bin:\#{ENV["PATH"]}" + if (local_gem = Dir[File.join(gems_dir, "specifications", "#{spec.name}-*.gemspec")].last) + version = File.basename(local_gem)[/-([\\.\\d]+)\\.gemspec$/, 1] + end +end + +version ||= "#{Gem::Requirement.default}" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem '#{spec.name}', version +load '#{bin_file_name}' +TEXT + end + + def find_gem_in_cache(gem, version) + spec = if version + version = Gem::Requirement.new ["= #{version}"] unless version.is_a?(Gem::Requirement) + Gem.source_index.find_name(gem, version).first + else + Gem.source_index.find_name(gem).sort_by { |g| g.version }.last + end + if spec && File.exists?(gem_file = "#{spec.installation_path}/cache/#{spec.full_name}.gem") + gem_file + end + end + + def update_source_index(dir) + Gem.source_index.load_gems_in(File.join(dir, 'specifications')) + end + +end + +############################################################################## + +class SourceManager + + include ColorfulMessages + + attr_accessor :source_dir + + def initialize(source_dir) + self.source_dir = source_dir + end + + def clone(name, url) + FileUtils.cd(source_dir) do + raise "destination directory already exists" if File.directory?(name) + system("git clone --depth 1 #{url} #{name}") + end + rescue => e + error "Unable to clone #{name} repository (#{e.message})" + end + + def update(name, url) + if File.directory?(repository_dir = File.join(source_dir, name)) + FileUtils.cd(repository_dir) do + repos = existing_repos(name) + fork_name = url[/.com\/+?(.+)\/.+\.git/u, 1] + if url == repos["origin"] + # Pull from the original repository - no branching needed + info "Pulling from origin: #{url}" + system "git fetch; git checkout master; git rebase origin/master" + elsif repos.values.include?(url) && fork_name + # Update and switch to a remote branch for a particular github fork + info "Switching to remote branch: #{fork_name}" + system "git checkout -b #{fork_name} #{fork_name}/master" + system "git rebase #{fork_name}/master" + elsif fork_name + # Create a new remote branch for a particular github fork + info "Adding a new remote branch: #{fork_name}" + system "git remote add -f #{fork_name} #{url}" + system "git checkout -b #{fork_name} #{fork_name}/master" + else + warning "No valid repository found for: #{name}" + end + end + return true + else + warning "No valid repository found at: #{repository_dir}" + end + rescue => e + error "Unable to update #{name} repository (#{e.message})" + return false + end + + def existing_repos(name) + repos = [] + FileUtils.cd(File.join(source_dir, name)) do + repos = %x[git remote -v].split("\n").map { |branch| branch.split(/\s+/) } + end + Hash[*repos.flatten] + end + +end + +############################################################################## + +module MerbThorHelper + + attr_accessor :force_gem_dir + + def self.included(base) + base.send(:include, ColorfulMessages) + base.extend ColorfulMessages + end + + def use_edge_gem_server + ::Gem.sources << 'http://edge.merbivore.com' + end + + def source_manager + @_source_manager ||= SourceManager.new(source_dir) + end + + def extract_repositories(names) + repos = [] + names.each do |name| + if repo_url = Merb::Source.repo(name, options[:sources]) + # A repository entry for this dependency exists + repo = [name, repo_url] + repos << repo unless repos.include?(repo) + elsif (repo_name = Merb::Stack.lookup_repository_name(name)) && + (repo_url = Merb::Source.repo(repo_name, options[:sources])) + # A parent repository entry for this dependency exists + repo = [repo_name, repo_url] + unless repos.include?(repo) + puts "Found #{repo_name}/#{name} at #{repo_url}" + repos << repo + end + end + end + repos + end + + def update_dependency_repositories(dependencies) + repos = extract_repositories(dependencies.map { |d| d.name }) + update_repositories(repos) + end + + def update_repositories(repos) + repos.each do |(name, url)| + if File.directory?(repository_dir = File.join(source_dir, name)) + message "Updating or branching #{name}..." + source_manager.update(name, url) + else + message "Cloning #{name} repository from #{url}..." + source_manager.clone(name, url) + end + end + end + + def install_dependency(dependency, opts = {}) + version = dependency.version_requirements.to_s + install_opts = default_install_options.merge(:version => version) + Merb::Gem.install(dependency.name, install_opts.merge(opts)) + end + + def install_dependency_from_source(dependency, opts = {}) + matches = Dir[File.join(source_dir, "**", dependency.name, "{Rakefile,Thorfile}")] + matches.reject! { |m| File.basename(m) == 'Thorfile' } + if matches.length == 1 && matches[0] + if File.directory?(gem_src_dir = File.dirname(matches[0])) + begin + Merb::Gem.install_gem_from_source(gem_src_dir, default_install_options.merge(opts)) + puts "Installed #{dependency.name}" + return true + rescue => e + warning "Unable to install #{dependency.name} from source (#{e.message})" + end + else + msg = "Unknown directory: #{gem_src_dir}" + warning "Unable to install #{dependency.name} from source (#{msg})" + end + elsif matches.length > 1 + error "Ambigous source(s) for dependency: #{dependency.name}" + matches.each { |m| puts "- #{m}" } + end + return false + end + + def clobber_dependencies! + if options[:force] && gem_dir && File.directory?(gem_dir) + # Remove all existing local gems by clearing the gems directory + if dry_run? + note 'Clearing existing local gems...' + else + message 'Clearing existing local gems...' + FileUtils.rm_rf(gem_dir) && FileUtils.mkdir_p(default_gem_dir) + end + elsif !local.empty? + # Uninstall all local versions of the gems to install + if dry_run? + note 'Uninstalling existing local gems:' + local.each { |gemspec| note "Uninstalled #{gemspec.name}" } + else + message 'Uninstalling existing local gems:' if local.size > 1 + local.each do |gemspec| + Merb::Gem.uninstall(gemspec.name, default_uninstall_options) + end + end + end + end + + def display_gemspecs(gemspecs) + if gemspecs.empty? + puts "- none" + else + gemspecs.each do |spec| + if hint = Dir[File.join(spec.full_gem_path, '*.strategy')][0] + strategy = File.basename(hint, '.strategy') + puts "- #{spec.full_name} (#{strategy})" + else + puts "~ #{spec.full_name}" # unknown strategy + end + end + end + end + + def display_dependencies(dependencies) + if dependencies.empty? + puts "- none" + else + dependencies.each { |d| puts "- #{d.name} (#{d.version_requirements})" } + end + end + + def default_install_options + { :install_dir => gem_dir, :bin_dir => bin_dir, :ignore_dependencies => ignore_dependencies? } + end + + def default_uninstall_options + { :install_dir => gem_dir, :bin_dir => bin_dir, :ignore => true, :all => true, :executables => true } + end + + def dry_run? + options[:"dry-run"] + end + + def ignore_dependencies? + options[:"ignore-dependencies"] + end + + # The current working directory, or Merb app root (--merb-root option). + def working_dir + @_working_dir ||= File.expand_path(options['merb-root'] || Dir.pwd) + end + + # We should have a ./src dir for local and system-wide management. + def source_dir + @_source_dir ||= File.join(working_dir, 'src') + create_if_missing(@_source_dir) + @_source_dir + end + + # If a local ./gems dir is found, return it. + def gem_dir + return force_gem_dir if force_gem_dir + if File.directory?(dir = default_gem_dir) + dir + end + end + + def default_gem_dir + File.join(working_dir, 'gems') + end + + # If we're in a Merb app, we can have a ./bin directory; + # create it if it's not there. + def bin_dir + @_bin_dir ||= begin + if gem_dir + dir = File.join(working_dir, 'bin') + create_if_missing(dir) + dir + end + end + end + + # Helper to create dir unless it exists. + def create_if_missing(path) + FileUtils.mkdir(path) unless File.exists?(path) + end + + def sudo + ENV['THOR_SUDO'] ||= "sudo" + sudo = Gem.win_platform? ? "" : ENV['THOR_SUDO'] + end + + def local_gemspecs(directory = gem_dir) + if File.directory?(specs_dir = File.join(directory, 'specifications')) + Dir[File.join(specs_dir, '*.gemspec')].map do |gemspec_path| + gemspec = Gem::Specification.load(gemspec_path) + gemspec.loaded_from = gemspec_path + gemspec + end + else + [] + end + end + +end + +############################################################################## + +$SILENT = true # don't output all the mess some rake package tasks spit out + +module Merb + + class Gem < Thor + + include MerbThorHelper + extend GemManagement + + attr_accessor :system, :local, :missing + + global_method_options = { + "--merb-root" => :optional, # the directory to operate on + "--version" => :optional, # gather specific version of gem + "--ignore-dependencies" => :boolean # don't install sub-dependencies + } + + method_options global_method_options + def initialize(*args); super; end + + # List gems that match the specified criteria. + # + # By default all local gems are listed. When the first argument is 'all' the + # list is partitioned into system an local gems; specify 'system' to show + # only system gems. A second argument can be used to filter on a set of known + # components, like all merb-more gems for example. + # + # Examples: + # + # merb:gem:list # list all local gems - the default + # merb:gem:list all # list system and local gems + # merb:gem:list system # list only system gems + # merb:gem:list all merb-more # list only merb-more related gems + # merb:gem:list --version 0.9.8 # list gems that match the version + + desc 'list [all|local|system] [comp]', 'Show installed gems' + def list(filter = 'local', comp = nil) + deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + case filter + when 'all' + message 'Installed system gems:' + display_gemspecs(system) + message 'Installed local gems:' + display_gemspecs(local) + when 'system' + message 'Installed system gems:' + display_gemspecs(system) + when 'local' + message 'Installed local gems:' + display_gemspecs(local) + else + warning "Invalid listing filter '#{filter}'" + end + end + + # Install the specified gems. + # + # All arguments should be names of gems to install. + # + # When :force => true then any existing versions of the gems to be installed + # will be uninstalled first. It's important to note that so-called meta-gems + # or gems that exactly match a set of Merb::Stack.components will have their + # sub-gems uninstalled too. For example, uninstalling merb-more will install + # all contained gems: merb-action-args, merb-assets, merb-gen, ... + # + # Examples: + # + # merb:gem:install merb-core merb-slices # install all specified gems + # merb:gem:install merb-core --version 0.9.8 # install a specific version of a gem + # merb:gem:install merb-core --force # uninstall then subsequently install the gem + # merb:gem:install merb-core --cache # try to install locally from system gems + # merb:gem:install merb --merb-edge # install from edge.merbivore.com + + desc 'install GEM_NAME [GEM_NAME, ...]', 'Install a gem from rubygems' + method_options "--cache" => :boolean, + "--dry-run" => :boolean, + "--force" => :boolean, + "--merb-edge" => :boolean + def install(*names) + opts = { :version => options[:version], :cache => options[:cache] } + use_edge_gem_server if options[:"merb-edge"] + current_gem = nil + + # uninstall existing gems of the ones we're going to install + uninstall(*names) if options[:force] + + message "Installing #{names.length} #{names.length == 1 ? 'gem' : 'gems'}..." + puts "This may take a while..." + + names.each do |gem_name| + current_gem = gem_name + if dry_run? + note "Installing #{current_gem}..." + else + message "Installing #{current_gem}..." + self.class.install(gem_name, default_install_options.merge(opts)) + end + end + rescue => e + error "Failed to install #{current_gem ? current_gem : 'gem'} (#{e.message})" + end + + # Uninstall the specified gems. + # + # By default all specified gems are uninstalled. It's important to note that + # so-called meta-gems or gems that match a set of Merb::Stack.components will + # have their sub-gems uninstalled too. For example, uninstalling merb-more + # will install all contained gems: merb-action-args, merb-assets, ... + # + # Existing dependencies will be clobbered; when :force => true then all gems + # will be cleared, otherwise only existing local dependencies of the + # matching component set will be removed. + # + # Examples: + # + # merb:gem:uninstall merb-core merb-slices # uninstall all specified gems + # merb:gem:uninstall merb-core --version 0.9.8 # uninstall a specific version of a gem + + desc 'uninstall GEM_NAME [GEM_NAME, ...]', 'Unstall a gem' + method_options "--dry-run" => :boolean + def uninstall(*names) + opts = { :version => options[:version] } + current_gem = nil + if dry_run? + note "Uninstalling any existing gems of: #{names.join(', ')}" + else + message "Uninstalling any existing gems of: #{names.join(', ')}" + names.each do |gem_name| + current_gem = gem_name + Merb::Gem.uninstall(gem_name, default_uninstall_options) rescue nil + # if this gem is a meta-gem or a component set name, remove sub-gems + (Merb::Stack.components(gem_name) || []).each do |comp| + Merb::Gem.uninstall(comp, default_uninstall_options) rescue nil + end + end + end + rescue => e + error "Failed to uninstall #{current_gem ? current_gem : 'gem'} (#{e.message})" + end + + # Recreate all gems from gems/cache on the current platform. + # + # This task should be executed as part of a deployment setup, where the + # deployment system runs this after the app has been installed. + # Usually triggered by Capistrano, God... + # + # It will regenerate gems from the bundled gems cache for any gem that has + # C extensions - which need to be recompiled for the target deployment platform. + # + # Note: at least gems/cache and gems/specifications should be in your SCM. + + desc 'redeploy', 'Recreate all gems on the current platform' + method_options "--dry-run" => :boolean, "--force" => :boolean + def redeploy + require 'tempfile' # for Dir::tmpdir access + if gem_dir && File.directory?(cache_dir = File.join(gem_dir, 'cache')) + specs = local_gemspecs + message "Recreating #{specs.length} gems from cache..." + puts "This may take a while..." + specs.each do |gemspec| + if File.exists?(gem_file = File.join(cache_dir, "#{gemspec.full_name}.gem")) + gem_file_copy = File.join(Dir::tmpdir, File.basename(gem_file)) + if dry_run? + note "Recreating #{gemspec.full_name}" + else + message "Recreating #{gemspec.full_name}" + if options[:force] && File.directory?(gem = File.join(gem_dir, 'gems', gemspec.full_name)) + puts "Removing existing #{gemspec.full_name}" + FileUtils.rm_rf(gem) + end + # Copy the gem to a temporary file, because otherwise RubyGems/FileUtils + # will complain about copying identical files (same source/destination). + FileUtils.cp(gem_file, gem_file_copy) + Merb::Gem.install(gem_file_copy, :install_dir => gem_dir, :ignore_dependencies => true) + File.delete(gem_file_copy) + end + end + end + else + error "No application local gems directory found" + end + end + + private + + # Return dependencies for all installed gems; both system-wide and locally; + # optionally filters on :version requirement. + def dependencies + version_req = if options[:version] + ::Gem::Requirement.create(options[:version]) + else + ::Gem::Requirement.default + end + if gem_dir + ::Gem.clear_paths; ::Gem.path.unshift(gem_dir) + ::Gem.source_index.refresh! + end + deps = [] + ::Gem.source_index.each do |fullname, gemspec| + if version_req.satisfied_by?(gemspec.version) + deps << ::Gem::Dependency.new(gemspec.name, "= #{gemspec.version}") + end + end + ::Gem.clear_paths if gem_dir + deps.sort + end + + public + + # Install gem with some default options. + def self.install(name, options = {}) + defaults = {} + defaults[:cache] = false unless opts[:install_dir] + install_gem(name, defaults.merge(options)) + end + + # Uninstall gem with some default options. + def self.uninstall(name, options = {}) + defaults = { :ignore => true, :executables => true } + uninstall_gem(name, defaults.merge(options)) + end + + end + + class Tasks < Thor + + include MerbThorHelper + + # Show merb.thor version information + # + # merb:tasks:version # show the current version info + # merb:tasks:version --info # show extended version info + + desc 'version', 'Show verion info' + method_options "--info" => :boolean + def version + message "Currently installed merb.thor version: #{MERB_THOR_VERSION}" + if options[:version] + self.options = { :"dry-run" => true } + self.update # run update task with dry-run enabled + end + end + + # Update merb.thor tasks from remotely available version + # + # merb:tasks:update # update merb.thor + # merb:tasks:update --force # force-update merb.thor + # merb:tasks:update --dry-run # show version info only + + desc 'update [URL]', 'Fetch the latest merb.thor and install it locally' + method_options "--dry-run" => :boolean, "--force" => :boolean + def update(url = 'http://merbivore.com/merb.thor') + require 'open-uri' + require 'rubygems/version' + remote_file = open(url) + code = remote_file.read + + # Extract version information from the source code + if version = code[/^MERB_THOR_VERSION\s?=\s?('|")([\.\d]+)('|")/,2] + # borrow version comparison from rubygems' Version class + current_version = ::Gem::Version.new(MERB_THOR_VERSION) + remote_version = ::Gem::Version.new(version) + + if current_version >= remote_version + puts "currently installed: #{current_version}" + if current_version != remote_version + puts "available version: #{remote_version}" + end + info "No update of merb.thor necessary#{options[:force] ? ' (forced)' : ''}" + proceed = options[:force] + elsif current_version < remote_version + puts "currently installed: #{current_version}" + puts "available version: #{remote_version}" + proceed = true + end + + if proceed && !dry_run? + File.open(File.join(__FILE__), 'w') do |f| + f.write(code) + end + success "Installed the latest merb.thor (v#{version})" + end + else + raise "invalid source-code data" + end + rescue OpenURI::HTTPError + error "Error opening #{url}" + rescue => e + error "An error occurred (#{e.message})" + end + + end + + #### MORE LOW-LEVEL TASKS #### + + class Source < Thor + + group 'core' + + include MerbThorHelper + extend GemManagement + + attr_accessor :system, :local, :missing + + global_method_options = { + "--merb-root" => :optional, # the directory to operate on + "--ignore-dependencies" => :boolean, # don't install sub-dependencies + "--sources" => :optional # a yml config to grab sources from + } + + method_options global_method_options + def initialize(*args); super; end + + # List source repositories, of either local or known sources. + # + # Examples: + # + # merb:source:list # list all local sources + # merb:source:list available # list all known sources + + desc 'list [local|available]', 'Show git source repositories' + def list(mode = 'local') + if mode == 'available' + message 'Available source repositories:' + repos = self.class.repos(options[:sources]) + repos.keys.sort.each { |name| puts "- #{name}: #{repos[name]}" } + elsif mode == 'local' + message 'Current source repositories:' + Dir[File.join(source_dir, '*')].each do |src| + next unless File.directory?(src) + src_name = File.basename(src) + unless (repos = source_manager.existing_repos(src_name)).empty? + puts "#{src_name}" + repos.keys.sort.each { |b| puts "- #{b}: #{repos[b]}" } + end + end + else + error "Unknown listing: #{mode}" + end + end + + # Install the specified gems. + # + # All arguments should be names of gems to install. + # + # When :force => true then any existing versions of the gems to be installed + # will be uninstalled first. It's important to note that so-called meta-gems + # or gems that exactly match a set of Merb::Stack.components will have their + # sub-gems uninstalled too. For example, uninstalling merb-more will install + # all contained gems: merb-action-args, merb-assets, merb-gen, ... + # + # Examples: + # + # merb:source:install merb-core merb-slices # install all specified gems + # merb:source:install merb-core --force # uninstall then subsequently install the gem + # merb:source:install merb-core --wipe # clear repo then install the gem + + desc 'install GEM_NAME [GEM_NAME, ...]', 'Install a gem from git source/edge' + method_options "--dry-run" => :boolean, + "--force" => :boolean, + "--wipe" => :boolean + def install(*names) + use_edge_gem_server + # uninstall existing gems of the ones we're going to install + uninstall(*names) if options[:force] || options[:wipe] + + # We want dependencies instead of just names + deps = names.map { |n| ::Gem::Dependency.new(n, ::Gem::Requirement.default) } + + # Selectively update repositories for the matching dependencies + update_dependency_repositories(deps) unless dry_run? + + current_gem = nil + deps.each do |dependency| + current_gem = dependency.name + if dry_run? + note "Installing #{current_gem} from source..." + else + message "Installing #{current_gem} from source..." + puts "This may take a while..." + unless install_dependency_from_source(dependency) + raise "gem source not found" + end + end + end + rescue => e + error "Failed to install #{current_gem ? current_gem : 'gem'} (#{e.message})" + end + + # Uninstall the specified gems. + # + # By default all specified gems are uninstalled. It's important to note that + # so-called meta-gems or gems that match a set of Merb::Stack.components will + # have their sub-gems uninstalled too. For example, uninstalling merb-more + # will install all contained gems: merb-action-args, merb-assets, ... + # + # Existing dependencies will be clobbered; when :force => true then all gems + # will be cleared, otherwise only existing local dependencies of the + # matching component set will be removed. Additionally when :wipe => true, + # the matching git repositories will be removed from the source directory. + # + # Examples: + # + # merb:source:uninstall merb-core merb-slices # uninstall all specified gems + # merb:source:uninstall merb-core --wipe # force-uninstall a gem and clear repo + + desc 'uninstall GEM_NAME [GEM_NAME, ...]', 'Unstall a gem (specify --force to remove the repo)' + method_options "--version" => :optional, "--dry-run" => :boolean, "--wipe" => :boolean + def uninstall(*names) + # Remove the repos that contain the gem + if options[:wipe] + extract_repositories(names).each do |(name, url)| + if File.directory?(src = File.join(source_dir, name)) + if dry_run? + note "Removing #{src}..." + else + info "Removing #{src}..." + FileUtils.rm_rf(src) + end + end + end + end + + # Use the Merb::Gem#uninstall task to handle this + gem_tasks = Merb::Gem.new + gem_tasks.options = options + gem_tasks.uninstall(*names) + end + + # Update the specified source repositories. + # + # The arguments can be actual repository names (from Merb::Source.repos) + # or names of known merb stack gems. If the repo doesn't exist already, + # it will be created and cloned. + # + # merb:source:pull merb-core # update source of specified gem + # merb:source:pull merb-slices # implicitly updates merb-more + + desc 'pull REPO_NAME [GEM_NAME, ...]', 'Update git source repository from edge' + def pull(*names) + repos = extract_repositories(names) + update_repositories(repos) + unless repos.empty? + message "Updated the following repositories:" + repos.each { |name, url| puts "- #{name}: #{url}" } + else + warning "No repositories found to update!" + end + end + + # Clone a git repository into ./src. + + # The repository can be a direct git url or a known -named- repository. + # + # Examples: + # + # merb:source:clone merb-core + # merb:source:clone dm-core awesome-repo + # merb:source:clone dm-core --sources ./path/to/sources.yml + # merb:source:clone git://github.com/sam/dm-core.git + + desc 'clone (REPO_NAME|URL) [DIR_NAME]', 'Clone git source repository by name or url' + def clone(repository, name = nil) + if repository =~ /^git:\/\// + repository_url = repository + repository_name = File.basename(repository_url, '.git') + elsif url = Merb::Source.repo(repository, options[:sources]) + repository_url = url + repository_name = repository + end + source_manager.clone(name || repository_name, repository_url) + end + + # Git repository sources - pass source_config option to load a yaml + # configuration file - defaults to ./config/git-sources.yml and + # ~/.merb/git-sources.yml - which you need to create yourself. + # + # Example of contents: + # + # merb-core: git://github.com/myfork/merb-core.git + # merb-more: git://github.com/myfork/merb-more.git + + def self.repos(source_config = nil) + source_config ||= begin + local_config = File.join(Dir.pwd, 'config', 'git-sources.yml') + user_config = File.join(ENV["HOME"] || ENV["APPDATA"], '.merb', 'git-sources.yml') + File.exists?(local_config) ? local_config : user_config + end + if source_config && File.exists?(source_config) + default_repos.merge(YAML.load(File.read(source_config))) + else + default_repos + end + end + + def self.repo(name, source_config = nil) + self.repos(source_config)[name] + end + + # Default Git repositories + def self.default_repos + @_default_repos ||= { + 'merb' => "git://github.com/wycats/merb.git", + 'merb-plugins' => "git://github.com/wycats/merb-plugins.git", + 'extlib' => "git://github.com/sam/extlib.git", + 'dm-core' => "git://github.com/sam/dm-core.git", + 'dm-more' => "git://github.com/sam/dm-more.git", + 'sequel' => "git://github.com/wayneeseguin/sequel.git", + 'do' => "git://github.com/sam/do.git", + 'thor' => "git://github.com/wycats/thor.git", + 'rake' => "git://github.com/jimweirich/rake.git" + } + end + + end + + class Dependencies < Thor + + group 'core' + + # The Dependencies tasks will install dependencies based on actual application + # dependencies. For this, the application is queried for any dependencies. + # All operations will be performed within this context. + + attr_accessor :system, :local, :missing, :extract_dependencies + + include MerbThorHelper + + global_method_options = { + "--merb-root" => :optional, # the directory to operate on + "--ignore-dependencies" => :boolean, # ignore sub-dependencies + "--stack" => :boolean, # gather only stack dependencies + "--no-stack" => :boolean, # gather only non-stack dependencies + "--extract" => :boolean, # gather dependencies from the app itself + "--config-file" => :optional, # gather from the specified yaml config + "--version" => :optional # gather specific version of framework + } + + method_options global_method_options + def initialize(*args); super; end + + # List application dependencies. + # + # By default all dependencies are listed, partitioned into system, local and + # currently missing dependencies. The first argument allows you to filter + # on any of the partitionings. A second argument can be used to filter on + # a set of known components, like all merb-more gems for example. + # + # Examples: + # + # merb:dependencies:list # list all dependencies - the default + # merb:dependencies:list local # list only local gems + # merb:dependencies:list all merb-more # list only merb-more related dependencies + # merb:dependencies:list --stack # list framework dependencies + # merb:dependencies:list --no-stack # list 3rd party dependencies + # merb:dependencies:list --extract # list dependencies by extracting them + # merb:dependencies:list --config-file file.yml # list from the specified config file + + desc 'list [all|local|system|missing] [comp]', 'Show application dependencies' + def list(filter = 'all', comp = nil) + deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + case filter + when 'all' + message 'Installed system gem dependencies:' + display_gemspecs(system) + message 'Installed local gem dependencies:' + display_gemspecs(local) + unless missing.empty? + error 'Missing gem dependencies:' + display_dependencies(missing) + end + when 'system' + message 'Installed system gem dependencies:' + display_gemspecs(system) + when 'local' + message 'Installed local gem dependencies:' + display_gemspecs(local) + when 'missing' + error 'Missing gem dependencies:' + display_dependencies(missing) + else + warning "Invalid listing filter '#{filter}'" + end + if missing.size > 0 + info "Some dependencies are currently missing!" + elsif local.size == deps.size + info "All dependencies have been bundled with the application." + elsif local.size > system.size + info "Most dependencies have been bundled with the application." + elsif system.size > 0 && local.size > 0 + info "Some dependencies have been bundled with the application." + elsif local.empty? && system.size == deps.size + info "All dependencies are available on the system." + end + end + + # Install application dependencies. + # + # By default all required dependencies are installed. The first argument + # specifies which strategy to use: stable or edge. A second argument can be + # used to filter on a set of known components. + # + # Existing dependencies will be clobbered; when :force => true then all gems + # will be cleared first, otherwise only existing local dependencies of the + # gems to be installed will be removed. + # + # Examples: + # + # merb:dependencies:install # install all dependencies using stable strategy + # merb:dependencies:install stable --version 0.9.8 # install a specific version of the framework + # merb:dependencies:install stable missing # install currently missing gems locally + # merb:dependencies:install stable merb-more # install only merb-more related dependencies + # merb:dependencies:install stable --stack # install framework dependencies + # merb:dependencies:install stable --no-stack # install 3rd party dependencies + # merb:dependencies:install stable --extract # extract dependencies from the actual app + # merb:dependencies:install stable --config-file file.yml # read from the specified config file + # + # In addition to the options above, edge install uses the following: + # + # merb:dependencies:install edge # install all dependencies using edge strategy + # merb:dependencies:install edge --sources file.yml # install edge from the specified git sources config + + desc 'install [stable|edge] [comp]', 'Install application dependencies' + method_options "--sources" => :optional, # only for edge strategy + "--local" => :boolean, # force local install + "--dry-run" => :boolean, + "--force" => :boolean + def install(strategy = 'stable', comp = nil) + if strategy?(strategy) + # Force local dependencies by creating ./gems before proceeding + create_if_missing(default_gem_dir) if options[:local] + + where = gem_dir ? 'locally' : 'system-wide' + + # When comp == 'missing' then filter on missing dependencies + if only_missing = comp == 'missing' + message "Preparing to install missing gems #{where} using #{strategy} strategy..." + comp = nil + clobber = false + else + message "Preparing to install #{where} using #{strategy} strategy..." + clobber = true + end + + # If comp given, filter on known stack components + deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + + # Only install currently missing gems (for comp == missing) + if only_missing + deps.reject! { |dep| not missing.include?(dep) } + end + + if deps.empty? + warning "No dependencies to install..." + else + puts "#{deps.length} dependencies to install..." + puts "This may take a while..." + install_dependencies(strategy, deps, clobber) + end + + # Show current dependency info now that we're done + puts # Seperate output + list('local', comp) + else + warning "Invalid install strategy '#{strategy}'" + puts + message "Please choose one of the following installation strategies: stable or edge:" + puts "$ thor merb:dependencies:install stable" + puts "$ thor merb:dependencies:install edge" + end + end + + # Uninstall application dependencies. + # + # By default all required dependencies are installed. An optional argument + # can be used to filter on a set of known components. + # + # Existing dependencies will be clobbered; when :force => true then all gems + # will be cleared, otherwise only existing local dependencies of the + # matching component set will be removed. + # + # Examples: + # + # merb:dependencies:uninstall # uninstall all dependencies - the default + # merb:dependencies:uninstall merb-more # uninstall merb-more related gems locally + + desc 'uninstall [comp]', 'Uninstall application dependencies' + method_options "--dry-run" => :boolean, "--force" => :boolean + def uninstall(comp = nil) + # If comp given, filter on known stack components + deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + # Clobber existing local dependencies - based on self.local + clobber_dependencies! + end + + # Recreate all gems from gems/cache on the current platform. + # + # Note: use merb:gem:redeploy instead + + desc 'redeploy', 'Recreate all gems on the current platform' + method_options "--dry-run" => :boolean, "--force" => :boolean + def redeploy + warning 'Warning: merb:dependencies:redeploy has been deprecated - use merb:gem:redeploy instead' + gem = Merb::Gem.new + gem.options = options + gem.redeploy + end + + # Create a dependencies configuration file. + # + # A configuration yaml file will be created from the extracted application + # dependencies. The format of the configuration is as follows: + # + # --- + # - merb-core (= 0.9.8, runtime) + # - merb-slices (= 0.9.8, runtime) + # + # This format is exactly the same as Gem::Dependency#to_s returns. + # + # Examples: + # + # merb:dependencies:configure --force # overwrite the default config file + # merb:dependencies:configure --version 0.9.8 # configure specific framework version + # merb:dependencies:configure --config-file file.yml # write to the specified config file + + desc 'configure [comp]', 'Create a dependencies config file' + method_options "--dry-run" => :boolean, "--force" => :boolean, "--versions" => :boolean + def configure(comp = nil) + self.extract_dependencies = true # of course we need to consult the app itself + # If comp given, filter on known stack components + deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies + + # If --versions is set, update the version_requirements with the actual version available + if options[:versions] + specs = local_gemspecs + deps.each do |dep| + if spec = specs.find { |s| s.name == dep.name } + dep.version_requirements = ::Gem::Requirement.create(spec.version) + end + end + end + + config = YAML.dump(deps.map { |d| d.to_s }) + puts "#{config}\n" + if File.exists?(config_file) && !options[:force] + error "File already exists! Use --force to overwrite." + else + if dry_run? + note "Written #{config_file}" + else + FileUtils.mkdir_p(config_dir) unless File.directory?(config_dir) + File.open(config_file, 'w') { |f| f.write config } + success "Written #{config_file}" + end + end + rescue + error "Failed to write to #{config_file}" + end + + ### Helper Methods + + def strategy?(strategy) + if self.respond_to?(method = :"#{strategy}_strategy", true) + method + end + end + + def install_dependencies(strategy, deps, clobber = true) + if method = strategy?(strategy) + # Clobber existing local dependencies + clobber_dependencies! if clobber + + # Run the chosen strategy - collect files installed from stable gems + installed_from_stable = send(method, deps).map { |d| d.name } + + unless dry_run? + # Sleep a bit otherwise the following steps won't see the new files + sleep(deps.length) if deps.length > 0 && deps.length <= 10 + + # Leave a file to denote the strategy that has been used for this dependency + self.local.each do |spec| + next unless File.directory?(spec.full_gem_path) + unless installed_from_stable.include?(spec.name) + FileUtils.touch(File.join(spec.full_gem_path, "#{strategy}.strategy")) + else + FileUtils.touch(File.join(spec.full_gem_path, "stable.strategy")) + end + end + end + return true + end + false + end + + def dependencies + if extract_dependencies? + # Extract dependencies from the current application + deps = Merb::Stack.core_dependencies(gem_dir, ignore_dependencies?) + deps += Merb::Dependencies.extract_dependencies(working_dir) + else + # Use preconfigured dependencies from yaml file + deps = config_dependencies + end + + stack_components = Merb::Stack.components + + if options[:stack] + # Limit to stack components only + deps.reject! { |dep| not stack_components.include?(dep.name) } + elsif options[:"no-stack"] + # Limit to non-stack components + deps.reject! { |dep| stack_components.include?(dep.name) } + end + + if options[:version] + version_req = ::Gem::Requirement.create("= #{options[:version]}") + elsif core = deps.find { |d| d.name == 'merb-core' } + version_req = core.version_requirements + end + + if version_req + # Handle specific version requirement for framework components + framework_components = Merb::Stack.framework_components + deps.each do |dep| + if framework_components.include?(dep.name) + dep.version_requirements = version_req + end + end + end + + deps + end + + def config_dependencies + if File.exists?(config_file) + self.class.parse_dependencies_yaml(File.read(config_file)) + else + warning "No dependencies.yml file found at: #{config_file}" + [] + end + end + + def extract_dependencies? + options[:extract] || extract_dependencies + end + + def config_file + @config_file ||= begin + options[:"config-file"] || File.join(working_dir, 'config', 'dependencies.yml') + end + end + + def config_dir + File.dirname(config_file) + end + + ### Strategy handlers + + private + + def stable_strategy(deps) + installed_from_rubygems = [] + if core = deps.find { |d| d.name == 'merb-core' } + if dry_run? + note "Installing #{core.name}..." + else + if install_dependency(core) + installed_from_rubygems << core + else + msg = "Try specifying a lower version of merb-core with --version" + if version_no = core.version_requirements.to_s[/([\.\d]+)$/, 1] + num = "%03d" % (version_no.gsub('.', '').to_i - 1) + puts "The required version (#{version_no}) probably isn't available as a stable rubygem yet." + info "#{msg} #{num.split(//).join('.')}" + else + puts "The required version probably isn't available as a stable rubygem yet." + info msg + end + end + end + end + + deps.each do |dependency| + next if dependency.name == 'merb-core' + if dry_run? + note "Installing #{dependency.name}..." + else + install_dependency(dependency) + installed_from_rubygems << dependency + end + end + installed_from_rubygems + end + + def edge_strategy(deps) + use_edge_gem_server + installed_from_rubygems = [] + + # Selectively update repositories for the matching dependencies + update_dependency_repositories(deps) unless dry_run? + + if core = deps.find { |d| d.name == 'merb-core' } + if dry_run? + note "Installing #{core.name}..." + else + if install_dependency_from_source(core) + elsif install_dependency(core) + info "Installed #{core.name} from rubygems..." + installed_from_rubygems << core + end + end + end + + deps.each do |dependency| + next if dependency.name == 'merb-core' + if dry_run? + note "Installing #{dependency.name}..." + else + if install_dependency_from_source(dependency) + elsif install_dependency(dependency) + info "Installed #{dependency.name} from rubygems..." + installed_from_rubygems << dependency + end + end + end + + installed_from_rubygems + end + + ### Class Methods + + public + + def self.list(filter = 'all', comp = nil, options = {}) + instance = Merb::Dependencies.new + instance.options = options + instance.list(filter, comp) + end + + # Extract application dependencies by querying the app directly. + def self.extract_dependencies(merb_root) + require 'merb-core' + if !@_merb_loaded || Merb.root != merb_root + Merb.start_environment( + :log_level => :fatal, + :testing => true, + :adapter => 'runner', + :environment => ENV['MERB_ENV'] || 'development', + :merb_root => merb_root + ) + @_merb_loaded = true + end + Merb::BootLoader::Dependencies.dependencies + rescue StandardError => e + error "Couldn't extract dependencies from application!" + error e.message + puts "Make sure you're executing the task from your app (--merb-root)" + return [] + rescue SystemExit + error "Couldn't extract dependencies from application!" + error "application failed to run" + puts "Please check if your application runs using 'merb'; for example," + puts "look for any gem version mismatches in dependencies.rb" + return [] + end + + # Parse the basic YAML config data, and process Gem::Dependency output. + # Formatting example: merb_helpers (>= 0.9.8, runtime) + def self.parse_dependencies_yaml(yaml) + dependencies = [] + entries = YAML.load(yaml) rescue [] + entries.each do |entry| + if matches = entry.match(/^(\S+) \(([^,]+)?, ([^\)]+)\)/) + name, version_req, type = matches.captures + dependencies << ::Gem::Dependency.new(name, version_req, type.to_sym) + else + error "Invalid entry: #{entry}" + end + end + dependencies + end + + end + + class Stack < Thor + + group 'core' + + # The Stack tasks will install dependencies based on known sets of gems, + # regardless of actual application dependency settings. + + DM_STACK = %w[ + extlib + data_objects + dm-core + dm-aggregates + dm-migrations + dm-timestamps + dm-types + dm-validations + merb_datamapper + ] + + MERB_STACK = %w[ + extlib + merb-core + merb-action-args + merb-assets + merb-cache + merb-helpers + merb-mailer + merb-slices + merb-auth + merb-auth-core + merb-auth-more + merb-auth-slice-password + merb-param-protection + merb-exceptions + ] + DM_STACK + + MERB_BASICS = %w[ + extlib + merb-core + merb-action-args + merb-assets + merb-cache + merb-helpers + merb-mailer + merb-slices + ] + + # The following sets are meant for repository lookup; unlike the sets above + # these correspond to specific git repository items. + + MERB_MORE = %w[ + merb-action-args + merb-assets + merb-auth + merb-auth-core + merb-auth-more + merb-auth-slice-password + merb-cache + merb-exceptions + merb-gen + merb-haml + merb-helpers + merb-mailer + merb-param-protection + merb-slices + merb_datamapper + ] + + MERB_PLUGINS = %w[ + merb_activerecord + merb_builder + merb_jquery + merb_laszlo + merb_parts + merb_screw_unit + merb_sequel + merb_stories + merb_test_unit + ] + + DM_MORE = %w[ + dm-adjust + dm-aggregates + dm-ar-finders + dm-cli + dm-constraints + dm-is-example + dm-is-list + dm-is-nested_set + dm-is-remixable + dm-is-searchable + dm-is-state_machine + dm-is-tree + dm-is-versioned + dm-migrations + dm-observer + dm-querizer + dm-serializer + dm-shorthand + dm-sweatshop + dm-tags + dm-timestamps + dm-types + dm-validations + + dm-couchdb-adapter + dm-ferret-adapter + dm-rest-adapter + ] + + DATA_OBJECTS = %w[ + data_objects + do_derby do_hsqldb + do_jdbc + do_mysql + do_postgres + do_sqlite3 + ] + + attr_accessor :system, :local, :missing + + include MerbThorHelper + + global_method_options = { + "--merb-root" => :optional, # the directory to operate on + "--ignore-dependencies" => :boolean, # skip sub-dependencies + "--version" => :optional # gather specific version of framework + } + + method_options global_method_options + def initialize(*args); super; end + + # List components and their dependencies. + # + # Examples: + # + # merb:stack:list # list all standard stack components + # merb:stack:list all # list all component sets + # merb:stack:list merb-more # list all dependencies of merb-more + + desc 'list [all|comp]', 'List available components (optionally filtered, defaults to merb stack)' + def list(comp = 'stack') + if comp == 'all' + Merb::Stack.component_sets.keys.sort.each do |comp| + unless (components = Merb::Stack.component_sets[comp]).empty? + message "Dependencies for '#{comp}' set:" + components.each { |c| puts "- #{c}" } + end + end + else + message "Dependencies for '#{comp}' set:" + Merb::Stack.components(comp).each { |c| puts "- #{c}" } + end + end + + # Install stack components or individual gems - from stable rubygems by default. + # + # See also: Merb::Dependencies#install and Merb::Dependencies#install_dependencies + # + # Examples: + # + # merb:stack:install # install the default merb stack + # merb:stack:install basics # install a basic set of dependencies + # merb:stack:install merb-core # install merb-core from stable + # merb:stack:install merb-more --edge # install merb-core from edge + # merb:stack:install merb-core thor merb-slices # install the specified gems + + desc 'install [COMP, ...]', 'Install stack components' + method_options "--edge" => :boolean, + "--sources" => :optional, + "--force" => :boolean, + "--dry-run" => :boolean, + "--strategy" => :optional + def install(*comps) + use_edge_gem_server if options[:edge] + mngr = self.dependency_manager + deps = gather_dependencies(comps) + mngr.system, mngr.local, mngr.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + mngr.install_dependencies(strategy, deps) + end + + # Uninstall stack components or individual gems. + # + # See also: Merb::Dependencies#uninstall + # + # Examples: + # + # merb:stack:uninstall # uninstall the default merb stack + # merb:stack:uninstall merb-more # uninstall merb-more + # merb:stack:uninstall merb-core thor merb-slices # uninstall the specified gems + + desc 'uninstall [COMP, ...]', 'Uninstall stack components' + method_options "--dry-run" => :boolean, "--force" => :boolean + def uninstall(*comps) + deps = gather_dependencies(comps) + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + # Clobber existing local dependencies - based on self.local + clobber_dependencies! + end + + # Install or uninstall minigems from the system. + # + # Due to the specific nature of MiniGems it can only be installed system-wide. + # + # Examples: + # + # merb:stack:minigems install # install minigems + # merb:stack:minigems uninstall # uninstall minigems + + desc 'minigems (install|uninstall)', 'Install or uninstall minigems (needs sudo privileges)' + def minigems(action) + case action + when 'install' + Kernel.system "#{sudo} thor merb:stack:install_minigems" + when 'uninstall' + Kernel.system "#{sudo} thor merb:stack:uninstall_minigems" + else + error "Invalid command: merb:stack:minigems #{action}" + end + end + + # hidden minigems install task + def install_minigems + message "Installing MiniGems" + mngr = self.dependency_manager + deps = gather_dependencies('minigems') + mngr.system, mngr.local, mngr.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + mngr.force_gem_dir = ::Gem.dir + mngr.install_dependencies(strategy, deps) + Kernel.system "#{sudo} minigem install" + end + + # hidden minigems uninstall task + def uninstall_minigems + message "Uninstalling MiniGems" + Kernel.system "#{sudo} minigem uninstall" + deps = gather_dependencies('minigems') + self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir) + # Clobber existing local dependencies - based on self.local + clobber_dependencies! + end + + protected + + def gather_dependencies(comps = []) + if comps.empty? + gems = MERB_STACK + else + gems = comps.map { |c| Merb::Stack.components(c) }.flatten + end + + version_req = if options[:version] + ::Gem::Requirement.create(options[:version]) + end + + framework_components = Merb::Stack.framework_components + + gems.map do |gem| + if version_req && framework_components.include?(gem) + ::Gem::Dependency.new(gem, version_req) + else + ::Gem::Dependency.new(gem, ::Gem::Requirement.default) + end + end + end + + def strategy + options[:strategy] || (options[:edge] ? 'edge' : 'stable') + end + + def dependency_manager + @_dependency_manager ||= begin + instance = Merb::Dependencies.new + instance.options = options + instance + end + end + + public + + def self.repository_sets + @_repository_sets ||= begin + # the component itself as a fallback + comps = Hash.new { |(hsh,c)| [c] } + + # git repository based component sets + comps["merb"] = ["merb-core"] + MERB_MORE + comps["merb-more"] = MERB_MORE.sort + comps["merb-plugins"] = MERB_PLUGINS.sort + comps["dm-more"] = DM_MORE.sort + comps["do"] = DATA_OBJECTS.sort + + comps + end + end + + def self.component_sets + @_component_sets ||= begin + # the component itself as a fallback + comps = Hash.new { |(hsh,c)| [c] } + comps.update(repository_sets) + + # specific set of dependencies + comps["stack"] = MERB_STACK.sort + comps["basics"] = MERB_BASICS.sort + + # orm dependencies + comps["datamapper"] = DM_STACK.sort + comps["sequel"] = ["merb_sequel", "sequel"] + comps["activerecord"] = ["merb_activerecord", "activerecord"] + + comps + end + end + + def self.framework_components + %w[merb-core merb-more].inject([]) do |all, comp| + all + components(comp) + end + end + + def self.components(comp = nil) + if comp + component_sets[comp] + else + comps = %w[merb-core merb-more merb-plugins dm-core dm-more] + comps.inject([]) do |all, grp| + all + (component_sets[grp] || []) + end + end + end + + def self.select_component_dependencies(dependencies, comp = nil) + comps = components(comp) || [] + dependencies.select { |dep| comps.include?(dep.name) } + end + + def self.base_components + %w[thor rake extlib] + end + + def self.all_components + base_components + framework_components + end + + # Find the latest merb-core and gather its dependencies. + # We check for 0.9.8 as a minimum release version. + def self.core_dependencies(gem_dir = nil, ignore_deps = false) + @_core_dependencies ||= begin + if gem_dir # add local gems to index + orig_gem_path = ::Gem.path + ::Gem.clear_paths; ::Gem.path.unshift(gem_dir) + end + deps = [] + merb_core = ::Gem::Dependency.new('merb-core', '>= 0.9.8') + if gemspec = ::Gem.source_index.search(merb_core).last + deps << ::Gem::Dependency.new('merb-core', gemspec.version) + if ignore_deps + deps += gemspec.dependencies.select do |d| + base_components.include?(d.name) + end + else + deps += gemspec.dependencies + end + end + ::Gem.path.replace(orig_gem_path) if gem_dir # reset + deps + end + end + + def self.lookup_repository_name(item) + set_name = nil + # The merb repo contains -more as well, so it needs special attention + return 'merb' if self.repository_sets['merb'].include?(item) + + # Proceed with finding the item in a known component set + self.repository_sets.find do |set, items| + next if set == 'merb' + items.include?(item) ? (set_name = set) : nil + end + set_name + end + + end + +end \ No newline at end of file