instiki 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +172 -0
- data/app/controllers/wiki.rb +389 -0
- data/app/models/author.rb +4 -0
- data/app/models/chunks/category.rb +31 -0
- data/app/models/chunks/chunk.rb +20 -0
- data/app/models/chunks/engines.rb +38 -0
- data/app/models/chunks/include.rb +29 -0
- data/app/models/chunks/literal.rb +19 -0
- data/app/models/chunks/match.rb +19 -0
- data/app/models/chunks/nowiki.rb +31 -0
- data/app/models/chunks/test.rb +18 -0
- data/app/models/chunks/uri.rb +97 -0
- data/app/models/chunks/wiki.rb +82 -0
- data/app/models/page.rb +86 -0
- data/app/models/page_lock.rb +24 -0
- data/app/models/page_set.rb +64 -0
- data/app/models/revision.rb +90 -0
- data/app/models/web.rb +89 -0
- data/app/models/wiki_content.rb +105 -0
- data/app/models/wiki_service.rb +83 -0
- data/app/models/wiki_words.rb +25 -0
- data/app/views/bottom.rhtml +4 -0
- data/app/views/markdown_help.rhtml +16 -0
- data/app/views/navigation.rhtml +19 -0
- data/app/views/rdoc_help.rhtml +16 -0
- data/app/views/static_style_sheet.rhtml +199 -0
- data/app/views/textile_help.rhtml +28 -0
- data/app/views/top.rhtml +49 -0
- data/app/views/wiki/authors.rhtml +13 -0
- data/app/views/wiki/edit.rhtml +31 -0
- data/app/views/wiki/edit_web.rhtml +138 -0
- data/app/views/wiki/export.rhtml +14 -0
- data/app/views/wiki/feeds.rhtml +10 -0
- data/app/views/wiki/list.rhtml +57 -0
- data/app/views/wiki/locked.rhtml +14 -0
- data/app/views/wiki/login.rhtml +11 -0
- data/app/views/wiki/new.rhtml +27 -0
- data/app/views/wiki/new_system.rhtml +78 -0
- data/app/views/wiki/new_web.rhtml +64 -0
- data/app/views/wiki/page.rhtml +81 -0
- data/app/views/wiki/print.rhtml +16 -0
- data/app/views/wiki/published.rhtml +10 -0
- data/app/views/wiki/recently_revised.rhtml +30 -0
- data/app/views/wiki/revision.rhtml +81 -0
- data/app/views/wiki/rollback.rhtml +31 -0
- data/app/views/wiki/rss_feed.rhtml +22 -0
- data/app/views/wiki/search.rhtml +15 -0
- data/app/views/wiki/tex.rhtml +23 -0
- data/app/views/wiki/tex_web.rhtml +35 -0
- data/app/views/wiki/web_list.rhtml +13 -0
- data/app/views/wiki_words_help.rhtml +8 -0
- data/instiki +67 -0
- data/libraries/action_controller_servlet.rb +177 -0
- data/libraries/diff/diff.rb +475 -0
- data/libraries/erb.rb +490 -0
- data/libraries/madeleine_service.rb +68 -0
- data/libraries/rdocsupport.rb +156 -0
- data/libraries/redcloth_for_tex.rb +869 -0
- data/libraries/view_helper.rb +33 -0
- data/libraries/web_controller_server.rb +81 -0
- metadata +142 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'chunks/chunk'
|
2
|
+
|
3
|
+
# The category chunk looks for "category: news" on a line by
|
4
|
+
# itself and parses the terms after the ':' as categories.
|
5
|
+
# Other classes can search for Category chunks within
|
6
|
+
# rendered content to find out what categories this page
|
7
|
+
# should be in.
|
8
|
+
#
|
9
|
+
# Category lines can be hidden using ':category: news', for example
|
10
|
+
class Category < Chunk::Abstract
|
11
|
+
def self.pattern() return /^(:)?category\s*:(.*)$/i end
|
12
|
+
|
13
|
+
attr_reader :hidden, :list
|
14
|
+
|
15
|
+
def initialize(match_data)
|
16
|
+
super(match_data)
|
17
|
+
@hidden = match_data[1]
|
18
|
+
@list = match_data[2].split(',').map { |c| c.strip }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Mark this chunk's start and end points but allow the terms
|
22
|
+
# after the ':' to be marked up.
|
23
|
+
def mask(content) pre_mask + list.join(', ') + post_mask end
|
24
|
+
|
25
|
+
# If the chunk is hidden, erase the mask and return this chunk
|
26
|
+
# otherwise, surround it with a 'div' block.
|
27
|
+
def unmask(content)
|
28
|
+
replacement = ( hidden ? '' : '<div class="property">category:\1</div>' )
|
29
|
+
self if content.sub!( Regexp.new( pre_mask+'(.*)?'+post_mask ), replacement )
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'uri/common'
|
3
|
+
|
4
|
+
# A chunk is a pattern of text that can be protected
|
5
|
+
# and interrogated by a renderer. Each Chunk class has a
|
6
|
+
# +pattern+ that states what sort of text it matches.
|
7
|
+
# Chunks are initalized by passing in the result of a
|
8
|
+
# match by its pattern.
|
9
|
+
module Chunk
|
10
|
+
class Abstract
|
11
|
+
attr_reader :text
|
12
|
+
|
13
|
+
def initialize(match_data) @text = match_data[0] end
|
14
|
+
def pre_mask() "chunk#{self.object_id}start " end
|
15
|
+
def post_mask() " chunk#{self.object_id}end" end
|
16
|
+
def mask(content) "chunk#{self.object_id}chunk" end
|
17
|
+
def revert(content) content.sub!( Regexp.new(mask(content)), text ) end
|
18
|
+
def unmask(content) self if revert(content) end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + "../../libraries"
|
2
|
+
|
3
|
+
require 'redcloth'
|
4
|
+
require 'bluecloth'
|
5
|
+
require 'rdocsupport'
|
6
|
+
require 'chunks/chunk'
|
7
|
+
|
8
|
+
# The markup engines are Chunks that call the one of RedCloth, BlueCloth
|
9
|
+
# or RDoc to convert text. This markup occurs when the chunk is required
|
10
|
+
# to mask itself.
|
11
|
+
module Engines
|
12
|
+
class Textile < Chunk::Abstract
|
13
|
+
def self.pattern() /^(.*)$/m end
|
14
|
+
def mask(content)
|
15
|
+
RedCloth.new(text,content.options[:engine_opts]).to_html
|
16
|
+
end
|
17
|
+
def unmask(content) self end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Markdown < Chunk::Abstract
|
21
|
+
def self.pattern() /^(.*)$/m end
|
22
|
+
def mask(content)
|
23
|
+
BlueCloth.new(text,content.options[:engine_opts]).to_html
|
24
|
+
end
|
25
|
+
def unmask(content) self end
|
26
|
+
end
|
27
|
+
|
28
|
+
class RDoc < Chunk::Abstract
|
29
|
+
def self.pattern() /^(.*)$/m end
|
30
|
+
def mask(content)
|
31
|
+
RDocSupport::RDocFormatter.new(text).to_html
|
32
|
+
end
|
33
|
+
def unmask(content) self end
|
34
|
+
end
|
35
|
+
|
36
|
+
MAP = { :textile => Textile, :markdown => Markdown, :rdoc => RDoc }
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'chunks/wiki'
|
2
|
+
|
3
|
+
# Includes the contents of another page for rendering.
|
4
|
+
# The include command looks like this: "[[!include PageName]]".
|
5
|
+
# It is a WikiLink since it refers to another page (PageName)
|
6
|
+
# and the wiki content using this command must be notified
|
7
|
+
# of changes to that page.
|
8
|
+
# If the included page could not be found, a warning is displayed.
|
9
|
+
class Include < WikiChunk::WikiLink
|
10
|
+
def self.pattern() /^\[\[!include(.*)\]\]\s*$/i end
|
11
|
+
|
12
|
+
attr_reader :page_name
|
13
|
+
|
14
|
+
def initialize(match_data)
|
15
|
+
super(match_data)
|
16
|
+
@page_name = match_data[1].strip
|
17
|
+
end
|
18
|
+
|
19
|
+
# This replaces the [[!include PageName]] text with
|
20
|
+
# the contents of PageName if it exists. Otherwise
|
21
|
+
# a warning is displayed.
|
22
|
+
def mask(content)
|
23
|
+
page = content.web.pages[page_name]
|
24
|
+
(page ? page.content : "<em>Could not include #{page_name}</em>")
|
25
|
+
end
|
26
|
+
|
27
|
+
# Keep this chunk regardless of what happens.
|
28
|
+
def unmask(content) self end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'chunks/chunk'
|
2
|
+
|
3
|
+
# These are basic chunks that have a pattern and can be protected.
|
4
|
+
# They are used by rendering process to prevent wiki rendering
|
5
|
+
# occuring within literal areas such as <code> and <pre> blocks
|
6
|
+
# and within HTML tags.
|
7
|
+
module Literal
|
8
|
+
# A literal chunk that protects 'code' and 'pre' tags from wiki rendering.
|
9
|
+
class Pre < Chunk::Abstract
|
10
|
+
PRE_BLOCKS = "a|pre|code"
|
11
|
+
def self.pattern() Regexp.new('<('+PRE_BLOCKS+')\b[^>]*?>.*?</\1>', Regexp::MULTILINE) end
|
12
|
+
end
|
13
|
+
|
14
|
+
# A literal chunk that protects HTML tags from wiki rendering.
|
15
|
+
class Tags < Chunk::Abstract
|
16
|
+
TAGS = "a|img|em|strong|div|span|table|td|th|ul|ol|li|dl|dt|dd"
|
17
|
+
def self.pattern() Regexp.new('<(?:'+TAGS+')[^>]*?>', Regexp::MULTILINE) end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This module is to be included in unit tests that involve matching chunks.
|
2
|
+
# It provides a easy way to test whether a chunk matches a particular string
|
3
|
+
# and any the values of any fields that should be set after a match.
|
4
|
+
module ChunkMatch
|
5
|
+
|
6
|
+
# Asserts a number of tests for the given type and text.
|
7
|
+
def match(type, test_text, expected)
|
8
|
+
pattern = type.pattern
|
9
|
+
assert_match(pattern, test_text)
|
10
|
+
pattern =~ test_text # Previous assertion guarantees match
|
11
|
+
chunk = type.new($~)
|
12
|
+
|
13
|
+
# Test if requested parts are correct.
|
14
|
+
for method_sym, value in expected do
|
15
|
+
assert_respond_to(chunk, method_sym)
|
16
|
+
assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'chunks/chunk'
|
2
|
+
|
3
|
+
# This chunks allows certain parts of a wiki page to be hidden from the
|
4
|
+
# rest of the rendering pipeline. It should be run at the beginning
|
5
|
+
# of the pipeline in `wiki_content.rb`.
|
6
|
+
#
|
7
|
+
# An example use of this chunk is to markup double brackets or
|
8
|
+
# auto URI links:
|
9
|
+
# <nowiki>Here are [[double brackets]] and a URI: www.uri.org</nowiki>
|
10
|
+
#
|
11
|
+
# The contents of the chunks will not be processed by any other chunk
|
12
|
+
# so the `www.uri.org` and the double brackets will appear verbatim.
|
13
|
+
#
|
14
|
+
# Author: Mark Reid <mark at threewordslong dot com>
|
15
|
+
# Created: 8th June 2004
|
16
|
+
class NoWiki < Chunk::Abstract
|
17
|
+
|
18
|
+
def self.pattern() Regexp.new('<nowiki>(.*?)</nowiki>') end
|
19
|
+
|
20
|
+
attr_reader :plain_text
|
21
|
+
|
22
|
+
def initialize(match_data)
|
23
|
+
super(match_data)
|
24
|
+
@plain_text = match_data[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
# The nowiki content is not unmasked. This means the chunk will be reverted
|
28
|
+
# using the plain text.
|
29
|
+
def unmask(content) nil end
|
30
|
+
def revert(content) content.sub!( Regexp.new(mask(content)), plain_text ) end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
class ChunkTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# Asserts a number of tests for the given type and text.
|
6
|
+
def match(type, test_text, expected)
|
7
|
+
pattern = type.pattern
|
8
|
+
assert_match(pattern, test_text)
|
9
|
+
pattern =~ test_text # Previous assertion guarantees match
|
10
|
+
chunk = type.new($~)
|
11
|
+
|
12
|
+
# Test if requested parts are correct.
|
13
|
+
for method_sym, value in expected do
|
14
|
+
assert_respond_to(chunk, method_sym)
|
15
|
+
assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'chunks/chunk'
|
2
|
+
|
3
|
+
# This wiki chunk matches arbitrary URIs, using patterns from the Ruby URI modules.
|
4
|
+
# It parses out a variety of fields that could be used by renderers to format
|
5
|
+
# the links in various ways (shortening domain names, hiding email addresses)
|
6
|
+
# It matches email addresses and host.com.au domains without schemes (http://)
|
7
|
+
# but adds these on as required.
|
8
|
+
#
|
9
|
+
# The heuristic used to match a URI is designed to err on the side of caution.
|
10
|
+
# That is, it is more likely to not autolink a URI than it is to accidently
|
11
|
+
# autolink something that is not a URI. The reason behind this is it is easier
|
12
|
+
# to force a URI link by prefixing 'http://' to it than it is to escape and
|
13
|
+
# incorrectly marked up non-URI.
|
14
|
+
#
|
15
|
+
# I'm using a part of the [ISO 3166-1 Standard][iso3166] for country name suffixes.
|
16
|
+
# The generic names are from www.bnoack.com/data/countrycode2.html)
|
17
|
+
# [iso3166]: http://geotags.com/iso3166/
|
18
|
+
class URIChunk < Chunk::Abstract
|
19
|
+
include URI::REGEXP::PATTERN
|
20
|
+
|
21
|
+
GENERIC = '(?:aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org)'
|
22
|
+
COUNTRY = '(?:au|at|be|ca|ch|de|dk|fr|hk|in|ir|it|jp|nl|no|pt|ru|se|sw|tv|tw|uk|us)'
|
23
|
+
|
24
|
+
# These are needed otherwise HOST will match almost anything
|
25
|
+
TLDS = "\\.(?:#{GENERIC}|#{COUNTRY})"
|
26
|
+
|
27
|
+
# Redefine USERINFO so that it must have non-zero length
|
28
|
+
USERINFO = "(?:[#{UNRESERVED};:&=+$,]|#{ESCAPED})+"
|
29
|
+
|
30
|
+
# Pattern of legal URI endings to stop interference with some Textile
|
31
|
+
# markup. (Images: !URI!) and other punctuation eg, (http://wiki.com/)
|
32
|
+
URI_ENDING = '[)!]'
|
33
|
+
|
34
|
+
# The basic URI expression as a string
|
35
|
+
URI_PATTERN =
|
36
|
+
"(?:(#{SCHEME})://)?" + # Optional scheme:// (\1|\8)
|
37
|
+
"(?:(#{USERINFO})@)?" + # Optional userinfo@ (\2|\9)
|
38
|
+
"(#{HOSTNAME}#{TLDS})" + # Mandatory host eg, HOST.com.au (\3|\10)
|
39
|
+
"(?::(#{PORT}))?" + # Optional :port (\4|\11)
|
40
|
+
"(#{ABS_PATH})?" + # Optional absolute path (\5|\12)
|
41
|
+
"(?:\\?(#{QUERY}))?" + # Optional ?query (\6|\13)
|
42
|
+
"(?:\\#(#{FRAGMENT}))?" # Optional #fragment (\7|\14)
|
43
|
+
|
44
|
+
def self.pattern()
|
45
|
+
# This pattern first tries to match the URI_PATTERN that ends with
|
46
|
+
# punctuation that is a valid URI character (eg, ')', '!'). If
|
47
|
+
# such a match occurs, there should be no backtracking (hence the ?> ).
|
48
|
+
# If the string cannot match a URI ending with URI_ENDING, then a second
|
49
|
+
# attempt is tried.
|
50
|
+
Regexp.new("(?>#{URI_PATTERN}(?=#{URI_ENDING}))|#{URI_PATTERN}", Regexp::EXTENDED, 'N')
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :uri, :scheme, :user, :host, :port, :path, :query, :fragment, :link_text
|
54
|
+
|
55
|
+
def initialize(match_data)
|
56
|
+
super(match_data)
|
57
|
+
# Since the URI_PATTERN is tried twice, there are two sets of
|
58
|
+
# groups, one from \1 to \7 and the second from \8 to \14.
|
59
|
+
# The fields are set by which ever group matches.
|
60
|
+
@scheme = match_data[1] || match_data[8]
|
61
|
+
@user = match_data[2] || match_data[9]
|
62
|
+
@host = match_data[3] || match_data[10]
|
63
|
+
@port = match_data[4] || match_data[11]
|
64
|
+
@path = match_data[5] || match_data[12]
|
65
|
+
@query = match_data[6] || match_data[13]
|
66
|
+
@fragment = match_data[7] || match_data[14]
|
67
|
+
|
68
|
+
# If there is no scheme, add an appropriate one, otherwise
|
69
|
+
# set the URI to the matched text.
|
70
|
+
@text_scheme = scheme
|
71
|
+
@uri = (scheme ? match_data[0] : nil )
|
72
|
+
@scheme = scheme || ( user ? 'mailto' : 'http' )
|
73
|
+
@delimiter = ( scheme == 'mailto' ? ':' : '://' )
|
74
|
+
@uri ||= scheme + @delimiter + match_data[0]
|
75
|
+
|
76
|
+
# Build up the link text. Schemes are omitted unless explicitly given.
|
77
|
+
@link_text = ''
|
78
|
+
@link_text << "#{@scheme}#{@delimiter}" if @text_scheme
|
79
|
+
@link_text << "#{@user}@" if @user
|
80
|
+
@link_text << "#{@host}" if @host
|
81
|
+
@link_text << ":#{@port}" if @port
|
82
|
+
@link_text << "#{@path}" if @path
|
83
|
+
@link_text << "?#{@query}" if @query
|
84
|
+
end
|
85
|
+
|
86
|
+
# If the text should be escaped then don't keep this chunk.
|
87
|
+
# Otherwise only keep this chunk if it was substituted back into the
|
88
|
+
# content.
|
89
|
+
def unmask(content)
|
90
|
+
return nil if escaped_text
|
91
|
+
return self if content.sub!( Regexp.new(mask(content)), "<a href=\"#{uri}\">#{link_text}</a>" )
|
92
|
+
end
|
93
|
+
|
94
|
+
# If there is no hostname in the URI, do not render it
|
95
|
+
# It's probably only contains the scheme, eg 'something:'
|
96
|
+
def escaped_text() ( host.nil? ? @uri : nil ) end
|
97
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'wiki_words'
|
2
|
+
require 'chunks/chunk'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
# Contains all the methods for finding and replacing wiki related
|
6
|
+
# links.
|
7
|
+
module WikiChunk
|
8
|
+
include Chunk
|
9
|
+
|
10
|
+
# A wiki link is the top-level class for anything that refers to
|
11
|
+
# another wiki page.
|
12
|
+
class WikiLink < Chunk::Abstract
|
13
|
+
# By default, no escaped text
|
14
|
+
def escaped_text() nil end
|
15
|
+
|
16
|
+
# Delimit the link text with markers to replace later unless
|
17
|
+
# the word is escaped. In that case, just return the link text
|
18
|
+
def mask(content) escaped_text || pre_mask + link_text + post_mask end
|
19
|
+
|
20
|
+
def regexp() Regexp.new(pre_mask + '(.*)?' + post_mask) end
|
21
|
+
|
22
|
+
def revert(content) content.sub!(regexp, text) end
|
23
|
+
|
24
|
+
# Do not keep this chunk if it is escaped.
|
25
|
+
# Otherwise, pass the link procedure a page_name and link_text and
|
26
|
+
# get back a string of HTML to replace the mask with.
|
27
|
+
def unmask(content)
|
28
|
+
return nil if escaped_text
|
29
|
+
return self if content.sub!(regexp) { |match| content.page_link(page_name, $1) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# This chunk matches a WikiWord. WikiWords can be escaped
|
34
|
+
# by prepending a '\'. When this is the case, the +escaped_text+
|
35
|
+
# method will return the WikiWord instead of the usual +nil+.
|
36
|
+
# The +page_name+ method returns the matched WikiWord.
|
37
|
+
class Word < WikiLink
|
38
|
+
def self.pattern
|
39
|
+
Regexp.new('(\\\\)?(' + WikiWords::WIKI_WORD_PATTERN + ')\b', 0, "utf-8")
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :page_name
|
43
|
+
|
44
|
+
def initialize(match_data)
|
45
|
+
super(match_data)
|
46
|
+
@escape = match_data[1]
|
47
|
+
@page_name = match_data[2]
|
48
|
+
end
|
49
|
+
|
50
|
+
def escaped_text() (@escape.nil? ? nil : page_name) end
|
51
|
+
def link_text() WikiWords.separate(page_name) end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This chunk handles [[bracketted wiki words]] and
|
55
|
+
# [[AliasedWords|aliased wiki words]]. The first part of an
|
56
|
+
# aliased wiki word must be a WikiWord. If the WikiWord
|
57
|
+
# is aliased, the +link_text+ field will contain the
|
58
|
+
# alias, otherwise +link_text+ will contain the entire
|
59
|
+
# contents within the double brackets.
|
60
|
+
#
|
61
|
+
# NOTE: This chunk must be tested before WikiWord since
|
62
|
+
# a WikiWords can be a substring of a WikiLink.
|
63
|
+
class Link < WikiLink
|
64
|
+
def self.pattern() /\[\[([^\]]+)\]\]/ end
|
65
|
+
ALIASED_LINK_PATTERN= Regexp.new('^(.*)?\|(.*)$', 0, "utf-8")
|
66
|
+
|
67
|
+
attr_reader :page_name, :link_text
|
68
|
+
|
69
|
+
def initialize(match_data)
|
70
|
+
super(match_data)
|
71
|
+
|
72
|
+
# If the like is aliased, set the page name to the first bit
|
73
|
+
# and the link text to the second, otherwise set both to the
|
74
|
+
# contents of the double brackets.
|
75
|
+
if match_data[1] =~ ALIASED_LINK_PATTERN
|
76
|
+
@page_name, @link_text = $1, $2
|
77
|
+
else
|
78
|
+
@page_name, @link_text = match_data[1], match_data[1]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/app/models/page.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "date"
|
2
|
+
require "page_lock"
|
3
|
+
require "revision"
|
4
|
+
require "wiki_words"
|
5
|
+
require "chunks/wiki"
|
6
|
+
|
7
|
+
class Page
|
8
|
+
include PageLock
|
9
|
+
|
10
|
+
CONTINOUS_REVISION_PERIOD = 30 * 60 # 30 minutes
|
11
|
+
|
12
|
+
attr_reader :name, :revisions, :web
|
13
|
+
|
14
|
+
def initialize(web, name, content, created_at, author)
|
15
|
+
@web, @name, @revisions = web, name, []
|
16
|
+
revise(content, created_at, author)
|
17
|
+
end
|
18
|
+
|
19
|
+
def revise(content, created_at, author)
|
20
|
+
if !@revisions.empty? && continous_revision?(created_at, author)
|
21
|
+
@revisions.last.created_at = Time.now
|
22
|
+
@revisions.last.content = content
|
23
|
+
@revisions.last.clear_display_cache
|
24
|
+
else
|
25
|
+
@revisions << Revision.new(self, @revisions.length, content, created_at, author)
|
26
|
+
end
|
27
|
+
|
28
|
+
web.refresh_pages_with_references(name) if @revisions.length == 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def rollback(revision_number, created_at, author_ip = nil)
|
32
|
+
roll_back_revision = @revisions[revision_number].dup
|
33
|
+
revise(roll_back_revision.content, created_at, Author.new(roll_back_revision.author, author_ip))
|
34
|
+
end
|
35
|
+
|
36
|
+
def revisions?
|
37
|
+
revisions.length > 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def revised_on
|
41
|
+
created_on
|
42
|
+
end
|
43
|
+
|
44
|
+
def pretty_revised_on
|
45
|
+
DateTime.new(revised_on.year, revised_on.mon, revised_on.day).strftime "%B %e, %Y"
|
46
|
+
end
|
47
|
+
|
48
|
+
def in_category?(cat)
|
49
|
+
cat.nil? || cat.empty? || categories.include?(cat)
|
50
|
+
end
|
51
|
+
|
52
|
+
def categories
|
53
|
+
display_content.find_chunks(Category).map { |cat| cat.list }.flatten
|
54
|
+
end
|
55
|
+
|
56
|
+
def authors
|
57
|
+
revisions.collect { |rev| rev.author }
|
58
|
+
end
|
59
|
+
|
60
|
+
def references
|
61
|
+
web.select.pages_that_reference(name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page".
|
65
|
+
def plain_name
|
66
|
+
WikiWords.separate(name, web.brackets_only)
|
67
|
+
end
|
68
|
+
|
69
|
+
def link(options = {})
|
70
|
+
web.make_link(name, nil, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def author_link(options = {})
|
74
|
+
web.make_link(author, nil, options)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def continous_revision?(created_at, author)
|
79
|
+
@revisions.last.author == author && @revisions.last.created_at + CONTINOUS_REVISION_PERIOD > created_at
|
80
|
+
end
|
81
|
+
|
82
|
+
# Forward method calls to the current revision, so the page responds to all revision calls
|
83
|
+
def method_missing(method_symbol)
|
84
|
+
revisions.last.send(method_symbol)
|
85
|
+
end
|
86
|
+
end
|