Updated to svn tags/tracks-1.6

This commit is contained in:
bsag 2008-06-03 19:40:22 +01:00
parent 103fcb8049
commit 02496f2d44
2274 changed files with 0 additions and 0 deletions

204
vendor/gems/highline-1.4.0/CHANGELOG vendored Normal file
View file

@ -0,0 +1,204 @@
= Change Log
Below is a complete listing of changes for each revision of HighLine.
== 1.4.0
* Made the code grabbing terminal size a little more cross-platform by
adding support for Solaris. (patch by Ronald Braswell and Coey Minear)
== 1.2.9
* Additional work on the backspacing issue. (patch by Jeremy Hinegardner)
* Fixed Readline prompt bug. (patch by Jeremy Hinegardner)
== 1.2.8
* Fixed backspacing past the prompt and interrupting a prompt bugs.
(patch by Jeremy Hinegardner)
== 1.2.7
* Fixed the stty indent bug.
* Fixed the echo backspace bug.
* Added HighLine::track_eof=() setting to work are threaded eof?() calls.
== 1.2.6
Patch by Jeremy Hinegardner:
* Added ColorScheme support.
* Added HighLine::Question.overwrite mode.
* Various documentation fixes.
== 1.2.5
* Really fixed the bug I tried to fix in 1.2.4.
== 1.2.4
* Fixed a crash causing bug when using menus, reported by Patrick Hof.
== 1.2.3
* Treat Cygwin like a Posix OS, instead of a native Windows environment.
== 1.2.2
* Minor documentation corrections.
* Applied Thomas Werschleiln's patch to fix termio buffering on Solaris.
* Applied Justin Bailey's patch to allow canceling paged output.
* Fixed a documentation bug in the description of character case settings.
* Added a notice about termios in HighLine::Question#echo.
* Finally working around the infamous "fast typing" bug
== 1.2.1
* Applied Justin Bailey's fix for the page_print() infinite loop bug.
* Made a SystemExtensions module to expose OS level functionality other
libraries may want to access.
* Publicly exposed the get_character() method, per user requests.
* Added terminal_size(), output_cols(), and output_rows() methods.
* Added :auto setting for warp_at=() and page_at=().
== 1.2.0
* Improved RubyForge and gem spec project descriptions.
* Added basic examples to README.
* Added a VERSION constant.
* Added support for hidden menu commands.
* Added Object.or_ask() when using highline/import.
== 1.0.4
* Moved the HighLine project to Subversion.
* HighLine's color escapes can now be disabled.
* Fixed EOF bug introduced in the last release.
* Updated HighLine web page.
* Moved to a forked development/stable version numbering.
== 1.0.2
* Removed old and broken help tests.
* Fixed test case typo found by David A. Black.
* Added ERb escapes processing to lists, for coloring list items. Color escapes
do not add to list element size.
* HighLine now throws EOFError when input is exhausted.
== 1.0.1
* Minor bug fix: Moved help initialization to before response building, so help
would show up in the default responses.
== 1.0.0
* Fixed documentation typo pointed out by Gavin Kistner.
* Added <tt>gather = ...</tt> option to question for fetching entire Arrays or
Hashes filled with answers. You can set +gather+ to a count of answers to
collect, a String or Regexp matching the end of input, or a Hash where each
key can be used in a new question.
* Added File support to HighLine.ask(). You can specify a _directory_ and a
_glob_ pattern that combine into a list of file choices the user can select
from. You can choose to receive the user's answer as an open filehandle or as
a Pathname object.
* Added Readline support for history and editing.
* Added tab completion for menu and file selection selection (requires
Readline).
* Added an optional character limit for input.
* Added a complete help system to HighLine's shell menu creation tools.
== 0.6.1
* Removed termios dependancy in gem, to fix Windows' install.
== 0.6.0
* Implemented HighLine.choose() for menu handling.
* Provided shortcut <tt>choose(item1, item2, ...)</tt> for simple menus.
* Allowed Ruby code to be attached to each menu item, to create a complete
menu solution.
* Provided for total customization of the menu layout.
* Allowed for menu selection by index, name or both.
* Added a _shell_ mode to allow menu selection with additional details
following the name.
* Added a list() utility method that can be invoked just like color(). It can
layout Arrays for you in any output in the modes <tt>:columns_across</tt>,
<tt>:columns_down</tt>, <tt>:inline</tt> and <tt>:rows</tt>
* Added support for <tt>echo = "*"</tt> style settings. User code can now
choose the echo character this way.
* Modified HighLine to user the "termios" library for character input, if
available. Will return to old behavior (using "stty"), if "termios" cannot be
loaded.
* Improved "stty" state restoring code.
* Fixed "stty" code to handle interrupt signals.
* Improved the default auto-complete error message and exposed this message
through the +responses+ interface as <tt>:no_completion</tt>.
== 0.5.0
* Implemented <tt>echo = false</tt> for HighLine::Question objects, primarily to
make fetching passwords trivial.
* Fixed an auto-complete bug that could cause a crash when the user gave an
answer that didn't complete to any valid choice.
* Implemented +case+ for HighLine::Question objects to provide character case
conversions on given answers. Can be set to <tt>:up</tt>, <tt>:down</tt>, or
<tt>:capitalize</tt>.
* Exposed <tt>@answer</tt> to the response system, to allow response that are
aware of incorrect input.
* Implemented +confirm+ for HighLine::Question objects to allow for verification
for sensitive user choices. If set to +true+, user will have to answer an
"Are you sure? " question. Can also be set to the question to confirm with
the user.
== 0.4.0
* Added <tt>@wrap_at</tt> and <tt>@page_at</tt> settings and accessors to
HighLine, to control text flow.
* Implemented line wrapping with adjustable limit.
* Implemented paged printing with adjustable limit.
== 0.3.0
* Added support for installing with setup.rb.
* All output is now treated as an ERb sequence, allowing Ruby code to be
embedded in output strings.
* Added support for ANSI color sequences in say(). (And everything else
by extension.)
* Added whitespace handling for answers. Can be set to <tt>:strip</tt>,
<tt>:chomp</tt>, <tt>:collapse</tt>, <tt>:strip_and_collapse</tt>,
<tt>:chomp_and_collapse</tt>, <tt>:remove</tt>, or <tt>:none</tt>.
* Exposed question details to ERb completion through @question, to allow for
intelligent responses.
* Simplified HighLine internals using @question.
* Added support for fetching single character input either with getc() or
HighLine's own cross-platform terminal input routine.
* Improved type conversion to handle user defined classes.
== 0.2.0
* Added Unit Tests to cover an already fixed output bug in the future.
* Added Rakefile and setup test action (default).
* Renamed HighLine::Answer to HighLine::Question to better illustrate its role.
* Renamed fetch_line() to get_response() to better define its goal.
* Simplified explain_error in terms of the Question object.
* Renamed accept?() to in_range?() to better define purpose.
* Reworked valid?() into valid_answer?() to better fit Question object.
* Reworked <tt>@member</tt> into <tt>@in</tt>, to make it easier to remember and
switched implementation to include?().
* Added range checks for @above and @below.
* Fixed the bug causing ask() to swallow NoMethodErrors.
* Rolled ask_on_error() into responses.
* Redirected imports to Kernel from Object.
* Added support for <tt>validate = lambda { ... }</tt>.
* Added default answer support.
* Fixed bug that caused ask() to die with an empty question.
* Added complete documentation.
* Improve the implemetation of agree() to be the intended "yes" or "no" only
question.
* Added Rake tasks for documentation and packaging.
* Moved project to RubyForge.
== 0.1.0
* Initial release as the solution to
{Ruby Quiz #29}[http://www.rubyquiz.com/quiz29.html].

35
vendor/gems/highline-1.4.0/INSTALL vendored Normal file
View file

@ -0,0 +1,35 @@
= Installing HighLine
RubyGems is the preferred easy install method for HighLine. However, you can
install HighLine manually as described below.
== Installing the Gem
HighLine is intended to be installed via the
RubyGems[http://rubyforge.org/projects/rubygems/] system. To get the latest
version, simply enter the following into your command prompt:
$ sudo gem install highline
You must have RubyGems[http://rubyforge.org/projects/rubygems/] installed for
the above to work.
== Installing Manually
Download the latest version of HighLine from the
{RubyForge project page}[http://rubyforge.org/frs/?group_id=683]. Navigate to
the root project directory and enter:
$ sudo ruby setup.rb
== Using termios
While not a requirement, HighLine will take advantage of the termios library if
installed (on Unix). This slightly improves HighLine's character reading
capabilities and thus is recommended for all Unix users.
If using the HighLine gem, you should be able to add termios as easily as:
$ sudo gem install termios
For manual installs, consult the termios documentation.

7
vendor/gems/highline-1.4.0/LICENSE vendored Normal file
View file

@ -0,0 +1,7 @@
= License Terms
Distributed under the user's choice of the {GPL Version 2}[http://www.gnu.org/licenses/old-licenses/gpl-2.0.html] (see COPYING for details) or the
{Ruby software license}[http://www.ruby-lang.org/en/LICENSE.txt] by
James Edward Gray II and Greg Brown.
Please email James[mailto:james@grayproductions.net] with any questions.

63
vendor/gems/highline-1.4.0/README vendored Normal file
View file

@ -0,0 +1,63 @@
= Read Me
by James Edward Gray II
== Description
Welcome to HighLine.
HighLine was designed to ease the tedious tasks of doing console input and
output with low-level methods like gets() and puts(). HighLine provides a
robust system for requesting data from a user, without needing to code all the
error checking and validation rules and without needing to convert the typed
Strings into what your program really needs. Just tell HighLine what you're
after, and let it do all the work.
== Documentation
See HighLine and HighLine::Question for documentation.
== Examples
Basic usage:
ask("Company? ") { |q| q.default = "none" }
Validation:
ask("Age? ", Integer) { |q| q.in = 0..105 }
ask("Name? (last, first) ") { |q| q.validate = /\A\w+, ?\w+\Z/ }
Type conversion for answers:
ask("Birthday? ", Date)
ask("Interests? (comma sep list) ", lambda { |str| str.split(/,\s*/) })
Reading passwords:
ask("Enter your password: ") { |q| q.echo = false }
ask("Enter your password: ") { |q| q.echo = "x" }
ERb based output (with HighLine's ANSI color tools):
say("This should be <%= color('bold', BOLD) %>!")
Menus:
choose do |menu|
menu.prompt = "Please choose your favorite programming language? "
menu.choice(:ruby) { say("Good choice!") }
menu.choices(:python, :perl) { say("Not from around here, are you?") }
end
For more examples see the examples/ directory of this project.
== Installing
See the INSTALL file for instructions.
== Questions and/or Comments
Feel free to email {James Edward Gray II}[mailto:james@grayproductions.net] or
{Gregory Brown}[mailto:gregory.t.brown@gmail.com] with any questions.

82
vendor/gems/highline-1.4.0/Rakefile vendored Normal file
View file

@ -0,0 +1,82 @@
require "rake/rdoctask"
require "rake/testtask"
require "rake/gempackagetask"
require "rubygems"
dir = File.dirname(__FILE__)
lib = File.join(dir, "lib", "highline.rb")
version = File.read(lib)[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d)\1/, 2]
task :default => [:test]
Rake::TestTask.new do |test|
test.libs << "test"
test.test_files = [ "test/ts_all.rb" ]
test.verbose = true
end
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_files.include( "README", "INSTALL",
"TODO", "CHANGELOG",
"AUTHORS", "COPYING",
"LICENSE", "lib/" )
rdoc.main = "README"
rdoc.rdoc_dir = "doc/html"
rdoc.title = "HighLine Documentation"
end
desc "Upload current documentation to Rubyforge"
task :upload_docs => [:rdoc] do
sh "scp -r doc/html/* " +
"bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/doc/"
sh "scp -r site/* " +
"bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/"
end
spec = Gem::Specification.new do |spec|
spec.name = "highline"
spec.version = version
spec.platform = Gem::Platform::RUBY
spec.summary = "HighLine is a high-level command-line IO library."
spec.files = Dir.glob("{examples,lib,test}/**/*.rb").
delete_if { |item| item.include?("CVS") } +
["Rakefile", "setup.rb"]
spec.test_suite_file = "test/ts_all.rb"
spec.has_rdoc = true
spec.extra_rdoc_files = %w{README INSTALL TODO CHANGELOG LICENSE}
spec.rdoc_options << '--title' << 'HighLine Documentation' <<
'--main' << 'README'
spec.require_path = 'lib'
spec.author = "James Edward Gray II"
spec.email = "james@grayproductions.net"
spec.rubyforge_project = "highline"
spec.homepage = "http://highline.rubyforge.org"
spec.description = <<END_DESC
A high-level IO library that provides validation, type conversion, and more for
command-line interfaces. HighLine also includes a complete menu system that can
crank out anything from simple list selection to complete shells with just
minutes of work.
END_DESC
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
end
desc "Show library's code statistics"
task :stats do
require 'code_statistics'
CodeStatistics.new( ["HighLine", "lib"],
["Functionals", "examples"],
["Units", "test"] ).to_s
end
desc "Add new files to Subversion"
task :add_to_svn do
sh %Q{svn status | ruby -nae 'system "svn add \#{$F[1]}" if $F[0] == "?"' }
end

6
vendor/gems/highline-1.4.0/TODO vendored Normal file
View file

@ -0,0 +1,6 @@
= To Do List
The following is a list of planned expansions for HighLine, in no particular
order.
* Rent this space.

View file

@ -0,0 +1,38 @@
#!/usr/local/bin/ruby -w
# ansi_colors.rb
#
# Created by James Edward Gray II on 2005-05-03.
# Copyright 2005 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
# Supported color sequences.
colors = %w{black red green yellow blue magenta cyan white}
# Using color() with symbols.
colors.each_with_index do |c, i|
say("This should be <%= color('#{c}', :#{c}) %>!")
if i == 0
say( "This should be " +
"<%= color('white on #{c}', :white, :on_#{c}) %>!")
else
say( "This should be " +
"<%= color( '#{colors[i - 1]} on #{c}',
:#{colors[i - 1]}, :on_#{c} ) %>!")
end
end
# Using color with constants.
say("This should be <%= color('bold', BOLD) %>!")
say("This should be <%= color('underlined', UNDERLINE) %>!")
# Using constants only.
say("This might even <%= BLINK %>blink<%= CLEAR %>!")
# It even works with list wrapping.
erb_digits = %w{Zero One Two Three Four} +
["<%= color('Five', :blue) %%>"] +
%w{Six Seven Eight Nine}
say("<%= list(#{erb_digits.inspect}, :columns_down, 3) %>")

View file

@ -0,0 +1,18 @@
#!/usr/local/bin/ruby -w
# asking_for_arrays.rb
#
# Created by James Edward Gray II on 2005-07-05.
# Copyright 2005 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
require "pp"
grades = ask( "Enter test scores (or a blank line to quit):",
lambda { |ans| ans =~ /^-?\d+$/ ? Integer(ans) : ans} ) do |q|
q.gather = ""
end
say("Grades:")
pp grades

View file

@ -0,0 +1,75 @@
#!/usr/local/bin/ruby -w
# basic_usage.rb
#
# Created by James Edward Gray II on 2005-04-28.
# Copyright 2005 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
require "yaml"
contacts = [ ]
class NameClass
def self.parse( string )
if string =~ /^\s*(\w+),\s*(\w+)\s*$/
self.new($2, $1)
else
raise ArgumentError, "Invalid name format."
end
end
def initialize(first, last)
@first, @last = first, last
end
attr_reader :first, :last
end
begin
entry = Hash.new
# basic output
say("Enter a contact:")
# basic input
entry[:name] = ask("Name? (last, first) ", NameClass) do |q|
q.validate = /\A\w+, ?\w+\Z/
end
entry[:company] = ask("Company? ") { |q| q.default = "none" }
entry[:address] = ask("Address? ")
entry[:city] = ask("City? ")
entry[:state] = ask("State? ") do |q|
q.case = :up
q.validate = /\A[A-Z]{2}\Z/
end
entry[:zip] = ask("Zip? ") do |q|
q.validate = /\A\d{5}(?:-?\d{4})?\Z/
end
entry[:phone] = ask( "Phone? ",
lambda { |p| p.delete("^0-9").
sub(/\A(\d{3})/, '(\1) ').
sub(/(\d{4})\Z/, '-\1') } ) do |q|
q.validate = lambda { |p| p.delete("^0-9").length == 10 }
q.responses[:not_valid] = "Enter a phone numer with area code."
end
entry[:age] = ask("Age? ", Integer) { |q| q.in = 0..105 }
entry[:birthday] = ask("Birthday? ", Date)
entry[:interests] = ask( "Interests? (comma separated list) ",
lambda { |str| str.split(/,\s*/) } )
entry[:description] = ask("Enter a description for this contact.") do |q|
q.whitespace = :strip_and_collapse
end
contacts << entry
# shortcut for yes and no questions
end while agree("Enter another contact? ", true)
if agree("Save these contacts? ", true)
file_name = ask("Enter a file name: ") do |q|
q.validate = /\A\w+\Z/
q.confirm = true
end
File.open("#{file_name}.yaml", "w") { |file| YAML.dump(contacts, file) }
end

View file

@ -0,0 +1,32 @@
#!/usr/bin/env ruby -w
# color_scheme.rb
#
# Created by Jeremy Hinegardner on 2007-01-24
# Copyright 2007 Jeremy Hinegardner. All rights reserved
require 'rubygems'
require 'highline/import'
# Create a color scheme, naming color patterns with symbol names.
ft = HighLine::ColorScheme.new do |cs|
cs[:headline] = [ :bold, :yellow, :on_black ]
cs[:horizontal_line] = [ :bold, :white, :on_blue]
cs[:even_row] = [ :green ]
cs[:odd_row] = [ :magenta ]
end
# Assign that color scheme to HighLine...
HighLine.color_scheme = ft
# ...and use it.
say("<%= color('Headline', :headline) %>")
say("<%= color('-'*20, :horizontal_line) %>")
# Setup a toggle for rows.
i = true
("A".."D").each do |row|
row_color = i ? :even_row : :odd_row
say("<%= color('#{row}', '#{row_color}') %>")
i = !i
end

View file

@ -0,0 +1,65 @@
#!/usr/local/bin/ruby -w
require "rubygems"
require "highline/import"
# The old way, using ask() and say()...
choices = %w{ruby python perl}
say("This is the old way using ask() and say()...")
say("Please choose your favorite programming language:")
say(choices.map { |c| " #{c}\n" }.join)
case ask("? ", choices)
when "ruby"
say("Good choice!")
else
say("Not from around here, are you?")
end
# The new and improved choose()...
say("\nThis is the new mode (default)...")
choose do |menu|
menu.prompt = "Please choose your favorite programming language? "
menu.choice :ruby do say("Good choice!") end
menu.choices(:python, :perl) do say("Not from around here, are you?") end
end
say("\nThis is letter indexing...")
choose do |menu|
menu.index = :letter
menu.index_suffix = ") "
menu.prompt = "Please choose your favorite programming language? "
menu.choice :ruby do say("Good choice!") end
menu.choices(:python, :perl) do say("Not from around here, are you?") end
end
say("\nThis is with a different layout...")
choose do |menu|
menu.layout = :one_line
menu.header = "Languages"
menu.prompt = "Favorite? "
menu.choice :ruby do say("Good choice!") end
menu.choices(:python, :perl) do say("Not from around here, are you?") end
end
say("\nYou can even build shells...")
loop do
choose do |menu|
menu.layout = :menu_only
menu.shell = true
menu.choice(:load, "Load a file.") do |command, details|
say("Loading file with options: #{details}...")
end
menu.choice(:save, "Save a file.") do |command, details|
say("Saving file with options: #{details}...")
end
menu.choice(:quit, "Exit program.") { exit }
end
end

View file

@ -0,0 +1,19 @@
#!/usr/local/bin/ruby -w
# overwrite.rb
#
# Created by Jeremy Hinegardner on 2007-01-24
# Copyright 2007 Jeremy Hinegardner. All rights reserved
require 'rubygems'
require 'highline/import'
prompt = "here is your password:"
ask(
"#{prompt} <%= color('mypassword', RED, BOLD) %> (Press Any Key to blank) "
) do |q|
q.overwrite = true
q.echo = false # overwrite works best when echo is false.
q.character = true # if this is set to :getc then overwrite does not work
end
say("<%= color('Look! blanked out!', GREEN) %>")

View file

@ -0,0 +1,322 @@
#!/usr/local/bin/ruby -w
# page_and_wrap.rb
#
# Created by James Edward Gray II on 2005-05-07.
# Copyright 2005 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
$terminal.wrap_at = 80
$terminal.page_at = 22
say(<<END)
THE UNITED STATES CONSTITUTION
We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.
Article. I.
Section 1.
All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives.
Section. 2.
Clause 1: The House of Representatives shall be composed of Members chosen every second Year by the People of the several States, and the Electors in each State shall have the Qualifications requisite for Electors of the most numerous Branch of the State Legislature.
Clause 2: No Person shall be a Representative who shall not have attained to the Age of twenty five Years, and been seven Years a Citizen of the United States, and who shall not, when elected, be an Inhabitant of that State in which he shall be chosen.
Clause 3: Representatives and direct Taxes shall be apportioned among the several States which may be included within this Union, according to their respective Numbers, which shall be determined by adding to the whole Number of free Persons, including those bound to Service for a Term of Years, and excluding Indians not taxed, three fifths of all other Persons. (See Note 2) The actual Enumeration shall be made within three Years after the first Meeting of the Congress of the United States, and within every subsequent Term of ten Years, in such Manner as they shall by Law direct. The Number of Representatives shall not exceed one for every thirty Thousand, but each State shall have at Least one Representative; and until such enumeration shall be made, the State of New Hampshire shall be entitled to chuse three, Massachusetts eight, Rhode-Island and Providence Plantations one, Connecticut five, New-York six, New Jersey four, Pennsylvania eight, Delaware one, Maryland six, Virginia ten, North Carolina five, South Carolina five, and Georgia three.
Clause 4: When vacancies happen in the Representation from any State, the Executive Authority thereof shall issue Writs of Election to fill such Vacancies.
Clause 5: The House of Representatives shall chuse their Speaker and other Officers; and shall have the sole Power of Impeachment.
Section. 3.
Clause 1: The Senate of the United States shall be composed of two Senators from each State, chosen by the Legislature thereof, (See Note 3) for six Years; and each Senator shall have one Vote.
Clause 2: Immediately after they shall be assembled in Consequence of the first Election, they shall be divided as equally as may be into three Classes. The Seats of the Senators of the first Class shall be vacated at the Expiration of the second Year, of the second Class at the Expiration of the fourth Year, and of the third Class at the Expiration of the sixth Year, so that one third may be chosen every second Year; and if Vacancies happen by Resignation, or otherwise, during the Recess of the Legislature of any State, the Executive thereof may make temporary Appointments until the next Meeting of the Legislature, which shall then fill such Vacancies. (See Note 4)
Clause 3: No Person shall be a Senator who shall not have attained to the Age of thirty Years, and been nine Years a Citizen of the United States, and who shall not, when elected, be an Inhabitant of that State for which he shall be chosen.
Clause 4: The Vice President of the United States shall be President of the Senate, but shall have no Vote, unless they be equally divided.
Clause 5: The Senate shall chuse their other Officers, and also a President pro tempore, in the Absence of the Vice President, or when he shall exercise the Office of President of the United States.
Clause 6: The Senate shall have the sole Power to try all Impeachments. When sitting for that Purpose, they shall be on Oath or Affirmation. When the President of the United States is tried, the Chief Justice shall preside: And no Person shall be convicted without the Concurrence of two thirds of the Members present.
Clause 7: Judgment in Cases of Impeachment shall not extend further than to removal from Office, and disqualification to hold and enjoy any Office of honor, Trust or Profit under the United States: but the Party convicted shall nevertheless be liable and subject to Indictment, Trial, Judgment and Punishment, according to Law.
Section. 4.
Clause 1: The Times, Places and Manner of holding Elections for Senators and Representatives, shall be prescribed in each State by the Legislature thereof; but the Congress may at any time by Law make or alter such Regulations, except as to the Places of chusing Senators.
Clause 2: The Congress shall assemble at least once in every Year, and such Meeting shall be on the first Monday in December, (See Note 5) unless they shall by Law appoint a different Day.
Section. 5.
Clause 1: Each House shall be the Judge of the Elections, Returns and Qualifications of its own Members, and a Majority of each shall constitute a Quorum to do Business; but a smaller Number may adjourn from day to day, and may be authorized to compel the Attendance of absent Members, in such Manner, and under such Penalties as each House may provide.
Clause 2: Each House may determine the Rules of its Proceedings, punish its Members for disorderly Behaviour, and, with the Concurrence of two thirds, expel a Member.
Clause 3: Each House shall keep a Journal of its Proceedings, and from time to time publish the same, excepting such Parts as may in their Judgment require Secrecy; and the Yeas and Nays of the Members of either House on any question shall, at the Desire of one fifth of those Present, be entered on the Journal.
Clause 4: Neither House, during the Session of Congress, shall, without the Consent of the other, adjourn for more than three days, nor to any other Place than that in which the two Houses shall be sitting.
Section. 6.
Clause 1: The Senators and Representatives shall receive a Compensation for their Services, to be ascertained by Law, and paid out of the Treasury of the United States. (See Note 6) They shall in all Cases, except Treason, Felony and Breach of the Peace, beprivileged from Arrest during their Attendance at the Session of their respective Houses, and in going to and returning from the same; and for any Speech or Debate in either House, they shall not be questioned in any other Place.
Clause 2: No Senator or Representative shall, during the Time for which he was elected, be appointed to any civil Office under the Authority of the United States, which shall have been created, or the Emoluments whereof shall have been encreased during such time; and no Person holding any Office under the United States, shall be a Member of either House during his Continuance in Office.
Section. 7.
Clause 1: All Bills for raising Revenue shall originate in the House of Representatives; but the Senate may propose or concur with Amendments as on other Bills.
Clause 2: Every Bill which shall have passed the House of Representatives and the Senate, shall, before it become a Law, be presented to the President of the United States; If he approve he shall sign it, but if not he shall return it, with his Objections to that House in which it shall have originated, who shall enter the Objections at large on their Journal, and proceed to reconsider it. If after such Reconsideration two thirds of that House shall agree to pass the Bill, it shall be sent, together with the Objections, to the other House, by which it shall likewise be reconsidered, and if approved by two thirds of that House, it shall become a Law. But in all such Cases the Votes of both Houses shall be determined by yeas and Nays, and the Names of the Persons voting for and against the Bill shall be entered on the Journal of each House respectively. If any Bill shall not be returned by the President within ten Days (Sundays excepted) after it shall have been presented to him, the Same shall be a Law, in like Manner as if he had signed it, unless the Congress by their Adjournment prevent its Return, in which Case it shall not be a Law.
Clause 3: Every Order, Resolution, or Vote to which the Concurrence of the Senate and House of Representatives may be necessary (except on a question of Adjournment) shall be presented to the President of the United States; and before the Same shall take Effect, shall be approved by him, or being disapproved by him, shall be repassed by two thirds of the Senate and House of Representatives, according to the Rules and Limitations prescribed in the Case of a Bill.
Section. 8.
Clause 1: The Congress shall have Power To lay and collect Taxes, Duties, Imposts and Excises, to pay the Debts and provide for the common Defence and general Welfare of the United States; but all Duties, Imposts and Excises shall be uniform throughout the United States;
Clause 2: To borrow Money on the credit of the United States;
Clause 3: To regulate Commerce with foreign Nations, and among the several States, and with the Indian Tribes;
Clause 4: To establish an uniform Rule of Naturalization, and uniform Laws on the subject of Bankruptcies throughout the United States;
Clause 5: To coin Money, regulate the Value thereof, and of foreign Coin, and fix the Standard of Weights and Measures;
Clause 6: To provide for the Punishment of counterfeiting the Securities and current Coin of the United States;
Clause 7: To establish Post Offices and post Roads;
Clause 8: To promote the Progress of Science and useful Arts, by securing for limited Times to Authors and Inventors the exclusive Right to their respective Writings and Discoveries;
Clause 9: To constitute Tribunals inferior to the supreme Court;
Clause 10: To define and punish Piracies and Felonies committed on the high Seas, and Offences against the Law of Nations;
Clause 11: To declare War, grant Letters of Marque and Reprisal, and make Rules concerning Captures on Land and Water;
Clause 12: To raise and support Armies, but no Appropriation of Money to that Use shall be for a longer Term than two Years;
Clause 13: To provide and maintain a Navy;
Clause 14: To make Rules for the Government and Regulation of the land and naval Forces;
Clause 15: To provide for calling forth the Militia to execute the Laws of the Union, suppress Insurrections and repel Invasions;
Clause 16: To provide for organizing, arming, and disciplining, the Militia, and for governing such Part of them as may be employed in the Service of the United States, reserving to the States respectively, the Appointment of the Officers, and the Authority of training the Militia according to the discipline prescribed by Congress;
Clause 17: To exercise exclusive Legislation in all Cases whatsoever, over such District (not exceeding ten Miles square) as may, byCession of particular States, and the Acceptance of Congress, become the Seat of the Government of the United States, and to exercise like Authority over all Places purchased by the Consent of the Legislature of the State in which the Same shall be, for the Erection of Forts, Magazines, Arsenals, dock-Yards, and other needful Buildings;--And
Clause 18: To make all Laws which shall be necessary and proper for carrying into Execution the foregoing Powers, and all other Powers vested by this Constitution in the Government of the United States, or in any Department or Officer thereof.
Section. 9.
Clause 1: The Migration or Importation of such Persons as any of the States now existing shall think proper to admit, shall not be prohibited by the Congress prior to the Year one thousand eight hundred and eight, but a Tax or duty may be imposed on such Importation, not exceeding ten dollars for each Person.
Clause 2: The Privilege of the Writ of Habeas Corpus shall not be suspended, unless when in Cases of Rebellion or Invasion the public Safety may require it.
Clause 3: No Bill of Attainder or ex post facto Law shall be passed.
Clause 4: No Capitation, or other direct, Tax shall be laid, unless in Proportion to the Census or Enumeration herein before directed to be taken. (See Note 7)
Clause 5: No Tax or Duty shall be laid on Articles exported from any State.
Clause 6: No Preference shall be given by any Regulation of Commerce or Revenue to the Ports of one State over those of another: nor shall Vessels bound to, or from, one State, be obliged to enter, clear, or pay Duties in another.
Clause 7: No Money shall be drawn from the Treasury, but in Consequence of Appropriations made by Law; and a regular Statement and Account of the Receipts and Expenditures of all public Money shall be published from time to time.
Clause 8: No Title of Nobility shall be granted by the United States: And no Person holding any Office of Profit or Trust under them, shall, without the Consent of the Congress, accept of any present, Emolument, Office, or Title, of any kind whatever, from any King, Prince, or foreign State.
Section. 10.
Clause 1: No State shall enter into any Treaty, Alliance, or Confederation; grant Letters of Marque and Reprisal; coin Money; emit Bills of Credit; make any Thing but gold and silver Coin a Tender in Payment of Debts; pass any Bill of Attainder, ex post facto Law, or Law impairing the Obligation of Contracts, or grant any Title of Nobility.
Clause 2: No State shall, without the Consent of the Congress, lay any Imposts or Duties on Imports or Exports, except what may be absolutely necessary for executing it's inspection Laws: and the net Produce of all Duties and Imposts, laid by any State on Imports or Exports, shall be for the Use of the Treasury of the United States; and all such Laws shall be subject to the Revision and Controul of the Congress.
Clause 3: No State shall, without the Consent of Congress, lay any Duty of Tonnage, keep Troops, or Ships of War in time of Peace, enter into any Agreement or Compact with another State, or with a foreign Power, or engage in War, unless actually invaded, or in such imminent Danger as will not admit of delay.
Article. II.
Section. 1.
Clause 1: The executive Power shall be vested in a President of the United States of America. He shall hold his Office during the Term of four Years, and, together with the Vice President, chosen for the same Term, be elected, as follows
Clause 2: Each State shall appoint, in such Manner as the Legislature thereof may direct, a Number of Electors, equal to the whole Number of Senators and Representatives to which the State may be entitled in the Congress: but no Senator or Representative, or Person holding an Office of Trust or Profit under the United States, shall be appointed an Elector.
Clause 3: The Electors shall meet in their respective States, and vote by Ballot for two Persons, of whom one at least shall not be an Inhabitant of the same State with themselves. And they shall make a List of all the Persons voted for, and of the Number of Votes for each; which List they shall sign and certify, and transmit sealed to the Seat of the Government of the United States, directed to the President of the Senate. The President of the Senate shall, in the Presence of the Senate and House of Representatives, open all the Certificates, and the Votes shall then be counted. The Person having the greatest Number of Votes shall be the President, if such Number be a Majority of the whole Number of Electors appointed; and if there be more than one who have such Majority, and have an equal Number of Votes, then the House of Representatives shall immediately chuse by Ballot one of them for President; and if no Person have a Majority, then from the five highest on the List the said House shall in like Manner chuse the President. But in chusing the President, the Votes shall be taken by States, the Representation from each State having one Vote; A quorum for this Purpose shall consist of a Member or Members from two thirds of the States, and a Majority of all the States shall be necessary to a Choice. In every Case, after the Choice of the President, the Person having the greatest Number of Votes of the Electors shall be the Vice President. But if there should remain two or more who have equal Votes, the Senate shall chuse from them by Ballot the Vice President. (See Note 8)
Clause 4: The Congress may determine the Time of chusing the Electors, and the Day on which they shall give their Votes; which Day shall be the same throughout the United States.
Clause 5: No Person except a natural born Citizen, or a Citizen of the United States, at the time of the Adoption of this Constitution, shall be eligible to the Office of President; neither shall any Person be eligible to that Office who shall not have attained to the Age of thirty five Years, and been fourteen Years a Resident within the United States.
Clause 6: In Case of the Removal of the President from Office, or of his Death, Resignation, or Inability to discharge the Powers and Duties of the said Office, (See Note 9) the Same shall devolve on the VicePresident, and the Congress may by Law provide for the Case of Removal, Death, Resignation or Inability, both of the President and Vice President, declaring what Officer shall then act as President, and such Officer shall act accordingly, until the Disability be removed, or a President shall be elected.
Clause 7: The President shall, at stated Times, receive for his Services, a Compensation, which shall neither be encreased nor diminished during the Period for which he shall have been elected, and he shall not receive within that Period any other Emolument from the United States, or any of them.
Clause 8: Before he enter on the Execution of his Office, he shall take the following Oath or Affirmation:--"I do solemnly swear (or affirm) that I will faithfully execute the Office of President of the United States, and will to the best of my Ability, preserve, protect and defend the Constitution of the United States."
Section. 2.
Clause 1: The President shall be Commander in Chief of the Army and Navy of the United States, and of the Militia of the several States, when called into the actual Service of the United States; he may require the Opinion, in writing, of the principal Officer in each of the executive Departments, upon any Subject relating to the Duties of their respective Offices, and he shall have Power to grant Reprieves and Pardons for Offences against the United States, except in Cases of Impeachment.
Clause 2: He shall have Power, by and with the Advice and Consent of the Senate, to make Treaties, provided two thirds of the Senators present concur; and he shall nominate, and by and with the Advice and Consent of the Senate, shall appoint Ambassadors, other public Ministers and Consuls, Judges of the supreme Court, and all other Officers of the United States, whose Appointments are not herein otherwise provided for, and which shall be established by Law: but the Congress may by Law vest the Appointment of such inferior Officers, as they think proper, in the President alone, in the Courts of Law, or in the Heads of Departments.
Clause 3: The President shall have Power to fill up all Vacancies that may happen during the Recess of the Senate, by granting Commissions which shall expire at the End of their next Session.
Section. 3.
He shall from time to time give to the Congress Information of the State of the Union, and recommend to their Consideration such Measures as he shall judge necessary and expedient; he may, on extraordinary Occasions, convene both Houses, or either of them, and in Case of Disagreement between them, with Respect to the Time of Adjournment, he may adjourn them to such Time as he shall think proper; he shall receive Ambassadors and other public Ministers; he shall take Care that the Laws be faithfully executed, and shall Commission all the Officers of the United States.
Section. 4.
The President, Vice President and all civil Officers of the United States, shall be removed from Office on Impeachment for, and Conviction of, Treason, Bribery, or other high Crimes and Misdemeanors.
Article. III.
Section. 1.
The judicial Power of the United States, shall be vested in one supreme Court, and in such inferior Courts as the Congress may from time to time ordain and establish. The Judges, both of the supreme and inferior Courts, shall hold their Offices during good Behaviour, and shall, at stated Times, receive for their Services, a Compensation, which shall not be diminished during their Continuance in Office.
Section. 2.
Clause 1: The judicial Power shall extend to all Cases, in Law and Equity, arising under this Constitution, the Laws of the United States, and Treaties made, or which shall be made, under their Authority;--to all Cases affecting Ambassadors, other public Ministers and Consuls;--to all Cases of admiralty and maritime Jurisdiction;--to Controversies to which the United States shall be a Party;--to Controversies between two or more States;--between a State and Citizens of another State; (See Note 10)--between Citizens of different States, --between Citizens of the same State claiming Lands under Grants of different States, and between a State, or the Citizens thereof, and foreign States, Citizens or Subjects.
Clause 2: In all Cases affecting Ambassadors, other public Ministers and Consuls, and those in which a State shall be Party, the supreme Court shall have original Jurisdiction. In all the other Cases before mentioned, the supreme Court shall have appellate Jurisdiction, both as to Law and Fact, with such Exceptions, and under such Regulations as the Congress shall make.
Clause 3: The Trial of all Crimes, except in Cases of Impeachment, shall be by Jury; and such Trial shall be held in the State where the said Crimes shall have been committed; but when not committed within any State, the Trial shall be at such Place or Places as the Congress may by Law have directed.
Section. 3.
Clause 1: Treason against the United States, shall consist only in levying War against them, or in adhering to their Enemies, giving them Aid and Comfort. No Person shall be convicted of Treason unless on the Testimony of two Witnesses to the same overt Act, or on Confession in open Court.
Clause 2: The Congress shall have Power to declare the Punishment of Treason, but no Attainder of Treason shall work Corruption of Blood, or Forfeiture except during the Life of the Person attainted.
Article. IV.
Section. 1.
Full Faith and Credit shall be given in each State to the public Acts, Records, and judicial Proceedings of every other State. And the Congress may by general Laws prescribe the Manner in which such Acts, Records and Proceedings shall be proved, and the Effect thereof.
Section. 2.
Clause 1: The Citizens of each State shall be entitled to all Privileges and Immunities of Citizens in the several States.
Clause 2: A Person charged in any State with Treason, Felony, or other Crime, who shall flee from Justice, and be found in another State, shall on Demand of the executive Authority of the State from which he fled, be delivered up, to be removed to the State having Jurisdiction of the Crime.
Clause 3: No Person held to Service or Labour in one State, under the Laws thereof, escaping into another, shall, in Consequence of any Law or Regulation therein, be discharged from such Service or Labour, but shall be delivered up on Claim of the Party to whom such Service or Labour may be due. (See Note 11)
Section. 3.
Clause 1: New States may be admitted by the Congress into this Union; but no new State shall be formed or erected within the Jurisdiction of any other State; nor any State be formed by the Junction of two or more States, or Parts of States, without the Consent of the Legislatures of the States concerned as well as of the Congress.
Clause 2: The Congress shall have Power to dispose of and make all needful Rules and Regulations respecting the Territory or other Property belonging to the United States; and nothing in this Constitution shall be so construed as to Prejudice any Claims of the United States, or of any particular State.
Section. 4.
The United States shall guarantee to every State in this Union a Republican Form of Government, and shall protect each of them against Invasion; and on Application of the Legislature, or of the Executive (when the Legislature cannot be convened) against domestic Violence.
Article. V.
The Congress, whenever two thirds of both Houses shall deem it necessary, shall propose Amendments to this Constitution, or, on the Application of the Legislatures of two thirds of the several States, shall call a Convention for proposing Amendments, which, in either Case, shall be valid to all Intents and Purposes, as Part of this Constitution, when ratified by the Legislatures of three fourths of the several States, or by Conventions in three fourths thereof, as the one or the other Mode of Ratification may be proposed by the Congress; Provided that no Amendment which may be made prior to the Year One thousand eight hundred and eight shall in any Manner affect the first and fourth Clauses in the Ninth Section of the first Article; and that no State, without its Consent, shall be deprived of its equal Suffrage in the Senate.
Article. VI.
Clause 1: All Debts contracted and Engagements entered into, before the Adoption of this Constitution, shall be as valid against the United States under this Constitution, as under the Confederation.
Clause 2: This Constitution, and the Laws of the United States which shall be made in Pursuance thereof; and all Treaties made, or which shall be made, under the Authority of the United States, shall be the supreme Law of the Land; and the Judges in every State shall be bound thereby, any Thing in the Constitution or Laws of any State to the Contrary notwithstanding.
Clause 3: The Senators and Representatives before mentioned, and the Members of the several State Legislatures, and all executive and judicial Officers, both of the United States and of the several States, shall be bound by Oath or Affirmation, to support this Constitution; but no religious Test shall ever be required as a Qualification to any Office or public Trust under the United States.
Article. VII.
The Ratification of the Conventions of nine States, shall be sufficient for the Establishment of this Constitution between the States so ratifying the Same.
done in Convention by the Unanimous Consent of the States present the Seventeenth Day of September in the Year of our Lord one thousand seven hundred and Eighty seven and of the Independence of the United States of America the Twelfth In witness whereof We have hereunto subscribed our Names,
GO WASHINGTON--Presidt. and deputy from Virginia
[Signed also by the deputies of twelve States.]
Delaware
Geo: Read
Gunning Bedford jun
John Dickinson
Richard Bassett
Jaco: Broom
Maryland
James MCHenry
Dan of ST ThoS. Jenifer
DanL Carroll.
Virginia
John Blair--
James Madison Jr.
North Carolina
WM Blount
RichD. Dobbs Spaight.
Hu Williamson
South Carolina
J. Rutledge
Charles 1ACotesworth Pinckney
Charles Pinckney
Pierce Butler.
Georgia
William Few
Abr Baldwin
New Hampshire
John Langdon
Nicholas Gilman
Massachusetts
Nathaniel Gorham
Rufus King
Connecticut
WM. SamL. Johnson
Roger Sherman
New York
Alexander Hamilton
New Jersey
Wil: Livingston
David Brearley.
WM. Paterson.
Jona: Dayton
Pennsylvania
B Franklin
Thomas Mifflin
RobT Morris
Geo. Clymer
ThoS. FitzSimons
Jared Ingersoll
James Wilson.
Gouv Morris
Attest William Jackson Secretary
END

View file

@ -0,0 +1,7 @@
#!/usr/local/bin/ruby -w
require "rubygems"
require "highline/import"
pass = ask("Enter your password: ") { |q| q.echo = false }
puts "Your password is #{pass}!"

View file

@ -0,0 +1,22 @@
#!/usr/local/bin/ruby -w
# trapping_eof.rb
#
# Created by James Edward Gray II on 2006-02-20.
# Copyright 2006 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
loop do
begin
name = ask("What's your name?")
break if name == "exit"
puts "Hello, #{name}!"
rescue EOFError # HighLine throws this if @input.eof?
break
end
end
puts "Goodbye, dear friend."
exit

View file

@ -0,0 +1,17 @@
#!/usr/local/bin/ruby -w
# using_readline.rb
#
# Created by James Edward Gray II on 2005-07-06.
# Copyright 2005 Gray Productions. All rights reserved.
require "rubygems"
require "highline/import"
loop do
cmd = ask("Enter command: ", %w{save load reset quit}) do |q|
q.readline = true
end
say("Executing \"#{cmd}\"...")
break if cmd == "quit"
end

View file

@ -0,0 +1,744 @@
#!/usr/local/bin/ruby -w
# highline.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# See HighLine for documentation.
#
# This is Free Software. See LICENSE and COPYING for details.
require "highline/system_extensions"
require "highline/question"
require "highline/menu"
require "highline/color_scheme"
require "erb"
require "optparse"
require "stringio"
require "abbrev"
#
# A HighLine object is a "high-level line oriented" shell over an input and an
# output stream. HighLine simplifies common console interaction, effectively
# replacing puts() and gets(). User code can simply specify the question to ask
# and any details about user interaction, then leave the rest of the work to
# HighLine. When HighLine.ask() returns, you'll have the answer you requested,
# even if HighLine had to ask many times, validate results, perform range
# checking, convert types, etc.
#
class HighLine
# The version of the installed library.
VERSION = "1.4.0".freeze
# An internal HighLine error. User code does not need to trap this.
class QuestionError < StandardError
# do nothing, just creating a unique error type
end
# The setting used to disable color output.
@@use_color = true
# Pass +false+ to _setting_ to turn off HighLine's color escapes.
def self.use_color=( setting )
@@use_color = setting
end
# Returns true if HighLine is currently using color escapes.
def self.use_color?
@@use_color
end
# The setting used to disable EOF tracking.
@@track_eof = true
# Pass +false+ to _setting_ to turn off HighLine's EOF tracking.
def self.track_eof=( setting )
@@track_eof = setting
end
# Returns true if HighLine is currently tracking EOF for input.
def self.track_eof?
@@track_eof
end
# The setting used to control color schemes.
@@color_scheme = nil
# Pass ColorScheme to _setting_ to turn set a HighLine color scheme.
def self.color_scheme=( setting )
@@color_scheme = setting
end
# Returns the current color scheme.
def self.color_scheme
@@color_scheme
end
# Returns +true+ if HighLine is currently using a color scheme.
def self.using_color_scheme?
not @@color_scheme.nil?
end
#
# Embed in a String to clear all previous ANSI sequences. This *MUST* be
# done before the program exits!
#
CLEAR = "\e[0m"
# An alias for CLEAR.
RESET = CLEAR
# Erase the current line of terminal output.
ERASE_LINE = "\e[K"
# Erase the character under the cursor.
ERASE_CHAR = "\e[P"
# The start of an ANSI bold sequence.
BOLD = "\e[1m"
# The start of an ANSI dark sequence. (Terminal support uncommon.)
DARK = "\e[2m"
# The start of an ANSI underline sequence.
UNDERLINE = "\e[4m"
# An alias for UNDERLINE.
UNDERSCORE = UNDERLINE
# The start of an ANSI blink sequence. (Terminal support uncommon.)
BLINK = "\e[5m"
# The start of an ANSI reverse sequence.
REVERSE = "\e[7m"
# The start of an ANSI concealed sequence. (Terminal support uncommon.)
CONCEALED = "\e[8m"
# Set the terminal's foreground ANSI color to black.
BLACK = "\e[30m"
# Set the terminal's foreground ANSI color to red.
RED = "\e[31m"
# Set the terminal's foreground ANSI color to green.
GREEN = "\e[32m"
# Set the terminal's foreground ANSI color to yellow.
YELLOW = "\e[33m"
# Set the terminal's foreground ANSI color to blue.
BLUE = "\e[34m"
# Set the terminal's foreground ANSI color to magenta.
MAGENTA = "\e[35m"
# Set the terminal's foreground ANSI color to cyan.
CYAN = "\e[36m"
# Set the terminal's foreground ANSI color to white.
WHITE = "\e[37m"
# Set the terminal's background ANSI color to black.
ON_BLACK = "\e[40m"
# Set the terminal's background ANSI color to red.
ON_RED = "\e[41m"
# Set the terminal's background ANSI color to green.
ON_GREEN = "\e[42m"
# Set the terminal's background ANSI color to yellow.
ON_YELLOW = "\e[43m"
# Set the terminal's background ANSI color to blue.
ON_BLUE = "\e[44m"
# Set the terminal's background ANSI color to magenta.
ON_MAGENTA = "\e[45m"
# Set the terminal's background ANSI color to cyan.
ON_CYAN = "\e[46m"
# Set the terminal's background ANSI color to white.
ON_WHITE = "\e[47m"
#
# Create an instance of HighLine, connected to the streams _input_
# and _output_.
#
def initialize( input = $stdin, output = $stdout,
wrap_at = nil, page_at = nil )
@input = input
@output = output
self.wrap_at = wrap_at
self.page_at = page_at
@question = nil
@answer = nil
@menu = nil
@header = nil
@prompt = nil
@gather = nil
@answers = nil
@key = nil
end
include HighLine::SystemExtensions
# The current column setting for wrapping output.
attr_reader :wrap_at
# The current row setting for paging output.
attr_reader :page_at
#
# A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
# answers ("y" and "n" are allowed) and returns +true+ or +false+
# (+true+ for "yes"). If provided a +true+ value, _character_ will cause
# HighLine to fetch a single character response.
#
# Raises EOFError if input is exhausted.
#
def agree( yes_or_no_question, character = nil )
ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
q.validate = /\Ay(?:es)?|no?\Z/i
q.responses[:not_valid] = 'Please enter "yes" or "no".'
q.responses[:ask_on_error] = :question
q.character = character
end
end
#
# This method is the primary interface for user input. Just provide a
# _question_ to ask the user, the _answer_type_ you want returned, and
# optionally a code block setting up details of how you want the question
# handled. See HighLine.say() for details on the format of _question_, and
# HighLine::Question for more information about _answer_type_ and what's
# valid in the code block.
#
# If <tt>@question</tt> is set before ask() is called, parameters are
# ignored and that object (must be a HighLine::Question) is used to drive
# the process instead.
#
# Raises EOFError if input is exhausted.
#
def ask( question, answer_type = String, &details ) # :yields: question
@question ||= Question.new(question, answer_type, &details)
return gather if @question.gather
# readline() needs to handle it's own output, but readline only supports
# full line reading. Therefore if @question.echo is anything but true,
# the prompt will not be issued. And we have to account for that now.
say(@question) unless (@question.readline and @question.echo == true)
begin
@answer = @question.answer_or_default(get_response)
unless @question.valid_answer?(@answer)
explain_error(:not_valid)
raise QuestionError
end
@answer = @question.convert(@answer)
if @question.in_range?(@answer)
if @question.confirm
# need to add a layer of scope to ask a question inside a
# question, without destroying instance data
context_change = self.class.new(@input, @output, @wrap_at, @page_at)
if @question.confirm == true
confirm_question = "Are you sure? "
else
# evaluate ERb under initial scope, so it will have
# access to @question and @answer
template = ERB.new(@question.confirm, nil, "%")
confirm_question = template.result(binding)
end
unless context_change.agree(confirm_question)
explain_error(nil)
raise QuestionError
end
end
@answer
else
explain_error(:not_in_range)
raise QuestionError
end
rescue QuestionError
retry
rescue ArgumentError
explain_error(:invalid_type)
retry
rescue Question::NoAutoCompleteMatch
explain_error(:no_completion)
retry
rescue NameError
raise if $!.is_a?(NoMethodError)
explain_error(:ambiguous_completion)
retry
ensure
@question = nil # Reset Question object.
end
end
#
# This method is HighLine's menu handler. For simple usage, you can just
# pass all the menu items you wish to display. At that point, choose() will
# build and display a menu, walk the user through selection, and return
# their choice amoung the provided items. You might use this in a case
# statement for quick and dirty menus.
#
# However, choose() is capable of much more. If provided, a block will be
# passed a HighLine::Menu object to configure. Using this method, you can
# customize all the details of menu handling from index display, to building
# a complete shell-like menuing system. See HighLine::Menu for all the
# methods it responds to.
#
# Raises EOFError if input is exhausted.
#
def choose( *items, &details )
@menu = @question = Menu.new(&details)
@menu.choices(*items) unless items.empty?
# Set _answer_type_ so we can double as the Question for ask().
@menu.answer_type = if @menu.shell
lambda do |command| # shell-style selection
first_word = command.to_s.split.first || ""
options = @menu.options
options.extend(OptionParser::Completion)
answer = options.complete(first_word)
if answer.nil?
raise Question::NoAutoCompleteMatch
end
[answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
end
else
@menu.options # normal menu selection, by index or name
end
# Provide hooks for ERb layouts.
@header = @menu.header
@prompt = @menu.prompt
if @menu.shell
selected = ask("Ignored", @menu.answer_type)
@menu.select(self, *selected)
else
selected = ask("Ignored", @menu.answer_type)
@menu.select(self, selected)
end
end
#
# This method provides easy access to ANSI color sequences, without the user
# needing to remember to CLEAR at the end of each sequence. Just pass the
# _string_ to color, followed by a list of _colors_ you would like it to be
# affected by. The _colors_ can be HighLine class constants, or symbols
# (:blue for BLUE, for example). A CLEAR will automatically be embedded to
# the end of the returned String.
#
# This method returns the original _string_ unchanged if HighLine::use_color?
# is +false+.
#
def color( string, *colors )
return string unless self.class.use_color?
colors.map! do |c|
if self.class.using_color_scheme? and self.class.color_scheme.include? c
self.class.color_scheme[c]
elsif c.is_a? Symbol
self.class.const_get(c.to_s.upcase)
else
c
end
end
"#{colors.flatten.join}#{string}#{CLEAR}"
end
#
# This method is a utility for quickly and easily laying out lists. It can
# be accessed within ERb replacements of any text that will be sent to the
# user.
#
# The only required parameter is _items_, which should be the Array of items
# to list. A specified _mode_ controls how that list is formed and _option_
# has different effects, depending on the _mode_. Recognized modes are:
#
# <tt>:columns_across</tt>:: _items_ will be placed in columns, flowing
# from left to right. If given, _option_ is the
# number of columns to be used. When absent,
# columns will be determined based on _wrap_at_
# or a default of 80 characters.
# <tt>:columns_down</tt>:: Identical to <tt>:columns_across</tt>, save
# flow goes down.
# <tt>:inline</tt>:: All _items_ are placed on a single line. The
# last two _items_ are separated by _option_ or
# a default of " or ". All other _items_ are
# separated by ", ".
# <tt>:rows</tt>:: The default mode. Each of the _items_ is
# placed on it's own line. The _option_
# parameter is ignored in this mode.
#
# Each member of the _items_ Array is passed through ERb and thus can contain
# their own expansions. Color escape expansions do not contribute to the
# final field width.
#
def list( items, mode = :rows, option = nil )
items = items.to_ary.map do |item|
ERB.new(item, nil, "%").result(binding)
end
case mode
when :inline
option = " or " if option.nil?
case items.size
when 0
""
when 1
items.first
when 2
"#{items.first}#{option}#{items.last}"
else
items[0..-2].join(", ") + "#{option}#{items.last}"
end
when :columns_across, :columns_down
max_length = actual_length(
items.max { |a, b| actual_length(a) <=> actual_length(b) }
)
if option.nil?
limit = @wrap_at || 80
option = (limit + 2) / (max_length + 2)
end
items = items.map do |item|
pad = max_length + (item.length - actual_length(item))
"%-#{pad}s" % item
end
row_count = (items.size / option.to_f).ceil
if mode == :columns_across
rows = Array.new(row_count) { Array.new }
items.each_with_index do |item, index|
rows[index / option] << item
end
rows.map { |row| row.join(" ") + "\n" }.join
else
columns = Array.new(option) { Array.new }
items.each_with_index do |item, index|
columns[index / row_count] << item
end
list = ""
columns.first.size.times do |index|
list << columns.map { |column| column[index] }.
compact.join(" ") + "\n"
end
list
end
else
items.map { |i| "#{i}\n" }.join
end
end
#
# The basic output method for HighLine objects. If the provided _statement_
# ends with a space or tab character, a newline will not be appended (output
# will be flush()ed). All other cases are passed straight to Kernel.puts().
#
# The _statement_ parameter is processed as an ERb template, supporting
# embedded Ruby code. The template is evaluated with a binding inside
# the HighLine instance, providing easy access to the ANSI color constants
# and the HighLine.color() method.
#
def say( statement )
statement = statement.to_str
return unless statement.length > 0
template = ERB.new(statement, nil, "%")
statement = template.result(binding)
statement = wrap(statement) unless @wrap_at.nil?
statement = page_print(statement) unless @page_at.nil?
if statement[-1, 1] == " " or statement[-1, 1] == "\t"
@output.print(statement)
@output.flush
else
@output.puts(statement)
end
end
#
# Set to an integer value to cause HighLine to wrap output lines at the
# indicated character limit. When +nil+, the default, no wrapping occurs. If
# set to <tt>:auto</tt>, HighLine will attempt to determing the columns
# available for the <tt>@output</tt> or use a sensible default.
#
def wrap_at=( setting )
@wrap_at = setting == :auto ? output_cols : setting
end
#
# Set to an integer value to cause HighLine to page output lines over the
# indicated line limit. When +nil+, the default, no paging occurs. If
# set to <tt>:auto</tt>, HighLine will attempt to determing the rows available
# for the <tt>@output</tt> or use a sensible default.
#
def page_at=( setting )
@page_at = setting == :auto ? output_rows : setting
end
#
# Returns the number of columns for the console, or a default it they cannot
# be determined.
#
def output_cols
return 80 unless @output.tty?
terminal_size.first
rescue
return 80
end
#
# Returns the number of rows for the console, or a default if they cannot be
# determined.
#
def output_rows
return 24 unless @output.tty?
terminal_size.last
rescue
return 24
end
private
#
# A helper method for sending the output stream and error and repeat
# of the question.
#
def explain_error( error )
say(@question.responses[error]) unless error.nil?
if @question.responses[:ask_on_error] == :question
say(@question)
elsif @question.responses[:ask_on_error]
say(@question.responses[:ask_on_error])
end
end
#
# Collects an Array/Hash full of answers as described in
# HighLine::Question.gather().
#
# Raises EOFError if input is exhausted.
#
def gather( )
@gather = @question.gather
@answers = [ ]
original_question = @question
@question.gather = false
case @gather
when Integer
@answers << ask(@question)
@gather -= 1
original_question.question = ""
until @gather.zero?
@question = original_question
@answers << ask(@question)
@gather -= 1
end
when String, Regexp
@answers << ask(@question)
original_question.question = ""
until (@gather.is_a?(String) and @answers.last.to_s == @gather) or
(@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
@question = original_question
@answers << ask(@question)
end
@answers.pop
when Hash
@answers = { }
@gather.keys.sort.each do |key|
@question = original_question
@key = key
@answers[key] = ask(@question)
end
end
@answers
end
#
# Read a line of input from the input stream and process whitespace as
# requested by the Question object.
#
# If Question's _readline_ property is set, that library will be used to
# fetch input. *WARNING*: This ignores the currently set input stream.
#
# Raises EOFError if input is exhausted.
#
def get_line( )
if @question.readline
require "readline" # load only if needed
# capture say()'s work in a String to feed to readline()
old_output = @output
@output = StringIO.new
say(@question)
question = @output.string
@output = old_output
# prep auto-completion
completions = @question.selection.abbrev
Readline.completion_proc = lambda { |string| completions[string] }
# work-around ugly readline() warnings
old_verbose = $VERBOSE
$VERBOSE = nil
answer = @question.change_case(
@question.remove_whitespace(
Readline.readline(question, true) ) )
$VERBOSE = old_verbose
answer
else
raise EOFError, "The input stream is exhausted." if @@track_eof and
@input.eof?
@question.change_case(@question.remove_whitespace(@input.gets))
end
end
#
# Return a line or character of input, as requested for this question.
# Character input will be returned as a single character String,
# not an Integer.
#
# This question's _first_answer_ will be returned instead of input, if set.
#
# Raises EOFError if input is exhausted.
#
def get_response( )
return @question.first_answer if @question.first_answer?
if @question.character.nil?
if @question.echo == true and @question.limit.nil?
get_line
else
raw_no_echo_mode if stty = CHARACTER_MODE == "stty"
line = ""
backspace_limit = 0
begin
while character = (stty ? @input.getc : get_character(@input))
# honor backspace and delete
if character == 127 or character == 8
line.slice!(-1, 1)
backspace_limit -= 1
else
line << character.chr
backspace_limit = line.size
end
# looking for carriage return (decimal 13) or
# newline (decimal 10) in raw input
break if character == 13 or character == 10 or
(@question.limit and line.size == @question.limit)
if @question.echo != false
if character == 127 or character == 8
# only backspace if we have characters on the line to
# eliminate, otherwise we'll tromp over the prompt
if backspace_limit >= 0 then
@output.print("\b#{ERASE_CHAR}")
else
# do nothing
end
else
@output.print(@question.echo)
end
@output.flush
end
end
ensure
restore_mode if stty
end
if @question.overwrite
@output.print("\r#{ERASE_LINE}")
@output.flush
else
say("\n")
end
@question.change_case(@question.remove_whitespace(line))
end
elsif @question.character == :getc
@question.change_case(@input.getc.chr)
else
response = get_character(@input).chr
if @question.overwrite
@output.print("\r#{ERASE_LINE}")
@output.flush
else
echo = if @question.echo == true
response
elsif @question.echo != false
@question.echo
else
""
end
say("#{echo}\n")
end
@question.change_case(response)
end
end
#
# Page print a series of at most _page_at_ lines for _output_. After each
# page is printed, HighLine will pause until the user presses enter/return
# then display the next page of data.
#
# Note that the final page of _output_ is *not* printed, but returned
# instead. This is to support any special handling for the final sequence.
#
def page_print( output )
lines = output.scan(/[^\n]*\n?/)
while lines.size > @page_at
@output.puts lines.slice!(0...@page_at).join
@output.puts
# Return last line if user wants to abort paging
return (["...\n"] + lines.slice(-2,1)).join unless continue_paging?
end
return lines.join
end
#
# Ask user if they wish to continue paging output. Allows them to type "q" to
# cancel the paging process.
#
def continue_paging?
command = HighLine.new(@input, @output).ask(
"-- press enter/return to continue or q to stop -- "
) { |q| q.character = true }
command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit.
end
#
# Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing
# newlines will not be affected by this process, but additional newlines
# may be added.
#
def wrap( lines )
wrapped = [ ]
lines.each do |line|
while line =~ /([^\n]{#{@wrap_at + 1},})/
search = $1.dup
replace = $1.dup
if index = replace.rindex(" ", @wrap_at)
replace[index, 1] = "\n"
replace.sub!(/\n[ \t]+/, "\n")
line.sub!(search, replace)
else
line[@wrap_at, 0] = "\n"
end
end
wrapped << line
end
return wrapped.join
end
#
# Returns the length of the passed +string_with_escapes+, minus and color
# sequence escapes.
#
def actual_length( string_with_escapes )
string_with_escapes.gsub(/\e\[\d{1,2}m/, "").length
end
end

View file

@ -0,0 +1,120 @@
#!/usr/local/bin/ruby -w
# color_scheme.rb
#
# Created by Jeremy Hinegardner on 2007-01-24
# Copyright 2007. All rights reserved
#
# This is Free Software. See LICENSE and COPYING for details
require 'highline'
class HighLine
#
# ColorScheme objects encapsulate a named set of colors to be used in the
# HighLine.colors() method call. For example, by applying a ColorScheme that
# has a <tt>:warning</tt> color then the following could be used:
#
# colors("This is a warning", :warning)
#
# A ColorScheme contains named sets of HighLine color constants.
#
# Example: Instantiating a color scheme, applying it to HighLine,
# and using it:
#
# ft = HighLine::ColorScheme.new do |cs|
# cs[:headline] = [ :bold, :yellow, :on_black ]
# cs[:horizontal_line] = [ :bold, :white ]
# cs[:even_row] = [ :green ]
# cs[:odd_row] = [ :magenta ]
# end
#
# HighLine.color_scheme = ft
# say("<%= color('Headline', :headline) %>")
# say("<%= color('-'*20, :horizontal_line) %>")
# i = true
# ("A".."D").each do |row|
# if i then
# say("<%= color('#{row}', :even_row ) %>")
# else
# say("<%= color('#{row}', :odd_row) %>")
# end
# i = !i
# end
#
#
class ColorScheme
#
# Create an instance of HighLine::ColorScheme. The customization can
# happen as a passed in Hash or via the yielded block. Key's are
# converted to <tt>:symbols</tt> and values are converted to HighLine
# constants.
#
def initialize( h = nil )
@scheme = Hash.new
load_from_hash(h) unless h.nil?
yield self if block_given?
end
# Load multiple colors from key/value pairs.
def load_from_hash( h )
h.each_pair do |color_tag, constants|
self[color_tag] = constants
end
end
# Does this color scheme include the given tag name?
def include?( color_tag )
@scheme.keys.include?(to_symbol(color_tag))
end
# Allow the scheme to be accessed like a Hash.
def []( color_tag )
@scheme[to_symbol(color_tag)]
end
# Allow the scheme to be set like a Hash.
def []=( color_tag, constants )
@scheme[to_symbol(color_tag)] = constants.map { |c| to_constant(c) }
end
private
# Return a normalized representation of a color name.
def to_symbol( t )
t.to_s.downcase
end
# Return a normalized representation of a color setting.
def to_constant( v )
v = v.to_s if v.is_a?(Symbol)
if v.is_a?(String) then
HighLine.const_get(v.upcase)
else
v
end
end
end
# A sample ColorScheme.
class SampleColorScheme < ColorScheme
#
# Builds the sample scheme with settings for <tt>:critical</tt>,
# <tt>:error</tt>, <tt>:warning</tt>, <tt>:notice</tt>, <tt>:info</tt>,
# <tt>:debug</tt>, <tt>:row_even</tt>, and <tt>:row_odd</tt> colors.
#
def initialize( h = nil )
scheme = {
:critical => [ :yellow, :on_red ],
:error => [ :bold, :red ],
:warning => [ :bold, :yellow ],
:notice => [ :bold, :magenta ],
:info => [ :bold, :cyan ],
:debug => [ :bold, :green ],
:row_even => [ :cyan ],
:row_odd => [ :magenta ]
}
super(scheme)
end
end
end

View file

@ -0,0 +1,43 @@
#!/usr/local/bin/ruby -w
# import.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "highline"
require "forwardable"
$terminal = HighLine.new
#
# <tt>require "highline/import"</tt> adds shortcut methods to Kernel, making
# agree(), ask(), choose() and say() globally available. This is handy for
# quick and dirty input and output. These methods use the HighLine object in
# the global variable <tt>$terminal</tt>, which is initialized to used
# <tt>$stdin</tt> and <tt>$stdout</tt> (you are free to change this).
# Otherwise, these methods are identical to their HighLine counterparts, see that
# class for detailed explanations.
#
module Kernel
extend Forwardable
def_delegators :$terminal, :agree, :ask, :choose, :say
end
class Object
#
# Tries this object as a _first_answer_ for a HighLine::Question. See that
# attribute for details.
#
# *Warning*: This Object will be passed to String() before set.
#
def or_ask( *args, &details )
ask(*args) do |question|
question.first_answer = String(self) unless nil?
details.call(question) unless details.nil?
end
end
end

View file

@ -0,0 +1,395 @@
#!/usr/local/bin/ruby -w
# menu.rb
#
# Created by Gregory Thomas Brown on 2005-05-10.
# Copyright 2005. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "highline/question"
class HighLine
#
# Menu objects encapsulate all the details of a call to HighLine.choose().
# Using the accessors and Menu.choice() and Menu.choices(), the block passed
# to HighLine.choose() can detail all aspects of menu display and control.
#
class Menu < Question
#
# Create an instance of HighLine::Menu. All customization is done
# through the passed block, which should call accessors and choice() and
# choices() as needed to define the Menu. Note that Menus are also
# Questions, so all that functionality is available to the block as
# well.
#
def initialize( )
#
# Initialize Question objects with ignored values, we'll
# adjust ours as needed.
#
super("Ignored", [ ], &nil) # avoiding passing the block along
@items = [ ]
@hidden_items = [ ]
@help = Hash.new("There's no help for that topic.")
@index = :number
@index_suffix = ". "
@select_by = :index_or_name
@flow = :rows
@list_option = nil
@header = nil
@prompt = "? "
@layout = :list
@shell = false
@nil_on_handled = false
# Override Questions responses, we'll set our own.
@responses = { }
# Context for action code.
@highline = nil
yield self if block_given?
init_help if @shell and not @help.empty?
update_responses # rebuild responses based on our settings
end
#
# An _index_ to append to each menu item in display. See
# Menu.index=() for details.
#
attr_reader :index
#
# The String placed between an _index_ and a menu item. Defaults to
# ". ". Switches to " ", when _index_ is set to a String (like "-").
#
attr_accessor :index_suffix
#
# The _select_by_ attribute controls how the user is allowed to pick a
# menu item. The available choices are:
#
# <tt>:index</tt>:: The user is allowed to type the numerical
# or alphetical index for their selection.
# <tt>:index_or_name</tt>:: Allows both methods from the
# <tt>:index</tt> option and the
# <tt>:name</tt> option.
# <tt>:name</tt>:: Menu items are selected by typing a portion
# of the item name that will be
# auto-completed.
#
attr_accessor :select_by
#
# This attribute is passed directly on as the mode to HighLine.list() by
# all the preset layouts. See that method for appropriate settings.
#
attr_accessor :flow
#
# This setting is passed on as the third parameter to HighLine.list()
# by all the preset layouts. See that method for details of its
# effects. Defaults to +nil+.
#
attr_accessor :list_option
#
# Used by all the preset layouts to display title and/or introductory
# information, when set. Defaults to +nil+.
#
attr_accessor :header
#
# Used by all the preset layouts to ask the actual question to fetch a
# menu selection from the user. Defaults to "? ".
#
attr_accessor :prompt
#
# An ERb _layout_ to use when displaying this Menu object. See
# Menu.layout=() for details.
#
attr_reader :layout
#
# When set to +true+, responses are allowed to be an entire line of
# input, including details beyond the command itself. Only the first
# "word" of input will be matched against the menu choices, but both the
# command selected and the rest of the line will be passed to provided
# action blocks. Defaults to +false+.
#
attr_accessor :shell
#
# When +true+, any selected item handled by provided action code, will
# return +nil+, instead of the results to the action code. This may
# prove handy when dealing with mixed menus where only the names of
# items without any code (and +nil+, of course) will be returned.
# Defaults to +false+.
#
attr_accessor :nil_on_handled
#
# Adds _name_ to the list of available menu items. Menu items will be
# displayed in the order they are added.
#
# An optional _action_ can be associated with this name and if provided,
# it will be called if the item is selected. The result of the method
# will be returned, unless _nil_on_handled_ is set (when you would get
# +nil+ instead). In _shell_ mode, a provided block will be passed the
# command chosen and any details that followed the command. Otherwise,
# just the command is passed. The <tt>@highline</tt> variable is set to
# the current HighLine context before the action code is called and can
# thus be used for adding output and the like.
#
def choice( name, help = nil, &action )
@items << [name, action]
@help[name.to_s.downcase] = help unless help.nil?
end
#
# A shortcut for multiple calls to the sister method choice(). <b>Be
# warned:</b> An _action_ set here will apply to *all* provided
# _names_. This is considered to be a feature, so you can easily
# hand-off interface processing to a different chunk of code.
#
def choices( *names, &action )
names.each { |n| choice(n, &action) }
end
# Identical to choice(), but the item will not be listed for the user.
def hidden( name, help = nil, &action )
@hidden_items << [name, action]
@help[name.to_s.downcase] = help unless help.nil?
end
#
# Sets the indexing style for this Menu object. Indexes are appended to
# menu items, when displayed in list form. The available settings are:
#
# <tt>:number</tt>:: Menu items will be indexed numerically, starting
# with 1. This is the default method of indexing.
# <tt>:letter</tt>:: Items will be indexed alphabetically, starting
# with a.
# <tt>:none</tt>:: No index will be appended to menu items.
# <i>any String</i>:: Will be used as the literal _index_.
#
# Setting the _index_ to <tt>:none</tt> a literal String, also adjusts
# _index_suffix_ to a single space and _select_by_ to <tt>:none</tt>.
# Because of this, you should make a habit of setting the _index_ first.
#
def index=( style )
@index = style
# Default settings.
if @index == :none or @index.is_a?(String)
@index_suffix = " "
@select_by = :name
end
end
#
# Initializes the help system by adding a <tt>:help</tt> choice, some
# action code, and the default help listing.
#
def init_help( )
return if @items.include?(:help)
topics = @help.keys.sort
help_help = @help.include?("help") ? @help["help"] :
"This command will display helpful messages about " +
"functionality, like this one. To see the help for " +
"a specific topic enter:\n\thelp [TOPIC]\nTry asking " +
"for help on any of the following:\n\n" +
"<%= list(#{topics.inspect}, :columns_across) %>"
choice(:help, help_help) do |command, topic|
topic.strip!
topic.downcase!
if topic.empty?
@highline.say(@help["help"])
else
@highline.say("= #{topic}\n\n#{@help[topic]}")
end
end
end
#
# Used to set help for arbitrary topics. Use the topic <tt>"help"</tt>
# to override the default message.
#
def help( topic, help )
@help[topic] = help
end
#
# Setting a _layout_ with this method also adjusts some other attributes
# of the Menu object, to ideal defaults for the chosen _layout_. To
# account for that, you probably want to set a _layout_ first in your
# configuration block, if needed.
#
# Accepted settings for _layout_ are:
#
# <tt>:list</tt>:: The default _layout_. The _header_ if set
# will appear at the top on its own line with
# a trailing colon. Then the list of menu
# items will follow. Finally, the _prompt_
# will be used as the ask()-like question.
# <tt>:one_line</tt>:: A shorter _layout_ that fits on one line.
# The _header_ comes first followed by a
# colon and spaces, then the _prompt_ with menu
# items between trailing parenthesis.
# <tt>:menu_only</tt>:: Just the menu items, followed up by a likely
# short _prompt_.
# <i>any ERb String</i>:: Will be taken as the literal _layout_. This
# String can access <tt>@header</tt>,
# <tt>@menu</tt> and <tt>@prompt</tt>, but is
# otherwise evaluated in the typical HighLine
# context, to provide access to utilities like
# HighLine.list() primarily.
#
# If set to either <tt>:one_line</tt>, or <tt>:menu_only</tt>, _index_
# will default to <tt>:none</tt> and _flow_ will default to
# <tt>:inline</tt>.
#
def layout=( new_layout )
@layout = new_layout
# Default settings.
case @layout
when :one_line, :menu_only
self.index = :none
@flow = :inline
end
end
#
# This method returns all possible options for auto-completion, based
# on the settings of _index_ and _select_by_.
#
def options( )
# add in any hidden menu commands
@items.concat(@hidden_items)
by_index = if @index == :letter
l_index = "`"
@items.map { "#{l_index.succ!}" }
else
(1 .. @items.size).collect { |s| String(s) }
end
by_name = @items.collect { |c| c.first }
case @select_by
when :index then
by_index
when :name
by_name
else
by_index + by_name
end
ensure
# make sure the hidden items are removed, before we return
@items.slice!(@items.size - @hidden_items.size, @hidden_items.size)
end
#
# This method processes the auto-completed user selection, based on the
# rules for this Menu object. If an action was provided for the
# selection, it will be executed as described in Menu.choice().
#
def select( highline_context, selection, details = nil )
# add in any hidden menu commands
@items.concat(@hidden_items)
# Find the selected action.
name, action = if selection =~ /^\d+$/
@items[selection.to_i - 1]
else
l_index = "`"
index = @items.map { "#{l_index.succ!}" }.index(selection)
@items.find { |c| c.first == selection } or @items[index]
end
# Run or return it.
if not @nil_on_handled and not action.nil?
@highline = highline_context
if @shell
action.call(name, details)
else
action.call(name)
end
elsif action.nil?
name
else
nil
end
ensure
# make sure the hidden items are removed, before we return
@items.slice!(@items.size - @hidden_items.size, @hidden_items.size)
end
#
# Allows Menu objects to pass as Arrays, for use with HighLine.list().
# This method returns all menu items to be displayed, complete with
# indexes.
#
def to_ary( )
case @index
when :number
@items.map { |c| "#{@items.index(c) + 1}#{@index_suffix}#{c.first}" }
when :letter
l_index = "`"
@items.map { |c| "#{l_index.succ!}#{@index_suffix}#{c.first}" }
when :none
@items.map { |c| "#{c.first}" }
else
@items.map { |c| "#{index}#{@index_suffix}#{c.first}" }
end
end
#
# Allows Menu to behave as a String, just like Question. Returns the
# _layout_ to be rendered, which is used by HighLine.say().
#
def to_str( )
case @layout
when :list
'<%= if @header.nil? then '' else "#{@header}:\n" end %>' +
"<%= list( @menu, #{@flow.inspect},
#{@list_option.inspect} ) %>" +
"<%= @prompt %>"
when :one_line
'<%= if @header.nil? then '' else "#{@header}: " end %>' +
"<%= @prompt %>" +
"(<%= list( @menu, #{@flow.inspect},
#{@list_option.inspect} ) %>)" +
"<%= @prompt[/\s*$/] %>"
when :menu_only
"<%= list( @menu, #{@flow.inspect},
#{@list_option.inspect} ) %><%= @prompt %>"
else
@layout
end
end
#
# This method will update the intelligent responses to account for
# Menu specific differences. This overrides the work done by
# Question.build_responses().
#
def update_responses( )
append_default unless default.nil?
@responses = { :ambiguous_completion =>
"Ambiguous choice. " +
"Please choose one of #{options.inspect}.",
:ask_on_error =>
"? ",
:invalid_type =>
"You must enter a valid #{options}.",
:no_completion =>
"You must choose one of " +
"#{options.inspect}.",
:not_in_range =>
"Your answer isn't within the expected range " +
"(#{expected_range}).",
:not_valid =>
"Your answer isn't valid (must match " +
"#{@validate.inspect})." }.merge(@responses)
end
end
end

View file

@ -0,0 +1,462 @@
#!/usr/local/bin/ruby -w
# question.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "optparse"
require "date"
require "pathname"
class HighLine
#
# Question objects contain all the details of a single invocation of
# HighLine.ask(). The object is initialized by the parameters passed to
# HighLine.ask() and then queried to make sure each step of the input
# process is handled according to the users wishes.
#
class Question
# An internal HighLine error. User code does not need to trap this.
class NoAutoCompleteMatch < StandardError
# do nothing, just creating a unique error type
end
#
# Create an instance of HighLine::Question. Expects a _question_ to ask
# (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
# The _answer_type_ parameter must be a type recongnized by
# Question.convert(). If given, a block is yeilded the new Question
# object to allow custom initializaion.
#
def initialize( question, answer_type )
# initialize instance data
@question = question
@answer_type = answer_type
@character = nil
@limit = nil
@echo = true
@readline = false
@whitespace = :strip
@case = nil
@default = nil
@validate = nil
@above = nil
@below = nil
@in = nil
@confirm = nil
@gather = false
@first_answer = nil
@directory = Pathname.new(File.expand_path(File.dirname($0)))
@glob = "*"
@responses = Hash.new
@overwrite = false
# allow block to override settings
yield self if block_given?
# finalize responses based on settings
build_responses
end
# The ERb template of the question to be asked.
attr_accessor :question
# The type that will be used to convert this answer.
attr_accessor :answer_type
#
# Can be set to +true+ to use HighLine's cross-platform character reader
# instead of fetching an entire line of input. (Note: HighLine's
# character reader *ONLY* supports STDIN on Windows and Unix.) Can also
# be set to <tt>:getc</tt> to use that method on the input stream.
#
# *WARNING*: The _echo_ and _overwrite_ attributes for a question are
# ignored when using the <tt>:getc</tt> method.
#
attr_accessor :character
#
# Allows you to set a character limit for input.
#
# *WARNING*: This option forces a character by character read.
#
attr_accessor :limit
#
# Can be set to +true+ or +false+ to control whether or not input will
# be echoed back to the user. A setting of +true+ will cause echo to
# match input, but any other true value will be treated as to String to
# echo for each character typed.
#
# This requires HighLine's character reader. See the _character_
# attribute for details.
#
# *Note*: When using HighLine to manage echo on Unix based systems, we
# recommend installing the termios gem. Without it, it's possible to type
# fast enough to have letters still show up (when reading character by
# character only).
#
attr_accessor :echo
#
# Use the Readline library to fetch input. This allows input editing as
# well as keeping a history. In addition, tab will auto-complete
# within an Array of choices or a file listing.
#
# *WARNING*: This option is incompatible with all of HighLine's
# character reading modes and it causes HighLine to ignore the
# specified _input_ stream.
#
attr_accessor :readline
#
# Used to control whitespace processing for the answer to this question.
# See HighLine::Question.remove_whitespace() for acceptable settings.
#
attr_accessor :whitespace
#
# Used to control character case processing for the answer to this question.
# See HighLine::Question.change_case() for acceptable settings.
#
attr_accessor :case
# Used to provide a default answer to this question.
attr_accessor :default
#
# If set to a Regexp, the answer must match (before type conversion).
# Can also be set to a Proc which will be called with the provided
# answer to validate with a +true+ or +false+ return.
#
attr_accessor :validate
# Used to control range checks for answer.
attr_accessor :above, :below
# If set, answer must pass an include?() check on this object.
attr_accessor :in
#
# Asks a yes or no confirmation question, to ensure a user knows what
# they have just agreed to. If set to +true+ the question will be,
# "Are you sure? " Any other true value for this attribute is assumed
# to be the question to ask. When +false+ or +nil+ (the default),
# answers are not confirmed.
#
attr_accessor :confirm
#
# When set, the user will be prompted for multiple answers which will
# be collected into an Array or Hash and returned as the final answer.
#
# You can set _gather_ to an Integer to have an Array of exactly that
# many answers collected, or a String/Regexp to match an end input which
# will not be returned in the Array.
#
# Optionally _gather_ can be set to a Hash. In this case, the question
# will be asked once for each key and the answers will be returned in a
# Hash, mapped by key. The <tt>@key</tt> variable is set before each
# question is evaluated, so you can use it in your question.
#
attr_accessor :gather
#
# When set to a non *nil* value, this will be tried as an answer to the
# question. If this answer passes validations, it will become the result
# without the user ever being prompted. Otherwise this value is discarded,
# and this Question is resolved as a normal call to HighLine.ask().
#
attr_writer :first_answer
#
# The directory from which a user will be allowed to select files, when
# File or Pathname is specified as an _answer_type_. Initially set to
# <tt>Pathname.new(File.expand_path(File.dirname($0)))</tt>.
#
attr_accessor :directory
#
# The glob pattern used to limit file selection when File or Pathname is
# specified as an _answer_type_. Initially set to <tt>"*"</tt>.
#
attr_accessor :glob
#
# A Hash that stores the various responses used by HighLine to notify
# the user. The currently used responses and their purpose are as
# follows:
#
# <tt>:ambiguous_completion</tt>:: Used to notify the user of an
# ambiguous answer the auto-completion
# system cannot resolve.
# <tt>:ask_on_error</tt>:: This is the question that will be
# redisplayed to the user in the event
# of an error. Can be set to
# <tt>:question</tt> to repeat the
# original question.
# <tt>:invalid_type</tt>:: The error message shown when a type
# conversion fails.
# <tt>:no_completion</tt>:: Used to notify the user that their
# selection does not have a valid
# auto-completion match.
# <tt>:not_in_range</tt>:: Used to notify the user that a
# provided answer did not satisfy
# the range requirement tests.
# <tt>:not_valid</tt>:: The error message shown when
# validation checks fail.
#
attr_reader :responses
#
# When set to +true+ the question is asked, but output does not progress to
# the next line. The Cursor is moved back to the beginning of the question
# line and it is cleared so that all the contents of the line disappear from
# the screen.
#
attr_accessor :overwrite
#
# Returns the provided _answer_string_ or the default answer for this
# Question if a default was set and the answer is empty.
#
def answer_or_default( answer_string )
if answer_string.length == 0 and not @default.nil?
@default
else
answer_string
end
end
#
# Called late in the initialization process to build intelligent
# responses based on the details of this Question object.
#
def build_responses( )
### WARNING: This code is quasi-duplicated in ###
### Menu.update_responses(). Check there too when ###
### making changes! ###
append_default unless default.nil?
@responses = { :ambiguous_completion =>
"Ambiguous choice. " +
"Please choose one of #{@answer_type.inspect}.",
:ask_on_error =>
"? ",
:invalid_type =>
"You must enter a valid #{@answer_type}.",
:no_completion =>
"You must choose one of " +
"#{@answer_type.inspect}.",
:not_in_range =>
"Your answer isn't within the expected range " +
"(#{expected_range}).",
:not_valid =>
"Your answer isn't valid (must match " +
"#{@validate.inspect})." }.merge(@responses)
### WARNING: This code is quasi-duplicated in ###
### Menu.update_responses(). Check there too when ###
### making changes! ###
end
#
# Returns the provided _answer_string_ after changing character case by
# the rules of this Question. Valid settings for whitespace are:
#
# +nil+:: Do not alter character case.
# (Default.)
# <tt>:up</tt>:: Calls upcase().
# <tt>:upcase</tt>:: Calls upcase().
# <tt>:down</tt>:: Calls downcase().
# <tt>:downcase</tt>:: Calls downcase().
# <tt>:capitalize</tt>:: Calls capitalize().
#
# An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
#
def change_case( answer_string )
if [:up, :upcase].include?(@case)
answer_string.upcase
elsif [:down, :downcase].include?(@case)
answer_string.downcase
elsif @case == :capitalize
answer_string.capitalize
else
answer_string
end
end
#
# Transforms the given _answer_string_ into the expected type for this
# Question. Currently supported conversions are:
#
# <tt>[...]</tt>:: Answer must be a member of the passed Array.
# Auto-completion is used to expand partial
# answers.
# <tt>lambda {...}</tt>:: Answer is passed to lambda for conversion.
# Date:: Date.parse() is called with answer.
# DateTime:: DateTime.parse() is called with answer.
# File:: The entered file name is auto-completed in
# terms of _directory_ + _glob_, opened, and
# returned.
# Float:: Answer is converted with Kernel.Float().
# Integer:: Answer is converted with Kernel.Integer().
# +nil+:: Answer is left in String format. (Default.)
# Pathname:: Same as File, save that a Pathname object is
# returned.
# String:: Answer is converted with Kernel.String().
# Regexp:: Answer is fed to Regexp.new().
# Symbol:: The method to_sym() is called on answer and
# the result returned.
# <i>any other Class</i>:: The answer is passed on to
# <tt>Class.parse()</tt>.
#
# This method throws ArgumentError, if the conversion cannot be
# completed for any reason.
#
def convert( answer_string )
if @answer_type.nil?
answer_string
elsif [Float, Integer, String].include?(@answer_type)
Kernel.send(@answer_type.to_s.to_sym, answer_string)
elsif @answer_type == Symbol
answer_string.to_sym
elsif @answer_type == Regexp
Regexp.new(answer_string)
elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
# cheating, using OptionParser's Completion module
choices = selection
choices.extend(OptionParser::Completion)
answer = choices.complete(answer_string)
if answer.nil?
raise NoAutoCompleteMatch
end
if @answer_type.is_a?(Array)
answer.last
elsif @answer_type == File
File.open(File.join(@directory.to_s, answer.last))
else
Pathname.new(File.join(@directory.to_s, answer.last))
end
elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
@answer_type.parse(answer_string)
elsif @answer_type.is_a?(Proc)
@answer_type[answer_string]
end
end
# Returns a english explination of the current range settings.
def expected_range( )
expected = [ ]
expected << "above #{@above}" unless @above.nil?
expected << "below #{@below}" unless @below.nil?
expected << "included in #{@in.inspect}" unless @in.nil?
case expected.size
when 0 then ""
when 1 then expected.first
when 2 then expected.join(" and ")
else expected[0..-2].join(", ") + ", and #{expected.last}"
end
end
# Returns _first_answer_, which will be unset following this call.
def first_answer( )
@first_answer
ensure
@first_answer = nil
end
# Returns true if _first_answer_ is set.
def first_answer?( )
not @first_answer.nil?
end
#
# Returns +true+ if the _answer_object_ is greater than the _above_
# attribute, less than the _below_ attribute and included?()ed in the
# _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
# are not checked.
#
def in_range?( answer_object )
(@above.nil? or answer_object > @above) and
(@below.nil? or answer_object < @below) and
(@in.nil? or @in.include?(answer_object))
end
#
# Returns the provided _answer_string_ after processing whitespace by
# the rules of this Question. Valid settings for whitespace are:
#
# +nil+:: Do not alter whitespace.
# <tt>:strip</tt>:: Calls strip(). (Default.)
# <tt>:chomp</tt>:: Calls chomp().
# <tt>:collapse</tt>:: Collapses all whitspace runs to a
# single space.
# <tt>:strip_and_collapse</tt>:: Calls strip(), then collapses all
# whitspace runs to a single space.
# <tt>:chomp_and_collapse</tt>:: Calls chomp(), then collapses all
# whitspace runs to a single space.
# <tt>:remove</tt>:: Removes all whitespace.
#
# An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
#
# This process is skipped, for single character input.
#
def remove_whitespace( answer_string )
if @whitespace.nil?
answer_string
elsif [:strip, :chomp].include?(@whitespace)
answer_string.send(@whitespace)
elsif @whitespace == :collapse
answer_string.gsub(/\s+/, " ")
elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace)
result = answer_string.send(@whitespace.to_s[/^[a-z]+/])
result.gsub(/\s+/, " ")
elsif @whitespace == :remove
answer_string.gsub(/\s+/, "")
else
answer_string
end
end
#
# Returns an Array of valid answers to this question. These answers are
# only known when _answer_type_ is set to an Array of choices, File, or
# Pathname. Any other time, this method will return an empty Array.
#
def selection( )
if @answer_type.is_a?(Array)
@answer_type
elsif [File, Pathname].include?(@answer_type)
Dir[File.join(@directory.to_s, @glob)].map do |file|
File.basename(file)
end
else
[ ]
end
end
# Stringifies the question to be asked.
def to_str( )
@question
end
#
# Returns +true+ if the provided _answer_string_ is accepted by the
# _validate_ attribute or +false+ if it's not.
#
# It's important to realize that an answer is validated after whitespace
# and case handling.
#
def valid_answer?( answer_string )
@validate.nil? or
(@validate.is_a?(Regexp) and answer_string =~ @validate) or
(@validate.is_a?(Proc) and @validate[answer_string])
end
private
#
# Adds the default choice to the end of question between <tt>|...|</tt>.
# Trailing whitespace is preserved so the function of HighLine.say() is
# not affected.
#
def append_default( )
if @question =~ /([\t ]+)\Z/
@question << "|#{@default}|#{$1}"
elsif @question == ""
@question << "|#{@default}| "
elsif @question[-1, 1] == "\n"
@question[-2, 0] = " |#{@default}|"
else
@question << " |#{@default}|"
end
end
end
end

View file

@ -0,0 +1,130 @@
#!/usr/local/bin/ruby -w
# system_extensions.rb
#
# Created by James Edward Gray II on 2006-06-14.
# Copyright 2006 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
class HighLine
module SystemExtensions
module_function
#
# This section builds character reading and terminal size functions
# to suit the proper platform we're running on. Be warned: Here be
# dragons!
#
begin
# Cygwin will look like Windows, but we want to treat it like a Posix OS:
raise LoadError, "Cygwin is a Posix OS." if RUBY_PLATFORM =~ /\bcygwin\b/i
require "Win32API" # See if we're on Windows.
CHARACTER_MODE = "Win32API" # For Debugging purposes only.
#
# Windows savvy getc().
#
# *WARNING*: This method ignores <tt>input</tt> and reads one
# character from +STDIN+!
#
def get_character( input = STDIN )
Win32API.new("crtdll", "_getch", [ ], "L").Call
end
# A Windows savvy method to fetch the console columns, and rows.
def terminal_size
m_GetStdHandle = Win32API.new( 'kernel32',
'GetStdHandle',
['L'],
'L' )
m_GetConsoleScreenBufferInfo = Win32API.new(
'kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L'
)
format = 'SSSSSssssSS'
buf = ([0] * format.size).pack(format)
stdout_handle = m_GetStdHandle.call(0xFFFFFFF5)
m_GetConsoleScreenBufferInfo.call(stdout_handle, buf)
bufx, bufy, curx, cury, wattr,
left, top, right, bottom, maxx, maxy = buf.unpack(format)
return right - left + 1, bottom - top + 1
end
rescue LoadError # If we're not on Windows try...
begin
require "termios" # Unix, first choice.
CHARACTER_MODE = "termios" # For Debugging purposes only.
#
# Unix savvy getc(). (First choice.)
#
# *WARNING*: This method requires the "termios" library!
#
def get_character( input = STDIN )
old_settings = Termios.getattr(input)
new_settings = old_settings.dup
new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
new_settings.c_cc[Termios::VMIN] = 1
begin
Termios.setattr(input, Termios::TCSANOW, new_settings)
input.getc
ensure
Termios.setattr(input, Termios::TCSANOW, old_settings)
end
end
rescue LoadError # If our first choice fails, default.
CHARACTER_MODE = "stty" # For Debugging purposes only.
#
# Unix savvy getc(). (Second choice.)
#
# *WARNING*: This method requires the external "stty" program!
#
def get_character( input = STDIN )
raw_no_echo_mode
begin
input.getc
ensure
restore_mode
end
end
#
# Switched the input mode to raw and disables echo.
#
# *WARNING*: This method requires the external "stty" program!
#
def raw_no_echo_mode
@state = `stty -g`
system "stty raw -echo cbreak isig"
end
#
# Restores a previously saved input mode.
#
# *WARNING*: This method requires the external "stty" program!
#
def restore_mode
system "stty #{@state}"
end
end
# A Unix savvy method to fetch the console columns, and rows.
def terminal_size
if /solaris/ =~ RUBY_PLATFORM and
`stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/
[$2, $1].map { |c| x.to_i }
else
`stty size`.split.map { |x| x.to_i }.reverse
end
end
end
end
end

1360
vendor/gems/highline-1.4.0/setup.rb vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
#!/usr/local/bin/ruby -w
# tc_color_scheme.rb
#
# Created by Jeremy Hinegardner on 2007-01-24.
# Copyright 2007 Jeremy Hinegardner. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "test/unit"
require "highline"
require "stringio"
class TestColorScheme < Test::Unit::TestCase
def setup
@input = StringIO.new
@output = StringIO.new
@terminal = HighLine.new(@input, @output)
@old_color_scheme = HighLine.color_scheme
end
def teardown
HighLine.color_scheme = @old_color_scheme
end
def test_using_color_scheme
assert_equal(false,HighLine.using_color_scheme?)
HighLine.color_scheme = HighLine::ColorScheme.new
assert_equal(true,HighLine.using_color_scheme?)
end
def test_scheme
HighLine.color_scheme = HighLine::SampleColorScheme.new
@terminal.say("This should be <%= color('warning yellow', :warning) %>.")
assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n",@output.string)
@output.rewind
@terminal.say("This should be <%= color('warning yellow', 'warning') %>.")
assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n",@output.string)
@output.rewind
@terminal.say("This should be <%= color('warning yellow', 'WarNing') %>.")
assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n",@output.string)
@output.rewind
# turn it back off, should raise an exception
HighLine.color_scheme = @old_color_scheme
assert_raises(NameError) {
@terminal.say("This should be <%= color('nothing at all', :error) %>.")
}
end
end

View file

@ -0,0 +1,815 @@
#!/usr/local/bin/ruby -w
# tc_highline.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "test/unit"
require "highline"
require "stringio"
if HighLine::CHARACTER_MODE == "Win32API"
class HighLine
# Override Windows' character reading so it's not tied to STDIN.
def get_character( input = STDIN )
input.getc
end
end
end
class TestHighLine < Test::Unit::TestCase
def setup
@input = StringIO.new
@output = StringIO.new
@terminal = HighLine.new(@input, @output)
end
def test_agree
@input << "y\nyes\nYES\nHell no!\nNo\n"
@input.rewind
assert_equal(true, @terminal.agree("Yes or no? "))
assert_equal(true, @terminal.agree("Yes or no? "))
assert_equal(true, @terminal.agree("Yes or no? "))
assert_equal(false, @terminal.agree("Yes or no? "))
@input.truncate(@input.rewind)
@input << "yellow"
@input.rewind
assert_equal(true, @terminal.agree("Yes or no? ", :getc))
end
def test_ask
name = "James Edward Gray II"
@input << name << "\n"
@input.rewind
assert_equal(name, @terminal.ask("What is your name? "))
assert_raise(EOFError) { @terminal.ask("Any input left? ") }
end
def test_bug_fixes
# auto-complete bug
@input << "ruby\nRuby\n"
@input.rewind
languages = [:Perl, :Python, :Ruby]
answer = @terminal.ask( "What is your favorite programming language? ",
languages )
assert_equal(languages.last, answer)
@input.truncate(@input.rewind)
@input << "ruby\n"
@input.rewind
answer = @terminal.ask( "What is your favorite programming language? ",
languages ) do |q|
q.case = :capitalize
end
assert_equal(languages.last, answer)
# poor auto-complete error message
@input.truncate(@input.rewind)
@input << "lisp\nruby\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask( "What is your favorite programming language? ",
languages ) do |q|
q.case = :capitalize
end
assert_equal(languages.last, answer)
assert_equal( "What is your favorite programming language? " +
"You must choose one of [:Perl, :Python, :Ruby].\n" +
"? ", @output.string )
end
def test_case_changes
@input << "jeg2\n"
@input.rewind
answer = @terminal.ask("Enter your initials ") do |q|
q.case = :up
end
assert_equal("JEG2", answer)
@input.truncate(@input.rewind)
@input << "cRaZY\n"
@input.rewind
answer = @terminal.ask("Enter a search string: ") do |q|
q.case = :down
end
assert_equal("crazy", answer)
end
def test_character_echo
@input << "password\r"
@input.rewind
answer = @terminal.ask("Please enter your password: ") do |q|
q.echo = "*"
end
assert_equal("password", answer)
assert_equal("Please enter your password: ********\n", @output.string)
@input.truncate(@input.rewind)
@input << "2"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask( "Select an option (1, 2 or 3): ",
Integer ) do |q|
q.echo = "*"
q.character = true
end
assert_equal(2, answer)
assert_equal("Select an option (1, 2 or 3): *\n", @output.string)
end
def test_backspace_does_not_enter_prompt
@input << "\b\b"
@input.rewind
answer = @terminal.ask("Please enter your password: ") do |q|
q.echo = "*"
end
assert_equal("", answer)
assert_equal("Please enter your password: \n",@output.string)
end
def test_readline_on_non_echo_question_has_prompt
@input << "you can't see me"
@input.rewind
answer = @terminal.ask("Please enter some hidden text: ") do |q|
q.readline = true
q.echo = "*"
end
assert_equal("you can't see me", answer)
assert_equal("Please enter some hidden text: ****************\n",@output.string)
end
def test_character_reading
# WARNING: This method does NOT cover Unix and Windows savvy testing!
@input << "12345"
@input.rewind
answer = @terminal.ask("Enter a single digit: ", Integer) do |q|
q.character = :getc
end
assert_equal(1, answer)
end
def test_color
@terminal.say("This should be <%= BLUE %>blue<%= CLEAR %>!")
assert_equal("This should be \e[34mblue\e[0m!\n", @output.string)
@output.truncate(@output.rewind)
@terminal.say( "This should be " +
"<%= BOLD + ON_WHITE %>bold on white<%= CLEAR %>!" )
assert_equal( "This should be \e[1m\e[47mbold on white\e[0m!\n",
@output.string )
@output.truncate(@output.rewind)
@terminal.say("This should be <%= color('cyan', CYAN) %>!")
assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string)
@output.truncate(@output.rewind)
@terminal.say( "This should be " +
"<%= color('blinking on red', :blink, :on_red) %>!" )
assert_equal( "This should be \e[5m\e[41mblinking on red\e[0m!\n",
@output.string )
@output.truncate(@output.rewind)
# turn off color
old_setting = HighLine.use_color?
assert_nothing_raised(Exception) { HighLine.use_color = false }
@terminal.say("This should be <%= color('cyan', CYAN) %>!")
assert_equal("This should be cyan!\n", @output.string)
HighLine.use_color = old_setting
end
def test_confirm
@input << "junk.txt\nno\nsave.txt\ny\n"
@input.rewind
answer = @terminal.ask("Enter a filename: ") do |q|
q.confirm = "Are you sure you want to overwrite <%= @answer %>? "
q.responses[:ask_on_error] = :question
end
assert_equal("save.txt", answer)
assert_equal( "Enter a filename: " +
"Are you sure you want to overwrite junk.txt? " +
"Enter a filename: " +
"Are you sure you want to overwrite save.txt? ",
@output.string )
@input.truncate(@input.rewind)
@input << "junk.txt\nyes\nsave.txt\nn\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Enter a filename: ") do |q|
q.confirm = "Are you sure you want to overwrite <%= @answer %>? "
end
assert_equal("junk.txt", answer)
assert_equal( "Enter a filename: " +
"Are you sure you want to overwrite junk.txt? ",
@output.string )
end
def test_defaults
@input << "\nNo Comment\n"
@input.rewind
answer = @terminal.ask("Are you sexually active? ") do |q|
q.validate = /\Ay(?:es)?|no?|no comment\Z/i
end
assert_equal("No Comment", answer)
@input.truncate(@input.rewind)
@input << "\nYes\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Are you sexually active? ") do |q|
q.default = "No Comment"
q.validate = /\Ay(?:es)?|no?|no comment\Z/i
end
assert_equal("No Comment", answer)
assert_equal( "Are you sexually active? |No Comment| ",
@output.string )
end
def test_empty
@input << "\n"
@input.rewind
answer = @terminal.ask("") do |q|
q.default = "yes"
q.validate = /\Ay(?:es)?|no?\Z/i
end
assert_equal("yes", answer)
end
def test_erb
@terminal.say( "The integers from 1 to 10 are:\n" +
"% (1...10).each do |n|\n" +
"\t<%= n %>,\n" +
"% end\n" +
"\tand 10" )
assert_equal( "The integers from 1 to 10 are:\n" +
"\t1,\n\t2,\n\t3,\n\t4,\n\t5,\n" +
"\t6,\n\t7,\n\t8,\n\t9,\n\tand 10\n",
@output.string )
end
def test_files
@input << "#{File.basename(__FILE__)[0, 5]}\n"
@input.rewind
file = @terminal.ask("Select a file: ", File) do |q|
q.directory = File.expand_path(File.dirname(__FILE__))
q.glob = "*.rb"
end
assert_instance_of(File, file)
assert_equal("#!/usr/local/bin/ruby -w\n", file.gets)
assert_equal("\n", file.gets)
assert_equal("# tc_highline.rb\n", file.gets)
file.close
@input.rewind
pathname = @terminal.ask("Select a file: ", Pathname) do |q|
q.directory = File.expand_path(File.dirname(__FILE__))
q.glob = "*.rb"
end
assert_instance_of(Pathname, pathname)
assert_equal(File.size(__FILE__), pathname.size)
end
def test_gather
@input << "James\nDana\nStorm\nGypsy\n\n"
@input.rewind
answers = @terminal.ask("Enter four names:") do |q|
q.gather = 4
end
assert_equal(%w{James Dana Storm Gypsy}, answers)
assert_equal("\n", @input.gets)
assert_equal("Enter four names:\n", @output.string)
@input.rewind
answers = @terminal.ask("Enter four names:") do |q|
q.gather = ""
end
assert_equal(%w{James Dana Storm Gypsy}, answers)
@input.rewind
answers = @terminal.ask("Enter four names:") do |q|
q.gather = /^\s*$/
end
assert_equal(%w{James Dana Storm Gypsy}, answers)
@input.truncate(@input.rewind)
@input << "29\n49\n30\n"
@input.rewind
@output.truncate(@output.rewind)
answers = @terminal.ask("<%= @key %>: ", Integer) do |q|
q.gather = { "Age" => 0, "Wife's Age" => 0, "Father's Age" => 0}
end
assert_equal( { "Age" => 29, "Wife's Age" => 30, "Father's Age" => 49},
answers )
assert_equal("Age: Father's Age: Wife's Age: ", @output.string)
end
def test_lists
digits = %w{Zero One Two Three Four Five Six Seven Eight Nine}
erb_digits = digits.dup
erb_digits[erb_digits.index("Five")] = "<%= color('Five', :blue) %%>"
@terminal.say("<%= list(#{digits.inspect}) %>")
assert_equal(digits.map { |d| "#{d}\n" }.join, @output.string)
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{digits.inspect}, :inline) %>")
assert_equal( digits[0..-2].join(", ") + " or #{digits.last}\n",
@output.string )
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{digits.inspect}, :inline, ' and ') %>")
assert_equal( digits[0..-2].join(", ") + " and #{digits.last}\n",
@output.string )
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{digits.inspect}, :columns_down, 3) %>")
assert_equal( "Zero Four Eight\n" +
"One Five Nine \n" +
"Two Six \n" +
"Three Seven\n",
@output.string )
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{erb_digits.inspect}, :columns_down, 3) %>")
assert_equal( "Zero Four Eight\n" +
"One \e[34mFive\e[0m Nine \n" +
"Two Six \n" +
"Three Seven\n",
@output.string )
colums_of_twenty = ["12345678901234567890"] * 5
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{colums_of_twenty.inspect}, :columns_down) %>")
assert_equal( "12345678901234567890 12345678901234567890 " +
"12345678901234567890\n" +
"12345678901234567890 12345678901234567890\n",
@output.string )
@output.truncate(@output.rewind)
@terminal.say("<%= list(#{digits.inspect}, :columns_across, 3) %>")
assert_equal( "Zero One Two \n" +
"Three Four Five \n" +
"Six Seven Eight\n" +
"Nine \n",
@output.string )
colums_of_twenty.pop
@output.truncate(@output.rewind)
@terminal.say("<%= list( #{colums_of_twenty.inspect}, :columns_across ) %>")
assert_equal( "12345678901234567890 12345678901234567890 " +
"12345678901234567890\n" +
"12345678901234567890\n",
@output.string )
end
def test_mode
assert(%w[Win32API termios stty].include?(HighLine::CHARACTER_MODE))
end
class NameClass
def self.parse( string )
if string =~ /^\s*(\w+),\s*(\w+)\s+(\w+)\s*$/
self.new($2, $3, $1)
else
raise ArgumentError, "Invalid name format."
end
end
def initialize(first, middle, last)
@first, @middle, @last = first, middle, last
end
attr_reader :first, :middle, :last
end
def test_my_class_conversion
@input << "Gray, James Edward\n"
@input.rewind
answer = @terminal.ask("Your name? ", NameClass) do |q|
q.validate = lambda do |name|
names = name.split(/,\s*/)
return false unless names.size == 2
return false if names.first =~ /\s/
names.last.split.size == 2
end
end
assert_instance_of(NameClass, answer)
assert_equal("Gray", answer.last)
assert_equal("James", answer.first)
assert_equal("Edward", answer.middle)
end
def test_no_echo
@input << "password\r"
@input.rewind
answer = @terminal.ask("Please enter your password: ") do |q|
q.echo = false
end
assert_equal("password", answer)
assert_equal("Please enter your password: \n", @output.string)
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Pick a letter or number: ") do |q|
q.character = true
q.echo = false
end
assert_equal("p", answer)
assert_equal("a", @input.getc.chr)
assert_equal("Pick a letter or number: \n", @output.string)
end
def test_paging
@terminal.page_at = 22
@input << "\n\n"
@input.rewind
@terminal.say((1..50).map { |n| "This is line #{n}.\n"}.join)
assert_equal( (1..22).map { |n| "This is line #{n}.\n"}.join +
"\n-- press enter/return to continue or q to stop -- \n\n" +
(23..44).map { |n| "This is line #{n}.\n"}.join +
"\n-- press enter/return to continue or q to stop -- \n\n" +
(45..50).map { |n| "This is line #{n}.\n"}.join,
@output.string )
end
def test_range_requirements
@input << "112\n-541\n28\n"
@input.rewind
answer = @terminal.ask("Tell me your age.", Integer) do |q|
q.in = 0..105
end
assert_equal(28, answer)
assert_equal( "Tell me your age.\n" +
"Your answer isn't within the expected range " +
"(included in 0..105).\n" +
"? " +
"Your answer isn't within the expected range " +
"(included in 0..105).\n" +
"? ", @output.string )
@input.truncate(@input.rewind)
@input << "1\n-541\n28\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Tell me your age.", Integer) do |q|
q.above = 3
end
assert_equal(28, answer)
assert_equal( "Tell me your age.\n" +
"Your answer isn't within the expected range " +
"(above 3).\n" +
"? " +
"Your answer isn't within the expected range " +
"(above 3).\n" +
"? ", @output.string )
@input.truncate(@input.rewind)
@input << "1\n28\n-541\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Lowest numer you can think of?", Integer) do |q|
q.below = 0
end
assert_equal(-541, answer)
assert_equal( "Lowest numer you can think of?\n" +
"Your answer isn't within the expected range " +
"(below 0).\n" +
"? " +
"Your answer isn't within the expected range " +
"(below 0).\n" +
"? ", @output.string )
@input.truncate(@input.rewind)
@input << "1\n-541\n6\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Enter a low even number: ", Integer) do |q|
q.above = 0
q.below = 10
q.in = [2, 4, 6, 8]
end
assert_equal(6, answer)
assert_equal( "Enter a low even number: " +
"Your answer isn't within the expected range " +
"(above 0, below 10, and included in [2, 4, 6, 8]).\n" +
"? " +
"Your answer isn't within the expected range " +
"(above 0, below 10, and included in [2, 4, 6, 8]).\n" +
"? ", @output.string )
end
def test_reask
number = 61676
@input << "Junk!\n" << number << "\n"
@input.rewind
answer = @terminal.ask("Favorite number? ", Integer)
assert_kind_of(Integer, number)
assert_instance_of(Fixnum, number)
assert_equal(number, answer)
assert_equal( "Favorite number? " +
"You must enter a valid Integer.\n" +
"? ", @output.string )
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Favorite number? ", Integer) do |q|
q.responses[:ask_on_error] = :question
q.responses[:invalid_type] = "Not a valid number!"
end
assert_kind_of(Integer, number)
assert_instance_of(Fixnum, number)
assert_equal(number, answer)
assert_equal( "Favorite number? " +
"Not a valid number!\n" +
"Favorite number? ", @output.string )
@input.truncate(@input.rewind)
@input << "gen\ngene\n"
@input.rewind
@output.truncate(@output.rewind)
answer = @terminal.ask("Select a mode: ", [:generate, :gentle])
assert_instance_of(Symbol, answer)
assert_equal(:generate, answer)
assert_equal( "Select a mode: " +
"Ambiguous choice. " +
"Please choose one of [:generate, :gentle].\n" +
"? ", @output.string )
end
def test_response_embedding
@input << "112\n-541\n28\n"
@input.rewind
answer = @terminal.ask("Tell me your age.", Integer) do |q|
q.in = 0..105
q.responses[:not_in_range] = "Need a <%= @question.answer_type %>" +
" <%= @question.expected_range %>."
end
assert_equal(28, answer)
assert_equal( "Tell me your age.\n" +
"Need a Integer included in 0..105.\n" +
"? " +
"Need a Integer included in 0..105.\n" +
"? ", @output.string )
end
def test_say
@terminal.say("This will have a newline.")
assert_equal("This will have a newline.\n", @output.string)
@output.truncate(@output.rewind)
@terminal.say("This will also have one newline.\n")
assert_equal("This will also have one newline.\n", @output.string)
@output.truncate(@output.rewind)
@terminal.say("This will not have a newline. ")
assert_equal("This will not have a newline. ", @output.string)
end
def test_type_conversion
number = 61676
@input << number << "\n"
@input.rewind
answer = @terminal.ask("Favorite number? ", Integer)
assert_kind_of(Integer, answer)
assert_instance_of(Fixnum, answer)
assert_equal(number, answer)
@input.truncate(@input.rewind)
number = 1_000_000_000_000_000_000_000_000_000_000
@input << number << "\n"
@input.rewind
answer = @terminal.ask("Favorite number? ", Integer)
assert_kind_of(Integer, answer)
assert_instance_of(Bignum, answer)
assert_equal(number, answer)
@input.truncate(@input.rewind)
number = 10.5002
@input << number << "\n"
@input.rewind
answer = @terminal.ask( "Favorite number? ",
lambda { |n| n.to_f.abs.round } )
assert_kind_of(Integer, answer)
assert_instance_of(Fixnum, answer)
assert_equal(11, answer)
@input.truncate(@input.rewind)
animal = :dog
@input << animal << "\n"
@input.rewind
answer = @terminal.ask("Favorite animal? ", Symbol)
assert_instance_of(Symbol, answer)
assert_equal(animal, answer)
@input.truncate(@input.rewind)
@input << "6/16/76\n"
@input.rewind
answer = @terminal.ask("Enter your birthday.", Date)
assert_instance_of(Date, answer)
assert_equal(16, answer.day)
assert_equal(6, answer.month)
assert_equal(76, answer.year)
@input.truncate(@input.rewind)
pattern = "^yes|no$"
@input << pattern << "\n"
@input.rewind
answer = @terminal.ask("Give me a pattern to match with: ", Regexp)
assert_instance_of(Regexp, answer)
assert_equal(/#{pattern}/, answer)
@input.truncate(@input.rewind)
@input << "gen\n"
@input.rewind
answer = @terminal.ask("Select a mode: ", [:generate, :run])
assert_instance_of(Symbol, answer)
assert_equal(:generate, answer)
end
def test_validation
@input << "system 'rm -rf /'\n105\n0b101_001\n"
@input.rewind
answer = @terminal.ask("Enter a binary number: ") do |q|
q.validate = /\A(?:0b)?[01_]+\Z/
end
assert_equal("0b101_001", answer)
assert_equal( "Enter a binary number: " +
"Your answer isn't valid " +
"(must match /\\A(?:0b)?[01_]+\\Z/).\n" +
"? " +
"Your answer isn't valid " +
"(must match /\\A(?:0b)?[01_]+\\Z/).\n" +
"? ", @output.string )
@input.truncate(@input.rewind)
@input << "Gray II, James Edward\n" +
"Gray, Dana Ann Leslie\n" +
"Gray, James Edward\n"
@input.rewind
answer = @terminal.ask("Your name? ") do |q|
q.validate = lambda do |name|
names = name.split(/,\s*/)
return false unless names.size == 2
return false if names.first =~ /\s/
names.last.split.size == 2
end
end
assert_equal("Gray, James Edward", answer)
end
def test_whitespace
@input << " A lot\tof \t space\t \there! \n"
@input.rewind
answer = @terminal.ask("Enter a whitespace filled string: ") do |q|
q.whitespace = :chomp
end
assert_equal(" A lot\tof \t space\t \there! ", answer)
@input.rewind
answer = @terminal.ask("Enter a whitespace filled string: ")
assert_equal("A lot\tof \t space\t \there!", answer)
@input.rewind
answer = @terminal.ask("Enter a whitespace filled string: ") do |q|
q.whitespace = :strip_and_collapse
end
assert_equal("A lot of space here!", answer)
@input.rewind
answer = @terminal.ask("Enter a whitespace filled string: ") do |q|
q.whitespace = :remove
end
assert_equal("Alotofspacehere!", answer)
@input.rewind
answer = @terminal.ask("Enter a whitespace filled string: ") do |q|
q.whitespace = :none
end
assert_equal(" A lot\tof \t space\t \there! \n", answer)
end
def test_wrap
@terminal.wrap_at = 80
@terminal.say("This is a very short line.")
assert_equal("This is a very short line.\n", @output.string)
@output.truncate(@output.rewind)
@terminal.say( "This is a long flowing paragraph meant to span " +
"several lines. This text should definitely be " +
"wrapped at the set limit, in the result. Your code " +
"does well with things like this.\n\n" +
" * This is a simple embedded list.\n" +
" * You're code should not mess with this...\n" +
" * Because it's already formatted correctly and " +
"does not\n" +
" exceed the limit!" )
assert_equal( "This is a long flowing paragraph meant to span " +
"several lines. This text should\n" +
"definitely be wrapped at the set limit, in the " +
"result. Your code does well with\n" +
"things like this.\n\n" +
" * This is a simple embedded list.\n" +
" * You're code should not mess with this...\n" +
" * Because it's already formatted correctly and does " +
"not\n" +
" exceed the limit!\n", @output.string )
@output.truncate(@output.rewind)
@terminal.say("-=" * 50)
assert_equal(("-=" * 40 + "\n") + ("-=" * 10 + "\n"), @output.string)
end
def test_track_eof
assert_raise(EOFError) { @terminal.ask("Any input left? ") }
# turn EOF tracking
old_setting = HighLine.track_eof?
assert_nothing_raised(Exception) { HighLine.track_eof = false }
begin
@terminal.ask("And now? ") # this will still blow up, nothing available
rescue
assert_not_equal(EOFError, $!.class) # but HighLine's safe guards are off
end
HighLine.track_eof = old_setting
end
def test_version
assert_not_nil(HighLine::VERSION)
assert_instance_of(String, HighLine::VERSION)
assert(HighLine::VERSION.frozen?)
assert_match(/\A\d\.\d\.\d\Z/, HighLine::VERSION)
end
end

View file

@ -0,0 +1,54 @@
#!/usr/local/bin/ruby -w
# tc_import.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "test/unit"
require "highline/import"
require "stringio"
class TestImport < Test::Unit::TestCase
def test_import
assert_respond_to(self, :agree)
assert_respond_to(self, :ask)
assert_respond_to(self, :choose)
assert_respond_to(self, :say)
end
def test_or_ask
old_terminal = $terminal
input = StringIO.new
output = StringIO.new
$terminal = HighLine.new(input, output)
input << "10\n"
input.rewind
assert_equal(10, nil.or_ask("How much? ", Integer))
input.rewind
assert_equal(20, "20".or_ask("How much? ", Integer))
assert_equal(20, 20.or_ask("How much? ", Integer))
assert_equal(10, 20.or_ask("How much? ", Integer) { |q| q.in = 1..10 })
ensure
$terminal = old_terminal
end
def test_redirection
old_terminal = $terminal
$terminal = HighLine.new(nil, (output = StringIO.new))
say("Testing...")
assert_equal("Testing...\n", output.string)
ensure
$terminal = old_terminal
end
end

View file

@ -0,0 +1,429 @@
#!/usr/local/bin/ruby -w
# tc_menu.rb
#
# Created by Gregory Thomas Brown on 2005-05-10.
# Copyright 2005. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "test/unit"
require "highline"
require "stringio"
class TestMenu < Test::Unit::TestCase
def setup
@input = StringIO.new
@output = StringIO.new
@terminal = HighLine.new(@input, @output)
end
def test_choices
@input << "2\n"
@input.rewind
output = @terminal.choose do |menu|
menu.choices("Sample1", "Sample2", "Sample3")
end
assert_equal("Sample2", output)
end
def test_flow
@input << "Sample1\n"
@input.rewind
@terminal.choose do |menu|
# Default: menu.flow = :rows
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string)
@output.truncate(@output.rewind)
@input.rewind
@terminal.choose do |menu|
menu.flow = :columns_across
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("1. Sample1 2. Sample2 3. Sample3\n? ", @output.string)
@output.truncate(@output.rewind)
@input.rewind
@terminal.choose do |menu|
menu.flow = :inline
menu.index = :none
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample1, Sample2 or Sample3? ", @output.string)
end
def test_help
@input << "help\nhelp load\nhelp rules\nhelp missing\n"
@input.rewind
4.times do
@terminal.choose do |menu|
menu.shell = true
menu.choice(:load, "Load a file.")
menu.choice(:save, "Save data in file.")
menu.choice(:quit, "Exit program.")
menu.help("rules", "The rules of this system are as follows...")
end
end
assert_equal( "1. load\n2. save\n3. quit\n4. help\n? " +
"This command will display helpful messages about " +
"functionality, like this one. To see the help for a " +
"specific topic enter:\n" +
"\thelp [TOPIC]\n" +
"Try asking for help on any of the following:\n" +
"\nload quit rules save \n" +
"1. load\n2. save\n3. quit\n4. help\n? " +
"= load\n\n" +
"Load a file.\n" +
"1. load\n2. save\n3. quit\n4. help\n? " +
"= rules\n\n" +
"The rules of this system are as follows...\n" +
"1. load\n2. save\n3. quit\n4. help\n? " +
"= missing\n\n" +
"There's no help for that topic.\n", @output.string )
end
def test_index
@input << "Sample1\n"
@input.rewind
@terminal.choose do |menu|
# Default: menu.index = :number
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string)
@output.truncate(@output.rewind)
@input.rewind
@terminal.choose do |menu|
menu.index = :letter
menu.index_suffix = ") "
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("a) Sample1\nb) Sample2\nc) Sample3\n? ", @output.string)
@output.truncate(@output.rewind)
@input.rewind
@terminal.choose do |menu|
menu.index = :none
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample1\nSample2\nSample3\n? ", @output.string)
@output.truncate(@output.rewind)
@input.rewind
@terminal.choose do |menu|
menu.index = "*"
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("* Sample1\n* Sample2\n* Sample3\n? ", @output.string)
end
def test_layouts
@input << "save\n"
@input.rewind
@terminal.choose(:load, :save, :quit) # Default: layout = :list
assert_equal("1. load\n2. save\n3. quit\n? ", @output.string)
@input.rewind
@output.truncate(@output.rewind)
@terminal.choose(:load, :save, :quit) do |menu|
menu.header = "File Menu"
end
assert_equal( "File Menu:\n" +
"1. load\n2. save\n3. quit\n? ", @output.string )
@input.rewind
@output.truncate(@output.rewind)
@terminal.choose(:load, :save, :quit) do |menu|
menu.layout = :one_line
menu.header = "File Menu"
menu.prompt = "Operation? "
end
assert_equal( "File Menu: Operation? " +
"(load, save or quit) ", @output.string )
@input.rewind
@output.truncate(@output.rewind)
@terminal.choose(:load, :save, :quit) do |menu|
menu.layout = :menu_only
end
assert_equal("load, save or quit? ", @output.string)
@input.rewind
@output.truncate(@output.rewind)
@terminal.choose(:load, :save, :quit) do |menu|
menu.layout = '<%= list(@menu) %>File Menu: '
end
assert_equal("1. load\n2. save\n3. quit\nFile Menu: ", @output.string)
end
def test_list_option
@input << "l\n"
@input.rewind
@terminal.choose(:load, :save, :quit) do |menu|
menu.layout = :menu_only
menu.list_option = ", or "
end
assert_equal("load, save, or quit? ", @output.string)
end
def test_nil_on_handled
@input << "3\n3\n2\n"
@input.rewind
# Shows that by default proc results are returned.
output = @terminal.choose do |menu|
menu.choice "Sample1" do "output1" end
menu.choice "Sample2" do "output2" end
menu.choice "Sample3" do "output3" end
end
assert_equal("output3", output)
#
# Shows that they can be replaced with +nil+ by setting
# _nil_on_handled to +true+.
#
output = @terminal.choose do |menu|
menu.nil_on_handled = true
menu.choice "Sample1" do "output1" end
menu.choice "Sample2" do "output2" end
menu.choice "Sample3" do "output3" end
end
assert_equal(nil, output)
# Shows that a menu item without a proc will be returned no matter what.
output = @terminal.choose do |menu|
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample2", output)
end
def test_passed_command
@input << "q\n"
@input.rewind
selected = nil
@terminal.choose do |menu|
menu.choices(:load, :save, :quit) { |command| selected = command }
end
assert_equal(:quit, selected)
end
def test_question_options
@input << "save\n"
@input.rewind
answer = @terminal.choose(:Load, :Save, :Quit) do |menu|
menu.case = :capitalize
end
assert_equal(:Save, answer)
@input.rewind
answer = @terminal.choose(:Load, :Save, :Quit) do |menu|
menu.case = :capitalize
menu.character = :getc
end
assert_equal(:Save, answer)
assert_equal(?a, @input.getc)
end
def test_select_by
@input << "Sample1\n2\n"
@input.rewind
selected = @terminal.choose do |menu|
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample1", selected)
@input.rewind
selected = @terminal.choose do |menu|
menu.select_by = :index
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample2", selected)
@input.rewind
selected = @terminal.choose do |menu|
menu.select_by = :name
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
end
assert_equal("Sample1", selected)
end
def test_hidden
@input << "Hidden\n4\n"
@input.rewind
selected = @terminal.choose do |menu|
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
menu.hidden "Hidden!"
end
assert_equal("Hidden!", selected)
assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string)
@input.rewind
selected = @terminal.choose do |menu|
menu.select_by = :index
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
menu.hidden "Hidden!"
end
assert_equal("Hidden!", selected)
@input.rewind
selected = @terminal.choose do |menu|
menu.select_by = :name
menu.choice "Sample1"
menu.choice "Sample2"
menu.choice "Sample3"
menu.hidden "Hidden!"
end
assert_equal("Hidden!", selected)
@input.rewind
end
def test_select_by_letter
@input << "b\n"
@input.rewind
selected = @terminal.choose do |menu|
menu.index = :letter
menu.choice :save
menu.choice :load
menu.choice :quit
end
assert_equal(:load, selected)
end
def test_shell
@input << "save --some-option my_file.txt\n"
@input.rewind
selected = nil
options = nil
answer = @terminal.choose do |menu|
menu.choices(:load, :quit)
menu.choice(:save) do |command, details|
selected = command
options = details
"Saved!"
end
menu.shell = true
end
assert_equal("Saved!", answer)
assert_equal(:save, selected)
assert_equal("--some-option my_file.txt", options)
end
def test_simple_menu_shortcut
@input << "3\n"
@input.rewind
selected = @terminal.choose(:save, :load, :quit)
assert_equal(:quit, selected)
end
def test_symbols
@input << "3\n"
@input.rewind
selected = @terminal.choose do |menu|
menu.choices(:save, :load, :quit)
end
assert_equal(:quit, selected)
end
def test_paged_print_infinite_loop_bug
@terminal.page_at = 5
# Will page twice, so start with two new lines
@input << "\n\n3\n"
@input.rewind
# Sadly this goes into an infinite loop without the fix to page_print
selected = @terminal.choose(* 1..10)
assert_equal(selected, 3)
end
def test_cancel_paging
# Tests that paging can be cancelled halfway through
@terminal.page_at = 5
# Will page twice, so stop after first page and make choice 3
@input << "q\n3\n"
@input.rewind
selected = @terminal.choose(* 1..10)
assert_equal(selected, 3)
# Make sure paging message appeared
assert( @output.string.index('press enter/return to continue or q to stop'),
"Paging message did not appear." )
# Make sure it only appeared once
assert( @output.string !~ /q to stop.*q to stop/m,
"Paging message appeared more than once." )
end
end

View file

@ -0,0 +1,15 @@
#!/usr/local/bin/ruby -w
# ts_all.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# This is Free Software. See LICENSE and COPYING for details.
require "test/unit"
require "tc_highline"
require "tc_import"
require "tc_menu"
require "tc_color_scheme"

23
vendor/plugins/acts_as_list/README vendored Normal file
View file

@ -0,0 +1,23 @@
ActsAsList
==========
This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
Example
=======
class TodoList < ActiveRecord::Base
has_many :todo_items, :order => "position"
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list :scope => :todo_list
end
todo_list.first.move_to_bottom
todo_list.last.move_higher
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

3
vendor/plugins/acts_as_list/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
$:.unshift "#{File.dirname(__FILE__)}/lib"
require 'active_record/acts/list'
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }

View file

@ -0,0 +1,256 @@
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
# The class that has this specified needs to have a +position+ column defined as an integer on
# the mapped database table.
#
# Todo list example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, :order => "position"
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list :scope => :todo_list
# end
#
# todo_list.first.move_to_bottom
# todo_list.last.move_higher
module ClassMethods
# Configuration options are:
#
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_list(options = {})
configuration = { :column => "position", :scope => "1 = 1" }
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
else
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include ActiveRecord::Acts::List::InstanceMethods
def acts_as_list_class
::#{self.name}
end
def position_column
'#{configuration[:column]}'
end
#{scope_condition_method}
before_destroy :remove_from_list
before_create :add_to_list_bottom
EOV
end
end
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
# the first in the list of all chapters.
module InstanceMethods
# Insert the item at the given position (defaults to the top position of 1).
def insert_at(position = 1)
insert_at_position(position)
end
# Swap positions with the next lower item, if one exists.
def move_lower
return unless lower_item
acts_as_list_class.transaction do
lower_item.decrement_position
increment_position
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
acts_as_list_class.transaction do
higher_item.increment_position
decrement_position
end
end
# Move to the bottom of the list. If the item is already in the list, the items below it have their
# position adjusted accordingly.
def move_to_bottom
return unless in_list?
acts_as_list_class.transaction do
decrement_positions_on_lower_items
assume_bottom_position
end
end
# Move to the top of the list. If the item is already in the list, the items above it have their
# position adjusted accordingly.
def move_to_top
return unless in_list?
acts_as_list_class.transaction do
increment_positions_on_higher_items
assume_top_position
end
end
# Removes the item from the list.
def remove_from_list
if in_list?
decrement_positions_on_lower_items
update_attribute position_column, nil
end
end
# Increase the position of this item without adjusting the rest of the list.
def increment_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i + 1
end
# Decrease the position of this item without adjusting the rest of the list.
def decrement_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i - 1
end
# Return +true+ if this object is the first in the list.
def first?
return false unless in_list?
self.send(position_column) == 1
end
# Return +true+ if this object is the last in the list.
def last?
return false unless in_list?
self.send(position_column) == bottom_position_in_list
end
# Return the next higher item in the list.
def higher_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
)
end
# Test if this record is in a list
def in_list?
!send(position_column).nil?
end
private
def add_to_list_top
increment_positions_on_all_items
end
def add_to_list_bottom
self[position_column] = bottom_position_in_list.to_i + 1
end
# Overwrite this method to define the scope of the list changes
def scope_condition() "1" end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil)
item = bottom_item(except)
item ? item.send(position_column) : 0
end
# Returns the bottom item
def bottom_item(except = nil)
conditions = scope_condition
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
end
# Forces item to assume the bottom position in the list.
def assume_bottom_position
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
end
# Forces item to assume the top position in the list.
def assume_top_position
update_attribute(position_column, 1)
end
# This has the effect of moving all the higher items up one.
def decrement_positions_on_higher_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
)
end
# This has the effect of moving all the lower items up one.
def decrement_positions_on_lower_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
)
end
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
)
end
# This has the effect of moving all the lower items down one.
def increment_positions_on_lower_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
)
end
# Increments position (<tt>position_column</tt>) of all items in the list.
def increment_positions_on_all_items
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
)
end
def insert_at_position(position)
remove_from_list
increment_positions_on_lower_items(position)
self.update_attribute(position_column, position)
end
end
end
end
end

View file

@ -0,0 +1,332 @@
require 'test/unit'
require 'rubygems'
gem 'activerecord', '>= 1.15.4.7794'
require 'active_record'
require "#{File.dirname(__FILE__)}/../init"
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
def setup_db
ActiveRecord::Schema.define(:version => 1) do
create_table :mixins do |t|
t.column :pos, :integer
t.column :parent_id, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
end
def teardown_db
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table)
end
end
class Mixin < ActiveRecord::Base
end
class ListMixin < Mixin
acts_as_list :column => "pos", :scope => :parent
def self.table_name() "mixins" end
end
class ListMixinSub1 < ListMixin
end
class ListMixinSub2 < ListMixin
end
class ListWithStringScopeMixin < ActiveRecord::Base
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
def self.table_name() "mixins" end
end
class ListTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new(:parent_id => 1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 0)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
new4 = ListMixin.create(:parent_id => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixin.create(:parent_id => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
def test_with_string_based_scope
new = ListWithStringScopeMixin.create(:parent_id => 500)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_nil_scope
new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
new2.move_higher
assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
end
def test_remove_from_list_should_then_fail_in_list?
assert_equal true, ListMixin.find(1).in_list?
ListMixin.find(1).remove_from_list
assert_equal false, ListMixin.find(1).in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal nil, ListMixin.find(2).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
end
class ListSubTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new("parent_id"=>1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert_at
new = ListMixin.create("parent_id" => 20)
assert_equal 1, new.pos
new = ListMixinSub1.create("parent_id" => 20)
assert_equal 2, new.pos
new = ListMixinSub2.create("parent_id" => 20)
assert_equal 3, new.pos
new4 = ListMixin.create("parent_id" => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixinSub1.create("parent_id" => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
end

View file

@ -0,0 +1,13 @@
* trunk *
break with true value [Kaspar Schiess]
* 2.1 *
After actions [Saimon Moore]
* 2.0 * (2006-01-20 15:26:28 -0500)
Enter / Exit actions
Transition guards
Guards and actions can be a symbol pointing to a method or a Proc
* 1.0 * (2006-01-15 12:16:55 -0500)
Initial Release

View file

@ -0,0 +1,20 @@
Copyright (c) 2006 Scott Barron
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,33 @@
= Acts As State Machine
This act gives an Active Record model the ability to act as a finite state
machine (FSM).
Acquire via subversion at:
http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
If prompted, use the user/pass anonymous/anonymous.
== Example
class Order < ActiveRecord::Base
acts_as_state_machine :initial => :opened
state :opened
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
state :returned
event :close do
transitions :to => :closed, :from => :opened
end
event :return do
transitions :to => :returned, :from => :closed
end
end
o = Order.create
o.close! # notice is sent by mailer
o.return!

View file

@ -0,0 +1,28 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => [:clean_db, :test]
desc 'Remove the stale db file'
task :clean_db do
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
end
desc 'Test the acts as state machine plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the acts as state machine plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Acts As State Machine'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('TODO')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,11 @@
* Currently invalid events are ignored, create an option so that they can be
ignored or raise an exception.
* Query for a list of possible next states.
* Make listing states optional since they can be inferred from the events.
Only required to list a state if you want to define a transition block for it.
* Real transition actions
* Default states

View file

@ -0,0 +1,5 @@
require 'acts_as_state_machine'
ActiveRecord::Base.class_eval do
include ScottBarron::Acts::StateMachine
end

View file

@ -0,0 +1,268 @@
module ScottBarron #:nodoc:
module Acts #:nodoc:
module StateMachine #:nodoc:
class InvalidState < Exception #:nodoc:
end
class NoInitialState < Exception #:nodoc:
end
def self.included(base) #:nodoc:
base.extend ActMacro
end
module SupportingClasses
class State
attr_reader :name
def initialize(name, opts)
@name, @opts = name, opts
end
def entering(record)
enteract = @opts[:enter]
record.send(:run_transition_action, enteract) if enteract
end
def entered(record)
afteractions = @opts[:after]
return unless afteractions
Array(afteractions).each do |afteract|
record.send(:run_transition_action, afteract)
end
end
def exited(record)
exitact = @opts[:exit]
record.send(:run_transition_action, exitact) if exitact
end
end
class StateTransition
attr_reader :from, :to, :opts
def initialize(opts)
@from, @to, @guard = opts[:from], opts[:to], opts[:guard]
@opts = opts
end
def guard(obj)
@guard ? obj.send(:run_transition_action, @guard) : true
end
def perform(record)
return false unless guard(record)
loopback = record.current_state == to
states = record.class.read_inheritable_attribute(:states)
next_state = states[to]
old_state = states[record.current_state]
next_state.entering(record) unless loopback
record.update_attribute(record.class.state_column, to.to_s)
next_state.entered(record) unless loopback
old_state.exited(record) unless loopback
true
end
def ==(obj)
@from == obj.from && @to == obj.to
end
end
class Event
attr_reader :name
attr_reader :transitions
attr_reader :opts
def initialize(name, opts, transition_table, &block)
@name = name.to_sym
@transitions = transition_table[@name] = []
instance_eval(&block) if block
@opts = opts
@opts.freeze
@transitions.freeze
freeze
end
def next_states(record)
@transitions.select { |t| t.from == record.current_state }
end
def fire(record)
next_states(record).each do |transition|
break true if transition.perform(record)
end
end
def transitions(trans_opts)
Array(trans_opts[:from]).each do |s|
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
end
end
end
end
module ActMacro
# Configuration options are
#
# * +column+ - specifies the column name to use for keeping the state (default: state)
# * +initial+ - specifies an initial state for newly created objects (required)
def acts_as_state_machine(opts)
self.extend(ClassMethods)
raise NoInitialState unless opts[:initial]
write_inheritable_attribute :states, {}
write_inheritable_attribute :initial_state, opts[:initial]
write_inheritable_attribute :transition_table, {}
write_inheritable_attribute :event_table, {}
write_inheritable_attribute :state_column, opts[:column] || 'state'
class_inheritable_reader :initial_state
class_inheritable_reader :state_column
class_inheritable_reader :transition_table
class_inheritable_reader :event_table
self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)
before_create :set_initial_state
after_create :run_initial_state_actions
end
end
module InstanceMethods
def set_initial_state #:nodoc:
write_attribute self.class.state_column, self.class.initial_state.to_s
end
def run_initial_state_actions
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_sym]
initial.entering(self)
initial.entered(self)
end
# Returns the current state the object is in, as a Ruby symbol.
def current_state
self.send(self.class.state_column).to_sym
end
# Returns what the next state for a given event would be, as a Ruby symbol.
def next_state_for_event(event)
ns = next_states_for_event(event)
ns.empty? ? nil : ns.first.to
end
def next_states_for_event(event)
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
s.from == current_state
end
end
def run_transition_action(action)
Symbol === action ? self.method(action).call : action.call(self)
end
private :run_transition_action
end
module ClassMethods
# Returns an array of all known states.
def states
read_inheritable_attribute(:states).keys
end
# Define an event. This takes a block which describes all valid transitions
# for this event.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed
#
# event :close_order do
# transitions :to => :closed, :from => :open
# end
# end
#
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
# to and <tt>:from</tt> is a state (or Array of states) from which this
# event can be fired.
#
# This creates an instance method used for firing the event. The method
# created is the name of the event followed by an exclamation point (!).
# Example: <tt>order.close_order!</tt>.
def event(event, opts={}, &block)
tt = read_inheritable_attribute(:transition_table)
et = read_inheritable_attribute(:event_table)
e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
define_method("#{event.to_s}!") { e.fire(self) }
end
# Define a state of the system. +state+ can take an optional Proc object
# which will be executed every time the system transitions into that
# state. The proc will be passed the current object.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
# end
def state(name, opts={})
state = SupportingClasses::State.new(name.to_sym, opts)
read_inheritable_attribute(:states)[name.to_sym] = state
define_method("#{state.name}?") { current_state == state.name }
end
# Wraps ActiveRecord::Base.find to conveniently find all records in
# a given state. Options:
#
# * +number+ - This is just :first or :all from ActiveRecord +find+
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def find_in_state(number, state, *args)
with_state_scope state do
find(number, *args)
end
end
# Wraps ActiveRecord::Base.count to conveniently count all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def count_in_state(state, *args)
with_state_scope state do
count(*args)
end
end
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
def calculate_in_state(state, *args)
with_state_scope state do
calculate(*args)
end
end
protected
def with_state_scope(state)
raise InvalidState unless states.include?(state)
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
yield if block_given?
end
end
end
end
end
end

View file

@ -0,0 +1,224 @@
require File.dirname(__FILE__) + '/test_helper'
include ScottBarron::Acts::StateMachine
class ActsAsStateMachineTest < Test::Unit::TestCase
fixtures :conversations
def test_no_initial_value_raises_exception
assert_raise(NoInitialState) {
Person.acts_as_state_machine({})
}
end
def test_initial_state_value
assert_equal :needs_attention, Conversation.initial_state
end
def test_column_was_set
assert_equal 'state_machine', Conversation.state_column
end
def test_initial_state
c = Conversation.create
assert_equal :needs_attention, c.current_state
assert c.needs_attention?
end
def test_states_were_set
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |s|
assert Conversation.states.include?(s)
end
end
def test_event_methods_created
c = Conversation.create
%w(new_message! view! reply! close! junk! unjunk!).each do |event|
assert c.respond_to?(event)
end
end
def test_query_methods_created
c = Conversation.create
%w(needs_attention? read? closed? awaiting_response? junk?).each do |event|
assert c.respond_to?(event)
end
end
def test_transition_table
tt = Conversation.transition_table
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
end
def test_next_state_for_event
c = Conversation.create
assert_equal :read, c.next_state_for_event(:view)
end
def test_change_state
c = Conversation.create
c.view!
assert c.read?
end
def test_can_go_from_read_to_closed_because_guard_passes
c = Conversation.create
c.can_close = true
c.view!
c.reply!
c.close!
assert_equal :closed, c.current_state
end
def test_cannot_go_from_read_to_closed_because_of_guard
c = Conversation.create
c.can_close = false
c.view!
c.reply!
c.close!
assert_equal :read, c.current_state
end
def test_ignore_invalid_events
c = Conversation.create
c.view!
c.junk!
# This is the invalid event
c.new_message!
assert_equal :junk, c.current_state
end
def test_entry_action_executed
c = Conversation.create
c.read_enter = false
c.view!
assert c.read_enter
end
def test_after_actions_executed
c = Conversation.create
c.read_after_first = false
c.read_after_second = false
c.closed_after = false
c.view!
assert c.read_after_first
assert c.read_after_second
c.can_close = true
c.close!
assert c.closed_after
assert_equal :closed, c.current_state
end
def test_after_actions_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_after_first = false
c.read_after_second = false
c.view!
assert !c.read_after_first
assert !c.read_after_second
c.can_close = true
c.close!
c.closed_after = false
c.close!
assert !c.closed_after
end
def test_exit_action_executed
c = Conversation.create
c.read_exit = false
c.view!
c.junk!
assert c.read_exit
end
def test_entry_and_exit_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_enter = false
c.read_exit = false
c.view!
assert !c.read_enter
assert !c.read_exit
end
def test_entry_and_after_actions_called_for_initial_state
c = Conversation.create
assert c.needs_attention_enter
assert c.needs_attention_after
end
def test_run_transition_action_is_private
c = Conversation.create
assert_raise(NoMethodError) { c.run_transition_action :foo }
end
def test_find_all_in_state
cs = Conversation.find_in_state(:all, :read)
assert_equal 2, cs.size
end
def test_find_first_in_state
c = Conversation.find_in_state(:first, :read)
assert_equal conversations(:first).id, c.id
end
def test_find_all_in_state_with_conditions
cs = Conversation.find_in_state(:all, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal 1, cs.size
assert_equal conversations(:second).id, cs.first.id
end
def test_find_first_in_state_with_conditions
c = Conversation.find_in_state(:first, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal conversations(:second).id, c.id
end
def test_count_in_state
cnt0 = Conversation.count(['state_machine = ?', 'read'])
cnt = Conversation.count_in_state(:read)
assert_equal cnt0, cnt
end
def test_count_in_state_with_conditions
cnt0 = Conversation.count(['state_machine = ? AND subject = ?', 'read', 'Foo'])
cnt = Conversation.count_in_state(:read, ['subject = ?', 'Foo'])
assert_equal cnt0, cnt
end
def test_find_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.find_in_state(:all, :dead)
}
end
def test_count_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.count_in_state(:dead)
}
end
def test_can_access_events_via_event_table
event = Conversation.event_table[:junk]
assert_equal :junk, event.name
assert_equal "finished", event.opts[:note]
end
end

View file

@ -0,0 +1,18 @@
sqlite:
:adapter: sqlite
:dbfile: state_machine.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: state_machine.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: state_machine_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: state_machine_test

View file

@ -0,0 +1,67 @@
class Conversation < ActiveRecord::Base
attr_writer :can_close
attr_accessor :read_enter, :read_exit, :read_after_first, :read_after_second,
:closed_after, :needs_attention_enter, :needs_attention_after
acts_as_state_machine :initial => :needs_attention, :column => 'state_machine'
state :needs_attention, :enter => Proc.new { |o| o.needs_attention_enter = true },
:after => Proc.new { |o| o.needs_attention_after = true }
state :read, :enter => :read_enter_action,
:exit => Proc.new { |o| o.read_exit = true },
:after => [:read_after_first_action, :read_after_second_action]
state :closed, :after => :closed_after_action
state :awaiting_response
state :junk
event :new_message do
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
end
event :view do
transitions :to => :read, :from => [:needs_attention, :read]
end
event :reply do
transitions :to => :awaiting_response, :from => [:read, :closed]
end
event :close do
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => Proc.new {|o| o.can_close?}
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
end
event :junk, :note => "finished" do
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
end
event :unjunk do
transitions :to => :closed, :from => :junk
end
def can_close?
@can_close
end
def read_enter_action
self.read_enter = true
end
def always_true
true
end
def read_after_first_action
self.read_after_first = true
end
def read_after_second_action
self.read_after_second = true
end
def closed_after_action
self.closed_after = true
end
end

View file

@ -0,0 +1,11 @@
first:
id: 1
state_machine: read
subject: This is a test
closed: false
second:
id: 2
state_machine: read
subject: Foo
closed: false

View file

@ -0,0 +1,2 @@
class Person < ActiveRecord::Base
end

View file

@ -0,0 +1,11 @@
ActiveRecord::Schema.define(:version => 1) do
create_table :conversations, :force => true do |t|
t.column :state_machine, :string
t.column :subject, :string
t.column :closed, :boolean
end
create_table :people, :force => true do |t|
t.column :name, :string
end
end

View file

@ -0,0 +1,38 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
RAILS_ROOT = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_support/binding_of_caller'
require 'active_support/breakpoint'
require "#{File.dirname(__FILE__)}/../init"
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

28
vendor/plugins/arts/README vendored Normal file
View file

@ -0,0 +1,28 @@
ARTS is Another RJS Test System
For a complete tutorial, see http://glu.ttono.us/articles/2006/05/29/guide-test-driven-rjs-with-arts.
Usage:
assert_rjs :alert, 'Hi!'
assert_rjs :assign, 'a', '2'
assert_rjs :call, 'foo', 'bar', 'baz'
assert_rjs :draggable, 'draggable_item'
assert_rjs :drop_receiving, 'receiving_item'
assert_rjs :hide, "post_1", "post_2", "post_3"
assert_rjs :insert_html, :bottom, 'posts'
assert_rjs :redirect_to, :action => 'list'
assert_rjs :remove, "post_1", "post_2", "post_3"
assert_rjs :replace, 'completely_replaced_div'
assert_rjs :replace, 'completely_replaced_div', '<p>This replaced the div</p>'
assert_rjs :replace, 'completely_replaced_div', /replaced the div/
assert_rjs :replace_html, 'replaceable_div', "This goes inside the div"
assert_rjs :show, "post_1", "post_2", "post_3"
assert_rjs :sortable, 'sortable_item'
assert_rjs :toggle, "post_1", "post_2", "post_3"
assert_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
For the square bracket syntax (page['some_id'].toggle) use :page followed by the id and then subsequent method calls. Assignment requires a '=' at the end of the method name followed by the value.
assert_rjs :page, 'some_id', :toggle
assert_rjs :page, 'some_id', :style, :color=, 'red'

7
vendor/plugins/arts/about.yml vendored Normal file
View file

@ -0,0 +1,7 @@
author: Kevin Clark
summary: RJS Assertion Plugin
homepage: http://glu.ttono.us
plugin:
version: 0.6
license: MIT
rails_version: 1.1.2+

3
vendor/plugins/arts/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
# Give testing some culture
require 'test/unit/testcase'
Test::Unit::TestCase.send :include, Arts

1
vendor/plugins/arts/install.rb vendored Normal file
View file

@ -0,0 +1 @@
puts IO.read(File.join(File.dirname(__FILE__), 'README'))

133
vendor/plugins/arts/lib/arts.rb vendored Normal file
View file

@ -0,0 +1,133 @@
module Arts
include ActionView::Helpers::PrototypeHelper
include ActionView::Helpers::ScriptaculousHelper
include ActionView::Helpers::JavaScriptHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
def assert_rjs(action, *args, &block)
respond_to?("assert_rjs_#{action}") ?
send("assert_rjs_#{action}", *args) :
assert_response_contains(create_generator.send(action, *args, &block),
generic_error(action, args))
end
def assert_no_rjs(action, *args, &block)
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs(action, *args, &block) }
end
def assert_rjs_insert_html(*args)
position = args.shift
item_id = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("new Insertion\.#{position.to_s.camelize}(.*#{item_id}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("new Insertion.#{position.to_s.camelize}(\"#{item_id}\", #{content});",
"No insert_html call found for \n" +
" position: '#{position}' id: '#{item_id}' \ncontent: \n" +
"#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("new Insertion\.#{position.to_s.camelize}(.*#{item_id}.*,.*?);"),
@response.body
end
end
def assert_rjs_replace_html(*args)
div = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("Element.update(.*#{div}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("Element.update(\"#{div}\", #{content});",
"No replace_html call found on div: '#{div}' and content: \n#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("Element.update(.*#{div}.*,.*?);"), @response.body
end
end
def assert_rjs_replace(*args)
div = args.shift
content = extract_matchable_content(args)
unless content.blank?
case content
when Regexp
assert_match Regexp.new("Element.replace(.*#{div}.*,.*#{content.source}.*);"),
@response.body
when String
assert_response_contains("Element.replace(\"#{div}\", #{content});",
"No replace call found on div: '#{div}' and content: \n#{content}\n" +
"in response:\n#{@response.body}")
else
raise "Invalid content type"
end
else
assert_match Regexp.new("Element.replace(.*#{div}.*,.*?);"), @response.body
end
end
# To deal with [] syntax. I hate JavaScriptProxy so.. SO very much
def assert_rjs_page(*args)
content = build_method_chain!(args)
assert_match Regexp.new(Regexp.escape(content)), @response.body,
"Content did not include:\n #{content.to_s}"
end
protected
def assert_response_contains(str, message)
assert @response.body.to_s.index(str), message
end
def build_method_chain!(args)
content = create_generator.send(:[], args.shift) # start $('some_id')....
while !args.empty?
if (method = args.shift.to_s) =~ /(.*)=$/
content = content.__send__(method, args.shift)
break
else
content = content.__send__(method)
content = content.__send__(:function_chain).first if args.empty?
end
end
content
end
def create_generator
block = Proc.new { |*args| yield *args if block_given? }
JavaScriptGenerator.new self, &block
end
def generic_error(action, args)
"#{action} with args [#{args.join(" ")}] does not show up in response:\n#{@response.body}"
end
def extract_matchable_content(args)
if args.size == 1 and args.first.is_a? Regexp
return args.first
else
return create_generator.send(:arguments_for_call, args)
end
end
end

402
vendor/plugins/arts/test/arts_test.rb vendored Normal file
View file

@ -0,0 +1,402 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'breakpoint'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
class ArtsController < ActionController::Base
def alert
render :update do |page|
page.alert 'This is an alert'
end
end
def assign
render :update do |page|
page.assign 'a', '2'
end
end
def call
render :update do |page|
page.call 'foo', 'bar', 'baz'
end
end
def draggable
render :update do |page|
page.draggable 'my_image', :revert => true
end
end
def drop_receiving
render :update do |page|
page.drop_receiving "my_cart", :url => { :controller => "cart", :action => "add" }
end
end
def hide
render :update do |page|
page.hide 'some_div'
end
end
def insert_html
render :update do |page|
page.insert_html :bottom, 'content', 'Stuff in the content div'
end
end
def redirect
render :update do |page|
page.redirect_to :controller => 'sample', :action => 'index'
end
end
def remove
render :update do |page|
page.remove 'offending_div'
end
end
def replace
render :update do |page|
page.replace 'person_45', '<div>This replaces person_45</div>'
end
end
def replace_html
render :update do |page|
page.replace_html 'person_45', 'This goes inside person_45'
end
end
def show
render :update do |page|
page.show 'post_1', 'post_2', 'post_3'
end
end
def sortable
render :update do |page|
page.sortable 'sortable_item'
end
end
def toggle
render :update do |page|
page.toggle "post_1", "post_2", "post_3"
end
end
def visual_effect
render :update do |page|
page.visual_effect :highlight, "posts", :duration => '1.0'
end
end
def page_with_one_chained_method
render :update do |page|
page['some_id'].toggle
end
end
def page_with_assignment
render :update do |page|
page['some_id'].style.color = 'red'
end
end
def rescue_errors(e) raise e end
end
class ArtsTest < Test::Unit::TestCase
def setup
@controller = ArtsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_alert
get :alert
assert_nothing_raised { assert_rjs :alert, 'This is an alert' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :alert, 'This is not an alert'
end
assert_nothing_raised { assert_no_rjs :alert, 'This is not an alert' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :alert, 'This is an alert'
end
end
def test_assign
get :assign
assert_nothing_raised { assert_rjs :assign, 'a', '2' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :assign, 'a', '3'
end
assert_nothing_raised { assert_no_rjs :assign, 'a', '3' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :assign, 'a', '2'
end
end
def test_call
get :call
assert_nothing_raised { assert_rjs :call, 'foo', 'bar', 'baz' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :call, 'foo', 'bar'
end
assert_nothing_raised { assert_no_rjs :call, 'foo', 'bar' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :call, 'foo', 'bar', 'baz'
end
end
def test_draggable
get :draggable
assert_nothing_raised { assert_rjs :draggable, 'my_image', :revert => true }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :draggable, 'not_my_image'
end
assert_nothing_raised { assert_no_rjs :draggable, 'not_my_image' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :draggable, 'my_image', :revert => true
end
end
def test_drop_receiving
get :drop_receiving
assert_nothing_raised { assert_rjs :drop_receiving, "my_cart", :url => { :controller => "cart", :action => "add" } }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :drop_receiving, "my_cart"
end
assert_nothing_raised { assert_no_rjs :drop_receiving, "my_cart" }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :drop_receiving, "my_cart", :url => { :controller => "cart", :action => "add" }
end
end
def test_hide
get :hide
assert_nothing_raised { assert_rjs :hide, 'some_div' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :hide, 'some_other_div'
end
assert_nothing_raised { assert_no_rjs :hide, 'not_some_div' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :hide, 'some_div'
end
end
def test_insert_html
get :insert_html
assert_nothing_raised do
# No content matching
assert_rjs :insert_html, :bottom, 'content'
# Exact content matching
assert_rjs :insert_html, :bottom, 'content', 'Stuff in the content div'
# Regex matching
assert_rjs :insert_html, :bottom, 'content', /in.*content/
assert_no_rjs :insert_html, :bottom, 'not_our_div'
assert_no_rjs :insert_html, :bottom, 'content', /in.*no content/
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :insert_html, :bottom, 'content'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :insert_html, :bottom, 'no_content'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :insert_html, :bottom, 'content', /in the/
end
end
def test_redirect_to
get :redirect
assert_nothing_raised do
assert_rjs :redirect_to, :controller => 'sample', :action => 'index'
assert_no_rjs :redirect_to, :controller => 'sample', :action => 'show'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :redirect_to, :controller => 'doesnt', :action => 'exist'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :redirect_to, :controller => 'sample', :action => 'index'
end
end
def test_remove
get :remove
assert_nothing_raised do
assert_rjs :remove, 'offending_div'
assert_no_rjs :remove, 'dancing_happy_div'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :remove, 'dancing_happy_div'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :remove, 'offending_div'
end
end
def test_replace
get :replace
assert_nothing_raised do
# No content matching
assert_rjs :replace, 'person_45'
# String content matching
assert_rjs :replace, 'person_45', '<div>This replaces person_45</div>'
# regexp content matching
assert_rjs :replace, 'person_45', /<div>.*person_45.*<\/div>/
assert_no_rjs :replace, 'person_45', '<div>This replaces person_46</div>'
assert_no_rjs :replace, 'person_45', /person_46/
end
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace, 'person_45' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace, 'person_45', /person_45/ }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_46' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_45', 'bad stuff' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace, 'person_45', /not there/}
end
def test_replace_html
get :replace_html
assert_nothing_raised do
# No content matching
assert_rjs :replace_html, 'person_45'
# String content matching
assert_rjs :replace_html, 'person_45', 'This goes inside person_45'
# Regexp content matching
assert_rjs :replace_html, 'person_45', /goes inside/
assert_no_rjs :replace_html, 'person_46'
assert_no_rjs :replace_html, 'person_45', /doesn't go inside/
end
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace_html, 'person_45' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :replace_html, 'person_45', /goes/ }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace_html, 'person_46' }
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :replace_html, 'person_45', /gos inside/ }
end
def test_show
get :show
assert_nothing_raised do
assert_rjs :show, "post_1", "post_2", "post_3"
assert_no_rjs :show, 'post_4'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :show, 'post_4' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :show, "post_1", "post_2", "post_3"
end
end
def test_sortable
get :sortable
assert_nothing_raised do
assert_rjs :sortable, 'sortable_item'
assert_no_rjs :sortable, 'non-sortable-item'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :sortable, 'non-sortable-item' }
assert_raises(Test::Unit::AssertionFailedError) { assert_no_rjs :sortable, 'sortable_item' }
end
def test_toggle
get :toggle
assert_nothing_raised do
assert_rjs :toggle, "post_1", "post_2", "post_3"
assert_no_rjs :toggle, 'post_4'
end
assert_raises(Test::Unit::AssertionFailedError) { assert_rjs :toggle, 'post_4' }
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :toggle, "post_1", "post_2", "post_3"
end
end
def test_visual_effect
get :visual_effect
assert_nothing_raised do
assert_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
assert_no_rjs :visual_effect, :highlight, "lists"
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :visual_effect, :highlight, "lists"
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :visual_effect, :highlight, "posts", :duration => '1.0'
end
end
# [] support
def test_page_with_one_chained_method
get :page_with_one_chained_method
assert_nothing_raised do
assert_rjs :page, 'some_id', :toggle
assert_no_rjs :page, 'some_other_id', :toggle
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_rjs :page, 'some_other_id', :toggle
assert_no_rjs :page, 'some_id', :toggle
end
end
def test_page_with_assignment
get :page_with_assignment
assert_nothing_raised do
assert_rjs :page, 'some_id', :style, :color=, 'red'
assert_no_rjs :page, 'some_id', :color=, 'red'
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_no_rjs :page, 'some_id', :style, :color=, 'red'
assert_rjs :page, 'some_other_id', :style, :color=, 'red'
end
end
end

122
vendor/plugins/asset_packager/CHANGELOG vendored Normal file
View file

@ -0,0 +1,122 @@
------------------------------------------------------------------------
r52 | sbecker | 2007-11-04 01:38:21 -0400 (Sun, 04 Nov 2007) | 3 lines
* Allow configuration of which environments the helpers should merge scripts with the Synthesis::AssetPackage.merge_environments variable.
* Refactored tests so they can all run together, and not depend on what the RAILS_ENV constant is.
* Only add file extension if it was explicitly passed in, fixes other helpers in rails.
------------------------------------------------------------------------
r51 | sbecker | 2007-10-26 16:24:48 -0400 (Fri, 26 Oct 2007) | 1 line
* Updated jsmin.rb to latest version from 2007-07-20
------------------------------------------------------------------------
r50 | sbecker | 2007-10-23 23:16:07 -0400 (Tue, 23 Oct 2007) | 1 line
Updated CHANGELOG
------------------------------------------------------------------------
r49 | sbecker | 2007-10-23 23:13:27 -0400 (Tue, 23 Oct 2007) | 1 line
* Finally committed the subdirectory patch. (Thanks James Coglan!)
------------------------------------------------------------------------
r48 | sbecker | 2007-10-15 15:10:43 -0400 (Mon, 15 Oct 2007) | 1 line
* Speed up rake tasks and remove rails environment dependencies
------------------------------------------------------------------------
r43 | sbecker | 2007-07-02 15:30:29 -0400 (Mon, 02 Jul 2007) | 1 line
* Updated the docs regarding testing.
------------------------------------------------------------------------
r42 | sbecker | 2007-07-02 15:27:00 -0400 (Mon, 02 Jul 2007) | 1 line
* For production helper test, build packages once - on first setup.
------------------------------------------------------------------------
r41 | sbecker | 2007-07-02 15:14:13 -0400 (Mon, 02 Jul 2007) | 1 line
* Put build_all in test setup and delete_all in test teardown so all tests will pass the on first run of test suite.
------------------------------------------------------------------------
r40 | sbecker | 2007-07-02 14:55:28 -0400 (Mon, 02 Jul 2007) | 1 line
* Fix quotes, add contact info
------------------------------------------------------------------------
r39 | sbecker | 2007-07-02 14:53:52 -0400 (Mon, 02 Jul 2007) | 1 line
* Add note on how to run the tests for asset packager.
------------------------------------------------------------------------
r38 | sbecker | 2007-01-25 15:36:42 -0500 (Thu, 25 Jan 2007) | 1 line
added CHANGELOG w/ subversion log entries
------------------------------------------------------------------------
r37 | sbecker | 2007-01-25 15:34:39 -0500 (Thu, 25 Jan 2007) | 1 line
updated jsmin with new version from 2007-01-23
------------------------------------------------------------------------
r35 | sbecker | 2007-01-15 19:22:16 -0500 (Mon, 15 Jan 2007) | 1 line
require synthesis/asset_package in rake tasks, as Rails 1.2 seems to necessitate
------------------------------------------------------------------------
r34 | sbecker | 2007-01-05 12:22:09 -0500 (Fri, 05 Jan 2007) | 1 line
do a require before including in action view, because when running migrations, the plugin lib files don't automatically get required, causing the include to error out
------------------------------------------------------------------------
r33 | sbecker | 2006-12-23 02:03:41 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r32 | sbecker | 2006-12-23 02:03:12 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r31 | sbecker | 2006-12-23 01:52:25 -0500 (Sat, 23 Dec 2006) | 1 line
updated readme to show how to use different media for stylesheets
------------------------------------------------------------------------
r28 | sbecker | 2006-11-27 21:02:14 -0500 (Mon, 27 Nov 2006) | 1 line
updated compute_public_path, added check for images
------------------------------------------------------------------------
r27 | sbecker | 2006-11-10 18:28:29 -0500 (Fri, 10 Nov 2006) | 1 line
tolerate extra periods in source asset names. fixed subversion revision checking to be file specific, instead of repository specific.
------------------------------------------------------------------------
r26 | sbecker | 2006-06-24 17:04:27 -0400 (Sat, 24 Jun 2006) | 1 line
convert asset_packages_yml var to a class var
------------------------------------------------------------------------
r25 | sbecker | 2006-06-24 12:37:47 -0400 (Sat, 24 Jun 2006) | 1 line
Added ability to include assets by package name. In development, include all uncompressed asset files. In production, include the single compressed asset.
------------------------------------------------------------------------
r24 | sbecker | 2006-06-19 21:57:23 -0400 (Mon, 19 Jun 2006) | 1 line
Updates to README and about.yml
------------------------------------------------------------------------
r23 | sbecker | 2006-06-19 14:55:39 -0400 (Mon, 19 Jun 2006) | 2 lines
Modifying about.yml and README
------------------------------------------------------------------------
r21 | sbecker | 2006-06-19 12:18:32 -0400 (Mon, 19 Jun 2006) | 2 lines
added "formerly known as MergeJS"
------------------------------------------------------------------------
r20 | sbecker | 2006-06-19 12:14:46 -0400 (Mon, 19 Jun 2006) | 2 lines
Updating docs
------------------------------------------------------------------------
r19 | sbecker | 2006-06-19 11:26:08 -0400 (Mon, 19 Jun 2006) | 2 lines
removing compiled test assets from subversion
------------------------------------------------------------------------
r18 | sbecker | 2006-06-19 11:19:59 -0400 (Mon, 19 Jun 2006) | 2 lines
Initial import.
------------------------------------------------------------------------
r17 | sbecker | 2006-06-19 11:18:56 -0400 (Mon, 19 Jun 2006) | 2 lines
Creating directory.
------------------------------------------------------------------------

173
vendor/plugins/asset_packager/README vendored Normal file
View file

@ -0,0 +1,173 @@
= AssetPackager
JavaScript and CSS Asset Compression for Production Rails Apps
== Description
When it comes time to deploy your new web application, instead of
sending down a dozen JavaScript and CSS files full of formatting
and comments, this Rails plugin makes it simple to merge and
compress JavaScript and CSS down into one or more files, increasing
speed and saving bandwidth.
When in development, it allows you to use your original versions
and retain formatting and comments for readability and debugging.
Because not all browsers will dependably cache JavaScript and CSS
files with query string parameters, AssetPackager writes a timestamp
or subversion revision stamp (if available) into the merged file names.
Therefore files are correctly cached by the browser AND your users
always get the latest version when you re-deploy.
This code is released under the MIT license (like Ruby). Youre free
to rip it up, enhance it, etc. And if you make any enhancements,
Id like to know so I can add them back in. Thanks!
* Formerly known as MergeJS.
== Credit
This Rails Plugin was inspired by Cal Henderson's article
"Serving JavaScript Fast" on Vitamin:
http://www.thinkvitamin.com/features/webapps/serving-javascript-fast
It also uses the Ruby JavaScript Minifier created by
Douglas Crockford.
http://www.crockford.com/javascript/jsmin.html
== Key Features
* Merges and compresses JavaScript and CSS when running in production.
* Uses uncompressed originals when running in development.
* Handles caching correctly. (No querystring parameters - filename timestamps)
* Versions each package individually. Updates to files in one won't re-trigger downloading the others.
* Uses subversion revision numbers instead of timestamps if within a subversion controlled directory.
* Guarantees new version will get downloaded the next time you deploy.
== Components
* Rake Task for merging and compressing JavaScript and CSS files.
* Helper functions for including these JavaScript and CSS files in your views.
* YAML configuration file for mapping JavaScript and CSS files to merged versions.
* Rake Task for auto-generating the YAML file from your existing JavaScript files.
== How to Use:
1. Download and install the plugin:
./script/plugin install http://sbecker.net/shared/plugins/asset_packager
2. Run the rake task "asset:packager:create_yml" to generate the /config/asset_packages.yml
file the first time. You will need to reorder files under 'base' so dependencies are loaded
in correct order. Feel free to rename or create new file packages.
IMPORTANT: JavaScript files can break once compressed if each statement doesn't end with a semi-colon.
The minifier puts multiple statements on one line, so if the semi-colon is missing, the statement may no
longer makes sense and cause a syntax error.
Example from a fresh rails app after running the rake task. (Stylesheets is blank because a
default rails app has no stylesheets yet.):
---
javascripts:
- base:
- prototype
- effects
- dragdrop
- controls
- application
stylesheets:
- base: []
Example with multiple merged files:
---
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- application
- secondary:
- foo
- bar
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
3. Run the rake task "asset:packager:build_all" to generate the compressed, merged versions
for each package. Whenever you rearrange the yaml file, you'll need to run this task again.
Merging and compressing is expensive, so this is something we want to do once, not every time
your app starts. Thats why its a rake task.
4. Use the helper functions whenever including these files in your application. See below for examples.
5. Potential warning: css compressor function currently removes CSS comments. This might blow
away some CSS hackery. To disable comment removal, comment out /lib/synthesis/asset_package.rb line 176.
== JavaScript Examples
Example call:
<%= javascript_include_merged 'prototype', 'effects', 'controls', 'dragdrop', 'application', 'foo', 'bar' %>
In development, this generates:
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<script type="text/javascript" src="/javascripts/effects.js"></script>
<script type="text/javascript" src="/javascripts/controls.js"></script>
<script type="text/javascript" src="/javascripts/dragdrop.js"></script>
<script type="text/javascript" src="/javascripts/application.js"></script>
<script type="text/javascript" src="/javascripts/foo.js"></script>
<script type="text/javascript" src="/javascripts/bar.js"></script>
In production, this generates:
<script type="text/javascript" src="/javascripts/base_1150571523.js"></script>
<script type="text/javascript" src="/javascripts/secondary_1150729166.js"></script>
Now supports symbols and :defaults as well:
<%= javascript_include_merged :defaults %>
<%= javascript_include_merged :foo, :bar %>
== Stylesheet Examples
Example call:
<%= stylesheet_link_merged 'screen', 'header' %>
In development, this generates:
<link href="/stylesheets/screen.css" media="screen" rel="Stylesheet" type="text/css" />
<link href="/stylesheets/header.css" media="screen" rel="Stylesheet" type="text/css" />
In production this generates:
<link href="/stylesheets/base_1150729166.css" media="screen" rel="Stylesheet" type="text/css" />
== Different CSS Media
All options for stylesheet_link_tag still work, so if you want to specify a different media type:
<%= stylesheet_link_merged :secondary, 'media' => 'print' %>
== Running the tests
So you want to run the tests eh? Ok, then listen:
This plugin has a full suite of tests. But since they
depend on rails, it has to be run in the context of a
rails app, in the vendor/plugins directory. Observe:
> rails newtestapp
> cd newtestapp
> ./script/plugin install http://sbecker.net/shared/plugins/asset_packager
> cd vendor/plugins/asset_packager/
> rake # all tests pass
== License
Copyright (c) 2006 Scott Becker - http://synthesis.sbecker.net
Contact Email: becker.scott@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

22
vendor/plugins/asset_packager/Rakefile vendored Normal file
View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the asset_packager plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the asset_packager plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AssetPackager'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,8 @@
author: Scott Becker
name: AssetPackager
summary: JavaScript and CSS Asset Compression for Production Rails Apps
homepage: http://synthesis.sbecker.net/pages/asset_packager
plugin: http://sbecker.net/shared/plugins/asset_packager
license: MIT
version: 0.2
rails_version: 1.1.2+

2
vendor/plugins/asset_packager/init.rb vendored Normal file
View file

@ -0,0 +1,2 @@
require 'synthesis/asset_package_helper'
ActionView::Base.send :include, Synthesis::AssetPackageHelper

View file

@ -0,0 +1 @@
# Install hook code here

View file

@ -0,0 +1,205 @@
#!/usr/bin/ruby
# jsmin.rb 2007-07-20
# Author: Uladzislau Latynski
# This work is a translation from C to Ruby of jsmin.c published by
# Douglas Crockford. Permission is hereby granted to use the Ruby
# version under the same conditions as the jsmin.c on which it is
# based.
#
# /* jsmin.c
# 2003-04-21
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
EOF = -1
$theA = ""
$theB = ""
# isAlphanum -- return true if the character is a letter, digit, underscore,
# dollar sign, or non-ASCII character
def isAlphanum(c)
return false if !c || c == EOF
return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
c == '\\' || c[0] > 126)
end
# get -- return the next character from stdin. Watch out for lookahead. If
# the character is a control character, translate it to a space or linefeed.
def get()
c = $stdin.getc
return EOF if(!c)
c = c.chr
return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
return "\n" if (c == "\r")
return " "
end
# Get the next character without getting it.
def peek()
lookaheadChar = $stdin.getc
$stdin.ungetc(lookaheadChar)
return lookaheadChar.chr
end
# mynext -- get the next character, excluding comments.
# peek() is used to see if a '/' is followed by a '/' or '*'.
def mynext()
c = get
if (c == "/")
if(peek == "/")
while(true)
c = get
if (c <= "\n")
return c
end
end
end
if(peek == "*")
get
while(true)
case get
when "*"
if (peek == "/")
get
return " "
end
when EOF
raise "Unterminated comment"
end
end
end
end
return c
end
# action -- do something! What you do is determined by the argument: 1
# Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
# (Delete A). 3 Get the next B. (Delete B). action treats a string as a
# single character. Wow! action recognizes a regular expression if it is
# preceded by ( or , or =.
def action(a)
if(a==1)
$stdout.write $theA
end
if(a==1 || a==2)
$theA = $theB
if ($theA == "\'" || $theA == "\"")
while (true)
$stdout.write $theA
$theA = get
break if ($theA == $theB)
raise "Unterminated string literal" if ($theA <= "\n")
if ($theA == "\\")
$stdout.write $theA
$theA = get
end
end
end
end
if(a==1 || a==2 || a==3)
$theB = mynext
if ($theB == "/" && ($theA == "(" || $theA == "," || $theA == "=" ||
$theA == ":" || $theA == "[" || $theA == "!" ||
$theA == "&" || $theA == "|" || $theA == "?" ||
$theA == "{" || $theA == "}" || $theA == ";" ||
$theA == "\n"))
$stdout.write $theA
$stdout.write $theB
while (true)
$theA = get
if ($theA == "/")
break
elsif ($theA == "\\")
$stdout.write $theA
$theA = get
elsif ($theA <= "\n")
raise "Unterminated RegExp Literal"
end
$stdout.write $theA
end
$theB = mynext
end
end
end
# jsmin -- Copy the input to the output, deleting the characters which are
# insignificant to JavaScript. Comments will be removed. Tabs will be
# replaced with spaces. Carriage returns will be replaced with linefeeds.
# Most spaces and linefeeds will be removed.
def jsmin
$theA = "\n"
action(3)
while ($theA != EOF)
case $theA
when " "
if (isAlphanum($theB))
action(1)
else
action(2)
end
when "\n"
case ($theB)
when "{","[","(","+","-"
action(1)
when " "
action(3)
else
if (isAlphanum($theB))
action(1)
else
action(2)
end
end
else
case ($theB)
when " "
if (isAlphanum($theA))
action(1)
else
action(3)
end
when "\n"
case ($theA)
when "}","]",")","+","-","\"","\\", "'", '"'
action(1)
else
if (isAlphanum($theA))
action(1)
else
action(3)
end
end
else
action(1)
end
end
end
end
ARGV.each do |anArg|
$stdout.write "// #{anArg}\n"
end
jsmin

View file

@ -0,0 +1,233 @@
require 'yaml'
module Synthesis
class AssetPackage
# class variables
@@asset_packages_yml = $asset_packages_yml ||
(File.exists?("#{RAILS_ROOT}/config/asset_packages.yml") ? YAML.load_file("#{RAILS_ROOT}/config/asset_packages.yml") : nil)
# singleton methods
class << self
def merge_environments=(environments)
@@merge_environments = environments
end
def merge_environments
@@merge_environments ||= ["production"]
end
def parse_path(path)
/^(?:(.*)\/)?([^\/]+)$/.match(path).to_a
end
def find_by_type(asset_type)
@@asset_packages_yml[asset_type].map { |p| self.new(asset_type, p) }
end
def find_by_target(asset_type, target)
package_hash = @@asset_packages_yml[asset_type].find {|p| p.keys.first == target }
package_hash ? self.new(asset_type, package_hash) : nil
end
def find_by_source(asset_type, source)
path_parts = parse_path(source)
package_hash = @@asset_packages_yml[asset_type].find do |p|
key = p.keys.first
p[key].include?(path_parts[2]) && (parse_path(key)[1] == path_parts[1])
end
package_hash ? self.new(asset_type, package_hash) : nil
end
def targets_from_sources(asset_type, sources)
package_names = Array.new
sources.each do |source|
package = find_by_target(asset_type, source) || find_by_source(asset_type, source)
package_names << (package ? package.current_file : source)
end
package_names.uniq
end
def sources_from_targets(asset_type, targets)
source_names = Array.new
targets.each do |target|
package = find_by_target(asset_type, target)
source_names += (package ? package.sources.collect do |src|
package.target_dir.gsub(/^(.+)$/, '\1/') + src
end : target.to_a)
end
source_names.uniq
end
def build_all
@@asset_packages_yml.keys.each do |asset_type|
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).build }
end
end
def delete_all
@@asset_packages_yml.keys.each do |asset_type|
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).delete_all_builds }
end
end
def create_yml
unless File.exists?("#{RAILS_ROOT}/config/asset_packages.yml")
asset_yml = Hash.new
asset_yml['javascripts'] = [{"base" => build_file_list("#{RAILS_ROOT}/public/javascripts", "js")}]
asset_yml['stylesheets'] = [{"base" => build_file_list("#{RAILS_ROOT}/public/stylesheets", "css")}]
File.open("#{RAILS_ROOT}/config/asset_packages.yml", "w") do |out|
YAML.dump(asset_yml, out)
end
log "config/asset_packages.yml example file created!"
log "Please reorder files under 'base' so dependencies are loaded in correct order."
else
log "config/asset_packages.yml already exists. Aborting task..."
end
end
end
# instance methods
attr_accessor :asset_type, :target, :target_dir, :sources
def initialize(asset_type, package_hash)
target_parts = self.class.parse_path(package_hash.keys.first)
@target_dir = target_parts[1].to_s
@target = target_parts[2].to_s
@sources = package_hash[package_hash.keys.first]
@asset_type = asset_type
@asset_path = ($asset_base_path ? "#{$asset_base_path}/" : "#{RAILS_ROOT}/public/") +
"#{@asset_type}#{@target_dir.gsub(/^(.+)$/, '/\1')}"
@extension = get_extension
@match_regex = Regexp.new("\\A#{@target}_\\d+.#{@extension}\\z")
end
def current_file
@target_dir.gsub(/^(.+)$/, '\1/') +
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.sort.reverse[0].chomp(".#{@extension}")
end
def build
delete_old_builds
create_new_build
end
def delete_old_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}") unless x.index(revision.to_s)
end
end
def delete_all_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}")
end
end
private
def revision
unless @revision
revisions = [1]
@sources.each do |source|
revisions << get_file_revision("#{@asset_path}/#{source}.#{@extension}")
end
@revision = revisions.max
end
@revision
end
def get_file_revision(path)
if File.exists?(path)
begin
`svn info #{path}`[/Last Changed Rev: (.*?)\n/][/(\d+)/].to_i
rescue # use filename timestamp if not in subversion
File.mtime(path).to_i
end
else
0
end
end
def create_new_build
if File.exists?("#{@asset_path}/#{@target}_#{revision}.#{@extension}")
log "Latest version already exists: #{@asset_path}/#{@target}_#{revision}.#{@extension}"
else
File.open("#{@asset_path}/#{@target}_#{revision}.#{@extension}", "w") {|f| f.write(compressed_file) }
log "Created #{@asset_path}/#{@target}_#{revision}.#{@extension}"
end
end
def merged_file
merged_file = ""
@sources.each {|s|
File.open("#{@asset_path}/#{s}.#{@extension}", "r") { |f|
merged_file += f.read + "\n"
}
}
merged_file
end
def compressed_file
case @asset_type
when "javascripts" then compress_js(merged_file)
when "stylesheets" then compress_css(merged_file)
end
end
def compress_js(source)
jsmin_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/lib"
tmp_path = "#{RAILS_ROOT}/tmp/#{@target}_#{revision}"
# write out to a temp file
File.open("#{tmp_path}_uncompressed.js", "w") {|f| f.write(source) }
# compress file with JSMin library
`ruby #{jsmin_path}/jsmin.rb <#{tmp_path}_uncompressed.js >#{tmp_path}_compressed.js \n`
# read it back in and trim it
result = ""
File.open("#{tmp_path}_compressed.js", "r") { |f| result += f.read.strip }
# delete temp files if they exist
File.delete("#{tmp_path}_uncompressed.js") if File.exists?("#{tmp_path}_uncompressed.js")
File.delete("#{tmp_path}_compressed.js") if File.exists?("#{tmp_path}_compressed.js")
result
end
def compress_css(source)
source.gsub!(/\s+/, " ") # collapse space
source.gsub!(/\/\*(.*?)\*\/ /, "") # remove comments - caution, might want to remove this if using css hacks
source.gsub!(/\} /, "}\n") # add line breaks
source.gsub!(/\n$/, "") # remove last break
source.gsub!(/ \{ /, " {") # trim inside brackets
source.gsub!(/; \}/, "}") # trim inside brackets
source
end
def get_extension
case @asset_type
when "javascripts" then "js"
when "stylesheets" then "css"
end
end
def log(message)
puts message
end
def self.build_file_list(path, extension)
re = Regexp.new(".#{extension}\\z")
file_list = Dir.new(path).entries.delete_if { |x| ! (x =~ re) }.map {|x| x.chomp(".#{extension}")}
# reverse javascript entries so prototype comes first on a base rails app
file_list.reverse! if extension == "js"
file_list
end
end
end

View file

@ -0,0 +1,67 @@
module Synthesis
module AssetPackageHelper
def should_merge?
AssetPackage.merge_environments.include?(RAILS_ENV)
end
def javascript_include_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
if sources.include?(:defaults)
sources = sources[0..(sources.index(:defaults))] +
['prototype', 'effects', 'dragdrop', 'controls'] +
(File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") ? ['application'] : []) +
sources[(sources.index(:defaults) + 1)..sources.length]
sources.delete(:defaults)
end
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("javascripts", sources) :
AssetPackage.sources_from_targets("javascripts", sources))
sources.collect {|source| javascript_include_tag(source, options) }.join("\n")
end
def stylesheet_link_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("stylesheets", sources) :
AssetPackage.sources_from_targets("stylesheets", sources))
sources.collect { |source|
source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n")
end
private
# rewrite compute_public_path to allow us to not include the query string timestamp
# used by ActionView::Helpers::AssetTagHelper
def compute_public_path(source, dir, ext=nil, add_asset_id=true)
source = source.dup
source << ".#{ext}" if File.extname(source).blank? && ext
unless source =~ %r{^[-a-z]+://}
source = "/#{dir}/#{source}" unless source[0] == ?/
asset_id = rails_asset_id(source)
source << '?' + asset_id if defined?(RAILS_ROOT) and add_asset_id and not asset_id.blank?
source = "#{ActionController::Base.asset_host}#{@controller.request.relative_url_root}#{source}"
end
source
end
# rewrite javascript path function to not include query string timestamp
def javascript_path(source)
compute_public_path(source, 'javascripts', 'js', false)
end
# rewrite stylesheet path function to not include query string timestamp
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css', false)
end
end
end

View file

@ -0,0 +1,22 @@
require File.dirname(__FILE__) + '/../lib/synthesis/asset_package'
namespace :asset do
namespace :packager do
desc "Merge and compress assets"
task :build_all do
Synthesis::AssetPackage.build_all
end
desc "Delete all asset builds"
task :delete_all do
Synthesis::AssetPackage.delete_all
end
desc "Generate asset_packages.yml from existing assets"
task :create_yml do
Synthesis::AssetPackage.create_yml
end
end
end

View file

@ -0,0 +1,107 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
ENV['RAILS_ENV'] = "development"
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackageHelperProductionTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
end
end.new
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
end
def test_js_basic
assert_dom_equal build_js_expected_string("prototype"),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
assert_dom_equal build_js_expected_string("prototype", "foo"),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
assert_dom_equal build_js_expected_string("prototype", "foo", "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "not_part_of_a_package", "foo"),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop"),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop", "foo", "bar", "application"),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
assert_dom_equal build_css_expected_string("screen"),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
assert_dom_equal build_css_expected_string("screen", "foo", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
assert_dom_equal build_css_expected_string("screen", "foo", "not_part_of_a_package", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package", "subdir/bar")
end
def test_css_multiple_from_same_package
assert_dom_equal build_css_expected_string("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
assert_dom_equal build_css_expected_string("screen", "header"),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
assert_dom_equal build_css_expected_string("screen", "header", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
end

View file

@ -0,0 +1,153 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackageHelperProductionTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
cattr_accessor :packages_built
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
self.stubs(:should_merge?).returns(true)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
end
end.new
build_packages_once
end
def build_packages_once
unless @@packages_built
Synthesis::AssetPackage.build_all
@@packages_built = true
end
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
end
def test_js_basic
current_file = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
assert_dom_equal build_js_expected_string(current_file),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2, "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, "not_part_of_a_package", current_file2),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
assert_dom_equal build_js_expected_string(package_name),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("javascripts", "secondary").current_file
assert_dom_equal build_js_expected_string(package_name1, package_name2),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
current_file = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
assert_dom_equal build_css_expected_string(current_file),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, current_file3),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, "not_part_of_a_package"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package")
end
def test_css_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, "not_part_of_a_package", current_file2, current_file3),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
assert_dom_equal build_css_expected_string(package_name),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("stylesheets", "secondary").current_file
package_name3 = Synthesis::AssetPackage.find_by_target("stylesheets", "subdir/styles").current_file
assert_dom_equal build_css_expected_string(package_name1, package_name2, package_name3),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
def test_image_tag
timestamp = rails_asset_id("images/rails.png")
assert_dom_equal %(<img alt="Rails" src="/images/rails.png?#{timestamp}" />), image_tag("rails")
end
end

View file

@ -0,0 +1,92 @@
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'mocha'
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackagerTest < Test::Unit::TestCase
include Synthesis
def setup
Synthesis::AssetPackage.any_instance.stubs(:log)
Synthesis::AssetPackage.build_all
end
def teardown
Synthesis::AssetPackage.delete_all
end
def test_find_by_type
js_asset_packages = Synthesis::AssetPackage.find_by_type("javascripts")
assert_equal 2, js_asset_packages.length
assert_equal "base", js_asset_packages[0].target
assert_equal ["prototype", "effects", "controls", "dragdrop"], js_asset_packages[0].sources
end
def test_find_by_target
package = Synthesis::AssetPackage.find_by_target("javascripts", "base")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_find_by_source
package = Synthesis::AssetPackage.find_by_source("javascripts", "controls")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_delete_and_build
Synthesis::AssetPackage.delete_all
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
assert_equal 0, js_package_names.length
assert_equal 0, css_package_names.length
assert_equal 0, css_subdir_package_names.length
Synthesis::AssetPackage.build_all
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }.sort
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
assert_equal 2, js_package_names.length
assert_equal 2, css_package_names.length
assert_equal 1, css_subdir_package_names.length
assert js_package_names[0].match(/\Abase_\d+.js\z/)
assert js_package_names[1].match(/\Asecondary_\d+.js\z/)
assert css_package_names[0].match(/\Abase_\d+.css\z/)
assert css_package_names[1].match(/\Asecondary_\d+.css\z/)
assert css_subdir_package_names[0].match(/\Astyles_\d+.css\z/)
end
def test_js_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("javascripts", ["prototype", "effects", "noexist1", "controls", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_\d+\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
assert_equal package_names[3], "noexist2"
end
def test_css_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("stylesheets", ["header", "screen", "noexist1", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_\d+\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
assert_equal package_names[3], "noexist2"
end
def test_should_return_merge_environments_when_set
Synthesis::AssetPackage.merge_environments = ["staging", "production"]
assert_equal ["staging", "production"], Synthesis::AssetPackage.merge_environments
end
def test_should_only_return_production_merge_environment_when_not_set
assert_equal ["production"], Synthesis::AssetPackage.merge_environments
end
end

View file

@ -0,0 +1,20 @@
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- secondary:
- foo
- bar
- application
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
- subdir/styles:
- foo
- bar

View file

@ -0,0 +1,2 @@
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

View file

@ -0,0 +1,4 @@
bar bar bar
bar bar bar
bar bar bar

View file

@ -0,0 +1,815 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okButton: true,
okText: "ok",
cancelLink: true,
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
submitOnBlur: false,
ajaxOptions: {},
evalScripts: false
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
if (this.options.okButton) {
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
okButton.className = 'editor_ok_button';
this.form.appendChild(okButton);
}
if (this.options.cancelLink) {
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
cancelLink.className = 'editor_cancel';
this.form.appendChild(cancelLink);
}
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
var obj = this;
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.obj = this;
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field';
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
if (this.options.submitOnBlur)
textField.onblur = this.onSubmit.bind(this);
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.obj = this;
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
textArea.className = 'editor_field';
if (this.options.submitOnBlur)
textArea.onblur = this.onSubmit.bind(this);
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
if (this.options.evalScripts) {
new Ajax.Request(
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this),
asynchronous:true,
evalScripts:true
}, this.options.ajaxOptions));
} else {
new Ajax.Updater(
{ success: this.element,
// don't update on failure (this could be an option)
failure: null },
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions));
}
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
createEditField: function() {
if (!this.cached_selectTag) {
var selectTag = document.createElement("select");
var collection = this.options.collection || [];
var optionTag;
collection.each(function(e,i) {
optionTag = document.createElement("option");
optionTag.value = (e instanceof Array) ? e[0] : e;
if(this.options.value==optionTag.value) optionTag.selected = true;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag);
}.bind(this));
this.cached_selectTag = selectTag;
}
this.editField = this.cached_selectTag;
if(this.options.loadTextURL) this.loadExternalText();
this.form.appendChild(this.editField);
this.options.callback = function(form, value) {
return "value=" + encodeURIComponent(value);
}
}
});
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

View file

@ -0,0 +1,913 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null,
tree: false
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
var affected = [];
if(this.last_active) this.deactivate(this.last_active);
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0) {
drop = Droppables.findDeepestChild(affected);
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
Droppables.activate(drop);
}
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string')) {
var h = Element.childrenWithClassName(this.element, options.handle, true);
if(h.length>0) this.handle = h[0];
}
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML)
options.scroll = $(options.scroll);
Element.makePositioned(this.element); // fix IE
this.delta = this.currentDelta();
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='OPTION' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
if(this.element._revert) {
this.element._revert.cancel();
this.element._revert = null;
}
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
this.originalScrollLeft = where.left;
this.originalScrollTop = where.top;
} else {
this.originalScrollLeft = this.options.scroll.scrollLeft;
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
Position.prepare();
Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft;
p[1] += this.options.scroll.scrollTop;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
var speed = [0,0];
if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]);
} else {
if(this.options.snap instanceof Array) {
p = p.map( function(v, i) {
return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
this.lastScrolled = current;
if(this.options.scroll == window) {
with (this._getWindowScroll(this.options.scroll)) {
if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
var d = delta / 1000;
this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
}
}
} else {
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
if (w.document.documentElement && documentElement.scrollTop) {
T = documentElement.scrollTop;
L = documentElement.scrollLeft;
} else if (w.document.body) {
T = body.scrollTop;
L = body.scrollLeft;
}
if (w.innerWidth) {
W = w.innerWidth;
H = w.innerHeight;
} else if (w.document.documentElement && documentElement.clientWidth) {
W = documentElement.clientWidth;
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight
}
}
return { top: T, left: L, width: W, height: H };
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: {},
_findRootElement: function(element) {
while (element.tagName != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
},
options: function(element) {
element = Sortable._findRootElement($(element));
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
var s = Sortable.options(element);
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false,
treeTag: 'ul',
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: /^[^_]*_(.*)$/,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
//greedy: !options.dropOnEmpty
}
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// drop on empty handling
if(options.dropOnEmpty || options.tree) {
Droppables.add(element, options_for_tree);
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
e.treeNode = element;
options.droppables.push(e);
});
}
// keep reference
this.sortables[element.id] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
},
onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;
if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Element.show(Sortable._marker);
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: new Array,
position: parent.children.length,
container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
}
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child)
parent.children.push (child);
}
return parent;
},
/* Finds the first element of the given tag type within a parent element.
Used for finding the first LI[ST] within a L[IST]I[TEM].*/
_findChildrenElement: function (element, containerTag) {
if (element && element.hasChildNodes)
for (var i = 0; i < element.childNodes.length; ++i)
if (element.childNodes[i].tagName == containerTag)
return element.childNodes[i];
return null;
},
tree: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
treeTag: sortableOptions.treeTag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
}, arguments[1] || {});
var root = {
id: null,
parent: null,
children: new Array,
container: element,
position: 0
}
return Sortable._tree (element, options, root);
},
/* Construct a [i] index for a particular node */
_constructIndex: function(node) {
var index = '';
do {
if (node.id) index = '[' + node.position + ']' + index;
} while ((node = node.parent) != null);
return index;
},
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || {});
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
},
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || {});
var nodeMap = {};
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
n[1].appendChild(n[0]);
delete nodeMap[ident];
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || {});
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
return Sortable.sequence(element, arguments[1]).map( function(item) {
return name + "[]=" + encodeURIComponent(item);
}).join('&');
}
}
}
/* Returns true if child is contained within element */
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==tagName &&
(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
elements.push(e);
if(recursive) {
var grandchildren = Element.findChildren(e, only, recursive, tagName);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : []);
}
Element.offsetSize = function (element, type) {
if (type == 'vertical' || type == 'height')
return element.offsetHeight;
else
return element.offsetWidth;
}

View file

@ -0,0 +1,958 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
// See scriptaculous.js for full license.
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
if(this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if(this.slice(0,1) == '#') {
if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if(this.length==7) color = this.toLowerCase();
}
}
return(color.length==7 ? color : (arguments[0] || this));
}
/*--------------------------------------------------------------------------*/
Element.collectTextNodes = function(element) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
(node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
}).flatten().join('');
}
Element.collectTextNodesIgnoreClass = function(element, className) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
Element.collectTextNodesIgnoreClass(node, className) : ''));
}).flatten().join('');
}
Element.setContentZoom = function(element, percent) {
element = $(element);
Element.setStyle(element, {fontSize: (percent/100) + 'em'});
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}
Element.getOpacity = function(element){
var opacity;
if (opacity = Element.getStyle(element, 'opacity'))
return parseFloat(opacity);
if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0;
}
Element.setOpacity = function(element, value){
element= $(element);
if (value == 1){
Element.setStyle(element, { opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null });
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else {
if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value});
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element,
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
}
}
Element.getInlineOpacity = function(element){
return $(element).style.opacity || '';
}
Element.childrenWithClassName = function(element, className, findFirst) {
var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) {
return (c.className && c.className.match(classNameRegExp));
});
if(!results) results = [];
return results;
}
Element.forceRerendering = function(element) {
try {
element = $(element);
var n = document.createTextNode(' ');
element.appendChild(n);
element.removeChild(n);
} catch(e) { }
};
/*--------------------------------------------------------------------------*/
Array.prototype.call = function() {
var args = arguments;
this.each(function(f){ f.apply(this, args) });
}
/*--------------------------------------------------------------------------*/
var Effect = {
tagifyText: function(element) {
var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
Builder.node('span',{style: tagifyStyle},
character == ' ' ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
}
});
},
multiple: function(element, effect) {
var elements;
if(((typeof element == 'object') ||
(typeof element == 'function')) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
}, arguments[2] || {});
var masterDelay = options.delay;
$A(elements).each( function(element, index) {
new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
});
},
PAIRS: {
'slide': ['SlideDown','SlideUp'],
'blind': ['BlindDown','BlindUp'],
'appear': ['Appear','Fade']
},
toggle: function(element, effect) {
element = $(element);
effect = (effect || 'appear').toLowerCase();
var options = Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
}, arguments[2] || {});
Effect[element.visible() ?
Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
}
};
var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
/* ------------- core effects ------------- */
Effect.ScopedQueue = Class.create();
Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
initialize: function() {
this.effects = [];
this.interval = null;
},
_each: function(iterator) {
this.effects._each(iterator);
},
add: function(effect) {
var timestamp = new Date().getTime();
var position = (typeof effect.options.queue == 'string') ?
effect.options.queue : effect.options.queue.position;
switch(position) {
case 'front':
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
});
break;
case 'end':
// start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
this.effects.push(effect);
if(!this.interval)
this.interval = setInterval(this.loop.bind(this), 40);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
if(this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
this.effects.invoke('loop', timePos);
}
});
Effect.Queues = {
instances: $H(),
get: function(queueName) {
if(typeof queueName != 'string') return queueName;
if(!this.instances[queueName])
this.instances[queueName] = new Effect.ScopedQueue();
return this.instances[queueName];
}
}
Effect.Queue = Effect.Queues.get('global');
Effect.DefaultOptions = {
transition: Effect.Transitions.sinoidal,
duration: 1.0, // seconds
fps: 25.0, // max. 25fps due to Effect.Queue implementation
sync: false, // true for combining
from: 0.0,
to: 1.0,
delay: 0.0,
queue: 'parallel'
}
Effect.Base = function() {};
Effect.Base.prototype = {
position: null,
start: function(options) {
this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
this.currentFrame = 0;
this.state = 'idle';
this.startOn = this.options.delay*1000;
this.finishOn = this.startOn + (this.options.duration*1000);
this.event('beforeStart');
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).add(this);
},
loop: function(timePos) {
if(timePos >= this.startOn) {
if(timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if(this.finish) this.finish();
this.event('afterFinish');
return;
}
var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
var frame = Math.round(pos * this.options.fps * this.options.duration);
if(frame > this.currentFrame) {
this.render(pos);
this.currentFrame = frame;
}
}
},
render: function(pos) {
if(this.state == 'idle') {
this.state = 'running';
this.event('beforeSetup');
if(this.setup) this.setup();
this.event('afterSetup');
}
if(this.state == 'running') {
if(this.options.transition) pos = this.options.transition(pos);
pos *= (this.options.to-this.options.from);
pos += this.options.from;
this.position = pos;
this.event('beforeUpdate');
if(this.update) this.update(pos);
this.event('afterUpdate');
}
},
cancel: function() {
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).remove(this);
this.state = 'finished';
},
event: function(eventName) {
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
if(this.options[eventName]) this.options[eventName](this);
},
inspect: function() {
return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
}
}
Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
},
update: function(position) {
this.effects.invoke('render', position);
},
finish: function(position) {
this.effects.each( function(effect) {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
if(effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
// make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
this.element.setStyle({zoom: 1});
var options = Object.extend({
from: this.element.getOpacity() || 0.0,
to: 1.0
}, arguments[1] || {});
this.start(options);
},
update: function(position) {
this.element.setOpacity(position);
}
});
Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({
x: 0,
y: 0,
mode: 'relative'
}, arguments[1] || {});
this.start(options);
},
setup: function() {
// Bug in Opera: Opera returns the "real" position of a static element or
// relative element that does not have top/left explicitly set.
// ==> Always set top and left for position relative elements in your stylesheets
// (to 0 if you do not need them)
this.element.makePositioned();
this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
this.originalTop = parseFloat(this.element.getStyle('top') || '0');
if(this.options.mode == 'absolute') {
// absolute movement, so we need to calc deltaX and deltaY
this.options.x = this.options.x - this.originalLeft;
this.options.y = this.options.y - this.originalTop;
}
},
update: function(position) {
this.element.setStyle({
left: this.options.x * position + this.originalLeft + 'px',
top: this.options.y * position + this.originalTop + 'px'
});
}
});
// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
return new Effect.Move(element,
Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
};
Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
var options = Object.extend({
scaleX: true,
scaleY: true,
scaleContent: true,
scaleFromCenter: false,
scaleMode: 'box', // 'box' or 'contents' or {} with provided values
scaleFrom: 100.0,
scaleTo: percent
}, arguments[2] || {});
this.start(options);
},
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = this.element.getStyle('position');
this.originalStyle = {};
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType;
}
}.bind(this));
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if(this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
if(/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
if(!this.dims)
this.dims = [this.options.scaleMode.originalHeight,
this.options.scaleMode.originalWidth];
},
update: function(position) {
var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
if(this.options.scaleContent && this.fontSize)
this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
},
finish: function(position) {
if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
},
setDimensions: function(height, width) {
var d = {};
if(this.options.scaleX) d.width = width + 'px';
if(this.options.scaleY) d.height = height + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2;
if(this.elementPositioning == 'absolute') {
if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
} else {
if(this.options.scaleY) d.top = -topd + 'px';
if(this.options.scaleX) d.left = -leftd + 'px';
}
}
this.element.setStyle(d);
}
});
Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options);
},
setup: function() {
// Prevent executing on elements not in the layout flow
if(this.element.getStyle('display')=='none') { this.cancel(); return; }
// Disable background image during the effect
this.oldStyle = {
backgroundImage: this.element.getStyle('background-image') };
this.element.setStyle({backgroundImage: 'none'});
if(!this.options.endcolor)
this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
if(!this.options.restorecolor)
this.options.restorecolor = this.element.getStyle('background-color');
// init color calculations
this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
},
update: function(position) {
this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
},
finish: function() {
this.element.setStyle(Object.extend(this.oldStyle, {
backgroundColor: this.options.restorecolor
}));
}
});
Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
this.start(arguments[1] || {});
},
setup: function() {
Position.prepare();
var offsets = Position.cumulativeOffset(this.element);
if(this.options.offset) offsets[1] += this.options.offset;
var max = window.innerHeight ?
window.height - window.innerHeight :
document.body.scrollHeight -
(document.documentElement.clientHeight ?
document.documentElement.clientHeight : document.body.clientHeight);
this.scrollStart = Position.deltaY;
this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
},
update: function(position) {
Position.prepare();
window.scrollTo(Position.deltaX,
this.scrollStart + (position*this.delta));
}
});
/* ------------- combination effects ------------- */
Effect.Fade = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
var options = Object.extend({
from: element.getOpacity() || 1.0,
to: 0.0,
afterFinishInternal: function(effect) {
if(effect.options.to!=0) return;
effect.element.hide();
effect.element.setStyle({opacity: oldOpacity});
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Appear = function(element) {
element = $(element);
var options = Object.extend({
from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
to: 1.0,
// force Safari to render floated elements properly
afterFinishInternal: function(effect) {
effect.element.forceRerendering();
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from);
effect.element.show();
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(element) {
element = $(element);
var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') };
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) {
effect.effects[0].element.setStyle({position: 'absolute'}); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.setStyle(oldStyle); }
}, arguments[1] || {})
);
}
Effect.BlindUp = function(element) {
element = $(element);
element.makeClipping();
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.BlindDown = function(element) {
element = $(element);
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
return new Effect.Appear(element, {
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makePositioned();
effect.element.makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.undoPositioned();
effect.element.setStyle({opacity: oldOpacity});
}
})
}
});
}
Effect.DropOut = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left'),
opacity: element.getInlineOpacity() };
return new Effect.Parallel(
[ new Effect.Move(element, {x: 0, y: 100, sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) {
effect.effects[0].element.makePositioned();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, arguments[1] || {}));
}
Effect.Shake = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left') };
return new Effect.Move(element,
{ x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
effect.element.undoPositioned();
effect.element.setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
element.cleanWhitespace();
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
// IE will crash if child is undoPositioned first
if(/MSIE/.test(navigator.userAgent)){
effect.element.undoPositioned();
effect.element.firstChild.undoPositioned();
}else{
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
}
effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
element.cleanWhitespace();
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
restoreAfterFinish: true,
beforeStartInternal: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); },
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
effect.element.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping(effect.element); },
afterFinishInternal: function(effect) {
effect.element.hide(effect.element);
effect.element.undoClipping(effect.element); }
});
}
Effect.Grow = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.full
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (options.direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = dims.width;
initialMoveY = moveY = 0;
moveX = -dims.width;
break;
case 'bottom-left':
initialMoveX = moveX = 0;
initialMoveY = dims.height;
moveY = -dims.height;
break;
case 'bottom-right':
initialMoveX = dims.width;
initialMoveY = dims.height;
moveX = -dims.width;
moveY = -dims.height;
break;
case 'center':
initialMoveX = dims.width / 2;
initialMoveY = dims.height / 2;
moveX = -dims.width / 2;
moveY = -dims.height / 2;
break;
}
return new Effect.Move(element, {
x: initialMoveX,
y: initialMoveY,
duration: 0.01,
beforeSetup: function(effect) {
effect.element.hide();
effect.element.makeClipping();
effect.element.makePositioned();
},
afterFinishInternal: function(effect) {
new Effect.Parallel(
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'});
effect.effects[0].element.show();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, options)
)
}
});
}
Effect.Shrink = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.none
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var moveX, moveY;
switch (options.direction) {
case 'top-left':
moveX = moveY = 0;
break;
case 'top-right':
moveX = dims.width;
moveY = 0;
break;
case 'bottom-left':
moveX = 0;
moveY = dims.height;
break;
case 'bottom-right':
moveX = dims.width;
moveY = dims.height;
break;
case 'center':
moveX = dims.width / 2;
moveY = dims.height / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({
beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned();
effect.effects[0].element.makeClipping(); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle); }
}, options)
);
}
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || {};
var oldOpacity = element.getInlineOpacity();
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
reverser.bind(transition);
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0,
afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
}, options), {transition: reverser}));
}
Effect.Fold = function(element) {
element = $(element);
var oldStyle = {
top: element.style.top,
left: element.style.left,
width: element.style.width,
height: element.style.height };
Element.makeClipping(element);
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.setStyle(oldStyle);
} });
}}, arguments[1] || {}));
};
['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each(
function(f) { Element.Methods[f] = Element[f]; }
);
Element.Methods.visualEffect = function(element, effect, options) {
s = effect.gsub(/_/, '-').camelize();
effect_class = s.charAt(0).toUpperCase() + s.substring(1);
new Effect[effect_class](element, options);
return $(element);
};
Element.addMethods();

View file

@ -0,0 +1,4 @@
foo foo foo
foo foo foo
foo foo foo

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#header1 {
background: #fff;
color: #000;
text-align: center;
}
#header2 {
background: #fff;
color: #000;
text-align: center;
}
#header3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#screen1 {
background: #fff;
color: #000;
text-align: center;
}
#screen2 {
background: #fff;
color: #000;
text-align: center;
}
#screen3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,16 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

View file

@ -0,0 +1,152 @@
* Exported the changelog of Pagination code for historical reference.
* Imported some patches from Rails Trac (others closed as "wontfix"):
#8176, #7325, #7028, #4113. Documentation is much cleaner now and there
are some new unobtrusive features!
* Extracted Pagination from Rails trunk (r6795)
#
# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb
#
# Generated by Trac 0.10.3
# 05/20/07 23:48:02
#
09/03/06 23:28:54 david [4953]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Docs and deprecation
08/07/06 12:40:14 bitsweat [4715]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Deprecate direct usage of @params. Update ActionView::Base for
instance var deprecation.
06/21/06 02:16:11 rick [4476]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Fix indent in pagination documentation. Closes #4990. [Kevin Clark]
04/25/06 17:42:48 marcel [4268]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Remove all remaining references to @params in the documentation.
03/16/06 06:38:08 rick [3899]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
trivial documentation patch for #pagination_links [Francois
Beausoleil] closes #4258
02/20/06 03:15:22 david [3620]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/test/activerecord/pagination_test.rb (modified)
* trunk/activerecord/CHANGELOG (modified)
* trunk/activerecord/lib/active_record/base.rb (modified)
* trunk/activerecord/test/base_test.rb (modified)
Added :count option to pagination that'll make it possible for the
ActiveRecord::Base.count call to using something else than * for the
count. Especially important for count queries using DISTINCT #3839
[skaes]. Added :select option to Base.count that'll allow you to
select something else than * to be counted on. Especially important
for count queries using DISTINCT (closes #3839) [skaes].
02/09/06 09:17:40 nzkoz [3553]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/test/active_record_unit.rb (added)
* trunk/actionpack/test/activerecord (added)
* trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added)
* trunk/actionpack/test/activerecord/pagination_test.rb (added)
* trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted)
* trunk/actionpack/test/fixtures/companies.yml (added)
* trunk/actionpack/test/fixtures/company.rb (added)
* trunk/actionpack/test/fixtures/db_definitions (added)
* trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added)
* trunk/actionpack/test/fixtures/developer.rb (added)
* trunk/actionpack/test/fixtures/developers_projects.yml (added)
* trunk/actionpack/test/fixtures/developers.yml (added)
* trunk/actionpack/test/fixtures/project.rb (added)
* trunk/actionpack/test/fixtures/projects.yml (added)
* trunk/actionpack/test/fixtures/replies.yml (added)
* trunk/actionpack/test/fixtures/reply.rb (added)
* trunk/actionpack/test/fixtures/topic.rb (added)
* trunk/actionpack/test/fixtures/topics.yml (added)
* Fix pagination problems when using include
* Introduce Unit Tests for pagination
* Allow count to work with :include by using count distinct.
[Kevin Clark &amp; Jeremy Hopple]
11/05/05 02:10:29 bitsweat [2878]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Update paginator docs. Closes #2744.
10/16/05 15:42:03 minam [2649]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Update/clean up AP documentation (rdoc)
08/31/05 00:13:10 ulysses [2078]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Add option to specify the singular name used by pagination. Closes
#1960
08/23/05 14:24:15 minam [2041]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
Add support for :include with pagination (subject to existing
constraints for :include with :limit and :offset) #1478
[michael@schubert.cx]
07/15/05 20:27:38 david [1839]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
More pagination speed #1334 [Stefan Kaes]
07/14/05 08:02:01 david [1832]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
* trunk/actionpack/test/controller/addresses_render_test.rb (modified)
Made pagination faster #1334 [Stefan Kaes]
04/13/05 05:40:22 david [1159]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/activerecord/lib/active_record/base.rb (modified)
Fixed pagination to work with joins #1034 [scott@sigkill.org]
04/02/05 09:11:17 david [1067]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_controller/scaffolding.rb (modified)
* trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified)
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified)
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified)
Added pagination for scaffolding (10 items per page) #964
[mortonda@dgrmm.net]
03/31/05 14:46:11 david [1048]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Improved the message display on the exception handler pages #963
[Johan Sorensen]
03/27/05 00:04:07 david [1017]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Fixed that pagination_helper would ignore :params #947 [Sebastian
Kanthak]
03/22/05 13:09:44 david [976]
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Fixed documentation and prepared for 0.11.0 release
03/21/05 14:35:36 david [967]
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
Tweaked the documentation
03/20/05 23:12:05 david [949]
* trunk/actionpack/CHANGELOG (modified)
* trunk/actionpack/lib/action_controller.rb (modified)
* trunk/actionpack/lib/action_controller/pagination.rb (added)
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added)
* trunk/activesupport/lib/active_support/core_ext/kernel.rb (added)
Added pagination support through both a controller and helper add-on
#817 [Sam Stephenson]

View file

@ -0,0 +1,18 @@
Pagination
==========
To install:
script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
This code was extracted from Rails trunk after the release 1.2.3.
WARNING: this code is dead. It is unmaintained, untested and full of cruft.
There is a much better pagination plugin called will_paginate.
Install it like this and glance through the README:
script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
It doesn't have the same API, but is in fact much nicer. You can
have both plugins installed until you change your controller/view code that
handles pagination. Then, simply uninstall classic_pagination.

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the classic_pagination plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the classic_pagination plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Pagination'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,33 @@
#--
# Copyright (c) 2004-2006 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'pagination'
require 'pagination_helper'
ActionController::Base.class_eval do
include ActionController::Pagination
end
ActionView::Base.class_eval do
include ActionView::Helpers::PaginationHelper
end

View file

@ -0,0 +1 @@
puts "\n\n" + File.read(File.dirname(__FILE__) + '/README')

View file

@ -0,0 +1,405 @@
module ActionController
# === Action Pack pagination for Active Record collections
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt>
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined
# by the <tt>params[:page]</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
# and <tt>@people</tt> for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:group => nil,
:parameter => 'page'
}
else
DEFAULT_OPTIONS[:group] = nil
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
#
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
#
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:group => options[:group], :offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end

View file

@ -0,0 +1,135 @@
module ActionView
module Helpers
# Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
# also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
#
# <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
# <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
module PaginationHelper
unless const_defined?(:DEFAULT_OPTIONS)
DEFAULT_OPTIONS = {
:name => :page,
:window_size => 2,
:always_show_anchors => true,
:link_to_current_page => false,
:params => {}
}
end
# Creates a basic HTML link bar for the given +paginator+. Links will be created
# for the next and/or previous page and for a number of other pages around the current
# pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
#
# ==== Options
# <tt>:name</tt>:: the routing name for this paginator
# (defaults to +page+)
# <tt>:prefix</tt>:: prefix for pagination links
# (i.e. Older Pages: 1 2 3 4)
# <tt>:suffix</tt>:: suffix for pagination links
# (i.e. 1 2 3 4 <- Older Pages)
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to <tt>2</tt>)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
# <tt>:params</tt>:: any additional routing parameters
# for page URLs
#
# ==== Examples
# # We'll assume we have a paginator setup in @person_pages...
#
# pagination_links(@person_pages)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :link_to_current_page => true)
# # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :always_show_anchors => false)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
#
# pagination_links(@person_pages, :window_size => 1)
# # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :params => { :viewer => "flash" })
# # => 1 <a href="/?page=2&amp;viewer=flash/">2</a> <a href="/?page=3&amp;viewer=flash/">3</a> ...
# # <a href="/?page=10&amp;viewer=flash/">10</a>
def pagination_links(paginator, options={}, html_options={})
name = options[:name] || DEFAULT_OPTIONS[:name]
params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
prefix = options[:prefix] || ''
suffix = options[:suffix] || ''
pagination_links_each(paginator, options, prefix, suffix) do |n|
params[name] = n
link_to(n.to_s, params, html_options)
end
end
# Iterate through the pages of a given +paginator+, invoking a
# block for each page number that needs to be rendered as a link.
#
# ==== Options
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to +2+)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
#
# ==== Example
# # Turn paginated links into an Ajax call
# pagination_links_each(paginator, page_options) do |link|
# options = { :url => {:action => 'list'}, :update => 'results' }
# html_options = { :href => url_for(:action => 'list') }
#
# link_to_remote(link.to_s, options, html_options)
# end
def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
options = DEFAULT_OPTIONS.merge(options)
link_to_current_page = options[:link_to_current_page]
always_show_anchors = options[:always_show_anchors]
current_page = paginator.current_page
window_pages = current_page.window(options[:window_size]).pages
return if window_pages.length <= 1 unless link_to_current_page
first, last = paginator.first, paginator.last
html = ''
html << prefix if prefix
if always_show_anchors and not (wp_first = window_pages[0]).first?
html << yield(first.number)
html << ' ... ' if wp_first.number - first.number > 1
html << ' '
end
window_pages.each do |page|
if current_page == page && !link_to_current_page
html << page.number.to_s
else
html << yield(page.number)
end
html << ' '
end
if always_show_anchors and not (wp_last = window_pages[-1]).last?
html << ' ... ' if last.number - wp_last.number > 1
html << yield(last.number)
end
html << suffix if suffix
html
end
end # PaginationHelper
end # Helpers
end # ActionView

View file

@ -0,0 +1,24 @@
thirty_seven_signals:
id: 1
name: 37Signals
rating: 4
TextDrive:
id: 2
name: TextDrive
rating: 4
PlanetArgon:
id: 3
name: Planet Argon
rating: 4
Google:
id: 4
name: Google
rating: 4
Ionist:
id: 5
name: Ioni.st
rating: 4

View file

@ -0,0 +1,9 @@
class Company < ActiveRecord::Base
attr_protected :rating
set_sequence_name :companies_nonstd_seq
validates_presence_of :name
def validate
errors.add('rating', 'rating should not be 2') if rating == 2
end
end

View file

@ -0,0 +1,7 @@
class Developer < ActiveRecord::Base
has_and_belongs_to_many :projects
end
class DeVeLoPeR < ActiveRecord::Base
set_table_name "developers"
end

View file

@ -0,0 +1,21 @@
david:
id: 1
name: David
salary: 80000
jamis:
id: 2
name: Jamis
salary: 150000
<% for digit in 3..10 %>
dev_<%= digit %>:
id: <%= digit %>
name: fixture_<%= digit %>
salary: 100000
<% end %>
poor_jamis:
id: 11
name: Jamis
salary: 9000

View file

@ -0,0 +1,13 @@
david_action_controller:
developer_id: 1
project_id: 2
joined_on: 2004-10-10
david_active_record:
developer_id: 1
project_id: 1
joined_on: 2004-10-10
jamis_active_record:
developer_id: 2
project_id: 1

View file

@ -0,0 +1,3 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, :uniq => true
end

View file

@ -0,0 +1,7 @@
action_controller:
id: 2
name: Active Controller
active_record:
id: 1
name: Active Record

View file

@ -0,0 +1,13 @@
witty_retort:
id: 1
topic_id: 1
content: Birdman is better!
created_at: <%= 6.hours.ago.to_s(:db) %>
updated_at: nil
another:
id: 2
topic_id: 2
content: Nuh uh!
created_at: <%= 1.hour.ago.to_s(:db) %>
updated_at: nil

View file

@ -0,0 +1,5 @@
class Reply < ActiveRecord::Base
belongs_to :topic, :include => [:replies]
validates_presence_of :content
end

View file

@ -0,0 +1,42 @@
CREATE TABLE 'companies' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL,
'rating' INTEGER DEFAULT 1
);
CREATE TABLE 'replies' (
'id' INTEGER PRIMARY KEY NOT NULL,
'content' text,
'created_at' datetime,
'updated_at' datetime,
'topic_id' integer
);
CREATE TABLE 'topics' (
'id' INTEGER PRIMARY KEY NOT NULL,
'title' varchar(255),
'subtitle' varchar(255),
'content' text,
'created_at' datetime,
'updated_at' datetime
);
CREATE TABLE 'developers' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL,
'salary' INTEGER DEFAULT 70000,
'created_at' DATETIME DEFAULT NULL,
'updated_at' DATETIME DEFAULT NULL
);
CREATE TABLE 'projects' (
'id' INTEGER PRIMARY KEY NOT NULL,
'name' TEXT DEFAULT NULL
);
CREATE TABLE 'developers_projects' (
'developer_id' INTEGER NOT NULL,
'project_id' INTEGER NOT NULL,
'joined_on' DATE DEFAULT NULL,
'access_level' INTEGER DEFAULT 1
);

View file

@ -0,0 +1,3 @@
class Topic < ActiveRecord::Base
has_many :replies, :include => [:user], :dependent => :destroy
end

View file

@ -0,0 +1,22 @@
futurama:
id: 1
title: Isnt futurama awesome?
subtitle: It really is, isnt it.
content: I like futurama
created_at: <%= 1.day.ago.to_s(:db) %>
updated_at:
harvey_birdman:
id: 2
title: Harvey Birdman is the king of all men
subtitle: yup
content: It really is
created_at: <%= 2.hours.ago.to_s(:db) %>
updated_at:
rails:
id: 3
title: Rails is nice
subtitle: It makes me happy
content: except when I have to hack internals to fix pagination. even then really.
created_at: <%= 20.minutes.ago.to_s(:db) %>

View file

@ -0,0 +1,117 @@
require 'test/unit'
unless defined?(ActiveRecord)
plugin_root = File.join(File.dirname(__FILE__), '..')
# first look for a symlink to a copy of the framework
if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
puts "found framework root: #{framework_root}"
# this allows for a plugin to be tested outside an app
$:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
else
# is the plugin installed in an application?
app_root = plugin_root + '/../../..'
if File.directory? app_root + '/config'
puts 'using config/boot.rb'
ENV['RAILS_ENV'] = 'test'
require File.expand_path(app_root + '/config/boot')
else
# simply use installed gems if available
puts 'using rubygems'
require 'rubygems'
gem 'actionpack'; gem 'activerecord'
end
end
%w(action_pack active_record action_controller active_record/fixtures action_controller/test_process).each {|f| require f}
Dependencies.load_paths.unshift "#{plugin_root}/lib"
end
# Define the connector
class ActiveRecordTestConnector
cattr_accessor :able_to_connect
cattr_accessor :connected
# Set our defaults
self.connected = false
self.able_to_connect = true
class << self
def setup
unless self.connected || !self.able_to_connect
setup_connection
load_schema
require_fixture_models
self.connected = true
end
rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
#$stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
private
def setup_connection
if Object.const_defined?(:ActiveRecord)
defaults = { :database => ':memory:' }
begin
options = defaults.merge :adapter => 'sqlite3', :timeout => 500
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
ActiveRecord::Base.connection
rescue Exception # errors from establishing a connection
$stderr.puts 'SQLite 3 unavailable; trying SQLite 2.'
options = defaults.merge :adapter => 'sqlite'
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options }
ActiveRecord::Base.connection
end
Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
else
raise "Can't setup connection since ActiveRecord isn't loaded."
end
end
# Load actionpack sqlite tables
def load_schema
File.read(File.dirname(__FILE__) + "/fixtures/schema.sql").split(';').each do |sql|
ActiveRecord::Base.connection.execute(sql) unless sql.blank?
end
end
def require_fixture_models
Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
end
end
end
# Test case for inheritance
class ActiveRecordTestCase < Test::Unit::TestCase
# Set our fixture path
if ActiveRecordTestConnector.able_to_connect
self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/"
self.use_transactional_fixtures = false
end
def self.fixtures(*args)
super if ActiveRecordTestConnector.connected
end
def run(*args)
super if ActiveRecordTestConnector.connected
end
# Default so Test::Unit::TestCase doesn't complain
def test_truth
end
end
ActiveRecordTestConnector.setup
ActionController::Routing::Routes.reload rescue nil
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
end

View file

@ -0,0 +1,38 @@
require File.dirname(__FILE__) + '/helper'
require File.dirname(__FILE__) + '/../init'
class PaginationHelperTest < Test::Unit::TestCase
include ActionController::Pagination
include ActionView::Helpers::PaginationHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TagHelper
def setup
@controller = Class.new do
attr_accessor :url, :request
def url_for(options, *parameters_for_method_reference)
url
end
end
@controller = @controller.new
@controller.url = "http://www.example.com"
end
def test_pagination_links
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page)
assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
end
def test_pagination_links_with_prefix
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page), :prefix => 'Newer '
assert_equal "Newer 1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> ", output
end
def test_pagination_links_with_suffix
total, per_page, page = 30, 10, 1
output = pagination_links Paginator.new(@controller, total, per_page, page), :suffix => 'Older'
assert_equal "1 <a href=\"http://www.example.com\">2</a> <a href=\"http://www.example.com\">3</a> Older", output
end
end

Some files were not shown because too many files have changed in this diff Show more