# # =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. # # $Rev$ # # ===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,""") 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' => %[systemServer], '.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>/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
/,'')
    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=true)

    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
            (f == 'modifier' 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
      # gotcha: the \n chars were being turned into newlines so don't do it this way:
      #"
\n
#{@fields['text'].encodeHTML}
\n
" "
\n
"+@fields['text'].encodeHTML+"
\n
" else "
"+@fields['text'].escapeLineBreaks.encodeHTML+"
" 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'] = @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=true) @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: [23456]/ # fix me @use_pre = false 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 = /^(.*
\n?)(.*)(\n?<\/div>\r?\n