sj-plist 3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,240 @@
1
+ #--###########################################################
2
+ # Copyright 2006, Ben Bleything <ben@bleything.net> and #
3
+ # Patrick May <patrick@hexane.org> #
4
+ # #
5
+ # Distributed under the MIT license. #
6
+ ##############################################################
7
+ #++
8
+
9
+ require "plist/binary"
10
+
11
+ # See Plist::Emit.
12
+ module Plist
13
+ # === Create a plist
14
+ # You can dump an object to a plist in one of two ways:
15
+ #
16
+ # * <tt>Plist::Emit.dump(obj)</tt>
17
+ # * <tt>obj.to_plist</tt>
18
+ # * This requires that you mixin the <tt>Plist::Emit</tt> module, which is already done for +Array+ and +Hash+.
19
+ #
20
+ # The following Ruby classes are converted into native plist types:
21
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false
22
+ # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the <array> and <dict> containers (respectively).
23
+ # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a <data> element.
24
+ # * User classes may implement +to_plist_node+ to dictate how they should be serialized; otherwise the object will be passed to <tt>Marshal.dump</tt> and the result placed in a <data> element.
25
+ #
26
+ # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below.
27
+ module Emit
28
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.dump</tt> with +self+.
29
+ def to_plist(envelope = true, format = :xml)
30
+ return Plist::Emit.dump(self, envelope, format)
31
+ end
32
+
33
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.save_plist</tt> with +self+.
34
+ def save_plist(filename, format = :xml)
35
+ Plist::Emit.save_plist(self, filename, format)
36
+ end
37
+
38
+ # The following Ruby classes are converted into native plist types:
39
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time
40
+ #
41
+ # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes.
42
+ #
43
+ # +IO+ and +StringIO+ objects are encoded and placed in <data> elements; other objects are <tt>Marshal.dump</tt>'ed unless they implement +to_plist_node+.
44
+ #
45
+ # The +envelope+ parameters dictates whether or not the resultant plist fragment is wrapped in the normal XML/plist header and footer. Set it to false if you only want the fragment.
46
+ def self.dump(obj, envelope = true, format = :xml)
47
+ case format
48
+ when :xml
49
+ output = plist_node(obj)
50
+ output = wrap(output) if envelope
51
+ when :binary
52
+ raise(ArgumentError, "binary plists must have an envelope") unless envelope
53
+ output = Plist::Binary.binary_plist(obj)
54
+ else
55
+ raise(ArgumentError, "unknown plist format `#{format}'")
56
+ end
57
+ return output
58
+ end
59
+
60
+ # Writes the serialized object's plist to the specified filename.
61
+ def self.save_plist(obj, filename, format = :xml)
62
+ File.open(filename, 'wb') do |f|
63
+ case format
64
+ when :xml
65
+ f.write(obj.to_plist(true, format))
66
+ when :binary
67
+ f.write(Plist::Binary.binary_plist(obj))
68
+ else
69
+ raise(ArgumentError, "unknown plist format `#{format}'")
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ HTML_ESCAPE_HASH = {
77
+ "&" => "&amp;",
78
+ "\"" => "&quot;",
79
+ ">" => "&gt;",
80
+ "<" => "&lt;"
81
+ }
82
+
83
+ def self.escape_html(string)
84
+ string.gsub(/(&|\"|>|<)/) do |mtch|
85
+ HTML_ESCAPE_HASH[mtch]
86
+ end
87
+ end
88
+
89
+ def self.plist_node(element)
90
+ output = StringIO.new
91
+
92
+ if element.respond_to? :to_plist_node
93
+ output << element.to_plist_node
94
+ else
95
+ case element
96
+ when Array
97
+ if element.empty?
98
+ output << "<array/>\n"
99
+ else
100
+ output << tag('array') {
101
+ element.collect { |e| plist_node(e) }
102
+ }
103
+ end
104
+ when Hash
105
+ if element.empty?
106
+ output << "<dict/>\n"
107
+ else
108
+ output << tag("dict") {
109
+ s = []
110
+ element.each { |k, v|
111
+ s << tag('key', Emit.escape_html(k.to_s))
112
+ s << plist_node(v)
113
+ }
114
+ s
115
+ }
116
+ end
117
+ when true, false
118
+ output << "<#{element}/>\n"
119
+ when Time
120
+ output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
121
+ when Date # also catches DateTime
122
+ output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ'))
123
+ when String, Symbol, Fixnum, Bignum, Integer, Float
124
+ output << tag(element_type(element), Emit.escape_html(element.to_s))
125
+ when IO, StringIO
126
+ element.rewind
127
+ contents = element.read
128
+ # note that apple plists are wrapped at a different length then
129
+ # what ruby's base64 wraps by default.
130
+ # I used #encode64 instead of #b64encode (which allows a length arg)
131
+ # because b64encode is b0rked and ignores the length arg.
132
+ data = "\n"
133
+ Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
134
+ output << tag('data', data)
135
+ else
136
+ output << comment( 'The <data> element below contains a Ruby object which has been serialized with Marshal.dump.' )
137
+ data = "\n"
138
+ Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
139
+ output << tag('data', data )
140
+ end
141
+ end
142
+ return output.string
143
+ end
144
+
145
+ def self.comment(content)
146
+ return "<!-- #{content} -->\n"
147
+ end
148
+
149
+ def self.indent_level
150
+ @indent_level ||= 0
151
+ end
152
+
153
+ def self.raise_indent_level
154
+ @indent_level = indent_level + 1
155
+ end
156
+
157
+ def self.lower_indent_level
158
+ if (ind = indent_level) > 0
159
+ @indent_level = ind - 1
160
+ end
161
+ end
162
+
163
+ def self.append_indented_to_io(io_object, obj)
164
+ if obj.is_a?(Array)
165
+ obj.each do |o|
166
+ append_indented_to_io(io_object, o)
167
+ end
168
+ else
169
+ unless obj.index("\t") == 0
170
+ indent = "\t" * indent_level
171
+ io_object << "#{indent}#{obj}"
172
+ else
173
+ io_object << obj
174
+ end
175
+ last = obj.length - 1
176
+ io_object << "\n" unless obj[last .. last] == "\n"
177
+ end
178
+ end
179
+
180
+ def self.tag(type, contents = '', &block)
181
+ out = nil
182
+ if block_given?
183
+
184
+ sio = StringIO.new
185
+ append_indented_to_io(sio, "<#{type}>")
186
+ raise_indent_level
187
+
188
+ append_indented_to_io(sio, block.call)
189
+
190
+ lower_indent_level
191
+ append_indented_to_io(sio, "</#{type}>")
192
+ out = sio.string
193
+
194
+ else
195
+ out = "<#{type}>#{contents.to_s}</#{type}>\n"
196
+ end
197
+ return out.to_s
198
+ end
199
+
200
+ def self.wrap(contents)
201
+ output = StringIO.new
202
+
203
+ output << '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
204
+ output << '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n"
205
+ output << '<plist version="1.0">' + "\n"
206
+
207
+ output << contents
208
+
209
+ output << '</plist>' + "\n"
210
+
211
+ output.string
212
+ end
213
+
214
+ def self.element_type(item)
215
+ return case item
216
+ when String, Symbol then 'string'
217
+ when Fixnum, Bignum, Integer then 'integer'
218
+ when Float then 'real'
219
+ else
220
+ raise "Don't know about this data type... something must be wrong!"
221
+ end
222
+ end
223
+
224
+ end
225
+ end
226
+
227
+ # we need to add this so sorting hash keys works properly
228
+ class Symbol #:nodoc:
229
+ def <=> (other)
230
+ self.to_s <=> other.to_s
231
+ end
232
+ end
233
+
234
+ class Array #:nodoc:
235
+ include Plist::Emit
236
+ end
237
+
238
+ class Hash #:nodoc:
239
+ include Plist::Emit
240
+ end
@@ -0,0 +1,224 @@
1
+ #--###########################################################
2
+ # Copyright 2006, Ben Bleything <ben@bleything.net> and #
3
+ # Patrick May <patrick@hexane.org> #
4
+ # #
5
+ # Distributed under the MIT license. #
6
+ ##############################################################
7
+ #++
8
+ # Plist parses Mac OS X xml property list files into ruby data structures.
9
+ #
10
+ # === Load a plist file
11
+ # This is the main point of the library:
12
+ #
13
+ # r = Plist::parse_xml( filename_or_xml )
14
+ module Plist
15
+ # Note that I don't use these two elements much:
16
+ #
17
+ # + Date elements are returned as DateTime objects.
18
+ # + Data elements are implemented as Tempfiles
19
+ #
20
+ # Plist::parse_xml will blow up if it encounters a data element.
21
+ # If you encounter such an error, or if you have a Date element which
22
+ # can't be parsed into a Time object, please send your plist file to
23
+ # plist@hexane.org so that I can implement the proper support.
24
+ def Plist::parse_xml( filename_or_xml )
25
+ listener = Listener.new
26
+ #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
27
+ parser = StreamParser.new(filename_or_xml, listener)
28
+ parser.parse
29
+ listener.result
30
+ end
31
+
32
+ class Listener
33
+ #include REXML::StreamListener
34
+
35
+ attr_accessor :result, :open
36
+
37
+ def initialize
38
+ @result = nil
39
+ @open = Array.new
40
+ end
41
+
42
+
43
+ def tag_start(name, attributes)
44
+ @open.push PTag::mappings[name].new
45
+ end
46
+
47
+ def text( contents )
48
+ @open.last.text = contents if @open.last
49
+ end
50
+
51
+ def tag_end(name)
52
+ last = @open.pop
53
+ if @open.empty?
54
+ @result = last.to_ruby
55
+ else
56
+ @open.last.children.push last
57
+ end
58
+ end
59
+ end
60
+
61
+ class StreamParser
62
+ def initialize( plist_data_or_file, listener )
63
+ if plist_data_or_file.respond_to? :read
64
+ @xml = plist_data_or_file.read
65
+ elsif File.exists? plist_data_or_file
66
+ @xml = File.read( plist_data_or_file )
67
+ else
68
+ @xml = plist_data_or_file
69
+ end
70
+
71
+ @listener = listener
72
+ end
73
+
74
+ TEXT = /([^<]+)/
75
+ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um
76
+ DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
77
+ COMMENT_START = /\A<!--/u
78
+ COMMENT_END = /.*?-->/um
79
+
80
+
81
+ def parse
82
+ plist_tags = PTag::mappings.keys.join('|')
83
+ start_tag = /<(#{plist_tags})([^>]*)>/i
84
+ end_tag = /<\/(#{plist_tags})[^>]*>/i
85
+
86
+ require 'strscan'
87
+
88
+ @scanner = StringScanner.new( @xml )
89
+ until @scanner.eos?
90
+ if @scanner.scan(COMMENT_START)
91
+ @scanner.scan(COMMENT_END)
92
+ elsif @scanner.scan(XMLDECL_PATTERN)
93
+ elsif @scanner.scan(DOCTYPE_PATTERN)
94
+ elsif @scanner.scan(start_tag)
95
+ @listener.tag_start(@scanner[1], nil)
96
+ if (@scanner[2] =~ /\/$/)
97
+ @listener.tag_end(@scanner[1])
98
+ end
99
+ elsif @scanner.scan(TEXT)
100
+ @listener.text(@scanner[1])
101
+ elsif @scanner.scan(end_tag)
102
+ @listener.tag_end(@scanner[1])
103
+ else
104
+ raise "Unimplemented element"
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class PTag
111
+ @@mappings = { }
112
+ def PTag::mappings
113
+ @@mappings
114
+ end
115
+
116
+ def PTag::inherited( sub_class )
117
+ key = sub_class.to_s.downcase
118
+ key.gsub!(/^plist::/, '' )
119
+ key.gsub!(/^p/, '') unless key == "plist"
120
+
121
+ @@mappings[key] = sub_class
122
+ end
123
+
124
+ attr_accessor :text, :children
125
+ def initialize
126
+ @children = Array.new
127
+ end
128
+
129
+ def to_ruby
130
+ raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}"
131
+ end
132
+ end
133
+
134
+ class PList < PTag
135
+ def to_ruby
136
+ children.first.to_ruby if children.first
137
+ end
138
+ end
139
+
140
+ class PDict < PTag
141
+ def to_ruby
142
+ dict = Hash.new
143
+ key = nil
144
+
145
+ children.each do |c|
146
+ if key.nil?
147
+ key = c.to_ruby
148
+ else
149
+ dict[key] = c.to_ruby
150
+ key = nil
151
+ end
152
+ end
153
+
154
+ dict
155
+ end
156
+ end
157
+
158
+ class PKey < PTag
159
+ def to_ruby
160
+ CGI::unescapeHTML(text || '')
161
+ end
162
+ end
163
+
164
+ class PString < PTag
165
+ def to_ruby
166
+ CGI::unescapeHTML(text || '')
167
+ end
168
+ end
169
+
170
+ class PArray < PTag
171
+ def to_ruby
172
+ children.collect do |c|
173
+ c.to_ruby
174
+ end
175
+ end
176
+ end
177
+
178
+ class PInteger < PTag
179
+ def to_ruby
180
+ text.to_i
181
+ end
182
+ end
183
+
184
+ class PTrue < PTag
185
+ def to_ruby
186
+ true
187
+ end
188
+ end
189
+
190
+ class PFalse < PTag
191
+ def to_ruby
192
+ false
193
+ end
194
+ end
195
+
196
+ class PReal < PTag
197
+ def to_ruby
198
+ text.to_f
199
+ end
200
+ end
201
+
202
+ require 'date'
203
+ class PDate < PTag
204
+ def to_ruby
205
+ DateTime.parse(text)
206
+ end
207
+ end
208
+
209
+ require 'base64'
210
+ class PData < PTag
211
+ def to_ruby
212
+ data = Base64.decode64(text.gsub(/\s+/, ''))
213
+
214
+ begin
215
+ return Marshal.load(data)
216
+ rescue Exception => e
217
+ io = StringIO.new
218
+ io.write data
219
+ io.rewind
220
+ return io
221
+ end
222
+ end
223
+ end
224
+ end