# # =r4tw # Author:: Simon Baird # URL:: http://simonbaird.com/r4tw # License:: http://en.wikipedia.org/wiki/MIT_license # r4tw is some ruby classes for manipuating TiddlyWikis and tiddlers. # It is similar to cook and ginsu but cooler. # # <i>$Rev: 2230 $</i> # # ===Known problems # from_remote_tw can be problematic if importing from a 2.1 TW into a 2.2 TW. # #--------------------------------------------------------------------- #-- General purpose utils require 'pathname' require 'open-uri' def read_file(file_name) #:nodoc: File.read(file_name) end def fetch_url(url) #:nodoc: open(url).read.to_s end def this_dir(this_file=$0) #:nodoc: Pathname.new(this_file).expand_path.dirname end class String def to_file(file_name) #:nodoc: File.open(file_name,"w") { |f| f << self } end end #--------------------------------------------------------------------- #-- TiddlyWiki related utils class String def escapeLineBreaks gsub(/\\/m,"\\s").gsub(/\n/m,"\\n").gsub(/\r/m,"") end def unescapeLineBreaks # not sure what \b is for gsub(/\\n/m,"\n").gsub(/\\b/m," ").gsub(/\\s/,"\\").gsub(/\r/m,"") end def encodeHTML gsub(/&/m,"&").gsub(/</m,"<").gsub(/>/m,">").gsub(/\"/m,""") end def decodeHTML gsub(/&/m,"&").gsub(/</m,"<").gsub(/>/m,">").gsub(/"/m,"\"") end def readBrackettedList # scan is a beautiful thing scan(/\[\[([^\]]+)\]\]|(\S+)/).map {|m| m[0]||m[1]} end def toBrackettedList self end end class Array def toBrackettedList map{ |i| (i =~ /\s/) ? ("[["+i+"]]") : i }.join(" ") end end class Time def convertToLocalYYYYMMDDHHMM() self.localtime.strftime("%Y%m%d%H%M") end def convertToYYYYMMDDHHMM() self.utc.strftime("%Y%m%d%H%M") end def Time.convertFromYYYYMMDDHHMM(date_string) m = date_string.match(/(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) Time.utc(m[1],m[2],m[3],m[4],m[5]) end end #--------------------------------------------------------------------- # Tiddler # # =Tiddler # For creating and manipulating tiddlers # ===Example # puts Tiddler.new({'tiddler'=>'Hello','text'=>'Hi there','tags'=>['tag1','tag2']}) class Tiddler @@main_fields = %w[tiddler modifier modified created tags] # and soon to be changecount? # text is not really a field in TiddlyWiki it makes # things easier to make it one here. It could possibly # clash with a real field called text. Ignore this fact for now... @@defaults = { 'tiddler' => 'New Tiddler', 'modified' => Time.now.convertToYYYYMMDDHHMM, 'created' => Time.now.convertToYYYYMMDDHHMM, 'modifier' => 'YourName', 'tags' => '', 'text' => '', } # used by from_file @@default_ext_tag_map = { '.js' => %[systemConfig], '.html' => %[html], '.css' => %[css], '.pub' => %[contentPublisher], '.palette' => %[palette], } attr_accessor :fields # Depending on the arguments this can be used to create or import a tiddler in various ways. # # ===From scratch # If the argument is a Hash then it is used to specify a tiddler to be created from # scratch. # # Example: # t = Tiddler.new.from({ # 'tiddler'=>'HelloThere', # 'text'=>'And welcome', # }) # Other built-in fields are +modified+, +created+, +modifier+ and +tags+. Any other # fields you add will be created as tiddler extended fields. Text is the contents of # the tiddler. Tiddler is the title of the tiddler. # # # ===From a file # If the argument looks like a file name (ie a string that doesn't match the other # criteria then create a tiddler with the name being the file name and the # contents being the contents of the file. Does some guessing about tags based on # the file's extension. (This is customisable, see code for details). Also reads the # file modified date and uses it. # # Example: # t = Tiddler.new.from("myplugin.js") # # ===From a TiddlyWiki # If the argument is in the form file.html#TiddlerName or http://sitename.com/#TiddlerName # then import TiddlerName from the specified location # # Example: # t1 = Tiddler.new.from("myfile.html#SomeTiddler") # t2 = Tiddler.new.from("http://www.tiddlywiki.com/#HelloThere") # # # ===From a url # Creates a tiddler from a url. The entire contents of the page are the contents # of the tiddler. You should set the 'tiddler' field and other fields using a hash # as the second argument in the same format as creating a tiddler from scratch. # There is no automatic tagging for this one so you should add tags yourself as required # # Example: # t = Tiddler.new.from( # "http://svn.somewhere.org/Trunk/HelloWorld.js", # {'tiddler'=>'HelloWorld','tags'=>'systemConfig'} # ) # # # ===From a div string # If the argument is a string containing a tiddler div such # as would be found in a TiddlyWiki storeArea then the tiddler # is created from that div # def initialize(*args) @fields = {} case args[0] when Hash from_scratch(*args) when Tiddler from_tiddler(*args) when /^\s*<div/ from_div(*args) when /#/ from_tiddler(from_tw(*args)) when /^(ftp|http|file):/ from_url(*args) when String from_file(*args) end end # Intende to become private but not yet because # all the test units use them # #private def from_tiddler(other_tiddler) @fields = {} @fields.update(other_tiddler.fields) end def from_scratch(fields={}) #:nodoc: @fields = @@defaults.merge(fields) @fields['tags'] &&= @fields['tags'].toBrackettedList # in case it's an array self end def from_div(div_str,use_pre=false) #:nodoc: match_data = div_str.match(/<div([^>]+)>(.*?)<\/div>/m) field_str = match_data[1] text_str = match_data[2] field_str.scan(/ ([\w\.]+)="([^"]+)"/) do |field_name,field_value| if field_name == "title" field_name = "tiddler" end @fields[field_name] = field_value end text_str.sub!(/\n<pre>/,'') text_str.sub!(/<\/pre>\n/,'') if (use_pre) @fields['text'] = text_str.decodeHTML else @fields['text'] = text_str.unescapeLineBreaks.decodeHTML end self end def from_file(file_name, fields={}, ext_tag_map=@@default_ext_tag_map) #:nodoc: ext = File.extname(file_name) base = File.basename(file_name,ext) @fields = @@defaults.merge(fields) @fields['tiddler'] = base @fields['text'] = read_file(file_name) @fields['created'] = File.mtime(file_name).convertToYYYYMMDDHHMM # @fields['modified'] = @fields['created'] @fields['tags'] = ext_tag_map[ext].toBrackettedList if ext_tag_map[ext] self end def from_url(url,fields={}) #:nodoc: @fields = @@defaults.merge(fields) @fields['text'] = fetch_url(url) self end def from_tw(tiddler_url) #:nodoc: # this works if url is a local file, eg "somefile.html#TiddlerName" # as well as if it's a remote file, eg "http://somewhere.com/#TiddlerName" location,tiddler_name = tiddler_url.split("#") TiddlyWiki.new.source_empty(location).get_tiddler(tiddler_name) end alias from_remote_tw from_tw #:nodoc: alias from_local_tw from_tw #:nodoc: # Returns a hash containing the tiddlers extended fields # Probably would include changecount at this stage at least def extended_fields @fields.keys.reject{ |f| @@main_fields.include?(f) || f == 'text' }.sort end # Converts to a div suitable for a TiddlyWiki store area def to_s(use_pre=false) fields_string = @@main_fields. reject{ |f| use_pre and ( # seems like we have to leave out modified if there is none (f == 'modified' and !@fields[f]) or # seems like we have to not print tags="" any more (f == 'tags' and (!@fields[f] or @fields[f].length == 0)) ) }. map { |f| # support old style tiddler="" # and new style title="" if f == 'tiddler' and use_pre field_name = 'title' else field_name = f end %{#{field_name}="#{@fields[f]}"} } + extended_fields. map{ |f| %{#{f}="#{@fields[f]}"} } if use_pre "<div #{fields_string.join(' ')}>\n<pre>#{@fields['text'].encodeHTML}</pre>\n</div>" else "<div #{fields_string.join(' ')}>#{@fields['text'].escapeLineBreaks.encodeHTML}</div>" end end alias to_div to_s #:nodoc: # Lets you access fields like this: # tiddler.name # tiddler.created # etc # def method_missing(method,*args) method = method.to_s synonyms = { 'name' => 'tiddler', 'title' => 'tiddler', 'content' => 'text', 'body' => 'text', } method = synonyms[method] || method if @@main_fields.include? method or @fields[method] @fields[method] else raise "No such field or method #{method}" end end # Add some text to the end of a tiddler's content def append_content(new_content) @fields['text'] += new_content self end # Add some text to the beginning of a tiddler's content def prepend_content(new_content) @fields['text'] = new_content + @fields['text'] self end # Renames a tiddler def rename(new_name) @fields['tiddler'] = new_name self end # Makes a copy of this tiddler def copy Tiddler.new.from_div(self.to_div) end # Makes a copy of this tiddler with a new title def copy_to(new_title) copy.rename(new_title) end # Adds a tag def add_tag(new_tag) @fields['tags'] = @fields['tags']. readBrackettedList. push(new_tag). uniq. toBrackettedList self end # Adds a list of tags def add_tags(tags) tags.each { |tag| add_tag(tag) } end # Removes a single tag def remove_tag(old_tag) @fields['tags'] = @fields['tags']. readBrackettedList. reject { |tag| tag == old_tag }. toBrackettedList self end # Removes a list of tags def remove_tags(tags) tags.each { |tag| remove_tags(tag) } end # Returns true if a tiddler has a particular tag def has_tag(tag) fields['tags'] && fields['tags'].readBrackettedList.include?(tag) end # Returns a Hash containing all tiddler slices def get_slices if not @slices @slices = {} # look familiar? slice_re = /(?:[\'\/]*~?(\w+)[\'\/]*\:[\'\/]*\s*(.*?)\s*$)|(?:\|[\'\/]*~?(\w+)\:?[\'\/]*\|\s*(.*?)\s*\|)/m text.scan(slice_re).each do |l1,v1,l2,v2| @slices[l1||l2] = v1||v2; end end @slices end # Returns a tiddler slice def get_slice(slice) get_slices[slice] end # # Experimental. Provides access to plugin meta slices. # Returns one meta value or a hash of them if no argument is given # def plugin_meta(slice=nil) # see http://www.tiddlywiki.com/#ExamplePlugin if not @plugin_meta meta = %w[Name Description Version Date Source Author License CoreVersion Browser] @plugin_meta = get_slices.reject{|k,v| not meta.include?(k)} end if slice @plugin_meta[slice] else @plugin_meta end end end #--------------------------------------------------------------------- # =Tiddlywiki # Create and manipulate TiddlyWiki files # class TiddlyWiki attr_accessor :orig_tiddlers, :tiddlers, :raw # doesn't do much. probably should allow an empty file param def initialize(use_pre=false) @use_pre = use_pre @tiddlers = [] end # this should replace all the add_tiddler_from_blah methods # but actually they are still there below # testing required def method_missing(method_name,*args); case method_name.to_s when /^add_tiddler_(.*)$/ add_tiddler(Tiddler.new.send($1,*args)) end end # initialise a TiddlyWiki from a source file # will treat empty_file as a url if it looks like one # note that it doesn't have to be literally empty def source_empty(empty_file,&block) @empty_file = empty_file if empty_file =~ /^https?/ @raw = fetch_url(@empty_file) else @raw = read_file(@empty_file) end # stupid ctrl (\r) char #@raw.eat_ctrl_m! if @raw =~ /var version = \{title: "TiddlyWiki", major: 2, minor: 2/ @use_pre = true end @core_hacks = [] @orig_tiddlers = get_orig_tiddlers @tiddlers = @orig_tiddlers instance_eval(&block) if block self end # reads an empty from a file on disk def source_file(file_name="empty.html") source_empty(file_name) end # reads an empty file from a url def source_url(url="http://www.tiddlywiki.com/empty.html") source_empty(url) end # important regexp # if this doesn't work we are screwed @@store_regexp = /^(.*<div id="storeArea">\n?)(.*)(\n?<\/div>\r?\n<!--.*)$/m # stupid ctrl-m \r char # everything before the store # TODO make these private def pre_store #:nodoc: @raw.sub(@@store_regexp,'\1') end # the store itself def store #:nodoc: @raw.sub(@@store_regexp,'\2') end # everything after the store def post_store #:nodoc: @raw.sub(@@store_regexp,'\3') end # returns an array of tiddler divs def tiddler_divs #:nodoc: ## the old way, one tiddler per line... # store.strip.to_a ## the new way store.scan(/(<div ti[^>]+>.*?<\/div>)/m).map { |m| m[0] } # did I mention that scan is a beautiful thing? end # add a core hack # it will be applied to the entire TW core like this gsub(regexp,replace) def add_core_hack(regexp,replace) # this is always a bad idea... ;) @core_hacks.push([regexp,replace]) end def get_orig_tiddlers #:nodoc: tiddler_divs.map do |tiddler_div| Tiddler.new.from_div(tiddler_div,@use_pre) end end # an array of tiddler titles def tiddler_titles @tiddlers.map { |t| t.name } end # returns an array of tiddlers containing a particular tag def tiddlers_with_tag(tag) @tiddlers.select{|t| t.has_tag(tag)} end # adds a tiddler def add_tiddler(tiddler) remove_tiddler(tiddler.name) @tiddlers << tiddler tiddler end # removes a tiddler by name def remove_tiddler(tiddler_name) @tiddlers.reject!{|t| t.name == tiddler_name} end # adds a shadow tiddler # note that tags and other fields aren't preserved def add_shadow_tiddler(tiddler) # shadow tiddlers currently implemented as core_hacks add_core_hack( /^\/\/ End of scripts\n/m, "\\0\nconfig.shadowTiddlers[\"#{tiddler.name}\"] = #{tiddler.text.dump};\n\n" ) end # adds a shadow tiddler from a file def add_shadow_tiddler_from_file(file_name) add_shadow_tiddler Tiddler.new.from_file("#{file_name}") end # add tiddlers from a list of file names # ignores file names starting with # # so you can do this # %w[ # foo # bar # #baz # ] # and it will skip baz def add_tiddlers(file_names) file_names.reject{|f| f.match(/^#/)}.each do |f| add_tiddler_from_file(f) end end def add_tiddler_from(*args) add_tiddler Tiddler.new(*args) end def add_tiddlers_from(tiddler_list) tiddler_list.each { |t| add_tiddler Tiddler.new(t) } end # add tiddlers from files found in directory dir_name # TODO exclude pattern? def add_tiddlers_from_dir(dir_name) add_tiddlers(Dir.glob("#{dir_name}/*")) end # add shadow tiddlers from files found in directory dir_name def add_shadow_tiddlers_from_dir(dir_name) Dir.glob("#{dir_name}/*").each do |f| add_shadow_tiddler_from_file(f) end end # add a list of files found in dir_name packaged as javascript to create shadow tiddlers # append the javascript to the contents of file_name # (needs more explanation perhaps) # see also package_as def package_as_from_dir(file_name,dir_name) package_as(file_name,Dir.glob("#{dir_name}/*")) end # if you have a file containing just tiddler divs you can read them # all in with this def add_tiddlers_from_file(file_name) # a file full of divs File.read(file_name).to_a.inject([]) do |tiddlers,tiddler_div| @tiddlers << Tiddler.new.from_div(tiddler_div,@use_pre) end end # get a tidler by name def get_tiddler(tiddler_title) @tiddlers.select{|t| t.name == tiddler_title}.first end # output the TiddlyWiki file def to_s pre_store_hacked = pre_store post_store_hacked = post_store @core_hacks.each do |hack| pre_store_hacked.gsub!(hack[0],hack[1]) post_store_hacked.gsub!(hack[0],hack[1]) end "#{pre_store_hacked}#{store_to_s}#{post_store_hacked}" end # output just the contents of the store def store_to_s # not sure about this bit. breaks some tests if I put it in #((@use_pre and @tiddlers.length > 0) ? "\n" : "") + @tiddlers.sort_by{|t| t.name}.inject(""){ |out,t|out << t.to_div(@use_pre) << "\n"} end # writes just the store area to a file # the file can be used with ImportTiddlers to save download bandwidth def store_to_file(file_name) File.open(file_name,"w") { |f| f << "<div id=\"storeArea\">\n#{store_to_s}</div>" } puts "Wrote store only to '#{file_name}'" end # writes just the store contents to a file def store_to_divs(file_name) File.open(file_name,"w") { |f| f << store_to_s } puts "Wrote tiddlers only to '#{file_name}'" end # writes the entire TiddlyWiki to a file def to_file(file_name) File.open(file_name,"w") { |f| f << to_s } puts "Wrote tw file to '#{file_name}'" end # takes a list of file_names, reads their content # and converts them to javascript creation of shadow tiddlers # then appends that to the contents of file_name # (sorry, confusing) def package_as(file_name,package_file_names) new_tiddler = add_tiddler Tiddler.new.from_file(file_name) new_tiddler.append_content(package(package_file_names)) # date of the most recently modified new_tiddler.fields['modified'] = package_file_names.push(file_name).map{|f| File.mtime(f)}.max.convertToYYYYMMDDHHMM end # TODO make private? def package(file_names) #:nodoc: "//{{{\nmerge(config.shadowTiddlers,{\n\n"+ ((file_names.map do |f| Tiddler.new.from_file(f) end).map do |t| "'" + t.name + "':[\n " + t.text.chomp.dump.gsub(/\\t/,"\t").gsub(/\\n/,"\",\n \"").gsub(/\\#/,"#") + "\n].join(\"\\n\")" end).join(",\n\n")+ "\n\n});\n//}}}\n" end # copy all tiddlers from another TW file into this TW # good for creating Tiddlyspot flavours def copy_all_tiddlers_from(file_name) TiddlyWiki.new.source_empty(file_name).tiddlers.each do |t| add_tiddler t end end end # # A short hand for DSL style TiddlyWiki creation. Takes a block of TiddlyWiki methods that get instance_eval'ed # def make_tw(source=nil,&block) tw = TiddlyWiki.new tw.source_empty(source) if source tw.instance_eval(&block) if block tw end