Checkout of Instiki Trunk 1/21/2007.
This commit is contained in:
commit
69b62b6f33
270
CHANGELOG
Executable file
270
CHANGELOG
Executable 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
113
README
Executable 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
|
94
app/controllers/admin_controller.rb
Normal file
94
app/controllers/admin_controller.rb
Normal 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
|
190
app/controllers/application.rb
Normal file
190
app/controllers/application.rb
Normal 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
|
23
app/controllers/cache_sweeping_helper.rb
Normal file
23
app/controllers/cache_sweeping_helper.rb
Normal 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
|
100
app/controllers/file_controller.rb
Normal file
100
app/controllers/file_controller.rb
Normal 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
|
29
app/controllers/revision_sweeper.rb
Normal file
29
app/controllers/revision_sweeper.rb
Normal 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
|
14
app/controllers/web_sweeper.rb
Normal file
14
app/controllers/web_sweeper.rb
Normal 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
|
429
app/controllers/wiki_controller.rb
Normal file
429
app/controllers/wiki_controller.rb
Normal 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
|
93
app/helpers/application_helper.rb
Normal file
93
app/helpers/application_helper.rb
Normal 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
|
89
app/helpers/wiki_helper.rb
Normal file
89
app/helpers/wiki_helper.rb
Normal 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
18
app/models/author.rb
Normal 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
121
app/models/page.rb
Normal 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
|
15
app/models/page_observer.rb
Normal file
15
app/models/page_observer.rb
Normal 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
92
app/models/page_set.rb
Normal 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
4
app/models/revision.rb
Normal 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
4
app/models/system.rb
Normal 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
147
app/models/web.rb
Normal 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
92
app/models/wiki.rb
Normal 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
64
app/models/wiki_file.rb
Normal 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
|
82
app/models/wiki_reference.rb
Normal file
82
app/models/wiki_reference.rb
Normal 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
|
86
app/views/admin/create_system.rhtml
Normal file
86
app/views/admin/create_system.rhtml
Normal 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" />
|
||||
|
||||
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" />
|
||||
|
||||
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>
|
72
app/views/admin/create_web.rhtml
Normal file
72
app/views/admin/create_web.rhtml
Normal 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();" />
|
||||
|
||||
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>
|
136
app/views/admin/edit_web.rhtml
Normal file
136
app/views/admin/edit_web.rhtml
Normal 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();" />
|
||||
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>
|
||||
|
||||
|
||||
|
||||
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 >></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 %>" />
|
||||
|
||||
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
33
app/views/file/file.rhtml
Normal 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 %>
|
23
app/views/file/import.rhtml
Normal file
23
app/views/file/import.rhtml
Normal 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>
|
79
app/views/layouts/default.rhtml
Normal file
79
app/views/layouts/default.rhtml
Normal 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>
|
12
app/views/markdown_help.rhtml
Normal file
12
app/views/markdown_help.rhtml
Normal 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">→</td><td><em>your text</em></td></tr>
|
||||
<tr><td>**your text**</td><td class="arrow">→</td><td><strong>your text</strong></td></tr>
|
||||
<tr><td>`my code`</td><td class="arrow">→</td><td><code>my code</code></td></tr>
|
||||
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">→</td><td>• Bulleted list<br />• Second item</td></tr>
|
||||
<tr><td>1. Numbered list<br />1. Second item</td><td class="arrow">→</td><td>1. Numbered list<br />2. Second item</td></tr>
|
||||
<tr><td>[link name](URL)</td><td class="arrow">→</td><td><a href="URL">link name</a></td></tr>
|
||||
<tr><td>***</td><td class="arrow">→</td><td>Horizontal ruler</td></tr>
|
||||
<tr><td><http://url><br /><email@add.com></td><td class="arrow">→</td><td>Auto-linked</td></tr>
|
||||
<tr><td>![Alt text](URL)</td><td class="arrow">→</td><td>Image</td></tr>
|
||||
</table>
|
7
app/views/mixed_help.rhtml
Normal file
7
app/views/mixed_help.rhtml
Normal 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>
|
28
app/views/navigation.rhtml
Normal file
28
app/views/navigation.rhtml
Normal 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
12
app/views/rdoc_help.rhtml
Normal 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">→</td><td><em>your text</em></td></tr>
|
||||
<tr><td>*your text*</td><td class="arrow">→</td><td><strong>your text</strong></td></tr>
|
||||
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">→</td><td>• Bulleted list<br />• Second item</td></tr>
|
||||
<tr><td>1. Numbered list<br />2. Second item</td><td class="arrow">→</td><td>1. Numbered list<br />2. Second item</td></tr>
|
||||
<tr><td>+my_code+</td><td class="arrow">→</td><td><code>my_code</code></td></tr>
|
||||
<tr><td>---</td><td class="arrow">→</td><td>Horizontal ruler</td></tr>
|
||||
<tr><td>[[URL linkname]]</td><td class="arrow">→</td><td><a href="URL">linkname</a></td></tr>
|
||||
<tr><td>http://url<br />mailto:e@add.com</td><td class="arrow">→</td><td>Auto-linked</td></tr>
|
||||
<tr><td>imageURL</td><td class="arrow">→</td><td>Image</td></tr>
|
||||
</table>
|
24
app/views/textile_help.rhtml
Normal file
24
app/views/textile_help.rhtml
Normal 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">→</td><td><em>your text</em></td></tr>
|
||||
<tr><td>*your text*</td><td class="arrow">→</td><td><strong>your text</strong></td></tr>
|
||||
<tr><td>%{color:red}hello%</td><td class="arrow">→</td><td><span style="color: red;">hello</span></td></tr>
|
||||
<tr><td>* Bulleted list<br />* Second item</td><td class="arrow">→</td><td>• Bulleted list<br />• Second item</td></tr>
|
||||
<tr><td># Numbered list<br /># Second item</td><td class="arrow">→</td><td>1. Numbered list<br />2. Second item</td></tr>
|
||||
<tr><td>"linkname":URL</td><td class="arrow">→</td><td><a href="URL">linkname</a></td></tr>
|
||||
<tr><td>|a|table|row|<br />|b|table|row|</td><td class="arrow">→</td><td>Table</td></tr>
|
||||
<tr><td>http://url<br />email@address.com</td><td class="arrow">→</td><td>Auto-linked</td></tr>
|
||||
<tr><td>!imageURL!</td><td class="arrow">→</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>
|
13
app/views/wiki/_inbound_links.rhtml
Normal file
13
app/views/wiki/_inbound_links.rhtml
Normal 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 %>
|
11
app/views/wiki/authors.rhtml
Normal file
11
app/views/wiki/authors.rhtml
Normal 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
40
app/views/wiki/edit.rhtml
Normal 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>
|
12
app/views/wiki/export.rhtml
Normal file
12
app/views/wiki/export.rhtml
Normal 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>
|
14
app/views/wiki/feeds.rhtml
Normal file
14
app/views/wiki/feeds.rhtml
Normal 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
64
app/views/wiki/list.rhtml
Normal 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>
|
23
app/views/wiki/locked.rhtml
Normal file
23
app/views/wiki/locked.rhtml
Normal 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>
|
22
app/views/wiki/login.rhtml
Normal file
22
app/views/wiki/login.rhtml
Normal 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
33
app/views/wiki/new.rhtml
Normal 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
51
app/views/wiki/page.rhtml
Normal 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>
|
14
app/views/wiki/print.rhtml
Normal file
14
app/views/wiki/print.rhtml
Normal 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>
|
9
app/views/wiki/published.rhtml
Normal file
9
app/views/wiki/published.rhtml
Normal 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 %>
|
19
app/views/wiki/recently_revised.rhtml
Normal file
19
app/views/wiki/recently_revised.rhtml
Normal 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 %>
|
28
app/views/wiki/revision.rhtml
Normal file
28
app/views/wiki/revision.rhtml
Normal 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>
|
39
app/views/wiki/rollback.rhtml
Normal file
39
app/views/wiki/rollback.rhtml
Normal 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>
|
21
app/views/wiki/rss_feed.rxml
Normal file
21
app/views/wiki/rss_feed.rxml
Normal 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
|
38
app/views/wiki/search.rhtml
Normal file
38
app/views/wiki/search.rhtml
Normal 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—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
23
app/views/wiki/tex.rhtml
Normal 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}
|
35
app/views/wiki/tex_web.rhtml
Normal file
35
app/views/wiki/tex_web.rhtml
Normal 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}
|
25
app/views/wiki/web_list.rhtml
Normal file
25
app/views/wiki/web_list.rhtml
Normal 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>
|
9
app/views/wiki_words_help.rhtml
Normal file
9
app/views/wiki_words_help.rhtml
Normal 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
17
config/boot.rb
Normal 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
105
config/database.yml
Normal 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
29
config/environment.rb
Normal 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'
|
17
config/environments/development.rb
Normal file
17
config/environments/development.rb
Normal 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
|
17
config/environments/production.rb
Normal file
17
config/environments/production.rb
Normal 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
|
23
config/environments/test.rb
Normal file
23
config/environments/test.rb
Normal 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
38
config/routes.rb
Normal 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
97
config/spam_patterns.txt
Normal 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\=
|
56
db/migrate/001_beta1_schema.rb
Normal file
56
db/migrate/001_beta1_schema.rb
Normal 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
|
36
db/migrate/002_beta2_changes_bulk.rb
Normal file
36
db/migrate/002_beta2_changes_bulk.rb
Normal 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
7
instiki
Executable 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
2
instiki.cmd
Normal file
|
@ -0,0 +1,2 @@
|
|||
set PATH=.\lib\native\win32;%PATH%
|
||||
ruby.exe script\server -e production
|
2
instiki.rb
Executable file
2
instiki.rb
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env ruby
|
||||
load File.dirname(__FILE__) + '/script/server'
|
1127
lib/bluecloth_tweaked.rb
Normal file
1127
lib/bluecloth_tweaked.rb
Normal file
File diff suppressed because it is too large
Load diff
33
lib/chunks/category.rb
Normal file
33
lib/chunks/category.rb
Normal 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
79
lib/chunks/chunk.rb
Normal 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
62
lib/chunks/engines.rb
Normal 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
49
lib/chunks/include.rb
Normal 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
31
lib/chunks/literal.rb
Normal 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
28
lib/chunks/nowiki.rb
Normal 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
18
lib/chunks/test.rb
Normal 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
182
lib/chunks/uri.rb
Normal 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
143
lib/chunks/wiki.rb
Normal 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
316
lib/diff.rb
Normal 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
15
lib/instiki_errors.rb
Normal 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
|
BIN
lib/native/win32/sqlite3.dll
Normal file
BIN
lib/native/win32/sqlite3.dll
Normal file
Binary file not shown.
134
lib/page_renderer.rb
Normal file
134
lib/page_renderer.rb
Normal 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
152
lib/rdocsupport.rb
Normal 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 "<br/>" if special.text[0,1] == '\\'
|
||||
special.text
|
||||
end
|
||||
|
||||
# We're invoked with a potential external hyperlink.
|
||||
# [mailto:] just gets inserted.
|
||||
# [http:] links are checked to see if they
|
||||
# reference an image. If so, that image gets inserted
|
||||
# using an <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
1130
lib/redcloth.rb
Normal file
File diff suppressed because it is too large
Load diff
736
lib/redcloth_for_tex.rb
Normal file
736
lib/redcloth_for_tex.rb
Normal 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’\2' ], # single closing
|
||||
[ /([^\s\[{(>])\'/, '\1’' ], # single closing
|
||||
[ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing
|
||||
[ /\'/, '‘' ], # single opening
|
||||
# [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
|
||||
[ /([^\s\[{(>])"/, '\1”' ], # double closing
|
||||
[ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing
|
||||
[ /"/, '“' ], # double opening
|
||||
[ /\b( )?\.{3}/, '\1…' ], # ellipsis
|
||||
[ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<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—' ], # em dash
|
||||
[ /\s->\s/, ' → ' ], # en dash
|
||||
[ /\s-\s/, ' – ' ], # en dash
|
||||
[ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
|
||||
[ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
|
||||
[ /\b ?[(\[]R[\])]/i, '®' ], # registered
|
||||
[ /\b ?[(\[]C[\])]/i, '©' ] # copyright
|
||||
]
|
||||
|
||||
I_ALGN_VALS = {
|
||||
'<' => 'left',
|
||||
'=' => 'center',
|
||||
'>' => 'right'
|
||||
}
|
||||
|
||||
H_ALGN_VALS = {
|
||||
'<' => 'left',
|
||||
'=' => 'center',
|
||||
'>' => 'right',
|
||||
'<>' => 'justify'
|
||||
}
|
||||
|
||||
V_ALGN_VALS = {
|
||||
'^' => 'top',
|
||||
'-' => 'middle',
|
||||
'~' => 'bottom'
|
||||
}
|
||||
|
||||
QTAGS = [
|
||||
['**', 'bf'],
|
||||
['*', 'bf'],
|
||||
['??', 'cite'],
|
||||
['-', 'del'],
|
||||
['__', 'underline'],
|
||||
['_', 'em'],
|
||||
['%', 'span'],
|
||||
['+', 'ins'],
|
||||
['^', 'sup'],
|
||||
['~', 'sub']
|
||||
]
|
||||
|
||||
def self.available?
|
||||
if not defined? @@available
|
||||
begin
|
||||
@@available = system "pdflatex -version"
|
||||
rescue Errno::ENOENT
|
||||
@@available = false
|
||||
end
|
||||
end
|
||||
@@available
|
||||
end
|
||||
|
||||
#
|
||||
# Two accessor for setting security restrictions.
|
||||
#
|
||||
# This is a nice thing if you're using RedCloth for
|
||||
# formatting in public places (e.g. Wikis) where you
|
||||
# don't want users to abuse HTML for bad things.
|
||||
#
|
||||
# If +:filter_html+ is set, HTML which wasn't
|
||||
# created by the Textile processor will be escaped.
|
||||
#
|
||||
# If +:filter_styles+ is set, it will also disable
|
||||
# the style markup specifier. ('{color: red}')
|
||||
#
|
||||
attr_accessor :filter_html, :filter_styles
|
||||
|
||||
#
|
||||
# Accessor for toggling line folding.
|
||||
#
|
||||
# If +:fold_lines+ is set, single newlines will
|
||||
# not be converted to break tags.
|
||||
#
|
||||
attr_accessor :fold_lines
|
||||
|
||||
def initialize( string, restrictions = [] )
|
||||
restrictions.each { |r| method( "#{ r }=" ).call( true ) }
|
||||
super( string )
|
||||
end
|
||||
|
||||
#
|
||||
# Generate tex.
|
||||
#
|
||||
def to_tex( lite = false )
|
||||
|
||||
# make our working copy
|
||||
text = self.dup
|
||||
|
||||
@urlrefs = {}
|
||||
@shelf = []
|
||||
|
||||
# incoming_entities text
|
||||
fix_entities text
|
||||
clean_white_space text
|
||||
|
||||
get_refs text
|
||||
|
||||
no_textile text
|
||||
|
||||
unless lite
|
||||
lists text
|
||||
table text
|
||||
end
|
||||
|
||||
glyphs text
|
||||
|
||||
unless lite
|
||||
fold text
|
||||
block text
|
||||
end
|
||||
|
||||
retrieve text
|
||||
encode_entities text
|
||||
|
||||
text.gsub!(/\[\[(.*?)\]\]/, "\\1")
|
||||
text.gsub!(/_/, "\\_")
|
||||
text.gsub!( /<\/?notextile>/, '' )
|
||||
# text.gsub!( /x%x%/, '&' )
|
||||
# text.gsub!( /<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!( /<(\/?#{ offtags })>/, '<\1>' )
|
||||
end
|
||||
## do htmlspecial if between <code>
|
||||
elsif codepre > 0
|
||||
line.texesc!( :NoQuotes )
|
||||
## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
|
||||
elsif not tagline
|
||||
inline line
|
||||
glyphs_deep line
|
||||
end
|
||||
|
||||
line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def glyphs( text )
|
||||
text.gsub!( /"\z/, "\" " )
|
||||
## if no html, do a simple search and replace...
|
||||
if text !~ /<.*>/
|
||||
inline text
|
||||
end
|
||||
glyphs_deep text
|
||||
end
|
||||
|
||||
def i_align( text )
|
||||
I_ALGN_VALS[text]
|
||||
end
|
||||
|
||||
def h_align( text )
|
||||
H_ALGN_VALS[text]
|
||||
end
|
||||
|
||||
def v_align( text )
|
||||
V_ALGN_VALS[text]
|
||||
end
|
||||
|
||||
def encode_high( text )
|
||||
## mb_encode_numericentity($text, $cmap, $charset);
|
||||
end
|
||||
|
||||
def decode_high( text )
|
||||
## mb_decode_numericentity($text, $cmap, $charset);
|
||||
end
|
||||
|
||||
def textile_popup_help( name, helpvar, windowW, windowH )
|
||||
' <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
121
lib/url_generator.rb
Normal 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
202
lib/wiki_content.rb
Normal 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
23
lib/wiki_words.rb
Normal 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
|
18
natives/osx/desktop_launcher/AppDelegate.h
Normal file
18
natives/osx/desktop_launcher/AppDelegate.h
Normal 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
|
109
natives/osx/desktop_launcher/AppDelegate.mm
Normal file
109
natives/osx/desktop_launcher/AppDelegate.mm
Normal 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
|
16
natives/osx/desktop_launcher/Credits.html
Normal file
16
natives/osx/desktop_launcher/Credits.html
Normal 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>
|
BIN
natives/osx/desktop_launcher/English.lproj/InfoPlist.strings
Normal file
BIN
natives/osx/desktop_launcher/English.lproj/InfoPlist.strings
Normal file
Binary file not shown.
13
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib
generated
Normal file
13
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib
generated
Normal 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;
|
||||
}
|
24
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib
generated
Normal file
24
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib
generated
Normal 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>
|
BIN
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib
generated
Normal file
BIN
natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib
generated
Normal file
Binary file not shown.
13
natives/osx/desktop_launcher/Info.plist
Normal file
13
natives/osx/desktop_launcher/Info.plist
Normal 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;
|
||||
}
|
592
natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj
Normal file
592
natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj
Normal 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;
|
||||
}
|
7
natives/osx/desktop_launcher/Instiki_Prefix.pch
Normal file
7
natives/osx/desktop_launcher/Instiki_Prefix.pch
Normal 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
|
9
natives/osx/desktop_launcher/MakeDMG.sh
Normal file
9
natives/osx/desktop_launcher/MakeDMG.sh
Normal 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
|
14
natives/osx/desktop_launcher/main.mm
Normal file
14
natives/osx/desktop_launcher/main.mm
Normal 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);
|
||||
}
|
16
natives/osx/desktop_launcher/version.plist
Normal file
16
natives/osx/desktop_launcher/version.plist
Normal 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
40
public/.htaccess
Normal 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
Loading…
Reference in a new issue