Soks 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/LICENSE.txt +60 -0
  2. data/README.txt +65 -0
  3. data/bin/soks-create-wiki.rb +41 -0
  4. data/contrib/diff/lcs.rb +1105 -0
  5. data/contrib/diff/lcs/array.rb +21 -0
  6. data/contrib/diff/lcs/block.rb +51 -0
  7. data/contrib/diff/lcs/callbacks.rb +322 -0
  8. data/contrib/diff/lcs/change.rb +169 -0
  9. data/contrib/diff/lcs/hunk.rb +257 -0
  10. data/contrib/diff/lcs/ldiff.rb +226 -0
  11. data/contrib/diff/lcs/string.rb +19 -0
  12. data/contrib/diff_licence.txt +76 -0
  13. data/contrib/redcloth-2.0.11.rb +894 -0
  14. data/contrib/redcloth-3.0.1.rb +1019 -0
  15. data/contrib/redcloth_license.txt +27 -0
  16. data/lib/authenticators.rb +79 -0
  17. data/lib/soks-helpers.rb +321 -0
  18. data/lib/soks-model.rb +208 -0
  19. data/lib/soks-servlet.rb +125 -0
  20. data/lib/soks-utils.rb +80 -0
  21. data/lib/soks-view.rb +424 -0
  22. data/lib/soks.rb +19 -0
  23. data/template/attachment/logo.png +0 -0
  24. data/template/attachment/stylesheet.css +63 -0
  25. data/template/content/How%20to%20export%20a%20site%20from%20this%20wiki.textile +5 -0
  26. data/template/content/How%20to%20hack%20soks.textile +60 -0
  27. data/template/content/How%20to%20import%20a%20site%20from%20instiki.textile +13 -0
  28. data/template/content/Improving%20the%20style%20of%20this%20wiki.textile +30 -0
  29. data/template/content/Picture%20of%20a%20pair%20of%20soks.textile +1 -0
  30. data/template/content/Pointers%20on%20adjusting%20the%20settings.textile +39 -0
  31. data/template/content/Pointers%20on%20how%20to%20use%20this%20wiki.textile +21 -0
  32. data/template/content/Recent%20Changes%20to%20This%20Site.textile +203 -0
  33. data/template/content/Soks%20Licence.textile +64 -0
  34. data/template/content/home%20page.textile +18 -0
  35. data/template/start.rb +74 -0
  36. data/template/views/AttachmentPage_edit.rhtml +36 -0
  37. data/template/views/ImagePage_edit.rhtml +36 -0
  38. data/template/views/Page_content.rhtml +1 -0
  39. data/template/views/Page_edit.rhtml +34 -0
  40. data/template/views/Page_print.rhtml +5 -0
  41. data/template/views/Page_revisions.rhtml +18 -0
  42. data/template/views/Page_rss.rhtml +34 -0
  43. data/template/views/Page_search_results.rhtml +19 -0
  44. data/template/views/Page_view.rhtml +3 -0
  45. data/template/views/frame.rhtml +34 -0
  46. metadata +88 -0
@@ -0,0 +1,27 @@
1
+ Redcloth License
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice,
7
+ this list of conditions and the following disclaimer.
8
+
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ * Neither the name Textile nor the names of its contributors may be used to
14
+ endorse or promote products derived from this software without specific
15
+ prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,79 @@
1
+ require 'webrick/config'
2
+ require 'webrick/httpstatus'
3
+ require 'webrick/httpauth/authenticator'
4
+ require 'base64'
5
+
6
+ module WEBrick
7
+ module HTTPAuth
8
+ class NotAuthentication
9
+
10
+ include WEBrick::HTTPAuth::Authenticator
11
+
12
+ AuthScheme = "Basic"
13
+
14
+ def initialize( realm = "editing" )
15
+ config = { :UserDB => "nodb" , :Realm => realm }
16
+ check_init(config)
17
+ @config = Config::BasicAuth.dup.update(config)
18
+ end
19
+
20
+ def authenticate(req, res)
21
+ unless basic_credentials = check_scheme(req)
22
+ challenge(req, res)
23
+ end
24
+ userid, password = decode64(basic_credentials).split(":", 2)
25
+ if userid.empty?
26
+ error("user id was not given.")
27
+ challenge(req, res)
28
+ end
29
+ info("%s: authentication succeeded.", userid)
30
+ req.user = userid
31
+ end
32
+
33
+ def challenge(req, res)
34
+ res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
35
+ raise @auth_exception
36
+ end
37
+ end
38
+
39
+ class OnePasswordAuthentication
40
+ include Authenticator
41
+
42
+ AuthScheme = "Basic"
43
+
44
+ attr_reader :realm, :userdb, :logger
45
+
46
+ def initialize( password = "", realm = "editing" )
47
+ config = { :UserDB => "nodb" , :Realm => realm }
48
+ check_init(config)
49
+ @config = Config::BasicAuth.dup.update(config)
50
+ @password = password
51
+ end
52
+
53
+ def authenticate(req, res)
54
+ unless basic_credentials = check_scheme(req)
55
+ challenge(req, res)
56
+ end
57
+ userid, password = decode64(basic_credentials).split(":", 2)
58
+ password ||= ""
59
+ if userid.empty?
60
+ error("user id was not given.")
61
+ challenge(req, res)
62
+ end
63
+
64
+ if password != @password
65
+ error("%s: password unmatch.", userid)
66
+ challenge(req, res)
67
+ end
68
+ info("%s: authentication succeeded.", userid)
69
+ req.user = userid
70
+ end
71
+
72
+ def challenge(req, res)
73
+ res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
74
+ raise @auth_exception
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,321 @@
1
+ require 'soks'
2
+ require 'net/smtp'
3
+
4
+ class AutomaticUpdateCrossLinks
5
+
6
+ def initialize( wiki )
7
+ @wiki = wiki
8
+ @wiki.watch_for( :page_created ) { |event, page| new_page( page ) }
9
+ @wiki.watch_for( :page_deleted ) { |event, page| delete_page( page ) }
10
+ @wiki.watch_for( :page_revised ) { |event, page| page_revised( page ) }
11
+ update_all_pages
12
+ end
13
+
14
+ def new_page( page )
15
+ @wiki.rollingmatch[ page.name ] = page
16
+ titleregex = Regexp.new( page.name, Regexp::IGNORECASE )
17
+ @wiki.each { |name, linkedpage |
18
+ unless name[0,10] == 'Site Index'
19
+ @wiki.refresh_redcloth( linkedpage ) if linkedpage.textile =~ titleregex
20
+ end
21
+ }
22
+ end
23
+
24
+ def delete_page( page )
25
+ @wiki.rollingmatch.delete( page.name )
26
+ page.links_to.each { |linkedpage| @wiki.refresh_redcloth( linkedpage ) }
27
+ end
28
+
29
+ def page_revised( page )
30
+ page.inserted_into.each { |including_page|
31
+ @wiki.refresh_redcloth( including_page )
32
+ }
33
+ end
34
+
35
+ def update_all_pages
36
+ @wiki.each { |pagename, page| @wiki.rollingmatch[ page.name ] = page }
37
+ @wiki.each { |pagename, page| @wiki.refresh_redcloth( page ) }
38
+ end
39
+ end
40
+
41
+ class AutomaticRecentChanges
42
+
43
+ IGNORED_AUTHORS = /^Automatic/
44
+
45
+ def initialize( wiki, lines = 200, pagename = "Recent Changes to This Site", author = "AutomaticRecentChanges" )
46
+ @wiki, @lines, @pagename, @author = wiki, lines, pagename, author
47
+ @wiki.watch_for( :page_revised ) { |event, page, revision| revised( page, revision ) }
48
+ end
49
+
50
+ def revised( page, revision )
51
+ unless ( revision.author =~ IGNORED_AUTHORS )
52
+ add_recent_change( page, revision )
53
+ end
54
+ end
55
+
56
+ def add_recent_change( page, revision )
57
+ changes_content = "On #{revision.created_at.strftime('%Y %b %d')}, #{revision.author} made these changes to [[#{page}]]:\n"
58
+ revision.changes.each do |change_group| change_group.each do |change|
59
+ next if change[2] == ""
60
+ changes_content << case change[0]
61
+ # Note, +/- reversed because diffs stored as diff from CURRENT version
62
+ when "-" ; "| #{change[1]} | -<notextile>#{change[2]}</notextile>- |\n"
63
+ when "+" ; "| #{change[1]} | <notextile>#{change[2]}</notextile> |\n"
64
+ end
65
+ end end
66
+ changes_content << "\n"
67
+ changes_content << @wiki.page( @pagename ).content.first_lines( @lines ) unless @wiki.page( @pagename ).empty?
68
+ @wiki.revise( @pagename , changes_content, @author)
69
+ end
70
+ end
71
+
72
+ class AutomaticOnePageIndex
73
+
74
+ def initialize( wiki, pagename = "Site Index", author = "AutomaticIndex" )
75
+ @wiki, @pagename, @author = wiki, pagename, author
76
+ @wiki.watch_for( :page_created, :page_deleted ) { update_index }
77
+ update_index
78
+ end
79
+
80
+ def update_index
81
+ index = @wiki.sort_by{ |name, page| page.name_for_index.capitalize }.map{ |name, page| "* [[ #{page} ]]\n" }
82
+ @wiki.revise( @pagename, "h1. Index\n\n#{index}", @author )
83
+ end
84
+ end
85
+
86
+ class AutomaticMultiPageIndex < AutomaticOnePageIndex
87
+
88
+ def update_index
89
+ buckets = separate_pages_by_first_character
90
+ link_bar = link_bar_from_buckets( buckets )
91
+ buckets.each do |firstcharacter, pages|
92
+ index = pages.sort_by{ |page| page.name_for_index.capitalize }.map{ |page| "* [[ #{page} ]]\n" }
93
+ @wiki.revise( "#{@pagename} #{firstcharacter}", "h1. Site Index #{firstcharacter}\n\n#{link_bar}\n\n#{index}", @author )
94
+ end
95
+ @wiki.revise( @pagename, "h1. Site Index\n\n#{link_bar}", @author )
96
+ end
97
+
98
+ def separate_pages_by_first_character
99
+ buckets = Hash.new
100
+ @wiki.each do |name, page|
101
+ ( buckets[ page.name_for_index[0,1].upcase ] ||= [] ) << page
102
+ end
103
+ buckets
104
+ end
105
+
106
+ def link_bar_from_buckets( buckets )
107
+ buckets.keys.sort.map { |letter| "[[ #{letter} : #{@pagename} #{letter} ]]\n" }.join(' ')
108
+ end
109
+ end
110
+
111
+ class FiniteUniqueList
112
+ include Enumerable
113
+
114
+ attr_accessor :max_size
115
+
116
+ def initialize( max_size = nil )
117
+ @max_size = max_size
118
+ @list = Array.new
119
+ end
120
+
121
+ def add( item )
122
+ remove( item )
123
+ @list << item
124
+ remove_excess_items
125
+ end
126
+
127
+ def remove( item )
128
+ @list.delete( item )
129
+ end
130
+
131
+ def each
132
+ @list.reverse_each { |item| yield item }
133
+ end
134
+
135
+ def empty?; @list.empty? end
136
+
137
+ private
138
+
139
+ def remove_excess_items
140
+ if @max_size
141
+ while @list.size > @max_size
142
+ @list.shift
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ class AutomaticSummary
149
+
150
+ DEFAULT_SETTINGS = {
151
+ :regexp_for_pages => /.*/,
152
+ :max_pages_to_show => nil,
153
+ :pagename => 'Summary',
154
+ :author => 'AutomaticSummary',
155
+ :lines_to_include => nil,
156
+ :only_new_pages => false,
157
+ }
158
+
159
+ attr_reader :settings, :pages_in_summary
160
+
161
+ def initialize( wiki, settings = {} )
162
+ @wiki = wiki
163
+ @settings = DEFAULT_SETTINGS.merge( settings )
164
+ @pages_in_summary = FiniteUniqueList.new( @settings[:max_pages_to_show] )
165
+ scan_for_pages_to_summarise
166
+ render_summary
167
+ @wiki.watch_for( @settings[:only_new_pages] ? :page_created : :page_revised ) { |event, page| check_whether_to_add_page( page ) }
168
+ @wiki.watch_for( :page_deleted ) { |event, page| check_whether_to_remove_page( page ) }
169
+ end
170
+
171
+ def check_whether_to_add_page( page )
172
+ return if page.name == @settings[:pagename]
173
+ if page.name=~ @settings[:regexp_for_pages]
174
+ @pages_in_summary.add( page )
175
+ render_summary
176
+ end
177
+ end
178
+
179
+ def check_whether_to_remove_page( page )
180
+ render_summary if @pages_in_summary.remove( page )
181
+ end
182
+
183
+ def scan_for_pages_to_summarise
184
+ @wiki.select do |name,page|
185
+ name=~ @settings[:regexp_for_pages]
186
+ end.sort_by do |name, page|
187
+ (@settings[:only_new_pages] ? page.revision[0] : page).created_at
188
+ end.each do |name, page|
189
+ @pages_in_summary.add page
190
+ end
191
+ end
192
+
193
+ def render_summary
194
+ if @pages_in_summary.empty?
195
+ @wiki.revise( @settings[:pagename], "No pages found to summarise" , @settings[:author] )
196
+ else
197
+ content = ""
198
+ @pages_in_summary.each do |page|
199
+ content << "<div class='subpage'>\n\n"
200
+ content << render_page( page )
201
+ content << "\n\np(more). [[(more) : #{page.name}]]\n\n"
202
+ content << "</div>\n"
203
+ end
204
+ @wiki.revise( @settings[:pagename], content , @settings[:author] )
205
+ end
206
+ end
207
+
208
+ def render_page( page )
209
+ if @settings[:lines_to_include]
210
+ page.textile.first_lines( @settings[:lines_to_include] )
211
+ else
212
+ page.textile
213
+ end
214
+ end
215
+ end
216
+
217
+ class AutomaticCalendar
218
+
219
+ attr_reader :month_pagename, :day_pagename
220
+
221
+ def initialize( wiki, month_pagename = '%Y %b', day_pagename = '%Y %b %d', author = "AutomaticCalendar" )
222
+ @wiki, @month_pagename, @day_pagename, @author = wiki, month_pagename, day_pagename, author
223
+ Thread.new do
224
+ loop do
225
+ Time.now.month.upto( Time.now.month+12 ) { |m| render_month( m ) }
226
+ sleep(60*60*24)
227
+ end
228
+ end
229
+ end
230
+
231
+ def render_month( month )
232
+ @wiki.revise( month_pagename( month ), calendar_for( month ) , @author ) unless @wiki.exists?( month_pagename( month ) )
233
+ end
234
+
235
+ def calendar_for( month )
236
+ content = "<div class='calendar'>\n"
237
+ content << "|_. Su |_. Mo |_. Tu |_. We |_. Th |_. Fr |_. Sa |\n"
238
+ 1.upto( time_for( month, 1 ).wday ) { content << "| . " }
239
+ day = nil
240
+ 1.upto( 31 ) do |day_no|
241
+ day = time_for( month, day_no )
242
+ break if day.month > month
243
+ content << "| [[ #{day_no} : #{day_pagename( day )} ]] "
244
+ content << "|\n" if day.wday == 6
245
+ end
246
+ day.wday.upto( 5 ) { content << "| . " }
247
+ content << "|\n"
248
+ content << "\n\n< #{month_pagename( month-1 )} #{month_pagename( month+1 )} >\n"
249
+ end
250
+
251
+ def time_for( month, day = 1 )
252
+ year = Time.now.year
253
+ if month > 12
254
+ year +=1
255
+ month -= 12
256
+ elsif month < 1
257
+ year -= 1
258
+ month = month + 12
259
+ end
260
+ if day > 31
261
+ month += 1
262
+ day -= 31
263
+ end
264
+ Time.local( year, month, day, 8, 0 )
265
+ end
266
+
267
+ def month_pagename( month = Time.now.month ) time_for( month ).strftime(@month_pagename) end
268
+
269
+ def day_pagename( date = Time.now ) date.strftime(@day_pagename) end
270
+ end
271
+
272
+ class AutomaticUpcomingEvents
273
+
274
+ def initialize( wiki, calendar, days_passed = 0, days_future = 7, pagename = 'Upcoming Events', author = "AutomaticUpcomingEvents" )
275
+ @wiki, @calendar, @days_passed, @days__future, @pagename, @author = wiki, calendar, days_passed, days_future, pagename, author
276
+ @wiki.watch_for( :page_revised ) { |event, page| page_revised( page ) }
277
+ Thread.new do
278
+ loop do
279
+ render_upcoming_events
280
+ sleep(60*60)
281
+ end
282
+ end
283
+ end
284
+
285
+ def page_revised( page )
286
+ render_upcoming_events if page.name =~ /^\d\d\d\d ... \d\d/
287
+ end
288
+
289
+ def render_upcoming_events
290
+ content = "<div class='upcomingevents'>\n"
291
+ Time.now.day.upto( Time.now.day+7 ) do |day|
292
+ time = @calendar.time_for( Time.now.month, day )
293
+ content << "| [[ #{relative_day( time ) } : #{@calendar.day_pagename( time )} ]] |"
294
+ content << (@wiki.exists?( @calendar.day_pagename(time) ) ? render_event( @calendar.day_pagename( time ) ) : "&nbsp; |\n")
295
+ end
296
+ content << "\n\np(more). [[(more) : #{@calendar.month_pagename}]]\n\n"
297
+ content << "</div>\n"
298
+ @wiki.revise( "Upcoming Events", content , "AutomaticCalendar" )
299
+ end
300
+
301
+ def relative_day( date )
302
+ case ( (date - @calendar.time_for( Time.now.month, Time.now.day ) ) / ( 60 * 60 * 24 ) ) # Days difference
303
+ when -7..-2 ; date.strftime('Last %A')
304
+ when -1 ; "Yesterday"
305
+ when 0 ; "Today"
306
+ when 1 ; "Tomorrow"
307
+ when 2..7 ; date.strftime('%A')
308
+ else ; date.strftime('%A %d')
309
+ end
310
+ end
311
+
312
+ def render_event( name )
313
+ page = @wiki.page( name )
314
+ headings = page.textile.select { |line| line =~ /^h\d\./ }
315
+ headings = headings.map { |heading| heading.to_s[4..-1].strip }
316
+ headings = [ page.textile[/.*?\n/] ] if headings.empty?
317
+ content = "[[ #{headings.shift} : #{page.name} ]] |\n"
318
+ headings.each { |heading| content << "| &nbsp; | [[ #{heading} : #{page.name} ]] |\n" }
319
+ content
320
+ end
321
+ end
data/lib/soks-model.rb ADDED
@@ -0,0 +1,208 @@
1
+ # Revision stores changes as a diff against the more recent content version
2
+ class Revision
3
+ attr_reader :number, :changes, :created_at, :author
4
+
5
+ def initialize( number, changes, author )
6
+ @number, @changes, @author = number, changes, author
7
+ @created_at = Time.now
8
+ end
9
+
10
+ # Recreates the content of the page when this revision was created, by recursively applying diffs
11
+ # to more recent versions.
12
+ def content( page )
13
+ $stderr.puts "#{number} #{page}"
14
+ page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content( page ) : page.content
15
+ end
16
+
17
+ def previous_content( page )
18
+ content( page ).split("\n").unpatch!(@changes).join("\n")
19
+ end
20
+ end
21
+
22
+ class Page
23
+ attr_reader :name, :content, :revisions
24
+ attr_accessor :links_from, :links_to, :inserted_into
25
+
26
+ # Returns an empty version of itself.
27
+ def self.empty( name )
28
+ empty = self.new( name, "Type what you want here and click save", "NoOne" )
29
+ class << empty
30
+ def textile; "[[Create #{name} (#{self.class}) : /edit/#{name} ]]" end
31
+ def empty?; true; end
32
+ end
33
+ empty
34
+ end
35
+
36
+ def initialize( name, content, author )
37
+ @name, @content, @revisions = name, "", []
38
+ @links_from, @links_to = [], []
39
+ @inserted_into, @watchers = [], []
40
+ revise content, author
41
+ end
42
+
43
+ alias :textile :content
44
+ alias :name_for_index :name
45
+ alias :to_s :name
46
+
47
+ # Revises the content of this page, creating a new revision class that stores the changes
48
+ def revise( content, author )
49
+ changes = @content.split("\n").diff( content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } }
50
+ unless changes.empty?
51
+ @revisions << Revision.new( @revisions.length, changes, author )
52
+ @content = content
53
+ end
54
+ end
55
+
56
+ # Returns the content of this page to that of a previous version
57
+ def rollback( number, author )
58
+ if number < 0
59
+ revise( "page deleted", author )
60
+ else
61
+ revise( @revisions[ number ].content( self ), author )
62
+ end
63
+ end
64
+
65
+ def revision( number ) @revisions[ number ] end
66
+
67
+ def deleted?
68
+ ( content.strip.downcase == 'page deleted' ) ||
69
+ ( content =~ /^content moved to /i )
70
+ end
71
+
72
+ def empty?; @revisions.empty? end
73
+
74
+ def score; @links_from.size + @links_to.size + @watchers.size end
75
+
76
+ def <=>( otherpage ) self.score <=> otherpage.score end
77
+
78
+ def watch( person ) ( @watchers << person ).uniq! end
79
+ def watching?( person ) @watchers.include? person end
80
+ def unwatch( person ) @watchers.delete( person ) end
81
+
82
+ def is_inserted_into( page ) (@inserted_into << page).uniq! end
83
+
84
+ # Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc)
85
+ def method_missing(method_symbol, *args)
86
+ @revisions.last.send( method_symbol, *args )
87
+ end
88
+ end
89
+
90
+ #Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates
91
+ class UploadPage < Page
92
+ end
93
+
94
+ class ImagePage < UploadPage
95
+ def textile()
96
+ deleted? ? content : "!#{content}(#{@name})!:/#{@name.url_encode}\n"
97
+ end
98
+
99
+ def name_for_index; @name[ 10..-1].strip end
100
+ end
101
+
102
+ class AttachmentPage < UploadPage
103
+ def textile()
104
+ deleted? ? content : %Q{"#{@name}":#{content}\n}
105
+ end
106
+ def name_for_index; @name[ 9..-1].strip end
107
+ end
108
+
109
+ class Wiki
110
+ include Enumerable
111
+
112
+ def initialize( folder )
113
+ @folder = folder
114
+ @pages = Hash.new
115
+ @page_classes = [
116
+ [ /^picture of/i, ImagePage ],
117
+ [ /^attached/i, AttachmentPage ],
118
+ [ /.*/, Page ]
119
+ ]
120
+ load_all_pages_in( folder )
121
+ end
122
+
123
+ def page( name )
124
+ @pages[ name.downcase ] || class_for( name ).empty( name )
125
+ end
126
+
127
+ def each
128
+ @pages.each { |name, page| yield [name, page] unless page.deleted? }
129
+ end
130
+
131
+ def exists?( name )
132
+ @pages.include?( name.downcase ) && !page( name.downcase ).deleted?
133
+ end
134
+
135
+ def revise( pagename, content, author )
136
+ mutate( pagename, content, author ) { |page| page.revise( content, author ) }
137
+ end
138
+
139
+ def rollback( pagename, number, author )
140
+ mutate( pagename ) { |page| page.rollback( number, author ) }
141
+ end
142
+
143
+ def watch( pagename, person )
144
+ mutate( pagename ) { |page| page.watch( person ) }
145
+ end
146
+
147
+ def unwatch( pagename, person )
148
+ mutate( pagename ) { |page| page.unwatch( person ) }
149
+ end
150
+
151
+ private
152
+
153
+ def mutate( pagename, content = nil, author = nil )
154
+ page = @pages[ pagename.downcase ]
155
+ if page
156
+ yield page
157
+ save_page page
158
+ elsif content && author
159
+ @pages[ pagename.downcase ] = page = class_for( pagename ).new( pagename, content, author)
160
+ save_page page
161
+ end
162
+ end
163
+
164
+ def load_all_pages_in( folder )
165
+ Dir.foreach( folder ) do |filename|
166
+ $stderr.puts "Loading #{filename}"
167
+ load_page( filename ) if filename =~ /\.textile$/
168
+ end
169
+ $stderr.puts "All loaded"
170
+ end
171
+
172
+ def load_page( filename )
173
+ if File.exists? File.join( @folder, filename )
174
+ page_name = File.basename( filename, '.*').url_decode
175
+ content = IO.readlines( File.join( @folder, filename ) ).join
176
+ revisions = load_revisions_for( filename )
177
+
178
+ page = class_for( page_name ).new( page_name, content, 'imported' )
179
+ page.revisions.replace( revisions ) if revisions
180
+ @pages[ page.name.downcase ] = page
181
+ end
182
+ end
183
+
184
+ def load_revisions_for( page_filename )
185
+ revision_filename = "#{File.basename( page_filename, '.*')}.marshal"
186
+ if File.exists? File.join( @folder, revision_filename )
187
+ File.open( File.join( @folder, revision_filename ) ) do |file|
188
+ return Marshal.load( file )
189
+ end
190
+ end
191
+ return nil
192
+ end
193
+
194
+ def save_page( page )
195
+ File.open( "#{filename_for( page )}.textile", 'w' ) { |file| file.puts page.content }
196
+ File.open( "#{filename_for( page )}.marshal", 'w' ) { |file| Marshal.dump( page.revisions, file ) }
197
+ end
198
+
199
+ def filename_for( page )
200
+ File.join( @folder, "#{page.name.url_encode}" )
201
+ end
202
+
203
+ def class_for( name )
204
+ @page_classes.each do |regex,klass|
205
+ return klass if name =~ regex
206
+ end
207
+ end
208
+ end