instiki 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README +172 -0
  2. data/app/controllers/wiki.rb +389 -0
  3. data/app/models/author.rb +4 -0
  4. data/app/models/chunks/category.rb +31 -0
  5. data/app/models/chunks/chunk.rb +20 -0
  6. data/app/models/chunks/engines.rb +38 -0
  7. data/app/models/chunks/include.rb +29 -0
  8. data/app/models/chunks/literal.rb +19 -0
  9. data/app/models/chunks/match.rb +19 -0
  10. data/app/models/chunks/nowiki.rb +31 -0
  11. data/app/models/chunks/test.rb +18 -0
  12. data/app/models/chunks/uri.rb +97 -0
  13. data/app/models/chunks/wiki.rb +82 -0
  14. data/app/models/page.rb +86 -0
  15. data/app/models/page_lock.rb +24 -0
  16. data/app/models/page_set.rb +64 -0
  17. data/app/models/revision.rb +90 -0
  18. data/app/models/web.rb +89 -0
  19. data/app/models/wiki_content.rb +105 -0
  20. data/app/models/wiki_service.rb +83 -0
  21. data/app/models/wiki_words.rb +25 -0
  22. data/app/views/bottom.rhtml +4 -0
  23. data/app/views/markdown_help.rhtml +16 -0
  24. data/app/views/navigation.rhtml +19 -0
  25. data/app/views/rdoc_help.rhtml +16 -0
  26. data/app/views/static_style_sheet.rhtml +199 -0
  27. data/app/views/textile_help.rhtml +28 -0
  28. data/app/views/top.rhtml +49 -0
  29. data/app/views/wiki/authors.rhtml +13 -0
  30. data/app/views/wiki/edit.rhtml +31 -0
  31. data/app/views/wiki/edit_web.rhtml +138 -0
  32. data/app/views/wiki/export.rhtml +14 -0
  33. data/app/views/wiki/feeds.rhtml +10 -0
  34. data/app/views/wiki/list.rhtml +57 -0
  35. data/app/views/wiki/locked.rhtml +14 -0
  36. data/app/views/wiki/login.rhtml +11 -0
  37. data/app/views/wiki/new.rhtml +27 -0
  38. data/app/views/wiki/new_system.rhtml +78 -0
  39. data/app/views/wiki/new_web.rhtml +64 -0
  40. data/app/views/wiki/page.rhtml +81 -0
  41. data/app/views/wiki/print.rhtml +16 -0
  42. data/app/views/wiki/published.rhtml +10 -0
  43. data/app/views/wiki/recently_revised.rhtml +30 -0
  44. data/app/views/wiki/revision.rhtml +81 -0
  45. data/app/views/wiki/rollback.rhtml +31 -0
  46. data/app/views/wiki/rss_feed.rhtml +22 -0
  47. data/app/views/wiki/search.rhtml +15 -0
  48. data/app/views/wiki/tex.rhtml +23 -0
  49. data/app/views/wiki/tex_web.rhtml +35 -0
  50. data/app/views/wiki/web_list.rhtml +13 -0
  51. data/app/views/wiki_words_help.rhtml +8 -0
  52. data/instiki +67 -0
  53. data/libraries/action_controller_servlet.rb +177 -0
  54. data/libraries/diff/diff.rb +475 -0
  55. data/libraries/erb.rb +490 -0
  56. data/libraries/madeleine_service.rb +68 -0
  57. data/libraries/rdocsupport.rb +156 -0
  58. data/libraries/redcloth_for_tex.rb +869 -0
  59. data/libraries/view_helper.rb +33 -0
  60. data/libraries/web_controller_server.rb +81 -0
  61. metadata +142 -0
@@ -0,0 +1,4 @@
1
+ class Author < String
2
+ attr_accessor :ip
3
+ def initialize(name, ip) @ip = ip; super(name) end
4
+ end
@@ -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
@@ -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