Checkout of Instiki Trunk 1/21/2007.
This commit is contained in:
commit
69b62b6f33
1138 changed files with 139586 additions and 0 deletions
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></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>
|
Loading…
Add table
Add a link
Reference in a new issue