#
# =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,"&amp;").gsub(/</m,"&lt;").gsub(/>/m,"&gt;").gsub(/\"/m,"&quot;")
  end

  def decodeHTML
    gsub(/&amp;/m,"&").gsub(/&lt;/m,"<").gsub(/&gt;/m,">").gsub(/&quot;/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