# # =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*
/,'')
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