sj-plist 3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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