vpim 0.16 → 0.17

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,86 @@
1
+ # http://www2a.biglobe.ne.jp/~seki/ruby/src/plist.rb
2
+ require 'rexml/document'
3
+ require 'time'
4
+
5
+ class Plist #:nodoc:
6
+ def self.file_to_plist(fname)
7
+ File.open(fname) do |fp|
8
+ doc = REXML::Document.new(fp)
9
+ return self.new.visit(REXML::XPath.match(doc, '/plist/')[0])
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ setup_method_table
15
+ end
16
+
17
+ def visit(node)
18
+ visit_one(node.elements[1])
19
+ end
20
+
21
+ def visit_one(node)
22
+ choose_method(node.name).call(node)
23
+ end
24
+
25
+ def visit_null(node)
26
+ p node if $DEBUG
27
+ nil
28
+ end
29
+
30
+ def visit_dict(node)
31
+ dict = {}
32
+ es = node.elements.to_a
33
+ while key = es.shift
34
+ next unless key.name == 'key'
35
+ dict[key.text] = visit_one(es.shift)
36
+ end
37
+ dict
38
+ end
39
+
40
+ def visit_array(node)
41
+ node.elements.collect do |x|
42
+ visit_one(x)
43
+ end
44
+ end
45
+
46
+ def visit_integer(node)
47
+ node.text.to_i
48
+ end
49
+
50
+ def visit_real(node)
51
+ node.text.to_f
52
+ end
53
+
54
+ def visit_string(node)
55
+ node.text.to_s
56
+ end
57
+
58
+ def visit_date(node)
59
+ Time.parse(node.text.to_s)
60
+ end
61
+
62
+ def visit_true(node)
63
+ true
64
+ end
65
+
66
+ def visit_false(node)
67
+ false
68
+ end
69
+
70
+ private
71
+ def choose_method(name)
72
+ @method.fetch(name, method(:visit_null))
73
+ end
74
+
75
+ def setup_method_table
76
+ @method = {}
77
+ @method['dict'] = method(:visit_dict)
78
+ @method['integer'] = method(:visit_integer)
79
+ @method['real'] = method(:visit_real)
80
+ @method['string'] = method(:visit_string)
81
+ @method['date'] = method(:visit_date)
82
+ @method['true'] = method(:visit_true)
83
+ @method['false'] = method(:visit_false)
84
+ @method['array'] = method(:visit_array)
85
+ end
86
+ end
data/lib/vpim/date.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: date.rb,v 1.8 2004/11/17 05:06:27 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
data/lib/vpim/date.rb~ ADDED
@@ -0,0 +1,198 @@
1
+ =begin
2
+ $Id: date.rb,v 1.8 2004/11/17 05:06:27 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'date'
12
+
13
+ # Extensions to the standard library Date.
14
+ class Date
15
+
16
+ TIME_START = Date.new(1970, 1, 1)
17
+ SECS_PER_DAY = 24 * 60 * 60
18
+
19
+ # Converts this object to a Time object, or throws an ArgumentError if
20
+ # conversion is not possible because it is before the start of epoch.
21
+ def to_time
22
+ raise ArgumentError, 'date is before the start of system time' if self < TIME_START
23
+ days = self - TIME_START
24
+
25
+ Time.at((days * SECS_PER_DAY).to_i)
26
+ end
27
+
28
+ # If wday responds to to_str, convert it to the wday number by searching for
29
+ # a wday that matches, using as many characters as are in wday to do the
30
+ # comparison. wday must be 2 or more characters long in order to be a unique
31
+ # match, other than that, "mo", "Mon", and "MonDay" are all valid strings
32
+ # for wday 1.
33
+ #
34
+ # This method can be called on a valid wday, and it will return it. Perhaps
35
+ # it should be called by default inside the Date#new*() methods so that
36
+ # non-integer wday arguments can be used? Perhaps a similar method should
37
+ # exist for months? But with months, we all know January is 1, who can
38
+ # remember where Date chooses to start its wday count!
39
+ #
40
+ # Examples:
41
+ # Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in
42
+ # February
43
+ # Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice
44
+ # that a valid wday integer can be passed right through.
45
+ #
46
+ def Date.str2wday(wdaystr)
47
+ return wdaystr unless wdaystr.respond_to? :to_str
48
+
49
+ str = wdaystr.to_str.upcase
50
+ if str.length < 2
51
+ raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name'
52
+ end
53
+
54
+ wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str)
55
+
56
+ return wday if wday
57
+
58
+ raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name'
59
+ end
60
+
61
+
62
+ # Create a new Date object for the date specified by year +year+, month
63
+ # +mon+, and day-of-the-week +wday+.
64
+ #
65
+ # The nth, +n+, occurrence of +wday+ within the period will be generated
66
+ # (+n+ defaults to 1). If +n+ is positive, the nth occurence from the
67
+ # beginning of the period will be returned, if negative, the nth occurrence
68
+ # from the end of the period will be returned.
69
+ #
70
+ # The period is a year, unless +month+ is non-nil, in which case it is just
71
+ # that month.
72
+ #
73
+ # Examples:
74
+ # - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004
75
+ # - Date.bywday(2004, nil, 1) => the first Sunday of 2004
76
+ # - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004
77
+ # - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004
78
+ # - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004
79
+ # - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004
80
+ #
81
+ # Compare this to Date.new, which allows a Date to be created by
82
+ # day-of-the-month, mday, to Date.new2, which allows a Date to be created by
83
+ # day-of-the-year, yday, and to Date.neww, which allows a Date to be created
84
+ # by day-of-the-week, but within a specific week.
85
+ def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY)
86
+ # Normalize mon to 1-12.
87
+ if mon
88
+ if mon > 12 || mon == 0 || mon < -12
89
+ raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12"
90
+ end
91
+ if mon < 0
92
+ mon = 13 + mon
93
+ end
94
+ end
95
+ if wday < 0 || wday > 6
96
+ raise ArgumentError, 'wday must be in range 0-6, or a weekday name'
97
+ end
98
+
99
+ # Determine direction of indexing.
100
+ inc = n <=> 0
101
+ if inc == 0
102
+ raise ArgumentError, 'n must be greater or less than zero'
103
+ end
104
+
105
+ # if !mon, n is index into year, but direction of search is determined by
106
+ # sign of n
107
+ d = Date.new(year, mon ? mon : inc, inc, sg)
108
+
109
+ while d.wday != wday
110
+ d += inc
111
+ end
112
+
113
+ # Now we have found the first/last day with the correct wday, search
114
+ # for nth occurrence, by jumping by n.abs-1 weeks forward or backward.
115
+ d += 7 * (n.abs - 1) * inc
116
+
117
+ if d.year != year
118
+ raise ArgumentError, 'n is out of bounds of year'
119
+ end
120
+ if mon && d.mon != mon
121
+ raise ArgumentError, 'n is out of bounds of month'
122
+ end
123
+ d
124
+ end
125
+ end
126
+
127
+ # DateGen generates arrays of dates matching simple criteria.
128
+ class DateGen
129
+ # Generate an array of dates on +wday+ (the day-of-week,
130
+ # 0-6, where 0 is Sunday).
131
+ #
132
+ # If +n+ is specified, only the nth occurrence of +wday+ within the period
133
+ # will be generated. If +n+ is positive, the nth occurence from the
134
+ # beginning of the period will be returned, if negative, the nth occurrence
135
+ # from the end of the period will be returned.
136
+ #
137
+ # The period is a year, unless +month+ is non-nil, in which case it is just
138
+ # that month.
139
+ #
140
+ # Examples:
141
+ # - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004
142
+ # - DateGen.bywday(2004, nil, 1) => all Sundays in 2004
143
+ # - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004
144
+ # - DateGen.bywday(2004, 12, 1) => all sundays in December 2004
145
+ # - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004
146
+ # - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004
147
+ #
148
+ # Compare to Date.bywday(), which allows a single Date to be created with
149
+ # similar criteria.
150
+ def DateGen.bywday(year, month, wday, n = nil)
151
+ seed = Date.bywday(year, month, wday, n ? n : 1)
152
+
153
+ dates = [ seed ]
154
+
155
+ return dates if n
156
+
157
+ succ = seed.clone
158
+
159
+ # Collect all matches until we're out of the year (or month, if specified)
160
+ loop do
161
+ succ += 7
162
+
163
+ break if succ.year != year
164
+ break if month && succ.month != seed.month
165
+
166
+ dates.push succ
167
+ end
168
+ dates.sort!
169
+ dates
170
+ end
171
+
172
+ # Generate an array of dates on +mday+ (the day-of-month, 1-31). For months
173
+ # in which the +mday+ is not present, no date will be generated.
174
+ #
175
+ # The period is a year, unless +month+ is non-nil, in which case it is just
176
+ # that month.
177
+ #
178
+ # Compare to Date.new(), which allows a single Date to be created with
179
+ # similar criteria.
180
+ def DateGen.bymonthday(year, month, mday)
181
+ months = month ? [ month ] : 1..12
182
+ dates = [ ]
183
+
184
+ months.each do |m|
185
+ begin
186
+ dates << Date.new(year, m, mday)
187
+ rescue ArgumentError
188
+ # Don't generate dates for invalid combinations (Feb 29, when it's not
189
+ # a leap year, for example).
190
+ #
191
+ # TODO - should we raise when month is out of range, or mday can never
192
+ # be in range (32)?
193
+ end
194
+ end
195
+ dates
196
+ end
197
+ end
198
+
data/lib/vpim/dirinfo.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: dirinfo.rb,v 1.19 2004/11/17 05:06:27 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -100,6 +98,7 @@ module Vpim
100
98
  # The value of the first field named +name+, or nil if no
101
99
  # match is found.
102
100
  def [](name)
101
+ enum_by_name(name).each { |f| return f.value if f.value != ''}
103
102
  enum_by_name(name).each { |f| return f.value }
104
103
  nil
105
104
  end
@@ -136,10 +135,18 @@ module Vpim
136
135
 
137
136
  # Returns an Enumerator for each Field for which #name?(+name+) is true.
138
137
  #
139
- # For example, to get an Array of all the email addresses in the card, you
140
- # could do:
138
+ # An Enumerator supports all the methods of Enumerable, so it allows iteration,
139
+ # collection, mapping, etc.
140
+ #
141
+ # Examples:
142
+ #
143
+ # Print all the nicknames in a card:
144
+ #
145
+ # card.enum_by_name('NICKNAME') { |f| puts f.value }
146
+ #
147
+ # Print an Array of the preferred email addresses in the card:
141
148
  #
142
- # card.enum_by_name('email').collect { |f| f.value }
149
+ # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? }
143
150
  def enum_by_name(name)
144
151
  Enumerator.new(self, Proc.new { |field| field.name?(name) })
145
152
  end
@@ -148,15 +155,15 @@ module Vpim
148
155
  #
149
156
  # For example, to print all the fields, sorted by group, you could do:
150
157
  #
151
- # card.groups.sort.each do |group|
152
- # card.enum_by_group(group).each do |field|
153
- # puts "#{group} -> #{field.name}"
154
- # end
155
- # end
158
+ # card.groups.sort.each do |group|
159
+ # card.enum_by_group(group).each do |field|
160
+ # puts "#{group} -> #{field.name}"
161
+ # end
162
+ # end
156
163
  #
157
164
  # or to get an array of all the fields in group 'agroup', you could do:
158
165
  #
159
- # card.enum_by_group('agroup').to_a
166
+ # card.enum_by_group('agroup').to_a
160
167
  def enum_by_group(group)
161
168
  Enumerator.new(self, Proc.new { |field| field.group?(group) })
162
169
  end
@@ -166,9 +173,15 @@ module Vpim
166
173
  Enumerator.new(self, cond )
167
174
  end
168
175
 
176
+ # Force card to be reencoded from the fields.
177
+ def dirty #:nodoc:
178
+ #string = nil
179
+ end
180
+
169
181
  # Append +field+ to the fields. Note that it won't be literally appended
170
182
  # to the fields, it will be inserted before the closing END field.
171
183
  def push(field)
184
+ dirty
172
185
  @fields[-1,0] = field
173
186
  self
174
187
  end
@@ -190,6 +203,25 @@ module Vpim
190
203
  self
191
204
  end
192
205
 
206
+ # Delete +field+.
207
+ #
208
+ # Warning: You can't delete BEGIN: or END: fields, but other
209
+ # profile-specific fields can be deleted, including mandatory ones. For
210
+ # vCards in particular, in order to avoid destroying them, I suggest
211
+ # creating a new Vcard, and copying over all the fields that you still
212
+ # want, rather than using #delete. This is easy with Maker::Vcard#copy, see
213
+ # the Maker::Vcard examples.
214
+ def delete(field)
215
+ case
216
+ when field.name?('BEGIN'), field.name?('END')
217
+ raise ArgumentError, 'Cannot delete BEGIN or END fields.'
218
+ else
219
+ @fields.delete field
220
+ end
221
+
222
+ self
223
+ end
224
+
193
225
  # The string encoding of the DirectoryInfo. See Field#encode for information
194
226
  # about the width parameter.
195
227
  def encode(width=nil)
@@ -208,14 +240,14 @@ module Vpim
208
240
  unless @fields.first
209
241
  raise "No fields to check"
210
242
  end
211
- unless @fields.first.name? "begin"
243
+ unless @fields.first.name? 'BEGIN'
212
244
  raise "Needs BEGIN, found: #{@fields.first.encode nil}"
213
245
  end
214
- unless @fields.last.name? "end"
246
+ unless @fields.last.name? 'END'
215
247
  raise "Needs END, found: #{@fields.last.encode nil}"
216
248
  end
217
249
  unless @fields.last.value? @fields.first.value
218
- raise "BEGIN/END mismatch: (#{@fields.first.value.downcase} != #{@fields.last.value.downcase}"
250
+ raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
219
251
  end
220
252
  if profile
221
253
  if ! @fields.first.value? profile
@@ -0,0 +1,242 @@
1
+ =begin
2
+ $Id: dirinfo.rb,v 1.19 2004/11/17 05:06:27 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/enumerator'
12
+ require 'vpim/field'
13
+ require 'vpim/rfc2425'
14
+ require 'vpim/vpim'
15
+
16
+ module Vpim
17
+ # An RFC 2425 directory info object.
18
+ #
19
+ # A directory information object is a sequence of fields. The basic
20
+ # structure of the object, and the way in which it is broken into fields
21
+ # is common to all profiles of the directory info type.
22
+ #
23
+ # A vCard, for example, is a specialization of a directory info object.
24
+ #
25
+ # [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
26
+ class DirectoryInfo
27
+ include Enumerable
28
+
29
+ private_class_method :new
30
+
31
+ # Initialize a DirectoryInfo object from +fields+. If +profile+ is
32
+ # specified, check the BEGIN/END fields.
33
+ def initialize(fields, profile = nil) #:nodoc:
34
+ if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field }
35
+ raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects'
36
+ end
37
+
38
+ @string = nil # this is used as a flag to indicate that recoding will be necessary
39
+ @fields = fields
40
+
41
+ check_begin_end(profile) if profile
42
+ end
43
+
44
+ # Decode +card+ into a DirectoryInfo object.
45
+ #
46
+ # +card+ may either be a something that is convertible to a string using
47
+ # #to_str or an array of objects that can be joined into a string using
48
+ # #join("\n"), or an IO object (which will be read to end-of-file).
49
+ #
50
+ # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
51
+ #
52
+ # A DirectoryInfo is mutable, you can add new fields to it, see
53
+ # Vpim::DirectoryInfo::Field#create() for how to create a new Field.
54
+ #
55
+ # TODO: I don't believe this is ever used, maybe I can remove it.
56
+ def DirectoryInfo.decode(card) #:nodoc:
57
+ if card.respond_to? :to_str
58
+ string = card.to_str
59
+ elsif card.kind_of? Array
60
+ string = card.join("\n")
61
+ elsif card.kind_of? IO
62
+ string = card.read(nil)
63
+ else
64
+ raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
65
+ end
66
+
67
+ fields = Vpim.decode(string)
68
+
69
+ new(fields)
70
+ end
71
+
72
+ # Create a new DirectoryInfo object. The +fields+ are an optional array of
73
+ # DirectoryInfo::Field objects to add to the new object, between the
74
+ # BEGIN/END. If the +profile+ string is not nil, then it is the name of
75
+ # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
76
+ # will be added.
77
+ #
78
+ # A DirectoryInfo is mutable, you can add new fields to it using #push(),
79
+ # and see Field#create().
80
+ def DirectoryInfo.create(fields = [], profile = nil)
81
+
82
+ if profile
83
+ p = profile.to_str
84
+ f = [ Field.create('BEGIN', p) ]
85
+ f.concat fields
86
+ f.push Field.create('END', p)
87
+ fields = f
88
+ end
89
+
90
+ new(fields, profile)
91
+ end
92
+
93
+ # The first field named +name+, or nil if no
94
+ # match is found.
95
+ def field(name)
96
+ enum_by_name(name).each { |f| return f }
97
+ nil
98
+ end
99
+
100
+ # The value of the first field named +name+, or nil if no
101
+ # match is found.
102
+ def [](name)
103
+ enum_by_name(name).each { |f| return f.value if f.value != ''}
104
+ enum_by_name(name).each { |f| return f.value }
105
+ nil
106
+ end
107
+
108
+ # An array of all the values of fields named +name+, converted to text
109
+ # (using Field#to_text()).
110
+ #
111
+ # TODO - call this #texts(), as in the plural?
112
+ def text(name)
113
+ enum_by_name(name).map { |f| f.to_text }
114
+ end
115
+
116
+ # Array of all the Field#group()s.
117
+ def groups
118
+ @fields.collect { |f| f.group } .compact.uniq
119
+ end
120
+
121
+ # All fields, frozen.
122
+ def fields #:nodoc:
123
+ @fields.dup.freeze
124
+ end
125
+
126
+ # Yields for each Field for which +cond+.call(field) is true. The
127
+ # (default) +cond+ of nil is considered true for all fields, so
128
+ # this acts like a normal #each() when called with no arguments.
129
+ def each(cond = nil) # :yields: Field
130
+ @fields.each do |field|
131
+ if(cond == nil || cond.call(field))
132
+ yield field
133
+ end
134
+ end
135
+ self
136
+ end
137
+
138
+ # Returns an Enumerator for each Field for which #name?(+name+) is true.
139
+ #
140
+ # An Enumerator supports all the methods of Enumerable, so it allows iteration,
141
+ # collection, mapping, etc.
142
+ #
143
+ # Examples:
144
+ #
145
+ # Print all the nicknames in a card:
146
+ #
147
+ # card.enum_by_name('nickname') { |f| puts f.value }
148
+ #
149
+ # Get an Array of the preferred email addresses in the card:
150
+ #
151
+ # card.enum_by_name('email').collect { |f| f.pref? ? f.value : nil }.compact
152
+ # FIXME - make sure this works!
153
+ def enum_by_name(name)
154
+ Enumerator.new(self, Proc.new { |field| field.name?(name) })
155
+ end
156
+
157
+ # Returns an Enumerator for each Field for which #group?(+group+) is true.
158
+ #
159
+ # For example, to print all the fields, sorted by group, you could do:
160
+ #
161
+ # card.groups.sort.each do |group|
162
+ # card.enum_by_group(group).each do |field|
163
+ # puts "#{group} -> #{field.name}"
164
+ # end
165
+ # end
166
+ #
167
+ # or to get an array of all the fields in group 'agroup', you could do:
168
+ #
169
+ # card.enum_by_group('agroup').to_a
170
+ def enum_by_group(group)
171
+ Enumerator.new(self, Proc.new { |field| field.group?(group) })
172
+ end
173
+
174
+ # Returns an Enumerator for each Field for which +cond+.call(field) is true.
175
+ def enum_by_cond(cond)
176
+ Enumerator.new(self, cond )
177
+ end
178
+
179
+ # Append +field+ to the fields. Note that it won't be literally appended
180
+ # to the fields, it will be inserted before the closing END field.
181
+ def push(field)
182
+ @fields[-1,0] = field
183
+ self
184
+ end
185
+
186
+ alias << push
187
+
188
+ # Push +field+ onto the fields, unless there is already a field
189
+ # with this name.
190
+ def push_unique(field)
191
+ push(field) unless @fields.detect { |f| f.name? field.name }
192
+ self
193
+ end
194
+
195
+ # Append +field+ to the end of all the fields. This isn't usually what you
196
+ # want to do, usually a DirectoryInfo's first and last fields are a
197
+ # BEGIN/END pair, see #push().
198
+ def push_end(field)
199
+ @fields << field
200
+ self
201
+ end
202
+
203
+ def delete(field)
204
+ end
205
+
206
+ # The string encoding of the DirectoryInfo. See Field#encode for information
207
+ # about the width parameter.
208
+ def encode(width=nil)
209
+ unless @string
210
+ @string = @fields.collect { |f| f.encode(width) } . join ""
211
+ end
212
+ @string
213
+ end
214
+
215
+ alias to_s encode
216
+
217
+ # Check that the DirectoryInfo object is correctly delimited by a BEGIN
218
+ # and END, that their profile values match, and if +profile+ is specified, that
219
+ # they are the specified profile.
220
+ def check_begin_end(profile=nil) #:nodoc:
221
+ unless @fields.first
222
+ raise "No fields to check"
223
+ end
224
+ unless @fields.first.name? "begin"
225
+ raise "Needs BEGIN, found: #{@fields.first.encode nil}"
226
+ end
227
+ unless @fields.last.name? "end"
228
+ raise "Needs END, found: #{@fields.last.encode nil}"
229
+ end
230
+ unless @fields.last.value? @fields.first.value
231
+ raise "BEGIN/END mismatch: (#{@fields.first.value.downcase} != #{@fields.last.value.downcase}"
232
+ end
233
+ if profile
234
+ if ! @fields.first.value? profile
235
+ raise "Mismatched profile"
236
+ end
237
+ end
238
+ true
239
+ end
240
+ end
241
+ end
242
+