Initial import of the sources from SVN

instiki-madeleine
Alexey Verkhovsky 2005-01-15 20:26:54 +00:00
parent e1a14bc27b
commit 223b2bec6c
204 changed files with 29431 additions and 0 deletions

137
CHANGELOG Executable file
View File

@ -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 <ins> 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

36
CONTROLLER_TESTS Executable file
View File

@ -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

68
README Executable file
View File

@ -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

View File

@ -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

View File

@ -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
<html>
<head>
<META HTTP-EQUIV="Refresh" CONTENT="0;URL=HomePage.#{file_type}">
</head>
</html>
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
%{<span class="selected">#{c}</span>}
else
%{<a href="?category=#{c}">#{c}</a>}
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

View File

@ -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"]])
# <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
#
# html_options([ "VISA", "Mastercard" ], "Mastercard")
# <option>VISA</option>\n<option selected>Mastercard</option>
#
# html_options({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# <option value="$20">Basic</option>\n<option value="$40" selected>Plus</option>
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 << "<option value=\"#{element.last}\">#{element.first}</option>"
else
options << "<option value=\"#{element.last}\" selected>#{element.first}</option>"
end
else
options << ((element != selected) ? "<option>#{element}</option>" : "<option selected>#{element}</option>")
end
end
html_options.join("\n")
end
end

4
app/models/author.rb Executable file
View File

@ -0,0 +1,4 @@
class Author < String
attr_accessor :ip
def initialize(name, ip) @ip = ip; super(name) end
end

31
app/models/chunks/category.rb Executable file
View File

@ -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 ? '' : '<div class="property">category:\1</div>' )
self if content.sub!( Regexp.new( pre_mask+'(.*)?'+post_mask ), replacement )
end
end

20
app/models/chunks/chunk.rb Executable file
View File

@ -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

38
app/models/chunks/engines.rb Executable file
View File

@ -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

29
app/models/chunks/include.rb Executable file
View File

@ -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 : "<em>Could not include #{page_name}</em>")
end
# Keep this chunk regardless of what happens.
def unmask(content) self end
end

19
app/models/chunks/literal.rb Executable file
View File

@ -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 <code> and <pre> 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[^>]*?>.*?</\1>', 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

19
app/models/chunks/match.rb Executable file
View File

@ -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

31
app/models/chunks/nowiki.rb Executable file
View File

@ -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:
# <nowiki>Here are [[double brackets]] and a URI: www.uri.org</nowiki>
#
# 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 <mark at threewordslong dot com>
# Created: 8th June 2004
class NoWiki < Chunk::Abstract
def self.pattern() Regexp.new('<nowiki>(.*?)</nowiki>') 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

18
app/models/chunks/test.rb Executable file
View File

@ -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

103
app/models/chunks/uri.rb Executable file
View File

@ -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)), "<a href=\"#{uri}\">#{link_text}</a>" )
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

83
app/models/chunks/wiki.rb Executable file
View File

@ -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

86
app/models/page.rb Executable file
View File

@ -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

24
app/models/page_lock.rb Executable file
View File

@ -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

73
app/models/page_set.rb Executable file
View File

@ -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

90
app/models/revision.rb Executable file
View File

@ -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

89
app/models/web.rb Executable file
View File

@ -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 "<a class=\"existingWikiWord\" href=\"#{link}.html\">#{text}</a>"
else "<span class=\"newWikiWord\">#{text}</span>" end
when :publish
if page then "<a class=\"existingWikiWord\" href=\"../published/#{link}\">#{text}</a>"
else "<span class=\"newWikiWord\">#{text}</span>" end
else
if page then "<a class=\"existingWikiWord\" href=\"../show/#{link}\">#{text}</a>"
else "<span class=\"newWikiWord\">#{text}<a href=\"../show/#{link}\">?</a></span>" 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

105
app/models/wiki_content.rb Executable file
View File

@ -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 <mark @ threewordslong . com>
# 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

168
app/models/wiki_service.rb Executable file
View File

@ -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

25
app/models/wiki_words.rb Executable file
View File

@ -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

4
app/views/bottom.rhtml Executable file
View File

@ -0,0 +1,4 @@
</div>
</div>
</body>
</html>

16
app/views/markdown_help.rhtml Executable file
View File

@ -0,0 +1,16 @@
<div id="TextileHelp" style="float: right; width: 250px; margin-top: 5px">
<h3>Markdown formatting tips (<a target="_new" href="http://daringfireball.net/projects/markdown/syntax">advanced</a>)</h3>
<table cellspacing="0" cellpadding="0">
<tr><td>_your text_</td><td class="arrow">&rarr;</td><td><em>your text</em></td></tr>
<tr><td>**your text**</td><td class="arrow">&rarr;</td><td><strong>your text</strong></td></tr>
<tr><td>`my code`</td><td class="arrow">&rarr;</td><td><code>my code</code></td></tr>
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">&rarr;</td><td>&#8226; Bulleted list<br />&#8226; Second item</td></tr>
<tr><td>1. Numbered list<br />1. Second item</td><td class="arrow">&rarr;</td><td>1. Numbered list<br />2. Second item</td></tr>
<tr><td>[link name](URL)</td><td class="arrow">&rarr;</td><td><a href="URL">link name</a></td></tr>
<tr><td>***</td><td class="arrow">&rarr;</td><td>Horizontal ruler</td></tr>
<tr><td>&lt;http://url><br />&lt;email@add.com></td><td class="arrow">&rarr;</td><td>Auto-linked</td></tr>
<tr><td>![Alt text](URL)</td><td class="arrow">&rarr;</td><td>Image</td></tr>
</table>
<%= render 'wiki_words_help' %>
</div>

25
app/views/navigation.rhtml Executable file
View File

@ -0,0 +1,25 @@
<%
def list_item(title, url, description, accesskey = nil)
if @title == title
"<b class=\"navOn\" title=\"#{description}\" accesskey=\"#{accesskey}\">#{title}</b>"
else
"<a href=\"#{url}\" title=\"#{description}\" accesskey=\"#{accesskey}\">#{title}</a>"
end
end
%>
<form id="navigationForm" class="navigation" action="../search/" method="get" style="font-size: 10px">
<% if @action_name != "published" then %>
<%= list_item "Home Page", "../show/HomePage", "Home, Sweet Home", "H" %> |
<%= list_item "All Pages", "../list/", "Alphabetically sorted list of pages", "A" %> |
<%= list_item "Recently Revised", "../recently_revised/", "Pages sorted by when they were last changed", "U" %> |
<%= list_item "Authors", "../authors/", "Who wrote what" %> |
<%= list_item "Feeds", "../feeds/", "Subscribe to changes by RSS" %> |
<%= list_item "Export", "../export/", "Download a zip with all the pages in this wiki", "X" %> |
<input type="text" id="searchField" name="query" style="font-size: 10px" value="Search" onClick="this.value == 'Search' ? this.value = '' : true">
<% else %>
<%= list_item "Home Page", "../published/HomePage", "Home, Sweet Home", "H" %> |
<% end%>
</form>

16
app/views/rdoc_help.rhtml Executable file
View File

@ -0,0 +1,16 @@
<div id="TextileHelp" style="float: right; width: 250px; margin-top: 5px">
<h3>RDoc formatting tips (<a target="_new" href="http://rdoc.sourceforge.net/doc/files/markup/simple_markup_rb.html">advanced</a>)</h3>
<table cellspacing="0" cellpadding="0">
<tr><td>_your text_</td><td class="arrow">&rarr;</td><td><em>your text</em></td></tr>
<tr><td>*your text*</td><td class="arrow">&rarr;</td><td><strong>your text</strong></td></tr>
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">&rarr;</td><td>&#8226; Bulleted list<br />&#8226; Second item</td></tr>
<tr><td>1. Numbered list<br />2. Second item</td><td class="arrow">&rarr;</td><td>1. Numbered list<br />2. Second item</td></tr>
<tr><td>+my_code+</td><td class="arrow">&rarr;</td><td><code>my_code</code></td></tr>
<tr><td>---</td><td class="arrow">&rarr;</td><td>Horizontal ruler</td></tr>
<tr><td>[[URL linkname]]</td><td class="arrow">&rarr;</td><td><a href="URL">linkname</a></td></tr>
<tr><td>http://url<br />mailto:e@add.com</td><td class="arrow">&rarr;</td><td>Auto-linked</td></tr>
<tr><td>imageURL</td><td class="arrow">&rarr;</td><td>Image</td></tr>
</table>
<%= render 'wiki_words_help' %>
</div>

28
app/views/textile_help.rhtml Executable file
View File

@ -0,0 +1,28 @@
<div id="TextileHelp" style="float: right; width: 250px; margin-top: 5px">
<h3>Textile formatting tips (<a href="#" onClick="quickRedReference(); return false;">advanced</a>)</h3>
<table cellspacing="0" cellpadding="0">
<tr><td>_your text_</td><td class="arrow">&rarr;</td><td><em>your text</em></td></tr>
<tr><td>*your text*</td><td class="arrow">&rarr;</td><td><strong>your text</strong></td></tr>
<tr><td>%{color:red}hello%</td><td class="arrow">&rarr;</td><td><span style="color: red;">hello</span></td></tr>
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">&rarr;</td><td>&#8226; Bulleted list<br />&#8226; Second item</td></tr>
<tr><td># Numbered list<br /># Second item</td><td class="arrow">&rarr;</td><td>1. Numbered list<br />2. Second item</td></tr>
<tr><td>"linkname":URL</td><td class="arrow">&rarr;</td><td><a href="URL">linkname</a></td></tr>
<tr><td>|a|table|row|<br />|b|table|row|</td><td class="arrow">&rarr;</td><td>Table</td></tr>
<tr><td>http://url<br />email@address.com</td><td class="arrow">&rarr;</td><td>Auto-linked</td></tr>
<tr><td>!imageURL!</td><td class="arrow">&rarr;</td><td>Image</td></tr>
</table>
<%= render 'wiki_words_help' %>
</div>
<script language="JavaScript">
function quickRedReference() {
window.open(
"http://hobix.com/textile/quick.html",
"redRef",
"height=600,width=550,channelmode=0,dependent=0," +
"directories=0,fullscreen=0,location=0,menubar=0," +
"resizable=0,scrollbars=1,status=1,toolbar=0"
);
}
</script>

49
app/views/top.rhtml Executable file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
<% 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 %>
</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style type="text/css">
h1#pageName, .newWikiWord a, a.existingWikiWord, .newWikiWord a:hover, #TextileHelp h3 {
color: #<%= @web ? @web.color : "393" %>;
}
#Container, #Content {
width: <%= @content_width || "600" %>px;
}
<%= File.read(RAILS_ROOT + '/public/stylesheets/instiki.css') if @inline_style %>
</style>
<link rel="Stylesheet" href="/stylesheets/instiki.css" type="text/css" media="screen" />
<style type="text/css">
<%= @style_additions %>
<%= @web ? @web.additional_style : "" %>
</style>
</head>
<body>
<div id="Container">
<div id="Content">
<h1 id="pageName">
<% if @page and (@page.name == 'HomePage') and %w( show published print ).include?(@action_name) %>
<%= @web.name %>
<% elsif @web %>
<small><%= @web.name %></small><br />
<%= @title %>
<% else %>
<%= @title %>
<% end %>
</h1>
<%= render 'navigation' unless @web.nil? || @hide_navigation %>

13
app/views/wiki/authors.rhtml Executable file
View File

@ -0,0 +1,13 @@
<% @title = 'Authors' %><%= render 'top' %>
<ul id="authorList">
<% for author in @authors %>
<li>
<%= @web.make_link(author) %>
co- or authored:
<%= @web.select.pages_authored_by(author).collect { |page| page.link }.join ', ' %>
</li>
<% end %>
</ul>
<%= render 'bottom' %>

31
app/views/wiki/edit.rhtml Executable file
View File

@ -0,0 +1,31 @@
<%
@title = "Editing #{@page.name}"
@content_width = 720
@hide_navigation = true
%><%= render 'top' %>
<%= "<p style='color:red'>Please correct the error that caused this error in rendering:<br/><small>#{@params["msg"]}</small></p>" if @params["msg"] %>
<%= render("#{@web.markup}_help") if @web %>
<form id="editForm" action="../save/<%= @page.name %>" method="post" onSubmit="cleanAuthorName();">
<p>
<textarea name="content" style="width: 450px; height: 500px"><%= @page.content %></textarea>
</p>
<p>
<input type="submit" value="Update"> as
<input type="text" name="author" id="authorName" value="<%= @author %>"
onClick="this.value == 'AnonymousCoward' ? this.value = '' : true">
| <a href="../cancel_edit/<%= @page.name %>">Cancel</a> <small>(unlocks page)</small>
</p>
</form>
<script language="JavaScript1.2">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
</script>
<%= render 'bottom' %>

138
app/views/wiki/edit_web.rhtml Executable file
View File

@ -0,0 +1,138 @@
<% @title = "Edit Web" %><%= render 'top' %>
<form action="../update_web" id="setup" method="post" onSubmit="cleanAddress(); return validateSetup()">
<h2 style="margin-bottom: 3px">Name and address</h2>
<div class="help">
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 <i>/rails/show/HomePage</i>.
</div>
<div class="inputBox">
Name: <input type="text" id="name" name="name" value="<%= @web.name %>" onChange="proposeAddress();"> &nbsp;&nbsp;
Address: <input type="text" id="address" name="address" value="<%= @web.address %>" onChange="cleanAddress();">
<i>(Letters & digits only)</i>
</div>
<h2 style="margin-bottom: 3px">Specialize</h2>
<div class="help">
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. <i>Hint:</i> View source on a page you want to
style to find ID names on individual tags. <a href="#" onClick="document.getElementById('additionalStyle').style.display='block';return false;">See styles >></a>
</div>
<div class="inputBox">
Markup:
<select name="markup">
<%= html_options({ "Textile" => :textile, "Markdown" => :markdown, "RDoc" => :rdoc }, @web.markup) %>
</select>
&nbsp;&nbsp;
Color:
<select name="color">
<%= html_options({ "Green" => "008B26", "Purple" => "504685", "Red" => "DA0006", "Orange" => "FA6F00", "Grey" => "8BA2B0" },
@web.color) %>
</select>
&nbsp;&nbsp;
<small>
<input type="checkbox" name="safe_mode"<%= " CHECKED" if @web.safe_mode %>> Safe mode
&nbsp;&nbsp;
<input type="checkbox" name="brackets_only"<%= " CHECKED" if @web.brackets_only %>> Brackets only
&nbsp;&nbsp;
<input type="checkbox" name="count_pages"<%= " CHECKED" if @web.count_pages %>> Count pages
</small>
<textarea id="additionalStyle" style="display: none; margin-top: 10px; margin-bottom: 5px; width: 560px; height: 200px" name="additional_style"><%= @web.additional_style %></textarea>
</div>
<h2 style="margin-bottom: 3px">Password protection for this web (<%= @web.name %>)</h2>
<div class="help">
This is the password that visitors need to view and edit this web. Setting the password to nothing will remove the password protection.
</div>
<div class="inputBox">
Password: <input type="password" id="password" name="password" value="<%= @web.password %>"> &nbsp;&nbsp;
Verify: <input type="password" id="password_check" value="<%= @web.password %>" name="password_check">
</div>
<h2 style="margin-bottom: 3px">Publish read-only version of this web (<%= @web.name %>)</h2>
<div class="help">
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.
</div>
<div class="inputBox">
<input type="checkbox" name="published"<%= " CHECKED" if @web.published %>> Publish this web
</div>
<p align="right">
<small>
Enter system password
<input type="password" id="system_password" name="system_password">
and
<input type="submit" value="Update Web">
<br/><br/>
...or forget changes and <a href="/new_web/">create a new web</a>
</small>
</p>
</form>
<br/>
<h1>Other administrative tasks</h1>
<form action="../remove_orphaned_pages" id="remove_orphaned_pages" method="post">
<p align="right">
<small>
Clean up by entering system password
<input type="password" id="system_password" name="system_password">
and
<input type="submit" value="Delete Orphan Pages">
</small>
</p>
<script>
function proposeAddress() {
document.getElementById('address').value =
document.getElementById('name').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function cleanAddress() {
document.getElementById('address').value =
document.getElementById('address').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function validateSetup() {
if (document.getElementById('system_password').value == "") {
alert("You must enter the system password");
return false;
}
if (document.getElementById('name').value == "") {
alert("You must pick a name for the web");
return false;
}
if (document.getElementById('address').value == "") {
alert("You must pick an address for the web");
return false;
}
if (document.getElementById('password').value != "" &&
document.getElementById('password').value != document.getElementById('password_check').value) {
alert("The password and its verification doesn't match");
return false;
}
return true;
}
</script>
<%= render 'bottom' %>

14
app/views/wiki/export.rhtml Executable file
View File

@ -0,0 +1,14 @@
<% @title = "Export" %><%= render 'top' %>
<p>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).</p>
<ul id="feedsList">
<li><a href="../export_html">HTML</a>
<li><a href="../export_markup">Markup (<%= @web.markup %>)</a>
<% if OPTIONS[:pdflatex] && @web.markup == :textile %>
<li><a href="../export_tex">TeX</a>
<li><a href="../export_pdf">PDF</a>
<% end %>
</ul>
<%= render 'bottom' %>

10
app/views/wiki/feeds.rhtml Executable file
View File

@ -0,0 +1,10 @@
<% @title = "Feeds" %><%= render 'top' %>
<p>You can subscribe to this wiki by RSS and get either just the headlines of the pages that change or the entire page.</p>
<ul id="feedsList">
<li><a href="../rss_with_content<%= "?password=#{web.password}" if @web.password %>">Full content (RSS 2.0)</a>
<li><a href="../rss_with_headlines<%= "?password=#{web.password}" if @web.password %>">Headlines (RSS 2.0)</a>
</ul>
<%= render 'bottom' %>

59
app/views/wiki/list.rhtml Executable file
View File

@ -0,0 +1,59 @@
<% @title = "All Pages" %><%= render 'top' %>
<% unless @categories.empty? %>
<div id="categories">
<strong>Categories</strong>:
[<a href=".">Any</a>]
<%= @category_links.join(', ') %>
</div>
<% end %>
<div id="allPages" style="float: left; width: 280px; margin-right: 30px">
<% unless @pages_that_are_orphaned.empty? && @page_names_that_are_wanted.empty? %>
<h2>
All Pages
<br/><small style="font-size: 12px"><i>All pages in <%= @set_name %> listed alphabetically</i></small>
</h2>
<% end %>
<ul><% for page in @pages_by_name %>
<li><a href="../show/<%= page.name %>"><%= truncate(page.plain_name, 35) %></a></li>
<% end %></ul>
<% if @web.count_pages %>
<% total_chars = @pages_in_category.characters %>
<p><small>All content: <%= total_chars %> chars / <%= sprintf("%-.1f", (total_chars / 2275 )) %> pages</small></p>
<% end %>
</div>
<div style="float: left; width: 280px">
<% unless @page_names_that_are_wanted.empty? %>
<h2>
Wanted Pages
<br/><small style="font-size: 12px"><i>Unexisting pages that other pages in <%= @set_name %> reference</i></small>
</h2>
<ul style="margin-bottom: 10px">
<% for page_name in @page_names_that_are_wanted %>
<li>
<a href="../show/<%= page_name %>"><%= truncate(WikiWords.separate(page_name), 35) %></a>
wanted by
<%= web.select.pages_that_reference(page_name).collect { |page| page.link }.join(", ") %>
</li>
<% end %>
</ul>
<% end %>
<% unless @pages_that_are_orphaned.empty? %>
<h2>
Orphaned Pages
<br/><small style="font-size: 12px"><i>Pages in <%= @set_name %> that no other page reference</i></small>
</h2>
<ul style="margin-bottom: 35px">
<% for page in @pages_that_are_orphaned %><li><a href="../show/<%= page.name %>"><%= truncate(page.plain_name, 35) %></a></li><% end %>
</ul>
<% end %>
</div>
<%= render 'bottom' %>

14
app/views/wiki/locked.rhtml Executable file
View File

@ -0,0 +1,14 @@
<% @title = "#{@page.plain_name} is locked" %><%= render 'top' %>
<% if @page.lock_duration(Time.now) == 0 %>
<p><%= @page.locked_by_link %> just started editing this page.</p>
<% else %>
<p><%= @page.locked_by_link %> has been editing this page for <%= @page.lock_duration(Time.now) %> minutes.</p>
<% end %>
<p>
<a href="<%= @page.name %>?break_lock=1" accesskey="E">Edit the page anyway</a> |
<a href="../show/<%= @page.name %>">Cancel</a>
</p>
<%= render 'bottom' %>

11
app/views/wiki/login.rhtml Executable file
View File

@ -0,0 +1,11 @@
<% @title = "#{@web_name} Login" %><% @hide_navigation = true %><%= render 'top' %>
<form action="authenticate" method="post">
<p>
<b>Password</b><br />
<input type="password" name="password" />
</p>
</form>
<%= render 'bottom' %>

27
app/views/wiki/new.rhtml Executable file
View File

@ -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 %>
<form action="../save/<%= @page_name %>" method="post" onSubmit="cleanAuthorName();">
<p>
<textarea name="content" style="width: 450px; height: 500px"></textarea>
</p>
<p>
<input type="submit" value="Create"> as
<input type="text" name="author" id="authorName" value="<%= @author %>" onClick="this.value == 'AnonymousCoward' ? this.value = '' : true">
</p>
</form>
<script language="JavaScript1.2">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
</script>
<%= render 'bottom' %>

78
app/views/wiki/new_system.rhtml Executable file
View File

@ -0,0 +1,78 @@
<% @title = "Instiki Setup"; @content_width = 500 %><%= render 'top' %>
<p>
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.
</p>
<form action="../create_system" id="setup" method="post" onSubmit="return validateSetup()">
<ol class="setup">
<li>
<h2 style="margin-bottom: 3px">Name and address for your first web</h2>
<div class="help">
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 <i>/rails/show/HomePage</i>. The address can only consist of letters & digits.
</div>
<div class="inputBox">
Name: <input type="text" id="web_name" name="web_name" value="Wiki" onChange="proposeAddress();"
onClick="this.value == 'Wiki' ? this.value = '' : true"> &nbsp;&nbsp;
Address: <input type="text" id="web_address" name="web_address" onChange="cleanAddress();" value="wiki">
</div>
</li>
<li>
<h2 style="margin-bottom: 3px">Password for creating and changing webs</h2>
<div class="help">
Administrative access allows you to make new webs and change existing ones.<br/>
Everyone with this password will be able to do this, so pick it carefully.
</div>
<div class="inputBox">
Password: <input type="password" id="password" name="password"> &nbsp;&nbsp;
Verify: <input type="password" id="password_check" name="password_check">
</div>
</li>
</ol>
<p align="right">
<input type="submit" value="Setup" style="margin-left: 40px">
</p>
</form>
<script>
function proposeAddress() {
document.getElementById('web_address').value =
document.getElementById('web_name').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function cleanAddress() {
document.getElementById('web_address').value =
document.getElementById('web_address').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function validateSetup() {
if (document.getElementById('web_name').value == "") {
alert("You must pick a name for the first web");
return false;
}
if (document.getElementById('web_address').value == "") {
alert("You must pick an address for the first web");
return false;
}
if (document.getElementById('password').value == "") {
alert("You must pick a system password");
return false;
}
if (document.getElementById('password_check').value == "" ||
document.getElementById('password').value != document.getElementById('password_check').value) {
alert("The password and its verification doesn't match");
return false;
}
return true;
}
</script>
<%= render 'bottom' %>

64
app/views/wiki/new_web.rhtml Executable file
View File

@ -0,0 +1,64 @@
<% @title = "New Wiki Web"; @content_width = 500 %><%= render 'top' %>
<p>
Each web serves as an isolated name space for wiki pages, so different subjects or projects can write about different <i>MuppetShows</i>.
</p>
<form action="../create_web" id="setup" method="post" onSubmit="cleanAddress(); return validateSetup()">
<ol class="setup">
<li>
<h2 style="margin-bottom: 3px">Name and address for your new web</h2>
<div class="help">
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 <i>/rails/show/HomePage</i>. The address can only consist of letters & digits.
</div>
<div class="inputBox">
Name: <input type="text" id="web_name" name="name" onChange="proposeAddress();" /> &nbsp;&nbsp;
Address: <input type="text" id="web_address" name="address" onChange="cleanAddress();" />
</div>
</li>
</ol>
<p align="right">
<small>
Enter system password
<input type="password" id="system_password" name="system_password">
and
<input type="submit" value="Create Web">
</small>
</p>
</form>
<script>
function proposeAddress() {
document.getElementById('web_address').value =
document.getElementById('web_name').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function cleanAddress() {
document.getElementById('web_address').value =
document.getElementById('web_address').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function validateSetup() {
if (document.getElementById('web_name').value == "") {
alert("You must pick a name for the new web");
return false;
}
if (document.getElementById('web_address').value == "") {
alert("You must pick an address for the new web");
return false;
}
if (document.getElementById('system_password').value == "") {
alert("You must enter the system password");
return false;
}
return true;
}
</script>
<%= render 'bottom' %>

81
app/views/wiki/page.rhtml Executable file
View File

@ -0,0 +1,81 @@
<% @title = @page.plain_name %>
<%= render 'top' %>
<div id="revision">
<%= @page.display_content %>
</div>
<div id="changes" style="display: none">
<p style="background: #eee; padding: 3px; border: 1px solid silver">
<small>
Showing changes from revision #<%= @page.number - 1 %> to #<%= @page.number %>:
<ins class="diffins">Added</ins> | <del class="diffdel">Removed</del>
</small>
</p>
<%= @page.display_diff %>
</div>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= @page.pretty_created_at %>
by <%= @page.author_link %>
<%= "(#{@page.author.ip})" if @page.author.respond_to?(:ip) %>
<% if @web.count_pages %>
<% total_chars = @page.content.length %>
(<%= total_chars %> characters / <%= sprintf("%-.1f", (total_chars / 2275 rescue 0)) %> pages)
<% end %>
</div>
<div class="navigation">
<% if @page.name == "HomePage" %>
<a href="../edit/<%= @page.name %>" class="navlink" accesskey="E">Edit Page</a>
| <a href="../edit_web/" class="navlink">Edit Web</a>
<% else %>
<a href="../edit/<%= @page.name %>" class="navlink" accesskey="E">Edit</a>
<% end %>
<% if @page.revisions.length > 1 %>
| <a href="../revision/<%= @page.name %>?rev=<%= @page.revisions.length - 2 %>" class="navlink" accesskey="R">Back in time</a>
<small>(<%= @page.revisions.length - 1 %> revisions)</small>
<% end %>
<% if @page.revisions.length > 1 %>
<span id="show_changes">
| <a href="#" onClick="toggleChanges(); return false;">See changes</a>
</span>
<span id="hide_changes" style="display: none">
| <a href="#" onClick="toggleChanges(); return false;">Hide changes</a>
</span>
<% end %>
<small>
| Views: <a href="../print/<%= @page.name %>">Print</a>
<% if defined? RedClothForTex and RedClothForTex.available? and @web.markup == :textile %>
| <a href="../tex/<%= @page.name %>">TeX</a> | <a href="../pdf/<%= @page.name %>">PDF</a>
<% end %>
</small>
<% if @page.references.length > 0 %>
<small>
| Linked from: <%= @page.references.collect { |ref| ref.link }.join(", ") %>
</small>
<% end %>
</div>
<script language="Javascript">
function toggleChanges() {
if (document.getElementById("changes").style.display == "none") {
document.getElementById("changes").style.display = "block";
document.getElementById("revision").style.display = "none";
document.getElementById("show_changes").style.display = "none";
document.getElementById("hide_changes").style.display = "inline";
} else {
document.getElementById("changes").style.display = "none";
document.getElementById("revision").style.display = "block";
document.getElementById("show_changes").style.display = "inline";
document.getElementById("hide_changes").style.display = "none";
}
}
</script>
<%= render 'bottom' %>

16
app/views/wiki/print.rhtml Executable file
View File

@ -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 %>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= @page.pretty_created_at %>
by
<%= @page.author_link({ :mode => :export }) %>
</div>
<%= render 'bottom' %>

10
app/views/wiki/published.rhtml Executable file
View File

@ -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' %>

View File

@ -0,0 +1,30 @@
<% @title = "Recently Revised" %><%= render 'top' %>
<% unless @categories.empty? %>
<div id="categories">
<strong>Categories</strong>:
[<a href=".">Any</a>]
<%= @category_links.join(', ') %>
</div>
<% end %>
<% revision_date = Date.new(2100) %>
<ul>
<% for page in @pages_by_revision %>
<% if page.revised_on < revision_date %>
</ul><b><%= page.pretty_revised_on %></b><ul>
<% end %>
<li>
<a href="../show/<%= page.name %>"><%= page.plain_name %></a>
<div class="byline" style="margin-bottom: 0px">
by <%= page.author_link %>
at <%= page.created_at.strftime "%H:%M" %>
<%= "from #{page.author.ip}" if page.author.respond_to?(:ip) %>
</div>
</li>
<% revision_date = page.revised_on %>
<% end %>
<%= render 'bottom' %>

81
app/views/wiki/revision.rhtml Executable file
View File

@ -0,0 +1,81 @@
<% @title = "#{@page.plain_name} (Rev ##{@revision.number})" %><%= render 'top' %>
<div id="revision">
<%= @revision.display_content %>
</div>
<div id="changes" style="display: none">
<p style="background: #eee; padding: 3px; border: 1px solid silver">
<small>
Showing changes from revision #<%= @revision.number - 1 %> to #<%= @revision.number %>:
<ins class="diffins">Added</ins> | <del class="diffdel">Removed</del>
</small>
</p>
<%= @revision.display_diff %>
</div>
<div class="byline">
<%= "Revision from #{@revision.pretty_created_at} by" %>
<%= @page.web.make_link(@revision.author) %>
</div>
<div class="navigation">
<% if @revision.next_revision %>
<% if @revision.next_revision.number < (@page.revisions.length - 1) %>
<a href="../revision/<%= @page.name %>?rev=<%= @revision.next_revision.number %>" class="navlink">
<% else %>
<a href="../show/<%= @page.name %>" class="navlink">
<% end %>
Forward in time</a>
(<%= @revision.page.revisions.length - @revision.next_revision.number %> more)
<% end %>
<% if @revision.next_revision && @revision.previous_revision %>
|
<% end %>
<% if @revision.previous_revision %>
<a href="../revision/<%= @page.name %>?rev=<%= @revision.previous_revision.number %>" class="navlink">Back in time</a>
(<%= @revision.previous_revision.number + 1 %> more)
<% end %>
| <a href="../show/<%= @page.name %>" class="navlink">See current</a>
<% if @revision.previous_revision %>
<span id="show_changes">
| <a href="#" onClick="toggleChanges(); return false;">See changes</a>
</span>
<span id="hide_changes" style="display: none">
| <a href="#" onClick="toggleChanges(); return false;">Hide changes</a>
</span>
<% end %>
| <a href="../rollback/<%= @page.name %>?rev=<%= @revision.number %>" class="navlink">Rollback</a>
<% if @page.references.length > 0 %>
<small>
| Linked from: <%= @page.references.collect { |ref| "<a href='#{ref.name}'>#{ref.name}</a>" }.join(", ") %>
</small>
<% end %>
</div>
<script language="Javascript">
function toggleChanges() {
if (document.getElementById("changes").style.display == "none") {
document.getElementById("changes").style.display = "block";
document.getElementById("revision").style.display = "none";
document.getElementById("show_changes").style.display = "none";
document.getElementById("hide_changes").style.display = "inline";
} else {
document.getElementById("changes").style.display = "none";
document.getElementById("revision").style.display = "block";
document.getElementById("show_changes").style.display = "inline";
document.getElementById("hide_changes").style.display = "none";
}
}
</script>
<%= render 'bottom' %>

31
app/views/wiki/rollback.rhtml Executable file
View File

@ -0,0 +1,31 @@
<%
@title = "Rollback to #{@page.plain_name} Rev ##{@revision.number}"
@content_width = 720
@hide_navigation = true
%><%= render 'top' %>
<%= "<p style='color:red'>Please correct the error that caused this error in rendering:<br/><small>#{@params["msg"]}</small></p>" if @params["msg"] %>
<form id="editForm" action="../save/<%= @page.name %>" method="post" onSubmit="cleanAuthorName();">
<p>
<textarea name="content" style="font-size: 12px; width: 450px; height: 500px"><%= @revision.content %></textarea>
</p>
<p>
<input type="submit" value="Update"> as
<input type="text" name="author" id="authorName" value="<%= @author %>"
onClick="this.value == 'AnonymousCoward' ? this.value = '' : true">
| <a href="../cancel_edit/<%= @page.name %>">Cancel</a> <small>(unlocks page)</small>
</p>
</form>
<%= render("#{@web.markup}_help") if @web %>
<script language="JavaScript1.2">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
</script>
<%= render 'bottom' %>

22
app/views/wiki/rss_feed.rhtml Executable file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title><%= @web.name %></title>
<link><%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => 'HomePage' %></link>
<description>An Instiki wiki</description>
<language>en-us</language>
<ttl>40</ttl>
<% for page in @pages_by_revision %>
<item>
<title><%= page.plain_name %></title>
<% unless @hide_description %>
<description><%= CGI.escapeHTML(page.display_content) %></description>
<% end %>
<pubDate><%= page.created_at.strftime "%a, %e %b %Y %H:%M:%S %Z" %></pubDate>
<guid><%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => page.name %></guid>
<link><%= url_for :only_path => false, :web => @web_name, :action => 'show', :id => page.name %></link>
<dc:creator><%= WikiWords.separate(page.author) %></dc:creator>
</item>
<% end %>
</channel>
</rss>

15
app/views/wiki/search.rhtml Executable file
View File

@ -0,0 +1,15 @@
<% @title = @results.length > 0 ? "#{@results.length} pages contains \"#{@params["query"]}\"" : "No pages contains \"#{@query}\"" %><%= render 'top' %>
<% if @results.length > 0 %>
<ul>
<% for page in @results %>
<li><a href="../show/<%= page.name %>"><%= page.plain_name %></a></li>
<% end %>
</ul>
<% else %>
<p>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&mdash;only as a sentence fragment.</p>
<p>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)"</p>
<% end %>
<%= render 'bottom' %>

23
app/views/wiki/tex.rhtml Executable file
View File

@ -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}

35
app/views/wiki/tex_web.rhtml Executable file
View File

@ -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}

20
app/views/wiki/web_list.rhtml Executable file
View File

@ -0,0 +1,20 @@
<% @title = "Wiki webs" %><%= render 'top' %>
<ul>
<% for web in @webs %>
<li>
<% 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 %>
<div class="byline" style="margin-bottom: 0px">
<%= web.pages.length %> pages by <%= web.authors.length %> authors
</div>
</li>
<% end %>
</ul>
<%= render 'bottom' %>

View File

@ -0,0 +1,9 @@
<h3>Wiki words</h3>
<p style="border-top: 1px dotted #ccc; margin-top: 0px">
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.
</p>
<p>
Wiki words: <i>HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]</i><br/>
Not wiki words: <i>IBM, School</i>
</p>

59
config/environment.rb Normal file
View File

@ -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/"

View File

@ -0,0 +1,4 @@
Dependencies.mechanism = :require
ActionController::Base.consider_all_requests_local = true
BREAKPOINT_SERVER_PORT = 42531
ActionController::Base.logger.level = Logger::DEBUG

View File

@ -0,0 +1,2 @@
Dependencies.mechanism = :require
ActionController::Base.consider_all_requests_local = false

View File

@ -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

2
instiki Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/ruby
load File.dirname(__FILE__) + "/script/server"

44
instiki.gemspec Executable file
View File

@ -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

23
libraries/active_record_stub.rb Executable file
View File

@ -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

475
libraries/diff.rb Executable file
View File

@ -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 <p> tags, but it appeared also with
# <li>, <ul> 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 << "</#{tagname}>"
end
# this tries to put <p> tags or newline chars before the opening diff tags (<ins> or <del>)
# 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 "</#{tagname}>"
@content += last_tags
remove_empty_diff(tagname, tagclass)
end
def remove_empty_diff(tagname, tagclass)
if @content[-2] == "<#{tagname} class=\"#{tagclass}\">" and @content[-1] == "</#{tagname}>" 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</(p|li|ul|ol|dir|dt|dl)!
end
def self.diff(a, b)
a, b = a.split(//), b.split(//) if a.kind_of? String and b.kind_of? String
a, b = html2list(a), html2list(b)
out = Builder.new(a, b)
s = SequenceMatcher.new(a, b)
s.get_opcodes.each do |opcode|
out.do_op(opcode)
end
out.result
end
def self.html2list(x, b=false)
mode = 'char'
cur = ''
out = []
x = x.split(//) if x.kind_of? String
x.each do |c|
if mode == 'tag'
if c == '>'
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 = "<p>this is the original string</p>" # \n<p>but around the world</p>"
# b = "<p>this is the original </p><p>other parag</p><p>string</p>"
a = "<ul>\n\t<li>one</li>\n\t<li>two</li>\n</ul>"
b = "<ul>\n\t<li>one</li>\n\t<li>two\n\t<ul><li>abc</li></ul></li>\n</ul>"
puts a
pp HTMLDiff.html2list(a)
puts
puts b
pp HTMLDiff.html2list(b)
puts
puts HTMLDiff.diff(a, b)
end

152
libraries/rdocsupport.rb Executable file
View File

@ -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
# [[<url> description with spaces]]
add_special(/((\\)?\[\[\S+?\s+.+?\]\])/,:TIDYLINK)
# and external references
add_special(/((\\)?(link:|anchor:|http:|mailto:|ftp:|img:|www\.)\S+\w\/?)/,
:HYPERLINK)
# <br/>
add_special(%r{(#{pre}<br/>)}, :BR)
# and <center> ... </center>
add_html("center", :CENTER)
end
def convert(text, handler)
super.sub(/^<p>\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, "<center>", "</center>")
end
# handle <br/>
def handle_special_BR(special)
return "&lt;br/&gt" 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 <img> tag. Otherwise a conventional <a href>
# is used.
# [img:] insert a <tt><img></tt> tag
# [link:] used to insert arbitrary <tt><a></tt> 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)$/
"<img src=\"#{url}\"/>"
else
"<a href=\"#{url}\">#{url.sub(%r{^\w+:/*}, '')}</a>"
end
when "img"
"<img src=\"#{path}\"/>"
when "link"
"<a href=\"#{path}\">#{path}</a>"
when "anchor"
"<a name=\"#{path}\"></a>"
else
"<a href=\"#{url}\">#{url.sub(%r{^\w+:/*}, '')}</a>"
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 %{<a href="#{$1}">#{label}</a>}
when /img:(\S+)/
return %{<img src="http://#{$1}" alt="#{label}" />}
when /rubytalk:(\S+)/
return %{<a href="http://ruby-talk.org/blade/#{$1}">#{label}</a>}
when /rubygarden:(\S+)/
return %{<a href="http://www.rubygarden.org/ruby?#{$1}">#{label}</a>}
when /c2:(\S+)/
return %{<a href="http://c2.com/cgi/wiki?#{$1}">#{label}</a>}
when /isbn:(\S+)/
return %{<a href="http://search.barnesandnoble.com/bookSearch/} +
%{isbnInquiry.asp?isbn=#{$1}">#{label}</a>}
end
unless url =~ /\w+?:/
url = "http://#{url}"
end
"<a href=\"#{url}\">#{label}</a>"
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

880
libraries/redcloth_for_tex.rb Executable file
View File

@ -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:
#
# <a href="http://www.textism.com" title="This is a title">This is a link</a>
#
# === 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:
#
# <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
#
# With a link:
#
# !/common/textist.gif(Textist)!:http://textism.com
#
# Will become:
#
# <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
#
# === 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:
#
# <acronym title="American Civil Liberties Union">ACLU</acronym>
#
# === 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&#8217;\2' ], # single closing
[ /([^\s\[{(>])\'/, '\1&#8217;' ], # single closing
[ /\'(?=\s|s\b|[#{PUNCT}])/, '&#8217;' ], # single closing
[ /\'/, '&#8216;' ], # single opening
# [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
[ /([^\s\[{(>])"/, '\1&#8221;' ], # double closing
[ /"(?=\s|[#{PUNCT}])/, '&#8221;' ], # double closing
[ /"/, '&#8220;' ], # double opening
[ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
[ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
[ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^<a-z0-9]|$)/, '\1<span class="caps">\2</span>\3' ], # 3+ uppercase caps
[ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
[ /\s->\s/, ' &rarr; ' ], # en dash
[ /\s-\s/, ' &#8211; ' ], # en dash
[ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
[ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
[ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
[ /\b ?[(\[]C[\])]/i, '&#169;' ] # 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%/, '&#38;' )
# text.gsub!( /<br \/>/, "<br />\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<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
end
end
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
end
"\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\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 ? ' ' : '<br />' }" )
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<p>\\1</p>" )
#line.gsub!( "<br />", "\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 }<a href=\"#{ url }#{ slash }\"#{ atts }>#{ text }</a>#{ 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 << "<a href=\"#{ href }\">" if href
out << "<img src=\"#{ url }\"#{ atts } />"
out << "</a>#{ 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#{ lang }>#{ code }</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<notextile>\2</notextile>\3' )
end
def footnote_ref( text )
text.gsub!( /\[([0-9]+?)\](\s)?/,
'\footnote{\1}\2')
#'<sup><a href="#fn\1">\1</a></sup>\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 <code>, <pre> 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!( /&lt;(\/?#{ offtags })&gt;/, '<\1>' )
end
## do htmlspecial if between <code>
elsif codepre > 0
line.texesc!( :NoQuotes )
## line.gsub!( /&lt;(\/?#{ offtags })&gt;/, '<\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 )
' <a target="_blank" href="http://www.textpattern.com/help/?item=' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
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

75
libraries/url_rewriting_hack.rb Executable file
View File

@ -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

View File

@ -0,0 +1,18 @@
/* AppDelegate */
#import <Cocoa/Cocoa.h>
@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

View File

@ -0,0 +1,109 @@
#include <unistd.h>
#include <sys/wait.h>
#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

View File

@ -0,0 +1,16 @@
<dl>
<dt>Engineering:</dt>
<dd>Some people</dd>
<dt>Human Interface Design:</dt>
<dd>Some other people</dd>
<dt>Testing:</dt>
<dd>Hopefully not nobody</dd>
<dt>Documentation:</dt>
<dd>Whoever</dd>
<dt>With special thanks to:</dt>
<dd>Mom</dd>
</dl>

View File

@ -0,0 +1,2 @@
*~.nib

Binary file not shown.

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBDocumentLocation</key>
<string>109 6 356 240 0 0 1440 878 </string>
<key>IBEditorPositions</key>
<dict>
<key>206</key>
<string>112 300 116 87 0 0 1440 878 </string>
<key>29</key>
<string>241 316 70 44 0 0 1440 878 </string>
</dict>
<key>IBFramework Version</key>
<string>349.0</string>
<key>IBOpenObjects</key>
<array>
<integer>206</integer>
<integer>29</integer>
</array>
<key>IBSystem Version</key>
<string>7H63</string>
</dict>
</plist>

Binary file not shown.

View File

@ -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;
}

View File

@ -0,0 +1,592 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 39;
objects = {
080E96DDFE201D6D7F000001 = {
children = (
174B2765065CE31400ED6208,
174B2766065CE31400ED6208,
);
isa = PBXGroup;
name = Classes;
refType = 4;
sourceTree = "<group>";
};
089C165CFE840E0CC02AAC07 = {
children = (
089C165DFE840E0CC02AAC07,
);
isa = PBXVariantGroup;
name = InfoPlist.strings;
refType = 4;
sourceTree = "<group>";
};
089C165DFE840E0CC02AAC07 = {
fileEncoding = 10;
isa = PBXFileReference;
lastKnownFileType = text.plist.strings;
name = English;
path = English.lproj/InfoPlist.strings;
refType = 4;
sourceTree = "<group>";
};
//080
//081
//082
//083
//084
//100
//101
//102
//103
//104
1058C7A0FEA54F0111CA2CBB = {
children = (
1058C7A1FEA54F0111CA2CBB,
);
isa = PBXGroup;
name = "Linked Frameworks";
refType = 4;
sourceTree = "<group>";
};
1058C7A1FEA54F0111CA2CBB = {
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = Cocoa.framework;
path = /System/Library/Frameworks/Cocoa.framework;
refType = 0;
sourceTree = "<absolute>";
};
1058C7A2FEA54F0111CA2CBB = {
children = (
29B97325FDCFA39411CA2CEA,
29B97324FDCFA39411CA2CEA,
);
isa = PBXGroup;
name = "Other Frameworks";
refType = 4;
sourceTree = "<group>";
};
//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 = "<group>";
};
174B2766065CE31400ED6208 = {
fileEncoding = 4;
isa = PBXFileReference;
lastKnownFileType = sourcecode.c.h;
path = AppDelegate.h;
refType = 4;
sourceTree = "<group>";
};
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 = "<group>";
};
17C1C5CD065D3A3C003526E7 = {
fileEncoding = 4;
isa = PBXFileReference;
lastKnownFileType = text.html;
path = Credits.html;
refType = 4;
sourceTree = "<group>";
};
17C1C5CE065D3A3C003526E7 = {
fileRef = 17C1C5CD065D3A3C003526E7;
isa = PBXBuildFile;
settings = {
};
};
17C1C6E2065D458D003526E7 = {
fileEncoding = 4;
isa = PBXFileReference;
lastKnownFileType = text.script.sh;
path = MakeDMG.sh;
refType = 4;
sourceTree = "<group>";
};
17F6C11106629574007E0BD0 = {
isa = PBXFileReference;
lastKnownFileType = "compiled.mach-o.executable";
name = ruby;
path = /usr/local/bin/ruby;
refType = 0;
sourceTree = "<absolute>";
};
17F6C11206629574007E0BD0 = {
fileRef = 17F6C11106629574007E0BD0;
isa = PBXBuildFile;
settings = {
};
};
17F6C113066295D0007E0BD0 = {
isa = PBXFileReference;
lastKnownFileType = folder;
name = ruby;
path = /usr/local/lib/ruby;
refType = 0;
sourceTree = "<absolute>";
};
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 = "<group>";
};
//170
//171
//172
//173
//174
//190
//191
//192
//193
//194
19C28FACFE9D520D11CA2CBB = {
children = (
8D1107320486CEB800E47090,
);
isa = PBXGroup;
name = Products;
refType = 4;
sourceTree = "<group>";
};
//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 = "<group>";
};
29B97315FDCFA39411CA2CEA = {
children = (
32CA4F630368D1EE00C91783,
29B97316FDCFA39411CA2CEA,
);
isa = PBXGroup;
name = "Other Sources";
path = "";
refType = 4;
sourceTree = "<group>";
};
29B97316FDCFA39411CA2CEA = {
fileEncoding = 30;
isa = PBXFileReference;
lastKnownFileType = sourcecode.cpp.objcpp;
path = main.mm;
refType = 4;
sourceTree = "<group>";
};
29B97317FDCFA39411CA2CEA = {
children = (
17BF6FD9067536EB003F37D6,
17F6C3D2066296E4007E0BD0,
8D1107310486CEB800E47090,
089C165CFE840E0CC02AAC07,
29B97318FDCFA39411CA2CEA,
17C1C5CD065D3A3C003526E7,
);
isa = PBXGroup;
name = Resources;
path = "";
refType = 4;
sourceTree = "<group>";
};
29B97318FDCFA39411CA2CEA = {
children = (
29B97319FDCFA39411CA2CEA,
);
isa = PBXVariantGroup;
name = MainMenu.nib;
path = "";
refType = 4;
sourceTree = "<group>";
};
29B97319FDCFA39411CA2CEA = {
isa = PBXFileReference;
lastKnownFileType = wrapper.nib;
name = English;
path = English.lproj/MainMenu.nib;
refType = 4;
sourceTree = "<group>";
};
29B97323FDCFA39411CA2CEA = {
children = (
1058C7A0FEA54F0111CA2CBB,
1058C7A2FEA54F0111CA2CBB,
);
isa = PBXGroup;
name = Frameworks;
path = "";
refType = 4;
sourceTree = "<group>";
};
29B97324FDCFA39411CA2CEA = {
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = AppKit.framework;
path = /System/Library/Frameworks/AppKit.framework;
refType = 0;
sourceTree = "<absolute>";
};
29B97325FDCFA39411CA2CEA = {
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = Foundation.framework;
path = /System/Library/Frameworks/Foundation.framework;
refType = 0;
sourceTree = "<absolute>";
};
//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 = "<group>";
};
//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 = "<absolute>";
};
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 = "<absolute>";
};
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 = "<absolute>";
};
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 = "<group>";
};
8D1107320486CEB800E47090 = {
explicitFileType = wrapper.application;
includeInIndex = 0;
isa = PBXFileReference;
path = Instiki.app;
refType = 3;
sourceTree = BUILT_PRODUCTS_DIR;
};
};
rootObject = 29B97313FDCFA39411CA2CEA;
}

View File

@ -0,0 +1,7 @@
//
// Prefix header for all source files of the 'Instiki' target in the 'Instiki' project
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#endif

View File

@ -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

View File

@ -0,0 +1,14 @@
//
// main.mm
// Instiki
//
// Created by Allan Odgaard on Thu May 20 2004.
// Copyright (c) 2004 MacroMates. All rights reserved.
//
#import <Cocoa/Cocoa.h>
int main (int argc, char const* argv[])
{
return NSApplicationMain(argc, argv);
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildVersion</key>
<string>17</string>
<key>CFBundleShortVersionString</key>
<string>0.1</string>
<key>CFBundleVersion</key>
<string>0.1</string>
<key>ProjectName</key>
<string>NibPBTemplates</string>
<key>SourceVersion</key>
<string>1150000</string>
</dict>
</plist>

29
public/.htaccess Normal file
View File

@ -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

6
public/404.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<body>
<h1>File not found</h1>
<p>HTTP 404</p>
</body>
</html>

6
public/500.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<body>
<h1>Application error</h1>
<p>HTTP 500</p>
</body>
</html>

11
public/dispatch.rb Executable file
View File

@ -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

0
public/images/.images_go_here Executable file
View File

View File

View File

199
public/stylesheets/instiki.css Executable file
View File

@ -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
}

79
rakefile.rb Executable file
View File

@ -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

35
script/breakpointer Executable file
View File

@ -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'

83
script/server Executable file
View File

@ -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)

5
storage/.cvsignore Executable file
View File

@ -0,0 +1,5 @@
2500
development
test
.cvsignore
*.zip

View File

View File

@ -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 ['<a href="?category=animals">animals</a>', '<a href="?category=trees">trees</a>'],
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 ['<a href="?category=categorized">categorized</a>'],
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 ['<a href="?category=animals">animals</a>',
'<a href="?category=trees">trees</a>'],
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 ['<span class="selected">animals</span>', '<a href="?category=trees">trees</a>'],
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 <a.*HomePage.*<\/a> 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 <a.*HomePage.*<\/a> 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

48
test/test_helper.rb Executable file
View File

@ -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

View File

@ -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

14
test/unit/chunks/nowiki_test.rb Executable file
View File

@ -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 <nowiki>[[raw text]]</nowiki>. Do not touch!',
:plain_text => '[[raw text]]'
)
end
end

36
test/unit/chunks/wiki_test.rb Executable file
View File

@ -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

80
test/unit/diff_test.rb Executable file
View File

@ -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 <del class="diffmod">was </del>' +
'<ins class="diffmod">is </ins>the ' +
'<del class="diffmod">original </del>' +
'<ins class="diffmod">super </ins>string',
HTMLDiff.diff(a, b)
end
def test_html_diff_with_multiple_paragraphs
a = "<p>this was the original string</p>"
b = "<p>this is</p>\r\n<p>the super string</p>\r\n<p>around the world</p>"
assert_equal(
"<p>this <del class=\"diffmod\">was </del>" +
"<ins class=\"diffmod\">is</ins></p>\r\n<p>the " +
"<del class=\"diffmod\">original </del>" +
"<ins class=\"diffmod\">super </ins>string</p>\r\n" +
"<p><ins class=\"diffins\">around the world</ins></p>",
HTMLDiff.diff(a, b)
)
end
end

76
test/unit/page_test.rb Executable file
View File

@ -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

View File

@ -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 = <<EOL
* [[A]]
** [[B]]
** [[C]]
* D
** [[E]]
*** F
EOL
expected_result = <<EOL
\\pagebreak
\\section{A}
Abe
\\subsection{B}
Babe
\\subsection{C}
\\pagebreak
\\section{D}
\\subsection{E}
\\subsubsection{F}
EOL
expected_result.chop!
assert_equal(expected_result, table_of_contents(source, 'A' => '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

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