# Revision stores changes as a diff against the more recent content version class Revision attr_reader :number, :changes, :created_at, :author def initialize( number, changes, author ) @number, @changes, @author = number, changes, author @created_at = Time.now end # Recreates the content of the page when this revision was created, by recursively applying diffs # to more recent versions. def content( page ) $stderr.puts "#{number} #{page}" page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content( page ) : page.content end def previous_content( page ) content( page ).split("\n").unpatch!(@changes).join("\n") end end class Page attr_reader :name, :content, :revisions attr_accessor :links_from, :links_to, :inserted_into # Returns an empty version of itself. def self.empty( name ) empty = self.new( name, "Type what you want here and click save", "NoOne" ) class << empty def textile; "[[Create #{name} (#{self.class}) : /edit/#{name} ]]" end def empty?; true; end end empty end def initialize( name, content, author ) @name, @content, @revisions = name, "", [] @links_from, @links_to = [], [] @inserted_into, @watchers = [], [] revise content, author end alias :textile :content alias :name_for_index :name alias :to_s :name # Revises the content of this page, creating a new revision class that stores the changes def revise( content, author ) changes = @content.split("\n").diff( content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } } unless changes.empty? @revisions << Revision.new( @revisions.length, changes, author ) @content = content end end # Returns the content of this page to that of a previous version def rollback( number, author ) if number < 0 revise( "page deleted", author ) else revise( @revisions[ number ].content( self ), author ) end end def revision( number ) @revisions[ number ] end def deleted? ( content.strip.downcase == 'page deleted' ) || ( content =~ /^content moved to /i ) end def empty?; @revisions.empty? end def score; @links_from.size + @links_to.size + @watchers.size end def <=>( otherpage ) self.score <=> otherpage.score end def watch( person ) ( @watchers << person ).uniq! end def watching?( person ) @watchers.include? person end def unwatch( person ) @watchers.delete( person ) end def is_inserted_into( page ) (@inserted_into << page).uniq! end # Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc) def method_missing(method_symbol, *args) @revisions.last.send( method_symbol, *args ) end end #Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates class UploadPage < Page end class ImagePage < UploadPage def textile() deleted? ? content : "!#{content}(#{@name})!:/#{@name.url_encode}\n" end def name_for_index; @name[ 10..-1].strip end end class AttachmentPage < UploadPage def textile() deleted? ? content : %Q{"#{@name}":#{content}\n} end def name_for_index; @name[ 9..-1].strip end end class Wiki include Enumerable def initialize( folder ) @folder = folder @pages = Hash.new @page_classes = [ [ /^picture of/i, ImagePage ], [ /^attached/i, AttachmentPage ], [ /.*/, Page ] ] load_all_pages_in( folder ) end def page( name ) @pages[ name.downcase ] || class_for( name ).empty( name ) end def each @pages.each { |name, page| yield [name, page] unless page.deleted? } end def exists?( name ) @pages.include?( name.downcase ) && !page( name.downcase ).deleted? end def revise( pagename, content, author ) mutate( pagename, content, author ) { |page| page.revise( content, author ) } end def rollback( pagename, number, author ) mutate( pagename ) { |page| page.rollback( number, author ) } end def watch( pagename, person ) mutate( pagename ) { |page| page.watch( person ) } end def unwatch( pagename, person ) mutate( pagename ) { |page| page.unwatch( person ) } end private def mutate( pagename, content = nil, author = nil ) page = @pages[ pagename.downcase ] if page yield page save_page page elsif content && author @pages[ pagename.downcase ] = page = class_for( pagename ).new( pagename, content, author) save_page page end end def load_all_pages_in( folder ) Dir.foreach( folder ) do |filename| $stderr.puts "Loading #{filename}" load_page( filename ) if filename =~ /\.textile$/ end $stderr.puts "All loaded" end def load_page( filename ) if File.exists? File.join( @folder, filename ) page_name = File.basename( filename, '.*').url_decode content = IO.readlines( File.join( @folder, filename ) ).join revisions = load_revisions_for( filename ) page = class_for( page_name ).new( page_name, content, 'imported' ) page.revisions.replace( revisions ) if revisions @pages[ page.name.downcase ] = page end end def load_revisions_for( page_filename ) revision_filename = "#{File.basename( page_filename, '.*')}.marshal" if File.exists? File.join( @folder, revision_filename ) File.open( File.join( @folder, revision_filename ) ) do |file| return Marshal.load( file ) end end return nil end def save_page( page ) File.open( "#{filename_for( page )}.textile", 'w' ) { |file| file.puts page.content } File.open( "#{filename_for( page )}.marshal", 'w' ) { |file| Marshal.dump( page.revisions, file ) } end def filename_for( page ) File.join( @folder, "#{page.name.url_encode}" ) end def class_for( name ) @page_classes.each do |regex,klass| return klass if name =~ regex end end end