Soks 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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