#require 'rexml/document' #require 'rexml/streamlistener' # Plist parses Mac OS X xml property list files into ruby data structures. # # === Load a plist file # This is the main point of the library: # # r = Plist::parse_xml( filename_or_xml ) # # === Save a plist # You can turn the variables back into a plist string: # # r.to_plist # # There is a convenience method for saving a variable to a file: # # r.save_plist(filename) # # Only these ruby types can be converted into a plist: # # String # Float # DateTime # Integer # FalseClass # TrueClass # Array # Hash # # Note that Array and Hash are recursive -- the elements of an Array and the values of a Hash # must convert to a plist. Also note that the keys of the Hash must be strings. # # If you have suggestions for mapping other Ruby types to the plist types, send a note to: # # mailto:plist@hexane.org # # I'll take a look and probably add it, I'm just reticent to create too many # "convenience" methods without at least agreeing with someone :-) # # === Credits # plist.rb has been implemented by Patrick May. A few other folks have been helpful in developing plist.rb: # # + Martin Dittus, who pointed out that Time wasn't enough for plist Dates, # especially those in "~/Library/Cookies/Cookies.plist" # # + Chuck Remes, who pushed me towards implementing #to_plist class Plist TEMPLATE = <<-XML %plist% XML def Plist::_xml( xml ) TEMPLATE.sub( /%plist%/, xml ) end # Note that I don't use these two elements much: # # + Date elements are returned as DateTime objects. # + Data elements are not yet implemented. # # Plist::parse_xml will blow up if it encounters a data element. # If you encounter such an error, or if you have a Date element which # can't be parsed into a Time object, please send your plist file to # plist@hexane.org so that I can implement the proper support. def Plist::parse_xml( filename_or_xml ) listener = Listener.new #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) parser = StreamParser.new(filename_or_xml, listener) parser.parse listener.result end class Listener #include REXML::StreamListener attr_accessor :result, :open def initialize @result = nil @open = Array.new end def tag_start(name, attributes) @open.push PTag::mappings[name].new end def text( contents ) @open.last.text = contents if @open.last end def tag_end(name) last = @open.pop if @open.empty? @result = last.to_ruby else @open.last.children.push last end end end class StreamParser def initialize( filename_or_xml, listener ) @filename_or_xml = filename_or_xml @listener = listener end TEXT = /([^<]+)/ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um DOCTYPE_PATTERN = /\s*)/um def parse plist_tags = PTag::mappings.keys.join('|') start_tag = /<(#{plist_tags})([^>]*)>/i end_tag = /<\/(#{plist_tags})[^>]*>/i require 'strscan' @scanner = StringScanner.new( if (File.exists? @filename_or_xml) File.open(@filename_or_xml, "r") {|f| f.read} else @filename_or_xml end ) until @scanner.eos? if @scanner.scan(XMLDECL_PATTERN) elsif @scanner.scan(DOCTYPE_PATTERN) elsif @scanner.scan(start_tag) @listener.tag_start(@scanner[1], nil) if (@scanner[2] =~ /\/$/) @listener.tag_end(@scanner[1]) end elsif @scanner.scan(TEXT) @listener.text(@scanner[1]) elsif @scanner.scan(end_tag) @listener.tag_end(@scanner[1]) else raise "Unimplemented element" end end end end class PTag @@mappings = { } def PTag::mappings @@mappings end def PTag::inherited( sub_class ) key = sub_class.to_s.downcase key.gsub!(/^plist::/, '' ) key.gsub!(/^p/, '') unless key == "plist" @@mappings[key] = sub_class end attr_accessor :text, :children def initialize @children = Array.new end def to_ruby raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}" end end class PList < PTag def to_ruby children.first.to_ruby end end class PDict < PTag def to_ruby dict = Hash.new key = nil children.each do |c| if key.nil? key = c.to_ruby else dict[key] = c.to_ruby key = nil end end dict end end class PKey < PTag def to_ruby text end end class PString < PTag def to_ruby text || '' end end class PArray < PTag def to_ruby children.collect do |c| c.to_ruby end end end class PInteger < PTag def to_ruby text.to_i end end class PTrue < PTag def to_ruby true end end class PFalse < PTag def to_ruby false end end class PReal < PTag def to_ruby text.to_f end end require 'date' class PDate < PTag def to_ruby DateTime.parse(text) end end module Emit def save_plist(filename) File.open(filename, 'wb') do |f| f.write(self.to_plist) end end # Only the expected classes can be emitted as a plist: # String, Float, DateTime, Integer, TrueClass, FalseClass, Array, Hash # # Write me if you think another class can be coerced safely into one of the # expected plist classes (plist@hexane.org) def to_plist Plist::_xml(self.to_plist_fragment) end end end class String include Plist::Emit def to_plist_fragment "#{self}" end end class Float include Plist::Emit def to_plist_fragment "#{self}" end end class DateTime include Plist::Emit def to_plist_fragment "#{self}" end end class Integer include Plist::Emit def to_plist_fragment "#{self}" end end class FalseClass include Plist::Emit def to_plist_fragment "" end end class TrueClass include Plist::Emit def to_plist_fragment "" end end class Array include Plist::Emit def to_plist_fragment fragment = "\n" self.each do |e| element_plist = e.to_plist_fragment element_plist.each do |l| fragment += " #{l}\n" end end fragment += "" fragment end end class Hash include Plist::Emit def to_plist_fragment fragment = "\n" self.keys.sort.each do |k| fragment += " #{k}\n" element_plist = self[k].to_plist_fragment element_plist.each do |l| fragment += " #{l}\n" end end fragment += "" fragment end end