#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