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