plist 3.1.0 → 3.6.0

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2a76784a6b7e953d1cd9b6deaf390eb7556fd0df83e8970db7ac4640c71493b4
4
+ data.tar.gz: 8324c2b332839aa772eb77a4a760b24e43e24c363c99d01c2b2b4b11e311938a
5
+ SHA512:
6
+ metadata.gz: c33686b606d723821333314b4a18fe6ac39d65a3712aa1cd43896dbbd4bfb95130aa0a1f62b4a3b260c2a633d44681eec0766fef5651ae70580340a905e4f6e4
7
+ data.tar.gz: 5b63b55469235ec79f14af6f25c05cbc8e6088ce9532e9dfc5aa0887e533a7ee67424953d62e0b3dfe046c7745d9a2add1e0b8ddf07c34f46c5a551c92a9a158
File without changes
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
2
- #
1
+ # encoding: utf-8
2
+
3
3
  # = plist
4
4
  #
5
5
  # This is the main file for plist. Everything interesting happens in
@@ -13,9 +13,6 @@ require 'base64'
13
13
  require 'cgi'
14
14
  require 'stringio'
15
15
 
16
- require 'plist/generator'
17
- require 'plist/parser'
18
-
19
- module Plist
20
- VERSION = '3.1.0'
21
- end
16
+ require_relative 'plist/generator'
17
+ require_relative 'plist/parser'
18
+ require_relative 'plist/version'
@@ -1,222 +1,173 @@
1
- #!/usr/bin/env ruby
2
- #
1
+ # encoding: utf-8
2
+
3
3
  # = plist
4
4
  #
5
5
  # Copyright 2006-2010 Ben Bleything and Patrick May
6
6
  # Distributed under the MIT License
7
7
  #
8
8
 
9
- module Plist ; end
10
-
11
- # === Create a plist
12
- # You can dump an object to a plist in one of two ways:
13
- #
14
- # * <tt>Plist::Emit.dump(obj)</tt>
15
- # * <tt>obj.to_plist</tt>
16
- # * This requires that you mixin the <tt>Plist::Emit</tt> module, which is already done for +Array+ and +Hash+.
17
- #
18
- # The following Ruby classes are converted into native plist types:
19
- # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false
20
- # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the <array> and <dict> containers (respectively).
21
- # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a <data> element.
22
- # * 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.
23
- #
24
- # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below.
25
- module Plist::Emit
26
- # Helper method for injecting into classes. Calls <tt>Plist::Emit.dump</tt> with +self+.
27
- def to_plist(envelope = true)
28
- return Plist::Emit.dump(self, envelope)
29
- end
30
-
31
- # Helper method for injecting into classes. Calls <tt>Plist::Emit.save_plist</tt> with +self+.
32
- def save_plist(filename)
33
- Plist::Emit.save_plist(self, filename)
34
- end
35
-
36
- # The following Ruby classes are converted into native plist types:
37
- # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time
9
+ module Plist
10
+ # === Create a plist
11
+ # You can dump an object to a plist in one of two ways:
38
12
  #
39
- # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes.
13
+ # * <tt>Plist::Emit.dump(obj)</tt>
14
+ # * <tt>obj.to_plist</tt>
15
+ # * This requires that you mixin the <tt>Plist::Emit</tt> module, which is already done for +Array+ and +Hash+.
40
16
  #
41
- # +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+.
17
+ # The following Ruby classes are converted into native plist types:
18
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false
19
+ # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the <array> and <dict> containers (respectively).
20
+ # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a <data> element.
21
+ # * 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.
42
22
  #
43
- # 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.
44
- def self.dump(obj, envelope = true)
45
- output = plist_node(obj)
23
+ # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below.
24
+ module Emit
25
+ DEFAULT_INDENT = "\t"
46
26
 
47
- output = wrap(output) if envelope
27
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.dump</tt> with +self+.
28
+ def to_plist(envelope = true, options = {})
29
+ Plist::Emit.dump(self, envelope, options)
30
+ end
48
31
 
49
- return output
50
- end
32
+ # Helper method for injecting into classes. Calls <tt>Plist::Emit.save_plist</tt> with +self+.
33
+ def save_plist(filename, options = {})
34
+ Plist::Emit.save_plist(self, filename, options)
35
+ end
51
36
 
52
- # Writes the serialized object's plist to the specified filename.
53
- def self.save_plist(obj, filename)
54
- File.open(filename, 'wb') do |f|
55
- f.write(obj.to_plist)
37
+ # The following Ruby classes are converted into native plist types:
38
+ # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time
39
+ #
40
+ # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes.
41
+ #
42
+ # +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+.
43
+ #
44
+ # 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.
45
+ def self.dump(obj, envelope = true, options = {})
46
+ options = { :indent => DEFAULT_INDENT }.merge(options)
47
+
48
+ output = PlistBuilder.new(options[:indent]).build(obj)
49
+ output = wrap(output) if envelope
50
+
51
+ output
56
52
  end
57
- end
58
53
 
59
- private
60
- def self.plist_node(element)
61
- output = ''
62
-
63
- if element.respond_to? :to_plist_node
64
- output << element.to_plist_node
65
- else
66
- case element
67
- when Array
68
- if element.empty?
69
- output << "<array/>\n"
54
+ # Writes the serialized object's plist to the specified filename.
55
+ def self.save_plist(obj, filename, options = {})
56
+ File.open(filename, 'wb') do |f|
57
+ f.write(obj.to_plist(true, options))
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ class PlistBuilder
64
+ def initialize(indent_str)
65
+ @indent_str = indent_str.to_s
66
+ end
67
+
68
+ def build(element, level=0)
69
+ if element.respond_to? :to_plist_node
70
+ element.to_plist_node
70
71
  else
71
- output << tag('array') {
72
- element.collect {|e| plist_node(e)}
73
- }
72
+ case element
73
+ when Array
74
+ if element.empty?
75
+ tag('array', nil, level)
76
+ else
77
+ tag('array', nil, level) {
78
+ element.collect {|e| build(e, level + 1) }.join
79
+ }
80
+ end
81
+ when Hash
82
+ if element.empty?
83
+ tag('dict', nil, level)
84
+ else
85
+ tag('dict', '', level) do
86
+ element.sort_by{|k,v| k.to_s }.collect do |k,v|
87
+ tag('key', CGI.escapeHTML(k.to_s), level + 1) +
88
+ build(v, level + 1)
89
+ end.join
90
+ end
91
+ end
92
+ when true, false
93
+ tag(element, nil, level)
94
+ when Time
95
+ tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ'), level)
96
+ when Date # also catches DateTime
97
+ tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ'), level)
98
+ when String, Symbol, Integer, Float
99
+ tag(element_type(element), CGI.escapeHTML(element.to_s), level)
100
+ when IO, StringIO
101
+ data = element.tap(&:rewind).read
102
+ data_tag(data, level)
103
+ else
104
+ data = Marshal.dump(element)
105
+ comment_tag('The <data> element below contains a Ruby object which has been serialized with Marshal.dump.') +
106
+ data_tag(data, level)
107
+ end
74
108
  end
75
- when Hash
76
- if element.empty?
77
- output << "<dict/>\n"
78
- else
79
- inner_tags = []
109
+ end
80
110
 
81
- element.keys.sort.each do |k|
82
- v = element[k]
83
- inner_tags << tag('key', CGI::escapeHTML(k.to_s))
84
- inner_tags << plist_node(v)
85
- end
111
+ private
86
112
 
87
- output << tag('dict') {
88
- inner_tags
89
- }
113
+ def tag(type, contents, level, &block)
114
+ if block_given?
115
+ indent("<#{type}>\n", level) +
116
+ block.call +
117
+ indent("</#{type}>\n", level)
118
+ elsif contents.to_s.empty?
119
+ indent("<#{type}/>\n", level)
120
+ else
121
+ indent("<#{type}>#{contents.to_s}</#{type}>\n", level)
90
122
  end
91
- when true, false
92
- output << "<#{element}/>\n"
93
- when Time
94
- output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ'))
95
- when Date # also catches DateTime
96
- output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ'))
97
- when String, Symbol, Fixnum, Bignum, Integer, Float
98
- output << tag(element_type(element), CGI::escapeHTML(element.to_s))
99
- when IO, StringIO
100
- element.rewind
101
- contents = element.read
123
+ end
124
+
125
+ def data_tag(data, level)
102
126
  # note that apple plists are wrapped at a different length then
103
127
  # what ruby's base64 wraps by default.
104
128
  # I used #encode64 instead of #b64encode (which allows a length arg)
105
129
  # because b64encode is b0rked and ignores the length arg.
106
- data = "\n"
107
- Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
108
- output << tag('data', data)
109
- else
110
- output << comment( 'The <data> element below contains a Ruby object which has been serialized with Marshal.dump.' )
111
- data = "\n"
112
- Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" }
113
- output << tag('data', data )
130
+ tag('data', nil, level) do
131
+ Base64.encode64(data)
132
+ .gsub(/\s+/, '')
133
+ .scan(/.{1,68}/o)
134
+ .collect { |line| indent(line, level) }
135
+ .join("\n")
136
+ .concat("\n")
137
+ end
114
138
  end
115
- end
116
-
117
- return output
118
- end
119
-
120
- def self.comment(content)
121
- return "<!-- #{content} -->\n"
122
- end
123
-
124
- def self.tag(type, contents = '', &block)
125
- out = nil
126
-
127
- if block_given?
128
- out = IndentedString.new
129
- out << "<#{type}>"
130
- out.raise_indent
131
-
132
- out << block.call
133
-
134
- out.lower_indent
135
- out << "</#{type}>"
136
- else
137
- out = "<#{type}>#{contents.to_s}</#{type}>\n"
138
- end
139
-
140
- return out.to_s
141
- end
142
-
143
- def self.wrap(contents)
144
- output = ''
145
-
146
- output << '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
147
- output << '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n"
148
- output << '<plist version="1.0">' + "\n"
149
-
150
- output << contents
151
-
152
- output << '</plist>' + "\n"
153
-
154
- return output
155
- end
156
-
157
- def self.element_type(item)
158
- case item
159
- when String, Symbol
160
- 'string'
161
-
162
- when Fixnum, Bignum, Integer
163
- 'integer'
164
-
165
- when Float
166
- 'real'
167
139
 
168
- else
169
- raise "Don't know about this data type... something must be wrong!"
170
- end
171
- end
172
- private
173
- class IndentedString #:nodoc:
174
- attr_accessor :indent_string
175
-
176
- def initialize(str = "\t")
177
- @indent_string = str
178
- @contents = ''
179
- @indent_level = 0
180
- end
181
-
182
- def to_s
183
- return @contents
184
- end
185
-
186
- def raise_indent
187
- @indent_level += 1
188
- end
189
-
190
- def lower_indent
191
- @indent_level -= 1 if @indent_level > 0
192
- end
193
-
194
- def <<(val)
195
- if val.is_a? Array
196
- val.each do |f|
197
- self << f
198
- end
199
- else
200
- # if it's already indented, don't bother indenting further
201
- unless val =~ /\A#{@indent_string}/
202
- indent = @indent_string * @indent_level
140
+ def indent(str, level)
141
+ @indent_str.to_s * level + str
142
+ end
203
143
 
204
- @contents << val.gsub(/^/, indent)
144
+ def element_type(item)
145
+ case item
146
+ when String, Symbol
147
+ 'string'
148
+ when Integer
149
+ 'integer'
150
+ when Float
151
+ 'real'
205
152
  else
206
- @contents << val
153
+ raise "Don't know about this data type... something must be wrong!"
207
154
  end
155
+ end
208
156
 
209
- # it already has a newline, don't add another
210
- @contents << "\n" unless val =~ /\n$/
157
+ def comment_tag(content)
158
+ return "<!-- #{content} -->\n"
211
159
  end
212
160
  end
213
- end
214
- end
215
161
 
216
- # we need to add this so sorting hash keys works properly
217
- class Symbol #:nodoc:
218
- def <=> (other)
219
- self.to_s <=> other.to_s
162
+ def self.wrap(contents)
163
+ output = '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
164
+ output << '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n"
165
+ output << '<plist version="1.0">' + "\n"
166
+ output << contents
167
+ output << '</plist>' + "\n"
168
+
169
+ output
170
+ end
220
171
  end
221
172
  end
222
173
 
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
2
- #
1
+ # encoding: utf-8
2
+
3
3
  # = plist
4
4
  #
5
5
  # Copyright 2006-2010 Ben Bleything and Patrick May
@@ -11,42 +11,48 @@
11
11
  # === Load a plist file
12
12
  # This is the main point of the library:
13
13
  #
14
- # r = Plist::parse_xml( filename_or_xml )
14
+ # r = Plist.parse_xml(filename_or_xml)
15
15
  module Plist
16
- # Note that I don't use these two elements much:
17
- #
18
- # + Date elements are returned as DateTime objects.
19
- # + Data elements are implemented as Tempfiles
20
- #
21
- # Plist::parse_xml will blow up if it encounters a data element.
22
- # If you encounter such an error, or if you have a Date element which
23
- # can't be parsed into a Time object, please send your plist file to
24
- # plist@hexane.org so that I can implement the proper support.
25
- def Plist::parse_xml( filename_or_xml )
16
+ # Raised when an element is not implemented
17
+ class UnimplementedElementError < RuntimeError; end
18
+
19
+ # Note that I don't use these two elements much:
20
+ #
21
+ # + Date elements are returned as DateTime objects.
22
+ # + Data elements are implemented as Tempfiles
23
+ #
24
+ # Plist.parse_xml will blow up if it encounters a Date element.
25
+ # If you encounter such an error, or if you have a Date element which
26
+ # can't be parsed into a Time object, please create an issue
27
+ # attaching your plist file at https://github.com/patsplat/plist/issues
28
+ # so folks can implement the proper support.
29
+ def self.parse_xml(filename_or_xml)
26
30
  listener = Listener.new
27
- #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
31
+ # parser = REXML::Parsers::StreamParser.new(File.new(filename), listener)
28
32
  parser = StreamParser.new(filename_or_xml, listener)
29
33
  parser.parse
30
34
  listener.result
31
35
  end
32
36
 
33
37
  class Listener
34
- #include REXML::StreamListener
38
+ # include REXML::StreamListener
35
39
 
36
40
  attr_accessor :result, :open
37
41
 
38
42
  def initialize
39
43
  @result = nil
40
- @open = Array.new
44
+ @open = []
41
45
  end
42
46
 
43
-
44
47
  def tag_start(name, attributes)
45
- @open.push PTag::mappings[name].new
48
+ @open.push PTag.mappings[name].new
46
49
  end
47
50
 
48
- def text( contents )
49
- @open.last.text = contents if @open.last
51
+ def text(contents)
52
+ if @open.last
53
+ @open.last.text ||= ''
54
+ @open.last.text.concat(contents)
55
+ end
50
56
  end
51
57
 
52
58
  def tag_end(name)
@@ -60,11 +66,11 @@ module Plist
60
66
  end
61
67
 
62
68
  class StreamParser
63
- def initialize( plist_data_or_file, listener )
69
+ def initialize(plist_data_or_file, listener)
64
70
  if plist_data_or_file.respond_to? :read
65
71
  @xml = plist_data_or_file.read
66
- elsif File.exists? plist_data_or_file
67
- @xml = File.read( plist_data_or_file )
72
+ elsif File.exist? plist_data_or_file
73
+ @xml = File.read(plist_data_or_file)
68
74
  else
69
75
  @xml = plist_data_or_file
70
76
  end
@@ -72,26 +78,35 @@ module Plist
72
78
  @listener = listener
73
79
  end
74
80
 
75
- TEXT = /([^<]+)/
76
- XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um
77
- DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um
78
- COMMENT_START = /\A<!--/u
79
- COMMENT_END = /.*?-->/um
80
-
81
+ TEXT = /([^<]+)/
82
+ CDATA = /<!\[CDATA\[(.*?)\]\]>/
83
+ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/m
84
+ DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/m
85
+ COMMENT_START = /\A<!--/
86
+ COMMENT_END = /.*?-->/m
87
+ UNIMPLEMENTED_ERROR = 'Unimplemented element. ' \
88
+ 'Consider reporting via https://github.com/patsplat/plist/issues'
81
89
 
82
90
  def parse
83
- plist_tags = PTag::mappings.keys.join('|')
91
+ plist_tags = PTag.mappings.keys.join('|')
84
92
  start_tag = /<(#{plist_tags})([^>]*)>/i
85
93
  end_tag = /<\/(#{plist_tags})[^>]*>/i
86
94
 
87
95
  require 'strscan'
88
96
 
89
- @scanner = StringScanner.new( @xml )
97
+ @scanner = StringScanner.new(@xml)
90
98
  until @scanner.eos?
91
99
  if @scanner.scan(COMMENT_START)
92
100
  @scanner.scan(COMMENT_END)
93
101
  elsif @scanner.scan(XMLDECL_PATTERN)
102
+ encoding = parse_encoding_from_xml_declaration(@scanner[1])
103
+ next if encoding.nil?
104
+
105
+ # use the specified encoding for the rest of the file
106
+ next unless String.method_defined?(:force_encoding)
107
+ @scanner.string = @scanner.rest.force_encoding(encoding)
94
108
  elsif @scanner.scan(DOCTYPE_PATTERN)
109
+ next
95
110
  elsif @scanner.scan(start_tag)
96
111
  @listener.tag_start(@scanner[1], nil)
97
112
  if (@scanner[2] =~ /\/$/)
@@ -99,32 +114,49 @@ module Plist
99
114
  end
100
115
  elsif @scanner.scan(TEXT)
101
116
  @listener.text(@scanner[1])
117
+ elsif @scanner.scan(CDATA)
118
+ @listener.text(@scanner[1])
102
119
  elsif @scanner.scan(end_tag)
103
120
  @listener.tag_end(@scanner[1])
104
121
  else
105
- raise "Unimplemented element"
122
+ raise UnimplementedElementError.new(UNIMPLEMENTED_ERROR)
106
123
  end
107
124
  end
108
125
  end
126
+
127
+ private
128
+
129
+ def parse_encoding_from_xml_declaration(xml_declaration)
130
+ return unless defined?(Encoding)
131
+
132
+ xml_encoding = xml_declaration.match(/(?:\A|\s)encoding=(?:"(.*?)"|'(.*?)')(?:\s|\Z)/)
133
+
134
+ return if xml_encoding.nil?
135
+
136
+ begin
137
+ Encoding.find(xml_encoding[1])
138
+ rescue ArgumentError
139
+ nil
140
+ end
141
+ end
109
142
  end
110
143
 
111
144
  class PTag
112
- @@mappings = { }
113
- def PTag::mappings
114
- @@mappings
145
+ def self.mappings
146
+ @mappings ||= {}
115
147
  end
116
148
 
117
- def PTag::inherited( sub_class )
149
+ def self.inherited(sub_class)
118
150
  key = sub_class.to_s.downcase
119
- key.gsub!(/^plist::/, '' )
151
+ key.gsub!(/^plist::/, '')
120
152
  key.gsub!(/^p/, '') unless key == "plist"
121
153
 
122
- @@mappings[key] = sub_class
154
+ mappings[key] = sub_class
123
155
  end
124
156
 
125
157
  attr_accessor :text, :children
126
158
  def initialize
127
- @children = Array.new
159
+ @children = []
128
160
  end
129
161
 
130
162
  def to_ruby
@@ -140,7 +172,7 @@ module Plist
140
172
 
141
173
  class PDict < PTag
142
174
  def to_ruby
143
- dict = Hash.new
175
+ dict = {}
144
176
  key = nil
145
177
 
146
178
  children.each do |c|
@@ -158,13 +190,13 @@ module Plist
158
190
 
159
191
  class PKey < PTag
160
192
  def to_ruby
161
- CGI::unescapeHTML(text || '')
193
+ CGI.unescapeHTML(text || '')
162
194
  end
163
195
  end
164
196
 
165
197
  class PString < PTag
166
198
  def to_ruby
167
- CGI::unescapeHTML(text || '')
199
+ CGI.unescapeHTML(text || '')
168
200
  end
169
201
  end
170
202
 
@@ -210,11 +242,10 @@ module Plist
210
242
  require 'base64'
211
243
  class PData < PTag
212
244
  def to_ruby
213
- data = Base64.decode64(text.gsub(/\s+/, ''))
214
-
245
+ data = Base64.decode64(text.gsub(/\s+/, '')) unless text.nil?
215
246
  begin
216
247
  return Marshal.load(data)
217
- rescue Exception => e
248
+ rescue Exception
218
249
  io = StringIO.new
219
250
  io.write data
220
251
  io.rewind