Checkout of Instiki Trunk 1/21/2007.

master
Jacques Distler 2007-01-22 07:43:50 -06:00
commit 69b62b6f33
1138 changed files with 139586 additions and 0 deletions

270
CHANGELOG Executable file
View File

@ -0,0 +1,270 @@
* TRUNK:
- ANTISPAM:
- updated and included spam_patterns.txt
- included dnsbl_check - DNS Blackhole Lists check
[thanks to joost from http://www.spacebabies.nl ]
- BUGFIXES:
- fix PDF output not to contain garbage chars [Jesse Newland]
- fixed the pages and authors display for single webs
- web list does not show a link to a published version if it has none
[Jesse Newland]
- Fixed bug that failed to expire cached diff view of a page
- Fixed rendering of WikiLinks in included pages in published or export
mode
- lots of small bugfixes and changes
- UPDATES:
- Rails 1.2 tested and packaged with instiki
- updated RubyZip to 0.9.1
- updated RedCloth to 3.0.4
- updated packaged sqlite3-ruby to 1.2.0
- FEATURES:
- Stylesheet tweaks
- visual display if webs are pass-protected (div background)
- Linux packaging
------------------------------------------------------------------------------
* 0.11.0:
- SQL-based backend (ActiveRecord)
- File uploads (finally)
- Upgraded to Rails 1.0.0
- Replaced internal link generator with routing
- Fixed --daemon option
- Removed Rubygem and native OS X distributions
- Improved HTML diff
- More accurate "See Changes"
------------------------------------------------------------------------------
* 0.10.2:
- Upgraded to Rails 0.13.1
- Fixed HTML export
- Added layout=no option to the export_html action (it exports page
contents processed by the markup engine, but without the default layout -
so that they can be wrapped in some other layout)
- <nowiki> tag can span several lines (before it was applied when both
opening and closing tags were on the same line only)
- Resolved the "endless redirection loop" condition and otherwise improved
handling of errors in the rendering engines
- Fixed rendering of Markdown hyperlinks such as
[Text](http://something.com/foo)
------------------------------------------------------------------------------
* 0.10.1:
- Upgraded Rails to 0.12.0
- Upgraded rubyzip to version 0.5.8
- BlueCloth is back (RedCloth didn't do pure Markdown well enough)
- Handling of line breaks in Textile is as in 0.9 (inserts <br/> tag)
- Fixed HTML export (to enclose the output in <html> tags, include the
stylesheet etc)
- Corrected some compatibility issues with storages from earlier Instiki
versions
- Some other bug fixes
------------------------------------------------------------------------------
* 0.10.0:
- Ported to ActionPack
- RedCloth 3.0.3
- BlueCloth is phased out, Markdown is rendered by RedCloth
- Mix markup option understands both Textile and Markdown on the same page
- Instiki can serve static content (such as HTML or plain-text files) from
./public directory
- Much friendlier admin interface
- Wiki link syntax doesn't conflict with Textile hyperlink syntax.
Therefore "textile link":LinkToSomePlace will not look insane.
- RSS feeds accept query parameters, such as
http://localhost:2500/wiki/rss_with_headlines?start=2005-02-18&end=2005-02-19&limit=10
- RSS feed with page contents for a password-protected web behaves as
follows: if the web is published, RSS feed links to the published version
of the web. otherwise, the feed is not available
Madeleine will check every hour if there are new commands in the log or
24 hours have passed since last snapshot, and take snapshot if either of
these conditions is true. Madeleine will also not log read-only
operations, resulting in a better performance
- Wiki extracts (to HTML and plain text) will leave only the last extract
file in ./storage
- Wiki search handles multibyte (UTF-8) characters correctly
- Local hyperlinks in published pages point to published pages [Michael
DeHaan]
- Fixed a bug that sometimes caused all past revisions of a page to be
"forgotten" on restart
- Fixed parsing of URIs with a port number (http://someplace.org:8080)
- Instiki will not fork itself on a *nix, unless explicitly asked to
- Instiki can bind to IPs other than 127.0.0.1 (command-line option)
- Revisions that do not change anything on the page are rejected
- Automated tests for all controller actions
- category: lines are presented as links to "All Pages" for relevant
categories
- Search looks at page titles, as well as content
- Multiple other usability enhancements and bug fixes
------------------------------------------------------------------------------
* 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.
Examples: [[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

113
README Executable file
View File

@ -0,0 +1,113 @@
===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"
If you are on Windows:
"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.
Having said all that, if you are not on Windows, in this version of Instiki it is a somewhat different story.
Since the author has no Linux or Mac at hand, and Instiki is moving to a SQL-based backend, this is what it takes
to install (until somebody sends a patch to properly package Instiki for all those other platforms):
3. Kill "instiki"
4. Install SQLite 3 database engine from http://www.sqlite.org/
5. Install SQLite 3 driver for Ruby from http://sqlite-ruby.rubyforge.org/
6. Install Rake from http://rake.rubyforge.org/
7. Execute rm -f db/*.db
8. Execute 'rake environment RAILS_ENV=production migrate'
9. Make an embarrassed sigh (as I do while writing this)
10. Run 'instiki' again
11. Pat yourself on the shoulder for being such a talented geek
12. At least, there is no step twelve! (TM)
===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
* Definitely can run on SQLite and MySQL
* May be able to run on Postgres, Oracle, DB2 and SqlServer. If you try this, and it works
(or, it doesn't, but you make it work) please write about it on Instiki.org.
===Command-line options:
* Run "ruby instiki --help"
===History:
* See CHANGELOG
===Migrating Instiki 0.10.2 storage to Instiki 0.11.0 database
1. Install Instiki 0.11 and check that it works (you should be able to create a web, edit and save a HomePage)
2. Execute
ruby script\import_storage \
-t /full/path/to/instiki0.10/storage \
-i /full/path/to/instiki0.10/installation \
-d sqlite (or mysql, or postgres, depending on what you use) \
-o instiki_import.sql
for example (Windows):
ruby script\import_storage -t c:\instiki-0.10.2\storage\2500 -i c:\instiki-0.10.2 -d sqlite -o instiki_import.sql
3. This will produce instiki_import.sql file in the current working directory.
Open it in a text editor and inspect carefully.
4. Connect to your production database (e.g., 'sqlite3 db\prod.db'),
and have it execute instiki_import.sql (e.g., '.read instiki_import.sql')
5. Execute ruby script\reset_references
(this script parses all pages for crosslinks between them, so it may take a few minutes)
6. Restart Instiki
7. Go over some pages, especially those with a lot of complex markup, and see if anything is broken.
The most common migration problem is this: if you open All Pages and see a lot of orphaned pages,
you forgot to run ruby script\reset_references after importing the data.
===Upgrading from Instiki-AR Beta 1
In Beta 2, we switch to ActiveRecord:Migrations. Therefore:
1. Back up your production database.
2. Open command-line session to your database and execute:
create table schema_info (version integer(11));
insert into schema_info (version) values (1);
3. Go back to the shell, change directory to the new Instiki and execute "rake migrate".
Step 2 creates a table that tells to ActiveRecord:Migrations that the current version
of this database is 1 (corresponding to Beta 1), and step 3 makes it up-to-date with
the current version of Instiki.
===Download the latest release from:
* http://rubyforge.org/project/showfiles.php?group_id=186
===Visit the "official" Instiki wiki:
* http://instiki.org
===License:
* same as Ruby's
---
Authors::
Versions 0.0 to 0.9.1:: David Heinemeier Hansson
Email:: david@loudthinking.com
Weblog:: http://www.loudthinking.com
From 0.9.2 onwards:: Alexey Verkhovsky
Email:: alex@verk.info

View File

@ -0,0 +1,94 @@
require 'application'
class AdminController < ApplicationController
layout 'default'
cache_sweeper :web_sweeper
def create_system
if @wiki.setup?
flash[:error] =
"Wiki has already been created in '#{@wiki.storage_path}'. " +
"Shut down Instiki and delete this directory if you want to recreate it from scratch." +
"\n\n" +
"(WARNING: this will destroy content of your current wiki)."
redirect_home(@wiki.webs.keys.first)
elsif @params['web_name']
# form submitted -> create a wiki
@wiki.setup(@params['password'], @params['web_name'], @params['web_address'])
flash[:info] = "Your new wiki '#{@params['web_name']}' is created!\n" +
"Please edit its home page and press Submit when finished."
redirect_to :web => @params['web_address'], :controller => 'wiki', :action => 'new',
:id => 'HomePage'
else
# no form submitted -> go to template
end
end
def create_web
if @params['address']
# form submitted
if @wiki.authenticate(@params['system_password'])
begin
@wiki.create_web(@params['name'], @params['address'])
flash[:info] = "New web '#{@params['name']}' successfully created."
redirect_to :web => @params['address'], :controller => 'wiki', :action => 'new',
:id => 'HomePage'
rescue Instiki::ValidationError => e
@error = e.message
# and re-render the form again
end
else
redirect_to :controller => 'wiki', :action => 'index'
end
else
# no form submitted -> render template
end
end
def edit_web
system_password = @params['system_password']
if system_password
# form submitted
if wiki.authenticate(system_password)
begin
wiki.edit_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,
@params['allow_uploads'] ? true : false,
@params['max_upload_size']
)
flash[:info] = "Web '#{@params['address']}' was successfully updated"
redirect_home(@params['address'])
rescue Instiki::ValidationError => e
logger.warn e.message
@error = e.message
# and re-render the same template again
end
else
@error = password_error(system_password)
# and re-render the same template again
end
else
# no form submitted - go to template
end
end
def remove_orphaned_pages
if wiki.authenticate(@params['system_password_orphaned'])
wiki.remove_orphaned_pages(@web_name)
flash[:info] = 'Orphaned pages removed'
redirect_to :controller => 'wiki', :web => @web_name, :action => 'list'
else
flash[:error] = password_error(@params['system_password_orphaned'])
redirect_to :controller => 'admin', :web => @web_name, :action => 'edit_web'
end
end
end

View File

@ -0,0 +1,190 @@
# 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
# require 'dnsbl_check'
before_filter :dnsbl_check, :connect_to_model, :check_authorization, :setup_url_generator, :set_content_type_header, :set_robots_metatag
after_filter :remember_location, :teardown_url_generator
# 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
Wiki.new
end
protected
def check_authorization
if in_a_web? and authorization_needed? and not authorized?
redirect_to :controller => 'wiki', :action => 'login', :web => @web_name
return false
end
end
def connect_to_model
@action_name = @params['action'] || 'index'
@web_name = @params['web']
@wiki = wiki
@author = cookies['author'] || 'AnonymousCoward'
if @web_name
@web = @wiki.webs[@web_name]
if @web.nil?
render(:status => 404, :text => "Unknown web '#{@web_name}'")
return false
end
end
end
FILE_TYPES = {
'.exe' => 'application/octet-stream',
'.gif' => 'image/gif',
'.jpg' => 'image/jpeg',
'.pdf' => 'application/pdf',
'.png' => 'image/png',
'.txt' => 'text/plain',
'.zip' => 'application/zip'
} unless defined? FILE_TYPES
DISPOSITION = {
'application/octet-stream' => 'attachment',
'image/gif' => 'inline',
'image/jpeg' => 'inline',
'application/pdf' => 'inline',
'image/png' => 'inline',
'text/plain' => 'inline',
'application/zip' => 'attachment'
} unless defined? DISPOSITION
def determine_file_options_for(file_name, original_options = {})
original_options[:type] ||= (FILE_TYPES[File.extname(file_name)] or 'application/octet-stream')
original_options[:disposition] ||= (DISPOSITION[original_options[:type]] or 'attachment')
original_options[:stream] ||= false
original_options
end
def send_file(file, options = {})
determine_file_options_for(file, options)
super(file, options)
end
def password_check(password)
if password == @web.password
cookies['web_address'] = password
true
else
false
end
end
def password_error(password)
if password.nil? or password.empty?
'Please enter the password.'
else
'You entered a wrong password. Please enter the right one.'
end
end
def redirect_home(web = @web_name)
if web
redirect_to_page('HomePage', web)
else
redirect_to_url '/'
end
end
def redirect_to_page(page_name = @page_name, web = @web_name)
redirect_to :web => web, :controller => 'wiki', :action => 'show',
:id => (page_name or 'HomePage')
end
def remember_location
if @request.method == :get and
@response.headers['Status'] == '200 OK' and not
%w(locked save back file pic import).include?(action_name)
@session[:return_to] = @request.request_uri
logger.debug "Session ##{session.object_id}: remembered URL '#{@session[:return_to]}'"
end
end
def rescue_action_in_public(exception)
render :status => 500, :text => <<-EOL
<html><body>
<h2>Internal Error</h2>
<p>An application error occurred while processing your request.</p>
<!-- \n#{exception}\n#{exception.backtrace.join("\n")}\n -->
</body></html>
EOL
end
def return_to_last_remembered
# Forget the redirect location
redirect_target, @session[:return_to] = @session[:return_to], nil
tried_home, @session[:tried_home] = @session[:tried_home], false
# then try to redirect to it
if redirect_target.nil?
if tried_home
raise 'Application could not render the index page'
else
logger.debug("Session ##{session.object_id}: no remembered redirect location, trying home")
redirect_home
end
else
logger.debug("Session ##{session.object_id}: " +
"redirect to the last remembered URL #{redirect_target}")
redirect_to_url(redirect_target)
end
end
def set_content_type_header
if %w(rss_with_content rss_with_headlines).include?(action_name)
@response.headers['Content-Type'] = 'text/xml; charset=UTF-8'
else
@response.headers['Content-Type'] = 'text/html; charset=UTF-8'
end
end
def set_robots_metatag
if controller_name == 'wiki' and %w(show published).include? action_name
@robots_metatag_value = 'index,follow'
else
@robots_metatag_value = 'noindex,nofollow'
end
end
def setup_url_generator
PageRenderer.setup_url_generator(UrlGenerator.new(self))
end
def teardown_url_generator
PageRenderer.teardown_url_generator
end
def wiki
self.class.wiki
end
private
def in_a_web?
not @web_name.nil?
end
def authorization_needed?
not %w( login authenticate published rss_with_content rss_with_headlines ).include?(action_name)
end
def authorized?
@web.nil? or
@web.password.nil? or
cookies['web_address'] == @web.password or
password_check(@params['password'])
end
end

View File

@ -0,0 +1,23 @@
module CacheSweepingHelper
def expire_cached_page(web, page_name)
expire_action :controller => 'wiki', :web => web.address,
:action => %w(show published), :id => page_name
expire_action :controller => 'wiki', :web => web.address,
:action => %w(show published), :id => page_name, :mode => 'diff'
end
def expire_cached_summary_pages(web)
categories = WikiReference.find(:all, :conditions => "link_type = 'C'")
%w(recently_revised list).each do |action|
expire_action :controller => 'wiki', :web => web.address, :action => action
categories.each do |category|
expire_action :controller => 'wiki', :web => web.address, :action => action, :category => category.referenced_name
end
end
expire_action :controller => 'wiki', :web => web.address, :action => 'authors'
expire_fragment :controller => 'wiki', :web => web.address, :action => %w(rss_with_headlines rss_with_content)
end
end

View File

@ -0,0 +1,100 @@
# Controller responsible for serving files and pictures.
require 'zip/zip'
class FileController < ApplicationController
layout 'default'
before_filter :check_allow_uploads
def file
@file_name = params['id']
if @params['file']
# form supplied
new_file = @web.wiki_files.create(@params['file'])
if new_file.valid?
flash[:info] = "File '#{@file_name}' successfully uploaded"
return_to_last_remembered
else
# pass the file with errors back into the form
@file = new_file
render
end
else
# no form supplied, this is a request to download the file
file = WikiFile.find_by_file_name(@file_name)
if file
send_data(file.content, determine_file_options_for(@file_name, :filename => @file_name))
else
@file = WikiFile.new(:file_name => @file_name)
render
end
end
end
def cancel_upload
return_to_last_remembered
end
def import
if @params['file']
@problems = []
import_file_name = "#{@web.address}-import-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}.zip"
import_from_archive(@params['file'].path)
if @problems.empty?
flash[:info] = 'Import successfully finished'
else
flash[:error] = 'Import finished, but some pages were not imported:<li>' +
@problems.join('</li><li>') + '</li>'
end
return_to_last_remembered
else
# to template
end
end
protected
def check_allow_uploads
render(:status => 404, :text => "Web #{@params['web'].inspect} not found") and return false unless @web
if @web.allow_uploads?
return true
else
render :status => 403, :text => 'File uploads are blocked by the webmaster'
return false
end
end
private
def import_from_archive(archive)
logger.info "Importing pages from #{archive}"
zip = Zip::ZipInputStream.open(archive)
while (entry = zip.get_next_entry) do
ext_length = File.extname(entry.name).length
page_name = entry.name[0..-(ext_length + 1)]
page_content = entry.get_input_stream.read
logger.info "Processing page '#{page_name}'"
begin
existing_page = @wiki.read_page(@web.address, page_name)
if existing_page
if existing_page.content == page_content
logger.info "Page '#{page_name}' with the same content already exists. Skipping."
next
else
logger.info "Page '#{page_name}' already exists. Adding a new revision to it."
wiki.revise_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new)
end
else
wiki.write_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new)
end
rescue => e
logger.error(e)
@problems << "#{page_name} : #{e.message}"
end
end
logger.info "Import from #{archive} finished"
end
end

View File

@ -0,0 +1,29 @@
require_dependency 'cache_sweeping_helper'
class RevisionSweeper < ActionController::Caching::Sweeper
include CacheSweepingHelper
observe Revision, Page
def after_save(record)
if record.is_a?(Revision)
expire_caches(record.page)
end
end
def after_delete(record)
if record.is_a?(Page)
expire_caches(record)
end
end
private
def expire_caches(page)
expire_cached_summary_pages(page.web)
pages_to_expire = ([page.name] + WikiReference.pages_that_reference(page.name)).uniq
pages_to_expire.each { |page_name| expire_cached_page(page.web, page_name) }
end
end

View File

@ -0,0 +1,14 @@
require_dependency 'cache_sweeping_helper'
class WebSweeper < ActionController::Caching::Sweeper
include CacheSweepingHelper
observe Web
def after_save(record)
web = record
web.pages.each { |page| expire_cached_page(web, page.name) }
expire_cached_summary_pages(web)
end
end

View File

@ -0,0 +1,429 @@
require 'fileutils'
require 'redcloth_for_tex'
require 'parsedate'
require 'zip/zip'
class WikiController < ApplicationController
before_filter :load_page
caches_action :show, :published, :authors, :recently_revised, :list
cache_sweeper :revision_sweeper
layout 'default', :except => [:rss_feed, :rss_with_content, :rss_with_headlines, :tex, :export_tex, :export_html]
def index
if @web_name
redirect_home
elsif not @wiki.setup?
redirect_to :controller => 'admin', :action => 'create_system'
elsif @wiki.webs.length == 1
redirect_home @wiki.webs.values.first.address
else
redirect_to :action => 'web_list'
end
end
# Outside a single web --------------------------------------------------------
def authenticate
if password_check(@params['password'])
redirect_home
else
flash[:info] = password_error(@params['password'])
redirect_to :action => 'login', :web => @web_name
end
end
def login
# to template
end
def web_list
@webs = wiki.webs.values.sort_by { |web| web.name }
end
# Within a single web ---------------------------------------------------------
def authors
@page_names_by_author = @web.page_names_by_author
@authors = @page_names_by_author.keys.sort
end
def export_html
stylesheet = File.read(File.join(RAILS_ROOT, 'public', 'stylesheets', 'instiki.css'))
export_pages_as_zip('html') do |page|
renderer = PageRenderer.new(page.revisions.last)
rendered_page = <<-EOL
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>#{page.plain_name} in #{@web.name}</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 {
color: ##{@web ? @web.color : "393" };
}
.newWikiWord { background-color: white; font-style: italic; }
#{stylesheet}
</style>
<style type="text/css">
#{@web.additional_style}
</style>
</head>
<body>
#{renderer.display_content_for_export}
<div class="byline">
#{page.revisions? ? "Revised" : "Created" } on #{ page.revised_at.strftime('%B %d, %Y %H:%M:%S') }
by
#{ UrlGenerator.new(self).make_link(page.author.name, @web, nil, { :mode => :export }) }
</div>
</body>
</html>
EOL
rendered_page
end
end
def export_markup
export_pages_as_zip(@web.markup) { |page| page.content }
end
def export_pdf
file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
file_path = File.join(@wiki.storage_path, file_name)
export_web_to_tex "#{file_path}.tex" unless FileTest.exists? "#{file_path}.tex"
convert_tex_to_pdf "#{file_path}.tex"
send_file "#{file_path}.pdf"
end
def export_tex
file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}.tex"
file_path = File.join(@wiki.storage_path, file_name)
export_web_to_tex(file_path) unless FileTest.exists?(file_path)
send_file file_path
end
def feeds
@rss_with_content_allowed = rss_with_content_allowed?
# show the template
end
def list
parse_category
@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
@pages_by_day = Hash.new { |h, day| h[day] = [] }
@pages_by_revision.each do |page|
day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day)
@pages_by_day[day] << page
end
end
def rss_with_content
if rss_with_content_allowed?
render_rss(hide_description = false, *parse_rss_params)
else
render_text 'RSS feed with content for this web is blocked for security reasons. ' +
'The web is password-protected and not published', '403 Forbidden'
end
end
def rss_with_headlines
render_rss(hide_description = true, *parse_rss_params)
end
def search
@query = @params['query']
@title_results = @web.select { |page| page.name =~ /#{@query}/i }.sort
@results = @web.select { |page| page.content =~ /#{@query}/i }.sort
all_pages_found = (@results + @title_results).uniq
if all_pages_found.size == 1
redirect_to_page(all_pages_found.first.name)
end
end
# Within a single page --------------------------------------------------------
def cancel_edit
@page.unlock
redirect_to_page(@page_name)
end
def edit
if @page.nil?
redirect_home
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
# to template
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.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
file_path = File.join(@wiki.storage_path, file_name)
export_page_to_tex("#{file_path}.tex") unless FileTest.exists?("#{file_path}.tex")
# NB: this is _very_ slow
convert_tex_to_pdf("#{file_path}.tex")
send_file "#{file_path}.pdf"
end
def print
if @page.nil?
redirect_home
end
@link_mode ||= :show
@renderer = PageRenderer.new(@page.revisions.last)
# to template
end
def published
if not @web.published?
render(:text => "Published version of web '#{@web_name}' is not available", :status => 404)
return
end
@page_name ||= 'HomePage'
@page ||= wiki.read_page(@web_name, @page_name)
render(:text => "Page '#{@page_name}' not found", :status => 404) and return unless @page
@renderer = PageRenderer.new(@page.revisions.last)
end
def revision
get_page_and_revision
@show_diff = (@params[:mode] == 'diff')
@renderer = PageRenderer.new(@revision)
end
def rollback
get_page_and_revision
end
def save
render(:status => 404, :text => 'Undefined page name') and return if @page_name.nil?
author_name = @params['author']
author_name = 'AnonymousCoward' if author_name =~ /^\s*$/
cookies['author'] = { :value => author_name, :expires => Time.utc(2030) }
begin
filter_spam(@params['content'])
if @page
wiki.revise_page(@web_name, @page_name, @params['content'], Time.now,
Author.new(author_name, remote_ip), PageRenderer.new)
@page.unlock
else
wiki.write_page(@web_name, @page_name, @params['content'], Time.now,
Author.new(author_name, remote_ip), PageRenderer.new)
end
redirect_to_page @page_name
rescue => e
flash[:error] = e
logger.error e
flash[:content] = @params['content']
if @page
@page.unlock
redirect_to :action => 'edit', :web => @web_name, :id => @page_name
else
redirect_to :action => 'new', :web => @web_name, :id => @page_name
end
end
end
def show
if @page
begin
@renderer = PageRenderer.new(@page.revisions.last)
@show_diff = (@params[:mode] == 'diff')
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
flash[:error] = e.message
if in_a_web?
redirect_to :action => 'edit', :web => @web_name, :id => @page_name
else
raise e
end
end
else
if not @page_name.nil? and not @page_name.empty?
redirect_to :web => @web_name, :action => 'new', :id => @page_name
else
render_text 'Page name is not specified', '404 Not Found'
end
end
end
def tex
@tex_content = RedClothForTex.new(@page.content).to_tex
end
protected
def load_page
@page_name = @params['id']
@page = @wiki.read_page(@web_name, @page_name) if @page_name
end
private
def convert_tex_to_pdf(tex_path)
# TODO remove earlier PDF files with the same prefix
# TODO handle gracefully situation where pdflatex is not available
begin
wd = Dir.getwd
Dir.chdir(File.dirname(tex_path))
logger.info `pdflatex --interaction=nonstopmode #{File.basename(tex_path)}`
ensure
Dir.chdir(wd)
end
end
def export_page_to_tex(file_path)
tex
File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex', :layout => false)) }
end
def export_pages_as_zip(file_type, &block)
file_prefix = "#{@web.address}-#{file_type}-"
timestamp = @web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')
file_path = File.join(@wiki.storage_path, 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("#{CGI.escape(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 "<html><head>" +
"<META HTTP-EQUIV=\"Refresh\" CONTENT=\"0;URL=HomePage.#{file_type}\"></head></html>"
end
end
FileUtils.rm_rf(Dir[File.join(@wiki.storage_path, file_prefix + '*.zip')])
FileUtils.mv(tmp_path, file_path)
send_file file_path
end
def export_web_to_tex(file_path)
@tex_content = table_of_contents(@web.page('HomePage').content, render_tex_web)
File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex_web', :layout => nil)) }
end
def get_page_and_revision
if @params['rev']
@revision_number = @params['rev'].to_i
else
@revision_number = @page.revisions.length
end
@revision = @page.revisions[@revision_number - 1]
end
def parse_category
@categories = WikiReference.list_categories.sort
@category = @params['category']
if @category
@set_name = "category '#{@category}'"
pages = WikiReference.pages_in_category(@category).sort.map { |page_name| @web.page(page_name) }
@pages_in_category = PageSet.new(@web, pages)
else
# no category specified, return all pages of the web
@pages_in_category = @web.select_all.by_name
@set_name = 'the web'
end
end
def parse_rss_params
if @params.include? 'limit'
limit = @params['limit'].to_i rescue nil
limit = nil if limit == 0
else
limit = 15
end
start_date = Time.local(*ParseDate::parsedate(@params['start'])) rescue nil
end_date = Time.local(*ParseDate::parsedate(@params['end'])) rescue nil
[ limit, start_date, end_date ]
end
def remote_ip
ip = @request.remote_ip
logger.info(ip)
ip
end
def render_rss(hide_description = false, limit = 15, start_date = nil, end_date = nil)
if limit && !start_date && !end_date
@pages_by_revision = @web.select.by_revision.first(limit)
else
@pages_by_revision = @web.select.by_revision
@pages_by_revision.reject! { |page| page.revised_at < start_date } if start_date
@pages_by_revision.reject! { |page| page.revised_at > end_date } if end_date
end
@hide_description = hide_description
@link_action = @web.password ? 'published' : 'show'
render :action => '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 rss_with_content_allowed?
@web.password.nil? or @web.published?
end
def truncate(text, length = 30, truncate_string = '...')
if text.length > length then text[0..(length - 3)] + truncate_string else text end
end
def filter_spam(content)
@@spam_patterns ||= load_spam_patterns
@@spam_patterns.each do |pattern|
raise "Your edit was blocked by spam filtering" if content =~ pattern
end
end
def load_spam_patterns
spam_patterns_file = "#{RAILS_ROOT}/config/spam_patterns.txt"
if File.exists?(spam_patterns_file)
File.readlines(spam_patterns_file).inject([]) { |patterns, line| patterns << Regexp.new(line.chomp, Regexp::IGNORECASE) }
else
[]
end
end
end

View File

@ -0,0 +1,93 @@
# 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.is_a? Array
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
# Creates a hyperlink to a Wiki page, without checking if the page exists or not
def link_to_existing_page(page, text = nil, html_options = {})
link_to(
text || page.plain_name,
{:web => @web.address, :action => 'show', :id => page.name, :only_path => true},
html_options)
end
# Creates a hyperlink to a Wiki page, or to a "new page" form if the page doesn't exist yet
def link_to_page(page_name, web = @web, text = nil, options = {})
raise 'Web not defined' if web.nil?
UrlGenerator.new(@controller).make_link(page_name, web, text,
options.merge(:base_url => "#{base_url}/#{web.address}"))
end
def author_link(page, options = {})
UrlGenerator.new(@controller).make_link(page.author.name, page.web, nil, options)
end
def base_url
home_page_url = url_for :controller => 'admin', :action => 'create_system', :only_path => true
home_page_url.sub(%r-/create_system/?$-, '')
end
# Creates a menu of categories
def categories_menu
if @categories.empty?
''
else
"<div id=\"categories\">\n" +
'<strong>Categories</strong>:' +
'[' + link_to_unless_current('Any', :web => @web.address, :action => @action_name) + "]\n" +
@categories.map { |c|
link_to_unless_current(c, :web => @web.address, :action => @action_name, :category => c)
}.join(', ') + "\n" +
'</div>'
end
end
# Performs HTML escaping on text, but keeps linefeeds intact (by replacing them with <br/>)
def escape_preserving_linefeeds(text)
h(text).gsub(/\n/, '<br/>')
end
def format_date(date, include_time = true)
# Must use DateTime because Time doesn't support %e on at least some platforms
if include_time
DateTime.new(date.year, date.mon, date.day, date.hour, date.min, date.sec).strftime("%B %e, %Y %H:%M:%S")
else
DateTime.new(date.year, date.mon, date.day).strftime("%B %e, %Y")
end
end
def rendered_content(page)
PageRenderer.new(page.revisions.last).display_content
end
end

View File

@ -0,0 +1,89 @@
module WikiHelper
def navigation_menu_for_revision
menu = []
menu << forward
menu << back_for_revision if @revision_number > 1
menu << current_revision
menu << see_or_hide_changes_for_revision if @revision_number > 1
menu << rollback
menu
end
def navigation_menu_for_page
menu = []
menu << edit_page
menu << edit_web if @page.name == "HomePage"
if @page.revisions.length > 1
menu << back_for_page
menu << see_or_hide_changes_for_page
end
menu
end
def edit_page
link_text = (@page.name == "HomePage" ? 'Edit Page' : 'Edit')
link_to(link_text, {:web => @web.address, :action => 'edit', :id => @page.name},
{:class => 'navlink', :accesskey => 'E', :name => 'edit'})
end
def edit_web
link_to('Edit Web', {:web => @web.address, :action => 'edit_web'},
{:class => 'navlink', :accesskey => 'W', :name => 'edit_web'})
end
def forward
if @revision_number < @page.revisions.length - 1
link_to('Forward in time',
{:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number + 1},
{:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) +
" <small>(#{@revision.page.revisions.length - @revision_number} more)</small> "
else
link_to('Forward in time', {:web => @web.address, :action => 'show', :id => @page.name},
{:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) +
" <small> (to current)</small>"
end
end
def back_for_revision
link_to('Back in time',
{:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number - 1},
{:class => 'navlink', :name => 'to_previous_revision'}) +
" <small>(#{@revision_number - 1} more)</small>"
end
def back_for_page
link_to('Back in time',
{:web => @web.address, :action => 'revision', :id => @page.name,
:rev => @page.revisions.length - 1},
{:class => 'navlink', :accesskey => 'B', :name => 'to_previous_revision'}) +
" <small>(#{@page.revisions.length - 1} #{@page.revisions.length - 1 == 1 ? 'revision' : 'revisions'})</small>"
end
def current_revision
link_to('See current', {:web => @web.address, :action => 'show', :id => @page.name},
{:class => 'navlink', :name => 'to_current_revision'})
end
def see_or_hide_changes_for_revision
link_to(@show_diff ? 'Hide changes' : 'See changes',
{:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number,
:mode => (@show_diff ? nil : 'diff') },
{:class => 'navlink', :accesskey => 'C', :name => 'see_changes'})
end
def see_or_hide_changes_for_page
link_to(@show_diff ? 'Hide changes' : 'See changes',
{:web => @web.address, :action => 'show', :id => @page.name, :mode => (@show_diff ? nil : 'diff') },
{:class => 'navlink', :accesskey => 'C', :name => 'see_changes'})
end
def rollback
link_to('Rollback',
{:web => @web.address, :action => 'rollback', :id => @page.name, :rev => @revision_number},
{:class => 'navlink', :name => 'rollback'})
end
end

18
app/models/author.rb Normal file
View File

@ -0,0 +1,18 @@
class Author < String
attr_accessor :ip
attr_reader :name
def initialize(name, ip = nil)
@ip = ip
super(name)
end
def name=(value)
self.gsub!(/.+/, value)
end
alias_method :name, :to_s
def <=>(other)
name <=> other.to_s
end
end

121
app/models/page.rb Normal file
View File

@ -0,0 +1,121 @@
class Page < ActiveRecord::Base
belongs_to :web
has_many :revisions, :order => 'id'
has_many :wiki_references, :order => 'referenced_name'
has_one :current_revision, :class_name => 'Revision', :order => 'id DESC'
def revise(content, time, author, renderer)
revisions_size = new_record? ? 0 : revisions.size
if (revisions_size > 0) and content == current_revision.content
raise Instiki::ValidationError.new(
"You have tried to save page '#{name}' without changing its content")
end
author = Author.new(author.to_s) unless author.is_a?(Author)
# Try to render content to make sure that markup engine can take it,
renderer.revision = Revision.new(
:page => self, :content => content, :author => author, :revised_at => time)
renderer.display_content(update_references = true)
# A user may change a page, look at it and make some more changes - several times.
# Not to record every such iteration as a new revision, if the previous revision was done
# by the same author, not more than 30 minutes ago, then update the last revision instead of
# creating a new one
if (revisions_size > 0) && continous_revision?(time, author)
current_revision.update_attributes(:content => content, :revised_at => time)
else
revisions.create(:content => content, :author => author, :revised_at => time)
end
save
self
end
def rollback(revision_number, time, author_ip, renderer)
roll_back_revision = self.revisions[revision_number]
if roll_back_revision.nil?
raise Instiki::ValidationError.new("Revision #{revision_number} not found")
end
author = Author.new(roll_back_revision.author.name, author_ip)
revise(roll_back_revision.content, time, author, renderer)
end
def revisions?
revisions.size > 1
end
def previous_revision(revision)
revision_index = revisions.each_with_index do |rev, index|
if rev.id == revision.id
break index
else
nil
end
end
if revision_index.nil? or revision_index == 0
nil
else
revisions[revision_index - 1]
end
end
def references
web.select.pages_that_reference(name)
end
def wiki_words
wiki_references.select { |ref| ref.wiki_word? }.map { |ref| ref.referenced_name }
end
def linked_from
web.select.pages_that_link_to(name)
end
def included_from
web.select.pages_that_include(name)
end
# Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page".
def plain_name
web.brackets_only? ? name : WikiWords.separate(name)
end
LOCKING_PERIOD = 30.minutes
def lock(time, locked_by)
update_attributes(:locked_at => time, :locked_by => locked_by)
end
def lock_duration(time)
((time - locked_at) / 60).to_i unless locked_at.nil?
end
def unlock
update_attribute(:locked_at, nil)
end
def locked?(comparison_time)
locked_at + LOCKING_PERIOD > comparison_time unless locked_at.nil?
end
def to_param
name
end
private
def continous_revision?(time, author)
(current_revision.author == author) && (revised_at + 30.minutes > time)
end
# Forward method calls to the current revision, so the page responds to all revision calls
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
# Perform a hand-off to AR::Base#method_missing
if @attributes.include?(method_name) or md = /(=|\?|_before_type_cast)$/.match(method_name)
super(method_id, *args, &block)
else
current_revision.send(method_id)
end
end
end

View File

@ -0,0 +1,15 @@
# This class maintains the state of wiki references for newly created or newly deleted pages
class PageObserver < ActiveRecord::Observer
def after_create(page)
WikiReference.update_all("link_type = '#{WikiReference::LINKED_PAGE}'",
['referenced_name = ?', page.name])
end
def before_destroy(page)
WikiReference.delete_all ['page_id = ?', page.id]
WikiReference.update_all("link_type = '#{WikiReference::WANTED_PAGE}'",
['referenced_name = ?', page.name])
end
end

92
app/models/page_set.rb Normal file
View File

@ -0,0 +1,92 @@
# 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)
# 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.revised_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.revised_at }).reverse
end
def pages_that_reference(page_name)
all_referring_pages = WikiReference.pages_that_reference(page_name)
self.select { |page| all_referring_pages.include?(page.name) }
end
def pages_that_link_to(page_name)
all_linking_pages = WikiReference.pages_that_link_to(page_name)
self.select { |page| all_linking_pages.include?(page.name) }
end
def pages_that_include(page_name)
all_including_pages = WikiReference.pages_that_include(page_name)
self.select { |page| all_including_pages.include?(page.name) }
end
def pages_authored_by(author)
all_pages_authored_by_the_author =
Page.connection.select_all(sanitize_sql([
"SELECT page_id FROM revision WHERE author = '?'", 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
# Pages that refer to themselves and have no links from outside are oprphans.
def orphaned_pages
never_orphans = web.authors + ['HomePage']
self.select { |page|
if never_orphans.include? page.name
false
else
references = pages_that_reference(page.name)
references.empty? or references == [page]
end
}
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.sort
end
end

4
app/models/revision.rb Normal file
View File

@ -0,0 +1,4 @@
class Revision < ActiveRecord::Base
belongs_to :page
composed_of :author, :mapping => [ %w(author name), %w(ip ip) ]
end

4
app/models/system.rb Normal file
View File

@ -0,0 +1,4 @@
class System < ActiveRecord::Base
set_table_name 'system'
validates_presence_of :password
end

147
app/models/web.rb Normal file
View File

@ -0,0 +1,147 @@
class Web < ActiveRecord::Base
has_many :pages
has_many :wiki_files
def wiki
Wiki.new
end
def settings_changed?(markup, safe_mode, brackets_only)
self.markup != markup ||
self.safe_mode != safe_mode ||
self.brackets_only != brackets_only
end
def add_page(name, content, time, author, renderer)
page = page(name) || Page.new(:web => self, :name => name)
page.revise(content, time, author, renderer)
end
def authors
connection.select_all(
'SELECT DISTINCT r.author AS author ' +
'FROM revisions r ' +
'JOIN pages p ON p.id = r.page_id ' +
'WHERE p.web_id = ' + self.id.to_s +
'ORDER by 1 '
).collect { |row| row['author'] }
end
def categories
select.map { |page| page.categories }.flatten.uniq.sort
end
def page(name)
pages.find(:first, :conditions => ['name = ?', name])
end
def last_page
return Page.find(:first, :order => 'id desc', :conditions => ['web_id = ?', self.id])
end
def has_page?(name)
Page.count(['web_id = ? AND name = ?', id, name]) > 0
end
def has_file?(file_name)
WikiFile.find_by_file_name(file_name) != nil
end
def markup
read_attribute('markup').to_sym
end
def page_names_by_author
connection.select_all(
'SELECT DISTINCT r.author AS author, p.name AS page_name ' +
'FROM revisions r ' +
'JOIN pages p ON r.page_id = p.id ' +
"WHERE p.web_id = #{self.id} " +
'ORDER by p.name'
).inject({}) { |result, row|
author, page_name = row['author'], row['page_name']
result[author] = [] unless result.has_key?(author)
result[author] << page_name
result
}
end
def remove_pages(pages_to_be_removed)
pages_to_be_removed.each { |p| p.destroy }
end
def revised_at
select.most_recent_revision
end
def select(&condition)
PageSet.new(self, pages, condition)
end
def select_all
PageSet.new(self, pages, nil)
end
def to_param
address
end
def create_files_directory
return unless allow_uploads == 1
dummy_file = self.wiki_files.build(:file_name => '0', :description => '0', :content => '0')
dir = File.dirname(dummy_file.content_path)
begin
require 'fileutils'
FileUtils.mkdir_p dir
dummy_file.save
dummy_file.destroy
rescue => e
logger.error("Failed create files directory for #{self.address}: #{e}")
raise "Instiki could not create directory to store uploaded files. " +
"Please make sure that Instiki is allowed to create directory " +
"#{File.expand_path(dir)} and add files to it."
end
end
private
# Returns an array of all the wiki words in any current revision
def wiki_words
pages.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.map { |p| p.name }
end
protected
before_save :sanitize_markup
after_save :create_files_directory
before_validation :validate_address
validates_uniqueness_of :address
validates_length_of :color, :in => 3..6
def sanitize_markup
self.markup = markup.to_s
end
def validate_address
unless address == CGI.escape(address)
self.errors.add(:address, 'should contain only valid URI characters')
raise Instiki::ValidationError.new("#{self.class.human_attribute_name('address')} #{errors.on(:address)}")
end
end
def default_web?
defined? DEFAULT_WEB and self.address == DEFAULT_WEB
end
def files_path
if default_web?
"#{RAILS_ROOT}/public/files"
else
"#{RAILS_ROOT}/public/#{self.address}/files"
end
end
end

92
app/models/wiki.rb Normal file
View File

@ -0,0 +1,92 @@
class Wiki
cattr_accessor :storage_path, :logger
self.storage_path = "#{RAILS_ROOT}/storage/"
self.logger = RAILS_DEFAULT_LOGGER
def authenticate(password)
password == (system.password || 'instiki')
end
def create_web(name, address, password = nil)
@webs = nil
Web.create(:name => name, :address => address, :password => password)
end
def delete_web(address)
web = Web.find_by_address(address)
unless web.nil?
web.destroy
@webs = nil
end
end
def edit_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false,
password = nil, published = false, brackets_only = false, count_pages = false,
allow_uploads = true, max_upload_size = nil)
if not (web = Web.find_by_address(old_address))
raise Instiki::ValidationError.new("Web with address '#{old_address}' does not exist")
end
web.update_attributes(:address => new_address, :name => name, :markup => markup, :color => color,
:additional_style => additional_style, :safe_mode => safe_mode, :password => password, :published => published,
:brackets_only => brackets_only, :count_pages => count_pages, :allow_uploads => allow_uploads, :max_upload_size => max_upload_size)
@webs = nil
raise Instiki::ValidationError.new("There is already a web with address '#{new_address}'") unless web.errors.on(:address).nil?
web
end
def read_page(web_address, page_name)
self.class.logger.debug "Reading page '#{page_name}' from web '#{web_address}'"
web = Web.find_by_address(web_address)
if web.nil?
self.class.logger.debug "Web '#{web_address}' not found"
return nil
else
page = web.pages.find(:first, :conditions => ['name = ?', page_name])
self.class.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found"
return page
end
end
def remove_orphaned_pages(web_address)
web = Web.find_by_address(web_address)
web.remove_pages(web.select.orphaned_pages)
end
def revise_page(web_address, page_name, content, revised_at, author, renderer)
page = read_page(web_address, page_name)
page.revise(content, revised_at, author, renderer)
end
def rollback_page(web_address, page_name, revision_number, time, author_id = nil)
page = read_page(web_address, page_name)
page.rollback(revision_number, time, author_id)
end
def setup(password, web_name, web_address)
system.update_attribute(:password, password)
create_web(web_name, web_address)
end
def system
@system ||= (System.find(:first) || System.create)
end
def setup?
Web.count > 0
end
def webs
@webs ||= Web.find(:all).inject({}) { |webs, web| webs.merge(web.address => web) }
end
def storage_path
self.class.storage_path
end
def write_page(web_address, page_name, content, written_on, author, renderer)
Web.find_by_address(web_address).add_page(page_name, content, written_on, author, renderer)
end
end

64
app/models/wiki_file.rb Normal file
View File

@ -0,0 +1,64 @@
class WikiFile < ActiveRecord::Base
belongs_to :web
before_save :write_content_to_file
before_destroy :delete_content_file
validates_presence_of %w( web file_name )
validates_length_of :file_name, :within=>1..50
validates_length_of :description, :maximum=>255
def self.find_by_file_name(file_name)
find(:first, :conditions => ['file_name = ?', file_name])
end
SANE_FILE_NAME = /^[a-zA-Z0-9\-_\. ]*$/
def validate
if file_name
if file_name !~ SANE_FILE_NAME
errors.add("file_name", "is invalid. Only latin characters, digits, dots, underscores, " +
"dashes and spaces are accepted")
elsif file_name == '.' or file_name == '..'
errors.add("file_name", "cannot be '.' or '..'")
end
end
if @web and @content
if (@content.size > @web.max_upload_size.kilobytes)
errors.add("content", "size (#{(@content.size / 1024.0).round} kilobytes) exceeds " +
"the maximum (#{web.max_upload_size} kilobytes) set for this wiki")
end
end
errors.add("content", "is empty") if @content.nil? or @content.empty?
end
def content=(content)
if content.respond_to? :read
@content = content.read
else
@content = content
end
end
def content
@content ||= ( File.open(content_path, 'rb') { |f| f.read } )
end
def content_path
web.files_path + '/' + file_name
end
def write_content_to_file
web.create_files_directory unless File.exists?(web.files_path)
File.open(self.content_path, 'wb') { |f| f.write(@content) }
end
def delete_content_file
require 'fileutils'
FileUtils.rm_f(content_path) if File.exists?(content_path)
end
end

View File

@ -0,0 +1,82 @@
class WikiReference < ActiveRecord::Base
LINKED_PAGE = 'L'
WANTED_PAGE = 'W'
INCLUDED_PAGE = 'I'
CATEGORY = 'C'
AUTHOR = 'A'
FILE = 'F'
WANTED_FILE = 'E'
belongs_to :page
validates_inclusion_of :link_type, :in => [LINKED_PAGE, WANTED_PAGE, INCLUDED_PAGE, CATEGORY, AUTHOR, FILE, WANTED_FILE]
# FIXME all finders below MUST restrict their results to pages belonging to a particular web
def self.link_type(web, page_name)
web.has_page?(page_name) ? LINKED_PAGE : WANTED_PAGE
end
def self.pages_that_reference(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ?' +
"AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}', '#{INCLUDED_PAGE}')"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_that_link_to(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}')"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_that_include(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type = '#{INCLUDED_PAGE}'"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_in_category(category)
query =
'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type = '#{CATEGORY}'"
names = connection.select_all(sanitize_sql([query, category])).map { |row| row['name'] }
end
def self.list_categories
query = "SELECT DISTINCT referenced_name FROM wiki_references WHERE link_type = '#{CATEGORY}'"
connection.select_all(query).map { |row| row['referenced_name'] }
end
def wiki_word?
linked_page? or wanted_page?
end
def wiki_link?
linked_page? or wanted_page? or file? or wanted_file?
end
def linked_page?
link_type == LINKED_PAGE
end
def wanted_page?
link_type == WANTED_PAGE
end
def included_page?
link_type == INCLUDED_PAGE
end
def file?
link_type == FILE
end
def wanted_file?
link_type == WANTED_FILE
end
end

View File

@ -0,0 +1,86 @@
<% @title = "Instiki Setup"; @content_width = 500 %>
<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_tag({ :controller => 'admin', :action => 'create_system' },
{ 'id' => 'setup', 'method' => 'post', 'onSubmit' => 'return validateSetup()',
'accept-charset' => 'utf-8' })
%>
<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 and 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.
</div>
<div class="help"><em>Everyone with this password will be able to do this, so pick it carefully!</em></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>
<%= end_form_tag %>
<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>

View File

@ -0,0 +1,72 @@
<% @title = "New Wiki Web"; @content_width = 500 %>
<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_tag({ :controller => 'admin', :action => 'create_web' },
{ 'id' => 'setup', 'method' => 'post',
'onSubmit' => 'cleanAddress(); return validateSetup()',
'accept-charset' => 'utf-8' })
%>
<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 and 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>
<%= end_form_tag %>
<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>

View File

@ -0,0 +1,136 @@
<% @title = "Edit Web" %>
<%= form_tag({ :controller => 'admin', :action => 'edit_web', :web => @web.address },
{ 'id' => 'setup', 'method' => 'post',
'onSubmit' => 'cleanAddress(); return validateSetup()',
'accept-charset' => 'utf-8' })
%>
<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" class="disableAutoComplete" value="<%= @web.name %>"
onChange="proposeAddress();" /> &nbsp;&nbsp;
Address: <input type="text" class="disableAutoComplete" id="address" name="address" value="<%= @web.address %>"
onChange="cleanAddress();" />
<small><em>(Letters and digits only)</em></small>
</div>
<h2 style="margin-bottom: 3px">Specialize</h2>
<div class="inputBox">
Markup:
<select name="markup">
<%= html_options({'Textile' => :textile, 'Markdown' => :markdown, 'Mixed' => :mixed,
'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>
<br/>
<p>
<small>
<input type="checkbox" class="disableAutoComplete" name="safe_mode" <%= 'checked="on"' if @web.safe_mode? %> />
Safe mode
<em>- strip HTML tags and stylesheet options from the content of all pages</em>
<br/>
<input type="checkbox" class="disableAutoComplete" name="brackets_only" <%= 'checked="on"' if @web.brackets_only? %> />
Brackets only
<em>- require all wiki words to be as [[wiki word]], WikiWord links won't be created</em>
<br/>
<input type="checkbox" class="disableAutoComplete" name="count_pages" <%= 'checked="on"' if @web.count_pages? %> />
Count pages
<br/>
<input type="checkbox" class="disableAutoComplete" name="allow_uploads" <%= 'checked="on"' if @web.allow_uploads? %> />
Allow uploads of no more than
<input type="text" class="disableAutoComplete" name="max_upload_size" value="<%= @web.max_upload_size %>"
width="20" />
kbytes
<em>-
allow users to upload pictures and other files and include them on wiki pages
</em>
<br/>
</small>
</p>
<a href="#"
onClick="document.getElementById('additionalStyle').style.display='block';return false;">
Stylesheet tweaks &gt;&gt;</a>
<small><em>
- add or change styles used by this web; styles defined here take precedence over
instiki.css. Hint: View HTML source of a page you want to style to find ID names on individual
tags.</em></small>
<br/>
<textarea id="additionalStyle" class="disableAutoComplete"
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 class="disableAutoComplete" type="password" id="password"
name="password" value="<%= @web.password %>" />
&nbsp;&nbsp;
Verify: <input class="disableAutoComplete" 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" class="disableAutoComplete" <%= 'checked="on"' if @web.published? %> />
Publish this web
</div>
<p align="right">
<small>
Enter system password
<input type="password" class="disableAutoComplete" id="system_password"
name="system_password" />
and
<input type="submit" value="Update Web" />
<br/><br/>
...or forget changes and <%= link_to 'create a new web', :action => 'create_web' %>
</small>
</p>
<%= end_form_tag %>
<br/>
<h1>Other administrative tasks</h1>
<%= form_tag({:controller => 'admin', :web => @web.address, :action => 'remove_orphaned_pages'},
{ :id => 'remove_orphaned_pages',
:onSubmit => "return checkSystemPassword(document.getElementById('system_password_orphaned').value)",
'accept-charset' => 'utf-8' })
%>
<p align="right">
<small>
Clean up by entering system password
<input type="password" id="system_password_orphaned" class="disableAutoComplete" name="system_password_orphaned" />
and
<input type="submit" value="Delete Orphan Pages" />
</small>
</p>
<%= end_form_tag %>
<%= javascript_include_tag 'edit_web' %>

33
app/views/file/file.rhtml Normal file
View File

@ -0,0 +1,33 @@
<%
@title = "Upload #{h @file_name}"
@hide_navigation = false
%>
<%= error_messages_for 'file' %>
<%= form_tag({ :controller => 'file', :web => @web_name, :action => 'file' },
{ 'multipart' => true , 'accept-charset' => 'utf-8' }) %>
<%= hidden_field 'file', 'file_name' %>
<div class="inputFieldWithPrompt">
<b>Content of <%= h @file_name %> to upload <small>(required)</small>:</b>
<br/>
<input type="file" name="file[content]" size="40" />
<br/>
<small>
Please note that the file you are uploadng will be named <%= h @file_name %> on the wiki -
regardless of how it is named on your computer. To change the wiki name of the file, please go
<%= link_to :back %> and edit the wiki page that refers to the file.
</small>
</div>
<div class="inputFieldWithPrompt">
<b>Description <small>(optional)</small>:</b>
<br/>
<%= text_field "file", "description", "size" => 40 %>
</div>
<div>
<input type="submit" value="Upload" /> as
<%= text_field_tag :author, @author,
:onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;",
:onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %>
</div>
<%= end_form_tag %>

View File

@ -0,0 +1,23 @@
<p>
<%= form_tag({}, { 'multipart' => true, 'accept-charset' => 'utf-8' }) %>
<p>
File to upload:
<br/>
<input type="file" name="file" size="40" />
</p>
<p>
System password:
<br/>
<input type="password" id="system_password" name="system_password" />
</p>
<p>
<input type="submit" value="Update" /> as
<input type="text" name="author" id="authorName" value="<%= @author %>"
onClick="this.value == 'AnonymousCoward' ? this.value = '' : true" />
<% if @page %>
| <%= link_to 'Cancel', :web => @web.address, :action => 'file'%> <small>(unlocks page)</small>
<% end %>
</p>
<%= end_form_tag %>
</p>

View File

@ -0,0 +1,79 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
<% if @page and (@page.name == 'HomePage') and (%w( show published print ).include?(@action_name)) %>
<%= h @web.name %>
<% elsif @web %>
<%= @title %> in <%= h @web.name %>
<% else %>
<%= @title %>
<% end %>
<%= @show_diff ? ' (changes)' : '' %>
</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="<%= @robots_metatag_value %>" />
<style type="text/css">
h1#pageName, .newWikiWord a, a.existingWikiWord, .newWikiWord a:hover, #TextileHelp h3 {
color: #<%= @web ? @web.color : "393" %>;
}
<%= File.read(RAILS_ROOT + '/public/stylesheets/instiki.css') if @inline_style %>
</style>
<%= stylesheet_link_tag 'instiki' unless @inline_style %>
<style type="text/css">
<%= @style_additions %>
<%= @web ? @web.additional_style : '' %>
</style>
<% if @web %>
<%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_headlines') %>
<%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_content') %>
<% end %>
</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) %>
<%= h(@web.name) + (@show_diff ? ' (changes)' : '') %>
<% elsif @web %>
<small><%= @web.name %></small><br />
<%= @title %>
<% else %>
<%= @title %>
<% end %>
</h1>
<%= render 'navigation' unless @web.nil? || @hide_navigation %>
<% if @flash[:info] %>
<div class="info"><%= escape_preserving_linefeeds @flash[:info] %></div>
<% end %>
<% if @error or @flash[:error] %>
<div class="errorExplanation"><%= escape_preserving_linefeeds(@error || @flash[:error]) %></div>
<% end %>
<%= @content_for_layout %>
<% if @show_footer %>
<div id="footer">
<div>This site is running on <a href="http://instiki.org/">Instiki</a></div>
<div>Powered by <a href="http://rubyonrails.com/">Ruby on Rails</a></div>
</div>
<% end %>
</div> <!-- Content -->
</div> <!-- Container -->
</body>
</html>

View File

@ -0,0 +1,12 @@
<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>

View File

@ -0,0 +1,7 @@
<%= render 'textile_help' %>
<h3>Markdown</h3>
<p>
In addition to Textile, this wiki also understands
<a target="_new" href="http://daringfireball.net/projects/markdown/syntax">Markdown</a>.
</p>

View File

@ -0,0 +1,28 @@
<%
def list_item(text, link_options, description, accesskey = nil)
link_options[:controller] = 'wiki'
link_options[:web] = @web.address
link_to_unless_current(text, link_options, :title => description, :accesskey => accesskey) {
content_tag('b', text, 'title' => description, 'class' => 'navOn')
}
end
%>
<div class="navigation">
<% if @action_name != 'published' then %>
<%= list_item 'Home Page', {:action => 'show', :id => 'HomePage'}, 'Home, Sweet Home', 'H' %> |
<%= list_item 'All Pages', {:action => 'list'}, 'Alphabetically sorted list of pages', 'A' %> |
<%= list_item 'Recently Revised', {:action =>'recently_revised'}, 'Pages sorted by when they were last changed', 'U' %> |
<%= list_item 'Authors', {:action => 'authors'}, 'Who wrote what' %> |
<%= list_item 'Feeds', {:action => 'feeds'}, 'Subscribe to changes by RSS' %> |
<%= list_item 'Export', {:action => 'export'}, 'Download a zip with all the pages in this wiki', 'X' %> |
<%= form_tag({ :controller => 'wiki', :action => 'search', :web => @web.address},
{'id' => 'navigationSearchForm', 'method' => 'get', 'accept-charset' => 'utf-8' }) %>
<input type="text" id="searchField" name="query" value="Search"
onfocus="this.value == 'Search' ? this.value = '' : true"
onblur="this.value == '' ? this.value = 'Search' : true" />
<%= end_form_tag %>
<% else %>
<%= list_item 'Home Page', {:action => 'published', :id => 'HomePage'}, 'Home, Sweet Home', 'H' %> |
<% end%>
</div>

12
app/views/rdoc_help.rhtml Normal file
View File

@ -0,0 +1,12 @@
<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>

View File

@ -0,0 +1,24 @@
<h3>Textile formatting tips (<a href="http://hobix.com/textile/quick.html" 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>
<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>

View File

@ -0,0 +1,13 @@
<% unless @page.linked_from.empty? %>
<small>
| Linked from:
<%= @page.linked_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %>
</small>
<% end %>
<% unless @page.included_from.empty? %>
<small>
| Included from:
<%= @page.included_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %>
</small>
<% end %>

View File

@ -0,0 +1,11 @@
<% @title = 'Authors' %>
<ul id="authorList">
<% for author in @authors %>
<li>
<%= link_to_page author %>
co- or authored:
<%= @page_names_by_author[author].collect { |page_name| link_to_page(page_name) }.sort.join ', ' %>
</li>
<% end %>
</ul>

40
app/views/wiki/edit.rhtml Normal file
View File

@ -0,0 +1,40 @@
<%
@title = "Editing #{@page.name}"
@content_width = 720
@hide_navigation = true
%>
<div id="MarkupHelp">
<%= render("#{@web.markup}_help") %>
<%= render 'wiki_words_help' %>
</div>
<div id="editForm">
<%= form_tag({ :action => 'save', :web => @web.address, :id => @page.name },
{ 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName()',
'accept-charset' => 'utf-8' }) %>
<textarea name="content" id="content"><%= h(@flash[:content] || @page.content) %></textarea>
<div id="editFormButtons">
<input type="submit" value="Submit" accesskey="s"/> as
<%= text_field_tag :author, @author,
:onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;",
:onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %>
|
<span>
<%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name},
{:accesskey => 'c'}) %>
<small>(unlocks page)</small>
</span>
</div>
<%= end_form_tag %>
</div>
<script type="text/javascript">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
document.forms["editForm"].elements["content"].focus();
</script>

View File

@ -0,0 +1,12 @@
<% @title = "Export" %>
<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><%= link_to 'HTML', :web => @web.address, :action => 'export_html' %></li>
<li><%= link_to "Markup (#{@web.markup.to_s.capitalize})", :web => @web.address, :action => 'export_markup' %></li>
<% if OPTIONS[:pdflatex] && @web.markup == :textile %>
<li><%= link_to 'TeX', :web => @web.address, :action => 'export_tex' %></li>
<li><%= link_to 'PDF', :web => @web.address, :action => 'export_pdf' %></li>
<% end %>
</ul>

View File

@ -0,0 +1,14 @@
<% @title = "Feeds" %>
<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>
<% if @rss_with_content_allowed %>
<%= link_to 'Full content (RSS 2.0)', :web => @web.address, :action => :rss_with_content %>
<% end %>
</li>
<li>
<%= link_to 'Headlines (RSS 2.0)', :web => @web.address, :action => :rss_with_headlines %>
</li>
</ul>

64
app/views/wiki/list.rhtml Normal file
View File

@ -0,0 +1,64 @@
<% @title = "All Pages" %>
<%= categories_menu unless @categories.empty? %>
<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>
<% @pages_in_category.each do |page| %>
<li>
<%= link_to_existing_page page, truncate(page.plain_name, 35) %>
</li>
<% end %></ul>
<% if @web.count_pages? %>
<% total_chars = @pages_in_category.characters %>
<p><small>All content: <%= total_chars %> chars / approx. <%= sprintf("%-.1f", (total_chars / 2275 )) %> printed 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">
<% @page_names_that_are_wanted.each do |wanted_page_name| %>
<li>
<%= link_to_page(wanted_page_name, @web, truncate(WikiWords.separate(wanted_page_name), 35)) %>
wanted by
<%= @web.select.pages_that_reference(wanted_page_name).collect { |referring_page|
link_to_existing_page referring_page
}.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">
<% @pages_that_are_orphaned.each do |orphan_page| %>
<li>
<%= link_to_existing_page orphan_page, truncate(orphan_page.plain_name, 35) %>
</li>
<% end %>
</ul>
<% end %>
</div>

View File

@ -0,0 +1,23 @@
<% @title = "#{@page.plain_name} is locked" %>
<p>
<%= link_to_page(@page.locked_by) %>
<% if @page.lock_duration(Time.now) == 0 %>
just started editing this page.
<% else %>
has been editing this page for <%= @page.lock_duration(Time.now) %> minutes.
<% end %>
</p>
<p>
<%= link_to 'Edit the page anyway',
{:web => @web_name, :action => 'edit', :id => @page.name, :params => {'break_lock' => '1'} },
{:accesskey => 'E'}
%>
<%= link_to 'Cancel',
{:web => @web_name, :action => 'show', :id => @page.name},
{:accesskey => 'C'}
%>
</p>

View File

@ -0,0 +1,22 @@
<% @title = "#{@web_name} Login" %><% @hide_navigation = true %>
<p>
<%= form_tag({ :controller => 'wiki', :action => 'authenticate', :web => @web.address},
{ 'name' => 'loginForm', 'id' => 'loginForm', 'method' => 'post', 'accept-charset' => 'utf-8' }) %>
<p>
This web is password-protected. Please enter the password.
<% if @web.published? %>
If you don't have the password, you can view this wiki as a <%= link_to 'read-only version', :action => 'published', :id => 'HomePage' %>.
<% end %>
</p>
<p>
<b>Password: </b>
<input type="password" name="password" id="password" />
<input type="submit" value="Login" default="yes" />
</p>
<%= end_form_tag %>
</p>
<script type="text/javascript">
document.forms["loginForm"].elements["password"].focus();
</script>

33
app/views/wiki/new.rhtml Normal file
View File

@ -0,0 +1,33 @@
<%
@title = "Creating #{WikiWords.separate(@page_name)}"
@content_width = 720
@hide_navigation = true
%>
<div id="MarkupHelp">
<%= render("#{@web.markup}_help") %>
<%= render 'wiki_words_help' %>
</div>
<div id="editForm">
<%= form_tag({ :action => 'save', :web => @web.address, :id => @page_name },
{ 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName();', 'accept-charset' => 'utf-8' }) %>
<textarea name="content" id="content"><%= h(@flash[:content] || '') %></textarea>
<div id="editFormButtons">
<input type="submit" value="Submit" accesskey="s"/> as
<%= text_field_tag :author, @author,
:onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;",
:onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %>
</div>
<%= end_form_tag %>
</div>
<script type="text/javascript">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
document.forms["editForm"].elements["content"].focus();
</script>

51
app/views/wiki/page.rhtml Normal file
View File

@ -0,0 +1,51 @@
<%
@title = @page.plain_name
@title += ' (changes)' if @show_diff
@show_footer = true
%>
<div id="revision">
<% if @show_diff %>
<p style="background: #eee; padding: 3px; border: 1px solid silver">
<small>
Showing changes from revision #<%= @page.revisions.size - 1 %> to #<%= @page.revisions.size %>:
<ins class="diffins">Added</ins> | <del class="diffdel">Removed</del>
</small>
</p>
<%= @renderer.display_diff %>
<% else %>
<%= @renderer.display_content %>
<% end %>
</div>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= format_date(@page.revised_at) %>
by <%= author_link(@page) %>
<%= "(#{@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">
<%= navigation_menu_for_page.join(' | ') %>
<small>
| Views:
<%= link_to('Print',
{ :web => @web.address, :action => 'print', :id => @page.name },
{ :accesskey => 'p', :name => 'view_print' }) %>
<% if defined? RedClothForTex and RedClothForTex.available? and @web.markup == :textile %>
|
<%= link_to 'TeX', {:web => @web.address, :action => 'tex', :id => @page.name},
{:name => 'view_tex'} %>
|
<%= link_to 'PDF', {:web => @web.address, :action => 'pdf', :id => @page.name},
{:name => 'view_pdf'} %>
<% end %>
</small>
<%= render :partial => 'inbound_links' %>
</div>

View File

@ -0,0 +1,14 @@
<%
@title = @page.plain_name
@hide_navigation = true
@style_additions = ".newWikiWord { background-color: white; font-style: italic; }"
@inline_style = true
%>
<%= @renderer.display_content_for_export %>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= format_date(@page.revised_at) %>
by
<%= author_link(@page, { :mode => (@link_mode || :show) }) %>
</div>

View File

@ -0,0 +1,9 @@
<%
@title = @page.plain_name
@hide_navigation = false
@style_additions = ".newWikiWord { background-color: white; font-style: italic; }"
@inline_style = true
@show_footer = true
%>
<%= @renderer.display_published %>

View File

@ -0,0 +1,19 @@
<% @title = "Recently Revised" %>
<%= categories_menu %>
<% @pages_by_day.keys.sort.reverse.each do |day| %>
<h3><%= format_date(day, include_time = false) %></h3>
<ul>
<% for page in @pages_by_day[day] %>
<li>
<%= link_to_existing_page page %>
<div class="byline" style="margin-bottom: 0px">
by <%= link_to_page(page.author) %>
at <%= format_date(page.revised_at) %>
<%= "from #{page.author.ip}" if page.author.respond_to?(:ip) %>
</div>
</li>
<% end %>
</ul>
<% end %>

View File

@ -0,0 +1,28 @@
<%
@title = "#{@page.plain_name} (Rev ##{@revision_number}#{@show_diff ? ', changes' : ''})"
%>
<div id="revision">
<% if @show_diff %>
<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>
<%= @renderer.display_diff %>
<% else %>
<%= @renderer.display_content %>
<% end %>
</div>
<div class="byline">
<%= "Revision from #{format_date(@revision.revised_at)} by" %>
<%= link_to_page @revision.author %>
</div>
<div class="navigation">
<%= navigation_menu_for_revision.join(' | ') %>
<%= render :partial => 'inbound_links' %>
</div>

View File

@ -0,0 +1,39 @@
<%
@title = "Rollback to #{@page.plain_name} Rev ##{@revision_number}"
@content_width = 720
@hide_navigation = true
%>
<%= "<p style='color:red'>Please correct the error that caused this error in rendering:<br/><small>#{@params["msg"]}</small></p>" if @params["msg"] %>
<div id="MarkupHelp">
<%= render("#{@web.markup}_help") %>
<%= render 'wiki_words_help' %>
</div>
<div id="editForm">
<%= form_tag({:web => @web.address, :action => 'save', :id => @page.name},
{ :id => 'editForm', :method => 'post', :onSubmit => 'cleanAuthorName();',
'accept-charset' => 'utf-8' }) %>
<textarea name="content" id="content"><%= @revision.content %></textarea>
<div id="editFormButtons">
<input type="submit" value="Update" accesskey="u" /> as
<input type="text" name="author" id="authorName" value="<%= @author %>"
onClick="this.value == 'AnonymousCoward' ? this.value = '' : true" />
|
<span>
<%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name},
{:accesskey => 'c'}) %>
<small>(unlocks page)</small>
</span>
</div>
<%= end_form_tag %>
</div>
<script type="text/javascript">
function cleanAuthorName() {
if (document.getElementById('authorName').value == "") {
document.getElementById('authorName').value = 'AnonymousCoward';
}
}
</script>

View File

@ -0,0 +1,21 @@
xml.rss('version' => '2.0') do
xml.channel do
xml.title(@web.name)
xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => 'HomePage'))
xml.description('An Instiki wiki')
xml.language('en-us')
xml.ttl('40')
for page in @pages_by_revision
xml.item do
xml.title(page.plain_name)
unless @hide_description
xml.description(rendered_content(page))
end
xml.pubDate(page.revised_at.getgm.strftime('%a, %d %b %Y %H:%M:%S Z'))
xml.guid(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name))
xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name))
end
end
end
end

View File

@ -0,0 +1,38 @@
<% @title = "Search results for \"#{h @params["query"]}\"" %>
<% unless @title_results.empty? %>
<h2><%= @title_results.length %> page(s) containing search string in the page name:</h2>
<ul>
<% for page in @title_results %>
<li>
<%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %>
</li>
<% end %>
</ul>
<% end %>
<% unless @results.empty? %>
<h2> <%= @results.length %> page(s) containing search string in the page text:</h2>
<ul>
<% for page in @results %>
<li>
<%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %>
</li>
<% end %>
</ul>
<% end %>
<% if (@results + @title_results).empty? %>
<h2>No pages contain "<%= h @params["query"] %>" </h2>
<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 Ruby regular
expression. That's actually what Instiki uses, so go right ahead and flex your
"[a-z]*Leet?RegExpSkill(s|z)"
</p>
<% end %>

23
app/views/wiki/tex.rhtml Normal 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[utf8x]{inputenc}
\input epsf
%-------------------------------------------------------------------
\begin{document}
\sloppy
%-------------------------------------------------------------------
\section*{<%= @page.name %>}
<%= @tex_content %>
\end{document}

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}

View File

@ -0,0 +1,25 @@
<% @title = "Wiki webs" %>
<br/>
<% @webs.each do |web| %>
<% if web.password %> <div class="web_protected">
<% else %> <div class="web_normal"> <% end %>
<span>
<%= link_to_page 'HomePage', web, web.name, :mode => 'show' %>
<% if web.published? %>
(<%= link_to_page 'HomePage', web, 'published version', :mode => 'publish' %>)
<% end %>
<div class="byline" style="margin-bottom: 0px">
<%= web.pages.length %> page<% if web.pages.length != 1 %>s<% end %> by <%= web.authors.length %> author<% if web.authors.length != 1 %>s<% end %>
- Last Update: <%= web.last_page.nil? ? format_date(web.created_at) : format_date(web.last_page.revised_at) %><br/>
<% if ! web.last_page.nil? %>
Last Document: <%= link_to_page(web.last_page.name,web) %>
<%= web.last_page.revisions? ? "Revised" : "Created" %> by <%= author_link(web.last_page) %> (<%= web.last_page.current_revision.ip %>)
<% end %>
</div>
</span>
</div><br>
<% end %>
</ul>

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>

17
config/boot.rb Normal file
View File

@ -0,0 +1,17 @@
unless defined?(RAILS_ROOT)
root_path = File.join(File.dirname(__FILE__), '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath.to_s
end
RAILS_ROOT = root_path
end
if File.directory?("#{RAILS_ROOT}/vendor/rails")
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
else
require 'rubygems'
require 'initializer'
end
Rails::Initializer.run(:set_load_path)

105
config/database.yml Normal file
View File

@ -0,0 +1,105 @@
# "Out of the box", Instiki stores it's data in sqlite3 database. Other options are listed below.
development:
adapter: sqlite3
database: db/development.db.sqlite3
test:
adapter: sqlite3
database: db/test.db.sqlite3
production:
adapter: sqlite3
database: db/production.db.sqlite3
# MySQL (default setup). Versions 4.1 and 5.0 are recommended.
#
# Install the MySQL driver:
# gem install mysql
# On MacOS X:
# gem install mysql -- --include=/usr/local/lib
# On Windows:
# There is no gem for Windows. Install mysql.so from RubyForApache.
# http://rubyforge.org/projects/rubyforapache
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
# Get the fast C bindings:
# gem install mysql
# (on OS X: gem install mysql -- --include=/usr/local/lib)
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
mysql_example:
adapter: mysql
database: instiki_development
username: root
password:
socket: /path/to/your/mysql.sock
# Connect on a TCP socket. If omitted, the adapter will connect on the
# domain socket given by socket instead.
#host: localhost
#port: 3306
# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
mysql_example:
adapter: mysql
database: instiki_test
username: root
password:
socket: /path/to/your/mysql.sock
# PostgreSQL versions 7.4 - 8.2
#
# Get the C bindings:
# gem install postgres
# or use the pure-Ruby bindings (the only know way on Windows):
# gem install postgres-pr
postgresql_example:
adapter: postgresql
database: instiki_development
username: instiki
password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration.
#host: remote-database
#port: 5432
# Schema search path. The server defaults to $user,public
#schema_search_path: myapp,sharedapp,public
# Character set encoding. The server defaults to sql_ascii.
#encoding: UTF8
# Minimum log levels, in increasing order:
# debug5, debug4, debug3, debug2, debug1,
# info, notice, warning, error, log, fatal, or panic
# The server defaults to notice.
#min_messages: warning
# SQLite version 2.x
# gem install sqlite-ruby
sqlite_example:
adapter: sqlite
database: db/development.sqlite2
# SQLite version 3.x
# gem install sqlite3-ruby
sqlite3_example:
adapter: sqlite3
database: db/development.sqlite3
# In-memory SQLite 3 database. Useful for tests.
sqlite3_in_memory_example:
adapter: sqlite3
database: ":memory:"

29
config/environment.rb Normal file
View File

@ -0,0 +1,29 @@
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
config.frameworks -= [ :action_web_service, :action_mailer ]
# Use the database for sessions instead of the file system
# (create the session table with 'rake create_sessions_table')
config.action_controller.session_store = :active_record_store
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
#config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
# Activate observers that should always be running
config.active_record.observers = :page_observer
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
config.active_record.schema_format = :ruby
config.load_paths << "#{RAILS_ROOT}/vendor/plugins/sqlite3-ruby"
end
# Instiki-specific configuration below
require_dependency 'instiki_errors'
require 'jcode'

View File

@ -0,0 +1,17 @@
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Enable the breakpoint server that script/breakpointer connects to
config.breakpoint_server = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,17 @@
# The production environment is meant for finished, "live" apps.
# Code is not reloaded between requests
config.cache_classes = true
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Enable serving of images, stylesheets, and javascripts from an asset server
# config.action_controller.asset_host = "http://assets.example.com"
# Disable delivery errors if you bad email addresses should just be ignored
# config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,23 @@
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Tell ActionMailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Overwrite the default settings for fixtures in tests. See Fixtures
# for more details about these settings.
# config.transactional_fixtures = true
# config.instantiated_fixtures = false
# config.pre_loaded_fixtures = false

38
config/routes.rb Normal file
View File

@ -0,0 +1,38 @@
# Create a route to DEFAULT_WEB, if such is specified; also register a generic route
def connect_to_web(map, generic_path, generic_routing_options)
if defined? DEFAULT_WEB
explicit_path = generic_path.gsub(/:web\/?/, '')
explicit_routing_options = generic_routing_options.merge(:web => DEFAULT_WEB)
map.connect(explicit_path, explicit_routing_options)
end
map.connect(generic_path, generic_routing_options)
end
ActionController::Routing::Routes.draw do |map|
map.connect 'create_system', :controller => 'admin', :action => 'create_system'
map.connect 'create_web', :controller => 'admin', :action => 'create_web'
map.connect 'remove_orphaned_pages', :controller => 'admin', :action => 'remove_orphaned_pages'
map.connect 'delete_web', :controller => 'admin', :action => 'delete_web'
map.connect 'web_list', :controller => 'wiki', :action => 'web_list'
connect_to_web map, ':web/edit_web', :controller => 'admin', :action => 'edit_web'
connect_to_web map, ':web/files/:id', :controller => 'file', :action => 'file'
connect_to_web map, ':web/import/:id', :controller => 'file', :action => 'import'
connect_to_web map, ':web/login', :controller => 'wiki', :action => 'login'
connect_to_web map, ':web/web_list', :controller => 'wiki', :action => 'web_list'
connect_to_web map, ':web/show/diff/:id', :controller => 'wiki', :action => 'show', :mode => 'diff'
connect_to_web map, ':web/revision/diff/:id', :controller => 'wiki', :action => 'revision', :mode => 'diff'
connect_to_web map, ':web/list', :controller => 'wiki', :action => 'list'
connect_to_web map, ':web/list/:category', :controller => 'wiki', :action => 'list'
connect_to_web map, ':web/recently_revised', :controller => 'wiki', :action => 'recently_revised'
connect_to_web map, ':web/recently_revised/:category', :controller => 'wiki', :action => 'recently_revised'
connect_to_web map, ':web/:action/:id', :controller => 'wiki'
connect_to_web map, ':web/:action', :controller => 'wiki'
connect_to_web map, ':web', :controller => 'wiki', :action => 'index'
if defined? DEFAULT_WEB
map.connect '', :controller => 'wiki', :web => DEFAULT_WEB, :action => 'index'
else
map.connect '', :controller => 'wiki', :action => 'index'
end
end

97
config/spam_patterns.txt Normal file
View File

@ -0,0 +1,97 @@
.*\[\/link\]
.*\[\/url\]
51wisdom
acupuncturealliance
acyclovir
Adipex
adultfriend
airline
allegra
ampicill
anafranil
atenolol
attacke\.ch
autocorp
awardspace
blogspot\.com
bravehost\.com
butalbital
buy cheap
buy computer
buy-online
calling-phone-cards
casino
celexa
cialis
computer-exchange\.com
Cool website!
debt\s*consolidation
diazepam
display:\s*none
domaindlx\.com
equity\s*loan
Erectol
Feel free to visit my page
fortunecity
fuck
funpic\.de
gambling
Good job man
gucci
guestbook
hamburger
hold-em
holdem
home\s*loan
hoodia
http://[A-Za-z0-9_\.]+\.cn
hydrocodone
I am really excited
I really like your site
igotfree
ketoconazole
lust cartoon
mijneigenweblog
Mortage
my homepage
myspace
naked
netfirms\.com
nice site
overflow:\s*auto
paxil
pbwiki\.com
penis
Phentermine
phpbbforfree
pochta\.ru
poker
porn
prohosting
protonix
rapidforum
replica
ringtone
rolex
serotonin
singtaotor
slot\s*machin
soma
super site
texas
thepussies
tits
Tramadol
versace
viagra
vuitton
websamba\.com
xanax
xoomer
xrumer
Your site is great!
zoloft
\.iwarp\.
\.tripod\.com
\[link\=
\[url\=

View File

@ -0,0 +1,56 @@
class Beta1Schema < ActiveRecord::Migration
def self.up
create_table "pages", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "web_id", :integer, :default => 0, :null => false
t.column "locked_by", :string, :limit => 60
t.column "name", :string, :limit => 60
t.column "locked_at", :datetime
end
create_table "revisions", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "revised_at", :datetime, :null => false
t.column "page_id", :integer, :default => 0, :null => false
t.column "content", :text, :default => "", :null => false
t.column "author", :string, :limit => 60
t.column "ip", :string, :limit => 60
end
create_table "system", :force => true do |t|
t.column "password", :string, :limit => 60
end
create_table "webs", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "name", :string, :limit => 60, :default => "", :null => false
t.column "address", :string, :limit => 60, :default => "", :null => false
t.column "password", :string, :limit => 60
t.column "additional_style", :string
t.column "allow_uploads", :integer, :default => 1
t.column "published", :integer, :default => 0
t.column "count_pages", :integer, :default => 0
t.column "markup", :string, :limit => 50, :default => "textile"
t.column "color", :string, :limit => 6, :default => "008B26"
t.column "max_upload_size", :integer, :default => 100
t.column "safe_mode", :integer, :default => 0
t.column "brackets_only", :integer, :default => 0
end
create_table "wiki_references", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "page_id", :integer, :default => 0, :null => false
t.column "referenced_name", :string, :limit => 60, :default => "", :null => false
t.column "link_type", :string, :limit => 1, :default => "", :null => false
end
end
def self.down
raise 'Initial schema - cannot be further reverted'
end
end

View File

@ -0,0 +1,36 @@
class Beta2ChangesBulk < ActiveRecord::Migration
def self.up
add_index "revisions", "page_id"
add_index "revisions", "created_at"
add_index "revisions", "author"
create_table "sessions", :force => true do |t|
t.column "session_id", :string
t.column "data", :text
t.column "updated_at", :datetime
end
add_index "sessions", "session_id"
create_table "wiki_files", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "web_id", :integer, :null => false
t.column "file_name", :string, :null => false
t.column "description", :string, :null => false
end
add_index "wiki_references", "page_id"
add_index "wiki_references", "referenced_name"
end
def self.down
remove_index "wiki_references", "referenced_name"
remove_index "wiki_references", "page_id"
drop_table "wiki_files"
remove_index "sessions", "session_id"
drop_table "sessions"
remove_index "revisions", "author"
remove_index "revisions", "created_at"
remove_index "revisions", "page_id"
end
end

7
instiki Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
cd $(dirname $0)
export LD_LIBRARY_PATH=./lib/native/linux-x86:$LD_LIBRARY_PATH
ruby script/server

2
instiki.cmd Normal file
View File

@ -0,0 +1,2 @@
set PATH=.\lib\native\win32;%PATH%
ruby.exe script\server -e production

2
instiki.rb Executable file
View File

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

1127
lib/bluecloth_tweaked.rb Normal file

File diff suppressed because it is too large Load Diff

33
lib/chunks/category.rb Normal file
View File

@ -0,0 +1,33 @@
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
CATEGORY_PATTERN = /^(:)?category\s*:(.*)$/i
def self.pattern() CATEGORY_PATTERN end
attr_reader :hidden, :list
def initialize(match_data, content)
super(match_data, content)
@hidden = match_data[1]
@list = match_data[2].split(',').map { |c| c.strip }
@unmask_text = ''
if @hidden
@unmask_text = ''
else
category_urls = @list.map { |category| url(category) }.join(', ')
@unmask_text = '<div class="property"> category: ' + category_urls + '</div>'
end
end
# TODO move presentation of page metadata to controller/view
def url(category)
%{<a class="category_link" href="../list/?category=#{category}">#{category}</a>}
end
end

79
lib/chunks/chunk.rb Normal file
View File

@ -0,0 +1,79 @@
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
# automatically construct the array of derivatives of Chunk::Abstract
@derivatives = []
class << self
attr_reader :derivatives
end
def self::inherited( klass )
Abstract::derivatives << klass
end
# the class name part of the mask strings
def self.mask_string
self.to_s.delete(':').downcase
end
# a regexp that matches all chunk_types masks
def Abstract::mask_re(chunk_types)
chunk_classes = chunk_types.map{|klass| klass.mask_string}.join("|")
/chunk(-?\d+)(#{chunk_classes})chunk/
end
attr_reader :text, :unmask_text, :unmask_mode
def initialize(match_data, content)
@text = match_data[0]
@content = content
@unmask_mode = :normal
end
# Find all the chunks of the given type in content
# Each time the pattern is matched, create a new
# chunk for it, and replace the occurance of the chunk
# in this content with its mask.
def self.apply_to(content)
content.gsub!( self.pattern ) do |match|
new_chunk = self.new($~, content)
content.add_chunk(new_chunk)
new_chunk.mask
end
end
# should contain only [a-z0-9]
def mask
@mask ||= "chunk#{self.object_id}#{self.class.mask_string}chunk"
end
def unmask
@content.sub!(mask, @unmask_text)
end
def rendered?
@unmask_mode == :normal
end
def escaped?
@unmask_mode == :escape
end
def revert
@content.sub!(mask, @text)
# unregister
@content.delete_chunk(self)
end
end
end

62
lib/chunks/engines.rb Normal file
View File

@ -0,0 +1,62 @@
$: << File.dirname(__FILE__) + "../../lib"
require_dependency 'chunks/chunk'
# The markup engines are Chunks that call the one of RedCloth
# or RDoc to convert text. This markup occurs when the chunk is required
# to mask itself.
module Engines
class AbstractEngine < Chunk::Abstract
# Create a new chunk for the whole content and replace it with its mask.
def self.apply_to(content)
new_chunk = self.new(content)
content.replace(new_chunk.mask)
end
private
# Never create engines by constructor - use apply_to instead
def initialize(content)
@content = content
end
end
class Textile < AbstractEngine
def mask
require_dependency 'redcloth'
redcloth = RedCloth.new(@content, [:hard_breaks] + @content.options[:engine_opts])
redcloth.filter_html = false
redcloth.no_span_caps = false
redcloth.to_html(:textile)
end
end
class Markdown < AbstractEngine
def mask
require_dependency 'bluecloth_tweaked'
BlueCloth.new(@content, @content.options[:engine_opts]).to_html
end
end
class Mixed < AbstractEngine
def mask
require_dependency 'redcloth'
redcloth = RedCloth.new(@content, @content.options[:engine_opts])
redcloth.filter_html = false
redcloth.no_span_caps = false
redcloth.to_html
end
end
class RDoc < AbstractEngine
def mask
require_dependency 'rdocsupport'
RDocSupport::RDocFormatter.new(@content).to_html
end
end
MAP = { :textile => Textile, :markdown => Markdown, :mixed => Mixed, :rdoc => RDoc }
MAP.default = Textile
end

49
lib/chunks/include.rb Normal file
View File

@ -0,0 +1,49 @@
require 'chunks/wiki'
# Includes the contents of another page for rendering.
# The include command looks like this: "[[!include PageName]]".
# It is a WikiReference 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::WikiReference
INCLUDE_PATTERN = /\[\[!include\s+(.*?)\]\]\s*/i
def self.pattern() INCLUDE_PATTERN end
def initialize(match_data, content)
super
@page_name = match_data[1].strip
rendering_mode = content.options[:mode] || :show
@unmask_text = get_unmask_text_avoiding_recursion_loops(rendering_mode)
end
private
def get_unmask_text_avoiding_recursion_loops(rendering_mode)
if refpage
# TODO This way of instantiating a renderer is ugly.
renderer = PageRenderer.new(refpage.current_revision)
if renderer.wiki_includes.include?(@content.page_name)
# this will break the recursion
@content.delete_chunk(self)
return "<em>Recursive include detected; #{@page_name} --> #{@content.page_name} " +
"--> #{@page_name}</em>\n"
else
included_content =
case rendering_mode
when :show then renderer.display_content
when :publish then renderer.display_published
when :export then renderer.display_content_for_export
else raise "Unsupported rendering mode #{@mode.inspect}"
end
@content.merge_chunks(included_content)
return included_content.pre_rendered
end
else
return "<em>Could not include #{@page_name}</em>\n"
end
end
end

31
lib/chunks/literal.rb Normal file
View File

@ -0,0 +1,31 @@
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
class AbstractLiteral < Chunk::Abstract
def initialize(match_data, content)
super
@unmask_text = @text
end
end
# A literal chunk that protects 'code' and 'pre' tags from wiki rendering.
class Pre < AbstractLiteral
PRE_BLOCKS = "a|pre|code"
PRE_PATTERN = Regexp.new('<('+PRE_BLOCKS+')\b[^>]*?>.*?</\1>', Regexp::MULTILINE)
def self.pattern() PRE_PATTERN end
end
# A literal chunk that protects HTML tags from wiki rendering.
class Tags < AbstractLiteral
TAGS = "a|img|em|strong|div|span|table|td|th|ul|ol|li|dl|dt|dd"
TAGS_PATTERN = Regexp.new('<(?:'+TAGS+')[^>]*?>', Regexp::MULTILINE)
def self.pattern() TAGS_PATTERN end
end
end

28
lib/chunks/nowiki.rb Normal file
View File

@ -0,0 +1,28 @@
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
NOWIKI_PATTERN = Regexp.new('<nowiki>(.*?)</nowiki>', Regexp::MULTILINE)
def self.pattern() NOWIKI_PATTERN end
attr_reader :plain_text
def initialize(match_data, content)
super
@plain_text = @unmask_text = match_data[1]
end
end

18
lib/chunks/test.rb Normal 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

182
lib/chunks/uri.rb Normal file
View File

@ -0,0 +1,182 @@
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? URIChunk::INTERNET_URI_REGEXP
GENERIC = 'aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org'
COUNTRY = 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|' +
'bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cf|cd|cg|ch|ci|ck|cl|' +
'cm|cn|co|cr|cs|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|' +
'fj|fk|fm|fo|fr|fx|ga|gb|gd|ge|gf|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|' +
'hk|hm|hn|hr|ht|hu|id|ie|il|in|io|iq|ir|is|it|jm|jo|jp|ke|kg|kh|ki|km|kn|' +
'kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|' +
'mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nt|' +
'nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|' +
'sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|' +
'tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|' +
'ws|ye|yt|yu|za|zm|zr|zw'
# 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})+"
# unreserved_no_ending = alphanum | mark, but URI_ENDING [)!] excluded
UNRESERVED_NO_ENDING = "-_.~*'(#{ALNUM}"
# this ensures that query or fragment do not end with URI_ENDING
# and enable us to use a much simpler self.pattern Regexp
# uric_no_ending = reserved | unreserved_no_ending | escaped
URIC_NO_ENDING = "(?:[#{UNRESERVED_NO_ENDING}#{RESERVED}]|#{ESCAPED})"
# query = *uric
QUERY = "#{URIC_NO_ENDING}*"
# fragment = *uric
FRAGMENT = "#{URIC_NO_ENDING}*"
# DOMLABEL is defined in the ruby uri library, TLDS is defined above
INTERNET_HOSTNAME = "(?:#{DOMLABEL}\\.)+#{TLDS}"
# Correct a typo bug in ruby 1.8.x lib/uri/common.rb
PORT = '\\d*'
INTERNET_URI =
"(?:(#{SCHEME}):/{0,2})?" + # Optional scheme: (\1)
"(?:(#{USERINFO})@)?" + # Optional userinfo@ (\2)
"(#{INTERNET_HOSTNAME})" + # Mandatory hostname (\3)
"(?::(#{PORT}))?" + # Optional :port (\4)
"(#{ABS_PATH})?" + # Optional absolute path (\5)
"(?:\\?(#{QUERY}))?" + # Optional ?query (\6)
"(?:\\#(#{FRAGMENT}))?" + # Optional #fragment (\7)
'(?=\.?(?:\s|\)|\z))' # ends only with optional dot + space or ")"
# or end of the string
SUSPICIOUS_PRECEDING_CHARACTER = '(!|\"\:|\"|\\\'|\]\()?' # any of !, ":, ", ', ](
INTERNET_URI_REGEXP =
Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + INTERNET_URI, Regexp::EXTENDED, 'N')
end
def URIChunk.pattern
INTERNET_URI_REGEXP
end
attr_reader :user, :host, :port, :path, :query, :fragment, :link_text
def self.apply_to(content)
content.gsub!( self.pattern ) do |matched_text|
chunk = self.new($~, content)
if chunk.avoid_autolinking?
# do not substitute nor register the chunk
matched_text
else
content.add_chunk(chunk)
chunk.mask
end
end
end
def initialize(match_data, content)
super
@link_text = match_data[0]
@suspicious_preceding_character = match_data[1]
@original_scheme, @user, @host, @port, @path, @query, @fragment = match_data[2..-1]
treat_trailing_character
@unmask_text = "<a href=\"#{uri}\">#{link_text}</a>"
end
def avoid_autolinking?
not @suspicious_preceding_character.nil?
end
def treat_trailing_character
# If the last character matched by URI pattern is in ! or ), this may be part of the markup,
# not a URL. We should handle it as such. It is possible to do it by a regexp, but
# much easier to do programmatically
last_char = @link_text[-1..-1]
if last_char == ')' or last_char == '!'
@trailing_punctuation = last_char
@link_text.chop!
[@original_scheme, @user, @host, @port, @path, @query, @fragment].compact.last.chop!
else
@trailing_punctuation = nil
end
end
def scheme
@original_scheme or (@user ? 'mailto' : 'http')
end
def scheme_delimiter
scheme == 'mailto' ? ':' : '://'
end
def user_delimiter
'@' unless @user.nil?
end
def port_delimiter
':' unless @port.nil?
end
def query_delimiter
'?' unless @query.nil?
end
def uri
[scheme, scheme_delimiter, user, user_delimiter, host, port_delimiter, port, path,
query_delimiter, query].compact.join
end
end
# uri with mandatory scheme but less restrictive hostname, like
# http://localhost:2500/blah.html
class LocalURIChunk < URIChunk
unless defined? LocalURIChunk::LOCAL_URI_REGEXP
# hostname can be just a simple word like 'localhost'
ANY_HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
# The basic URI expression as a string
# Scheme and hostname are mandatory
LOCAL_URI =
"(?:(#{SCHEME})://)+" + # Mandatory scheme:// (\1)
"(?:(#{USERINFO})@)?" + # Optional userinfo@ (\2)
"(#{ANY_HOSTNAME})" + # Mandatory hostname (\3)
"(?::(#{PORT}))?" + # Optional :port (\4)
"(#{ABS_PATH})?" + # Optional absolute path (\5)
"(?:\\?(#{QUERY}))?" + # Optional ?query (\6)
"(?:\\#(#{FRAGMENT}))?" + # Optional #fragment (\7)
'(?=\.?(?:\s|\)|\z))' # ends only with optional dot + space or ")"
# or end of the string
LOCAL_URI_REGEXP = Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + LOCAL_URI, Regexp::EXTENDED, 'N')
end
def LocalURIChunk.pattern
LOCAL_URI_REGEXP
end
end

143
lib/chunks/wiki.rb Normal file
View File

@ -0,0 +1,143 @@
require 'wiki_words'
require 'chunks/chunk'
require 'chunks/wiki'
require 'cgi'
# Contains all the methods for finding and replacing wiki related links.
module WikiChunk
include Chunk
# A wiki reference is the top-level class for anything that refers to
# another wiki page.
class WikiReference < Chunk::Abstract
# Name of the referenced page
attr_reader :page_name
# the referenced page
def refpage
@content.web.page(@page_name)
end
end
# A wiki link is the top-level class for links that refers to
# another wiki page.
class WikiLink < WikiReference
attr_reader :link_text, :link_type
def initialize(match_data, content)
super
@link_type = :show
end
def self.apply_to(content)
content.gsub!( self.pattern ) do |matched_text|
chunk = self.new($~, content)
if chunk.textile_url?
# do not substitute
matched_text
else
content.add_chunk(chunk)
chunk.mask
end
end
end
def textile_url?
not @textile_link_suffix.nil?
end
# replace any sequence of whitespace characters with a single space
def normalize_whitespace(line)
line.gsub(/\s+/, ' ')
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
attr_reader :escaped_text
unless defined? WIKI_WORD
WIKI_WORD = Regexp.new('(":)?(\\\\)?(' + WikiWords::WIKI_WORD_PATTERN + ')\b', 0, "utf-8")
end
def self.pattern
WIKI_WORD
end
def initialize(match_data, content)
super
@textile_link_suffix, @escape, @page_name = match_data[1..3]
if @escape
@unmask_mode = :escape
@escaped_text = @page_name
else
@escaped_text = nil
end
@link_text = WikiWords.separate(@page_name)
@unmask_text = (@escaped_text || @content.page_link(@page_name, @link_text, @link_type))
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
unless defined? WIKI_LINK
WIKI_LINK = /(":)?\[\[\s*([^\]\s][^\]]+?)\s*\]\]/
LINK_TYPE_SEPARATION = Regexp.new('^(.+):((file)|(pic))$', 0, 'utf-8')
ALIAS_SEPARATION = Regexp.new('^(.+)\|(.+)$', 0, 'utf-8')
end
def self.pattern() WIKI_LINK end
def initialize(match_data, content)
super
@textile_link_suffix = match_data[1]
@link_text = @page_name = normalize_whitespace(match_data[2])
separate_link_type
separate_alias
@unmask_text = @content.page_link(@page_name, @link_text, @link_type)
end
private
# if link wihin the brackets has a form of [[filename:file]] or [[filename:pic]],
# this means a link to a picture or a file
def separate_link_type
link_type_match = LINK_TYPE_SEPARATION.match(@page_name)
if link_type_match
@link_text = @page_name = link_type_match[1]
@link_type = link_type_match[2..3].compact[0].to_sym
end
end
# link text may be different from page name. this will look like [[actual page|link text]]
def separate_alias
alias_match = ALIAS_SEPARATION.match(@page_name)
if alias_match
@page_name = normalize_whitespace(alias_match[1])
@link_text = alias_match[2]
end
# note that [[filename|link text:file]] is also supported
end
end
end

316
lib/diff.rb Normal file
View File

@ -0,0 +1,316 @@
module HTMLDiff
Match = Struct.new(:start_in_old, :start_in_new, :size)
class Match
def end_in_old
self.start_in_old + self.size
end
def end_in_new
self.start_in_new + self.size
end
end
Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
class DiffBuilder
def initialize(old_version, new_version)
@old_version, @new_version = old_version, new_version
@content = []
end
def build
split_inputs_to_words
index_new_words
operations.each { |op| perform_operation(op) }
return @content.join
end
def split_inputs_to_words
@old_words = convert_html_to_list_of_words(explode(@old_version))
@new_words = convert_html_to_list_of_words(explode(@new_version))
end
def index_new_words
@word_indices = Hash.new { |h, word| h[word] = [] }
@new_words.each_with_index { |word, i| @word_indices[word] << i }
end
def operations
position_in_old = position_in_new = 0
operations = []
matches = matching_blocks
# an empty match at the end forces the loop below to handle the unmatched tails
# I'm sure it can be done more gracefully, but not at 23:52
matches << Match.new(@old_words.length, @new_words.length, 0)
matches.each_with_index do |match, i|
match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
action_upto_match_positions =
case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
when [false, false]
:replace
when [true, false]
:insert
when [false, true]
:delete
else
# this happens if the first few words are same in both versions
:none
end
if action_upto_match_positions != :none
operation_upto_match_positions =
Operation.new(action_upto_match_positions,
position_in_old, match.start_in_old,
position_in_new, match.start_in_new)
operations << operation_upto_match_positions
end
if match.size != 0
match_operation = Operation.new(:equal,
match.start_in_old, match.end_in_old,
match.start_in_new, match.end_in_new)
operations << match_operation
end
position_in_old = match.end_in_old
position_in_new = match.end_in_new
end
operations
end
def matching_blocks
matching_blocks = []
recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
matching_blocks
end
def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
if match
if start_in_old < match.start_in_old and start_in_new < match.start_in_new
recursively_find_matching_blocks(
start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks)
end
matching_blocks << match
if match.end_in_old < end_in_old and match.end_in_new < end_in_new
recursively_find_matching_blocks(
match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
end
end
end
def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
best_match_in_old = start_in_old
best_match_in_new = start_in_new
best_match_size = 0
match_length_at = Hash.new { |h, index| h[index] = 0 }
start_in_old.upto(end_in_old - 1) do |index_in_old|
new_match_length_at = Hash.new { |h, index| h[index] = 0 }
@word_indices[@old_words[index_in_old]].each do |index_in_new|
next if index_in_new < start_in_new
break if index_in_new >= end_in_new
new_match_length = match_length_at[index_in_new - 1] + 1
new_match_length_at[index_in_new] = new_match_length
if new_match_length > best_match_size
best_match_in_old = index_in_old - new_match_length + 1
best_match_in_new = index_in_new - new_match_length + 1
best_match_size = new_match_length
end
end
match_length_at = new_match_length_at
end
# best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
# best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
# best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
# best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
end
def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
while match_in_old > start_in_old and
match_in_new > start_in_new and
@old_words[match_in_old - 1] == @new_words[match_in_new - 1]
match_in_old -= 1
match_in_new -= 1
match_size += 1
end
[match_in_old, match_in_new, match_size]
end
def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
while match_in_old + match_size < end_in_old and
match_in_new + match_size < end_in_new and
@old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
match_size += 1
end
[match_in_old, match_in_new, match_size]
end
VALID_METHODS = [:replace, :insert, :delete, :equal]
def perform_operation(operation)
@operation = operation
self.send operation.action, operation
end
def replace(operation)
delete(operation, 'diffmod')
insert(operation, 'diffmod')
end
def insert(operation, tagclass = 'diffins')
insert_tag('ins', tagclass, @new_words[operation.start_in_new...operation.end_in_new])
end
def delete(operation, tagclass = 'diffdel')
insert_tag('del', tagclass, @old_words[operation.start_in_old...operation.end_in_old])
end
def equal(operation)
# no tags to insert, simply copy the matching words from one of the versions
@content += @new_words[operation.start_in_new...operation.end_in_new]
end
def opening_tag?(item)
item =~ %r!^\s*<[^>]+>\s*$!
end
def closing_tag?(item)
item =~ %r!^\s*</[^>]+>\s*$!
end
def tag?(item)
opening_tag?(item) or closing_tag?(item)
end
def extract_consecutive_words(words, &condition)
index_of_first_tag = nil
words.each_with_index do |word, i|
if !condition.call(word)
index_of_first_tag = i
break
end
end
if index_of_first_tag
return words.slice!(0...index_of_first_tag)
else
return words.slice!(0..words.length)
end
end
# This method encloses words within a specified tag (ins or del), and adds this into @content,
# with a twist: if there are words contain tags, it actually creates multiple ins or del,
# so that they don't include any ins or del. This handles cases like
# old: '<p>a</p>'
# new: '<p>ab</p><p>c</b>'
# diff result: '<p>a<ins>b</ins></p><p><ins>c</ins></p>'
# this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or
# del tags), but handles correctly more cases than the earlier version.
#
# P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day.
def insert_tag(tagname, cssclass, words)
loop do
break if words.empty?
non_tags = extract_consecutive_words(words) { |word| not tag?(word) }
@content << wrap_text(non_tags.join, tagname, cssclass) unless non_tags.empty?
break if words.empty?
@content += extract_consecutive_words(words) { |word| tag?(word) }
end
end
def wrap_text(text, tagname, cssclass)
%(<#{tagname} class="#{cssclass}">#{text}</#{tagname}>)
end
def explode(sequence)
sequence.is_a?(String) ? sequence.split(//) : sequence
end
def end_of_tag?(char)
char == '>'
end
def start_of_tag?(char)
char == '<'
end
def whitespace?(char)
char =~ /\s/
end
def convert_html_to_list_of_words(x, use_brackets = false)
mode = :char
current_word = ''
words = []
explode(x).each do |char|
case mode
when :tag
if end_of_tag? char
current_word << (use_brackets ? ']' : '>')
words << current_word
current_word = ''
if whitespace?(char)
mode = :whitespace
else
mode = :char
end
else
current_word << char
end
when :char
if start_of_tag? char
words << current_word unless current_word.empty?
current_word = (use_brackets ? '[' : '<')
mode = :tag
elsif /\s/.match char
words << current_word unless current_word.empty?
current_word = char
mode = :whitespace
else
current_word << char
end
when :whitespace
if start_of_tag? char
words << current_word unless current_word.empty?
current_word = (use_brackets ? '[' : '<')
mode = :tag
elsif /\s/.match char
current_word << char
else
words << current_word unless current_word.empty?
current_word = char
mode = :char
end
else
raise "Unknown mode #{mode.inspect}"
end
end
words << current_word unless current_word.empty?
words
end
end # of class Diff Builder
def diff(a, b)
DiffBuilder.new(a, b).build
end
end

15
lib/instiki_errors.rb Normal file
View File

@ -0,0 +1,15 @@
# Model methods that want to rollback transactions gracefully
# (i.e, returning the user back to the form from which the request was posted)
# should raise Instiki::ValidationError.
#
# E.g. if a model object does
# raise "Foo: '#{foo}' is not equal to Bar: '#{bar}'" if (foo != bar)
#
# then the operation is not committed; Rails returns the user to the page
# where s/he was entering foo and bar, and the error message will be displayed
# on the page
module Instiki
class ValidationError < StandardError
end
end

Binary file not shown.

134
lib/page_renderer.rb Normal file
View File

@ -0,0 +1,134 @@
require 'diff'
# Temporary class containing all rendering stuff from a Revision
# I want to shift all rendering loguc to the controller eventually
class PageRenderer
include HTMLDiff
def self.setup_url_generator(url_generator)
@@url_generator = url_generator
end
def self.teardown_url_generator
@@url_generator = nil
end
attr_reader :revision
def initialize(revision = nil)
self.revision = revision
end
def revision=(r)
@revision = r
@display_content = @display_published = @wiki_words_cache = @wiki_includes_cache =
@wiki_references_cache = nil
end
def display_content(update_references = false)
@display_content ||= render(:update_references => update_references)
end
def display_content_for_export
render :mode => :export
end
def display_published
@display_published ||= render(:mode => :publish)
end
def display_diff
previous_revision = @revision.page.previous_revision(@revision)
if previous_revision
rendered_previous_revision = WikiContent.new(previous_revision, @@url_generator).render!
diff(rendered_previous_revision, display_content)
else
display_content
end
end
# Returns an array of all the WikiIncludes present in the content of this revision.
def wiki_includes
unless @wiki_includes_cache
chunks = display_content.find_chunks(Include)
@wiki_includes_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_includes_cache
end
# Returns an array of all the WikiReferences present in the content of this revision.
def wiki_references
unless @wiki_references_cache
chunks = display_content.find_chunks(WikiChunk::WikiReference)
@wiki_references_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_references_cache
end
# Returns an array of all the WikiWords present in the content of this revision.
def wiki_words
@wiki_words_cache ||= find_wiki_words(display_content)
end
def find_wiki_words(rendering_result)
wiki_links = rendering_result.find_chunks(WikiChunk::WikiLink)
# Exclude backslash-escaped wiki words, such as \WikiWord, as well as links to files
# and pictures, such as [[foo.txt:file]] or [[foo.jpg:pic]]
wiki_links.delete_if { |link| link.escaped? or [:pic, :file].include?(link.link_type) }
# convert to the list of unique page names
wiki_links.map { |link| ( link.page_name ) }.uniq
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| @revision.page.web.page(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
private
def render(options = {})
rendering_result = WikiContent.new(@revision, @@url_generator, options).render!
update_references(rendering_result) if options[:update_references]
rendering_result
end
def update_references(rendering_result)
WikiReference.delete_all ['page_id = ?', @revision.page_id]
references = @revision.page.wiki_references
wiki_words = find_wiki_words(rendering_result)
# TODO it may be desirable to save links to files and pictures as WikiReference objects
# present version doesn't do it
wiki_words.each do |referenced_name|
# Links to self are always considered linked
if referenced_name == @revision.page.name
link_type = WikiReference::LINKED_PAGE
else
link_type = WikiReference.link_type(@revision.page.web, referenced_name)
end
references.create :referenced_name => referenced_name, :link_type => link_type
end
include_chunks = rendering_result.find_chunks(Include)
includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
includes.each do |included_page_name|
references.create :referenced_name => included_page_name,
:link_type => WikiReference::INCLUDED_PAGE
end
categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten
categories.each do |category|
references.create :referenced_name => category, :link_type => WikiReference::CATEGORY
end
end
end

152
lib/rdocsupport.rb Normal 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

1130
lib/redcloth.rb Normal file

File diff suppressed because it is too large Load Diff

736
lib/redcloth_for_tex.rb Normal file
View File

@ -0,0 +1,736 @@
# This is RedCloth (http://www.whytheluckystiff.net/ruby/redcloth/)
# converted by David Heinemeier Hansson to emit Tex
class String
# Flexible HTML escaping
def texesc!( mode )
gsub!( '&', '\\\\&' )
gsub!( '%', '\%' )
gsub!( '$', '\$' )
gsub!( '~', '$\sim$' )
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.gsub!(/(\\)(.)/, '\2')
url = check_refs( url )
atts = pba( atts )
atts << " title=\"#{ title }\"" if title
atts = shelve( atts ) if atts
"#{ pre }\\textit{#{ text }} \\footnote{\\texttt{\\textless #{ url }#{ slash }" +
"\\textgreater}#{ 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

121
lib/url_generator.rb Normal file
View File

@ -0,0 +1,121 @@
class AbstractUrlGenerator
def initialize(controller)
raise 'Controller cannot be nil' if controller.nil?
@controller = controller
end
# Create a link for the given page (or file) name and link text based
# on the render mode in options and whether the page (file) exists
# in the web.
def make_link(name, web, text = nil, options = {})
text = CGI.escapeHTML(text || WikiWords.separate(name))
mode = (options[:mode] || :show).to_sym
link_type = (options[:link_type] || :show).to_sym
if (link_type == :show)
known_page = web.has_page?(name)
else
known_page = web.has_file?(name)
end
case link_type
when :show
page_link(mode, name, text, web.address, known_page)
when :file
file_link(mode, name, text, web.address, known_page)
when :pic
pic_link(mode, name, text, web.address, known_page)
else
raise "Unknown link type: #{link_type}"
end
end
end
class UrlGenerator < AbstractUrlGenerator
private
def file_link(mode, name, text, web_address, known_file)
case mode
when :export
if known_file
%{<a class="existingWikiWord" href="#{CGI.escape(name)}.html">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
when :publish
if known_file
href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file',
:id => name
if known_file
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
def page_link(mode, name, text, web_address, known_page)
case mode
when :export
if known_page
%{<a class="existingWikiWord" href="#{CGI.escape(name)}.html">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
when :publish
if known_page
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'published',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
if known_page
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'show',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'new',
:id => name
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
def pic_link(mode, name, text, web_address, known_pic)
case mode
when :export
if known_pic
%{<img alt="#{text}" src="#{CGI.escape(name)}" />}
else
%{<img alt="#{text}" src="no image" />}
end
when :publish
if known_pic
%{<img alt="#{text}" src="#{CGI.escape(name)}" />}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file',
:id => name
if known_pic
%{<img alt="#{text}" src="#{href}" />}
else
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
end

202
lib/wiki_content.rb Normal file
View File

@ -0,0 +1,202 @@
require 'cgi'
require_dependency 'chunks/engines'
require_dependency 'chunks/category'
require_dependency 'chunks/include'
require_dependency 'chunks/wiki'
require_dependency 'chunks/literal'
require_dependency 'chunks/uri'
require_dependency '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 (show),
# publishing (:publish) or export (:export)?
module ChunkManager
attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id
ACTIVE_CHUNKS = [ NoWiki, Category, WikiChunk::Link, URIChunk, LocalURIChunk,
WikiChunk::Word ]
HIDE_CHUNKS = [ Literal::Pre, Literal::Tags ]
MASK_RE = {
ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS),
HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS)
}
def init_chunk_manager
@chunks_by_type = Hash.new
Chunk::Abstract::derivatives.each{|chunk_type|
@chunks_by_type[chunk_type] = Array.new
}
@chunks_by_id = Hash.new
@chunks = []
@chunk_id = 0
end
def add_chunk(c)
@chunks_by_type[c.class] << c
@chunks_by_id[c.object_id] = c
@chunks << c
@chunk_id += 1
end
def delete_chunk(c)
@chunks_by_type[c.class].delete(c)
@chunks_by_id.delete(c.object_id)
@chunks.delete(c)
end
def merge_chunks(other)
other.chunks.each{|c| add_chunk(c)}
end
def scan_chunkid(text)
text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] }
end
def find_chunks(chunk_type)
@chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? }
end
end
# A simplified version of WikiContent. Useful to avoid recursion problems in
# WikiContent.new
class WikiContentStub < String
attr_reader :options
include ChunkManager
def initialize(content, options)
super(content)
@options = options
init_chunk_manager
end
# Detects the mask strings contained in the text of chunks of type chunk_types
# and yields the corresponding chunk ids
# example: content = "chunk123categorychunk <pre>chunk456categorychunk</pre>"
# inside_chunks(Literal::Pre) ==> yield 456
def inside_chunks(chunk_types)
chunk_types.each{|chunk_type| chunk_type.apply_to(self) }
chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk|
scan_chunkid(hide_chunk.text){|id| yield id }
}
}
end
end
class WikiContent < String
include ChunkManager
DEFAULT_OPTS = {
:active_chunks => ACTIVE_CHUNKS,
:engine => Engines::Textile,
:engine_opts => [],
:mode => :show
}.freeze
attr_reader :web, :options, :revision, :not_rendered, :pre_rendered
# Create a new wiki content string from the given one.
# The options are explained at the top of this file.
def initialize(revision, url_generator, options = {})
@revision = revision
@url_generator = url_generator
@web = @revision.page.web
@options = DEFAULT_OPTS.dup.merge(options)
@options[:engine] = Engines::MAP[@web.markup]
@options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode?
@options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only?
@not_rendered = @pre_rendered = nil
super(@revision.content)
init_chunk_manager
build_chunks
@not_rendered = String.new(self)
end
# Call @web.page_link using current options.
def page_link(name, text, link_type)
@options[:link_type] = (link_type || :show)
@url_generator.make_link(name, @web, text, @options)
end
def build_chunks
# create and mask Includes and "active_chunks" chunks
Include.apply_to(self)
@options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self)}
# Handle hiding contexts like "pre" and "code" etc..
# The markup (textile, rdoc etc) can produce such contexts with its own syntax.
# To reveal them, we work on a copy of the content.
# The copy is rendered and used to detect the chunks that are inside protecting context
# These chunks are reverted on the original content string.
copy = WikiContentStub.new(self, @options)
@options[:engine].apply_to(copy)
copy.inside_chunks(HIDE_CHUNKS) do |id|
@chunks_by_id[id.to_i].revert
end
end
def pre_render!
unless @pre_rendered
@chunks_by_type[Include].each{|chunk| chunk.unmask }
@pre_rendered = String.new(self)
end
@pre_rendered
end
def render!
pre_render!
@options[:engine].apply_to(self)
# unmask in one go. $~[1] is the chunk id
gsub!(MASK_RE[ACTIVE_CHUNKS]) do
chunk = @chunks_by_id[$~[1].to_i]
if chunk.nil?
# if we match a chunkmask that existed in the original content string
# just keep it as it is
$~[0]
else
chunk.unmask_text
end
end
self
end
def page_name
@revision.page.name
end
end

23
lib/wiki_words.rb Normal file
View File

@ -0,0 +1,23 @@
# Contains all the methods for finding and replacing wiki words
module WikiWords
# In order of appearance: Latin, greek, cyrillian, armenian
I18N_HIGHER_CASE_LETTERS =
"À<EFBFBD>?ÂÃÄÅĀĄĂÆÇĆČĈĊĎ<C48A>?ÈÉÊËĒĘĚĔĖĜĞĠĢĤĦÌ<C4A6><>?ĪĨĬĮİIJĴĶ<C4B4>?ĽĹĻĿÑŃŇŅŊÒÓÔÕÖØŌ<C398>?ŎŒŔŘŖŚŠŞŜȘŤŢŦȚÙÚÛÜŪŮŰŬŨŲŴ<C5B2>?ŶŸŹŽŻ" +
"ΑΒΓΔΕΖΗΘΙΚΛΜ<EFBFBD>?ΞΟΠΡΣΤΥΦΧΨΩ" +
"ΆΈΉΊΌΎ<EFBFBD>?ѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎ<D28C>?ҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾ<D2BC>?ӃӅӇӉӋ<D389>?<3F>?ӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӸЖ" +
"ԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀ<EFBFBD>?ՂՃՄՅՆՇՈՉՊՋՌ<D58B>?<3F>?<3F>?ՑՒՓՔՕՖ"
I18N_LOWER_CASE_LETTERS =
"àáâãäå<EFBFBD>?ąăæçć<C3A7>?ĉċ<C489>?đèéêëēęěĕėƒ<C497>?ğġģĥħìíîïīĩĭįıijĵķĸłľĺļŀñńňņʼnŋòóôõöø<C3B6><>?œŕřŗśšş<C5A1>?șťţŧțùúûüūůűŭũųŵýÿŷžżźÞþßſ<C39F>" +
"άέήίΰαβγδεζηθικλμνξοπ<EFBFBD>στυφχψωϊϋό<CF8B><>?" +
"абвгдежзийклмнопр<EFBFBD>уфхцчшщъыь<D18B><>?<3F>?ёђѓєѕіїјљћќ<D19B>?ўџѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿ<D1BD><>?<3F>?ґғҕҗҙқ<D299>?ҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӀӂӄӆӈӊӌӎӑӓӕӗәӛ<D399>?ӟӡӣӥӧөӫӭӯӱӳӵӹ" +
"աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտր<EFBFBD>?ւփքօֆև"
WIKI_WORD_PATTERN = '[A-Z' + I18N_HIGHER_CASE_LETTERS + '][a-z' + I18N_LOWER_CASE_LETTERS + ']+[A-Z' + I18N_HIGHER_CASE_LETTERS + ']\w+'
CAMEL_CASED_WORD_BORDER = /([a-z#{I18N_LOWER_CASE_LETTERS}])([A-Z#{I18N_HIGHER_CASE_LETTERS}])/u
def self.separate(wiki_word)
wiki_word.gsub(CAMEL_CASED_WORD_BORDER, '\1 \2')
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,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>

40
public/.htaccess Normal file
View File

@ -0,0 +1,40 @@
# General Apache options
AddHandler fastcgi-script .fcgi
AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI
# If you don't want Rails to look in certain directories,
# use the following rewrite rules so that Apache won't rewrite certain requests
#
# Example:
# RewriteCond %{REQUEST_URI} ^/notrails.*
# RewriteRule .* - [L]
# Redirect all requests not available on the filesystem to Rails
# By default the cgi dispatcher is used which is very slow
#
# For better performance replace the dispatcher with the fastcgi one
#
# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On
# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# RewriteBase /myrailsapp
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead
#
# Example:
# ErrorDocument 500 /500.html
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

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