Class: Vcard::DirectoryInfo::Field

Inherits:
Object
  • Object
show all
Defined in:
lib/vcard/field.rb

Overview

A field in a directory info object.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(line) ⇒ Field

:nodoc:



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/vcard/field.rb', line 173

def initialize(line) # :nodoc:
  @line = line.to_str
  @valid, @group, @name, @params, @value = Field.decode0(@line)

  if valid?
    @params.each do |pname,pvalues|
      pvalues.freeze
    end
  else
    @group = @name = ''
  end
  self
end

Class Method Details

.create(name, value = "", params = {}) ⇒ Object

Create a field with name name (a String), value value (see below), and optional parameters, params. params is a hash of the parameter name (a String) to either a single string or symbol, or an array of strings and symbols (parameters can be multi-valued).

If “ENCODING” => :b64 is specified as a parameter, the value will be base-64 encoded. If it’s already base-64 encoded, then use String values (“ENCODING” => “B”), and no further encoding will be done by this routine.

Currently handled value types are:

  • Time, encoded as a date-time value

  • Date, encoded as a date value

  • String, encoded directly

  • Array of String, concatentated with “;” between them.

TODO - need a way to encode String values as TEXT, at least optionally, so as to escape special chars, etc.



215
216
217
218
219
220
221
222
223
# File 'lib/vcard/field.rb', line 215

def Field.create(name, value="", params={})
  line = Field.encode0(nil, name, params, value)

  begin
    new(line)
  rescue ::Vcard::InvalidEncodingError => e
    raise ArgumentError, e.to_s
  end
end

.create_array(fields) ⇒ Object



25
26
27
28
29
30
31
32
33
34
# File 'lib/vcard/field.rb', line 25

def Field.create_array(fields)
  case fields
  when Hash
    fields.map do |name,value|
      DirectoryInfo::Field.create( name, value )
    end
  else
    fields.to_ary
  end
end

.decode(line) ⇒ Object

Create a field by decoding line, a String which must already be unfolded. Decoded fields are frozen, but see #copy().



193
194
195
# File 'lib/vcard/field.rb', line 193

def Field.decode(line)
  new(line).freeze
end

.decode0(atline) ⇒ Object

Decode a field.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/vcard/field.rb', line 106

def Field.decode0(atline) # :nodoc:
  if !(atline =~ Bnf::LINE)
    raise(::Vcard::InvalidEncodingError, atline) if ::Vcard.configuration.raise_on_invalid_line?
    return false
  end

  atgroup = $1.upcase
  atname = $2.upcase
  paramslist = $3
  atvalue = $~[-1]

  # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so
  # strip it. I'm not absolutely sure this is allowed... it certainly
  # breaks round-trip encoding.
  atvalue.strip!

  if atgroup.length > 0
    atgroup.chomp!(".")
  else
    atgroup = nil
  end

  atparams = {}

  # Collect the params, if any.
  if paramslist.size > 1

    # v3.0 and v2.1 params
    paramslist.scan( Bnf::PARAM ) do

      # param names are case-insensitive, and multi-valued
      name = $1.upcase
      params = $3

      # v2.1 params have no "=" sign, figure out what kind of param it
      # is (either its a known encoding, or we treat it as a "TYPE"
      # param).

      if $2 == ""
        params = $1
        case $1
        when /quoted-printable/i
          name = "ENCODING"

        when /base64/i
          name = "ENCODING"

        else
          name = "TYPE"
        end
      end

      # TODO - In ruby1.8 I can give an initial value to the atparams
      # hash values instead of this.
      unless atparams.key? name
        atparams[name] = []
      end

      params.scan( Bnf::PVALUE ) do
        atparams[name] << ($1 || $2)
      end
    end
  end

  [ true, atgroup, atname, atparams, atvalue ]
end

.encode0(group, name, params = {}, value = "") ⇒ Object

Encode a field.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/vcard/field.rb', line 37

def Field.encode0(group, name, params={}, value="") # :nodoc:
  line = ""

  # A reminder of the line format:
  #   [<group>.]<name>;<pname>=<pvalue>,<pvalue>:<value>

  if group
    if group.class == Symbol
      # Explicitly allow symbols
      group = group.to_s
    end
    line << group.to_str << "."
  end

  line << name

  params.each do |pname, pvalues|

    unless pvalues.respond_to? :to_ary
      pvalues = [ pvalues ]
    end

    line << ";" << pname << "="

    sep = "" # set to "," after one pvalue has been appended

    pvalues.each do |pvalue|
      # check if we need to do any encoding
      if pname.casecmp("ENCODING") == 0 && pvalue == :b64
        pvalue = "B" # the RFC definition of the base64 param value
        value = [ value.to_str ].pack("m").gsub("\n", "")
      end

      line << sep << pvalue
      sep =",";
    end
  end

  line << ":"

  line << Field.value_str(value)

  line
end

.value_str(value) ⇒ Object

:nodoc:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/vcard/field.rb', line 82

def Field.value_str(value) # :nodoc:
  line = ""
  case value
  when Date
    line << ::Vcard.encode_date(value)

  when Time #, DateTime
    line << ::Vcard.encode_date_time(value)

  when Array
    line << value.map { |v| Field.value_str(v) }.join(";")

  when Symbol
    line << value.to_s

  else
    # FIXME - somewhere along here, values with special chars need escaping...
    line << value.to_str
  end
  line
end

Instance Method Details

#[]=(pname, pvalue) ⇒ Object

Set a the param pname‘s value to pvalue, replacing any value it currently has. See Field.create() for a description of pvalue.

Example:

if field["TYPE"]
  field["TYPE"] << "HOME"
else
  field["TYPE"] = [ "HOME" ]
end

TODO - this could be an alias to #pvalue_set



533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/vcard/field.rb', line 533

def []=(pname,pvalue)
  unless pvalue.respond_to?(:to_ary)
    pvalue = [ pvalue ]
  end

  h = @params.dup

  h[pname.upcase] = pvalue

  mutate(@group, @name, h, @value)
  pvalue
end

#copyObject

Create a copy of Field. If the original Field was frozen, this one won’t be.



227
228
229
# File 'lib/vcard/field.rb', line 227

def copy
  Marshal.load(Marshal.dump(self))
end

#each_param(&block) ⇒ Object

Yield once for each param, name is the parameter name, value is an array of the parameter values.



300
301
302
303
304
# File 'lib/vcard/field.rb', line 300

def each_param(&block) #:yield: name, value
  if @params
    @params.each(&block)
  end
end

#encode(width = nil) ⇒ Object Also known as: to_s

The String encoding of the Field. The String will be wrapped to a maximum line width of width, where 0 means no wrapping, and nil is to accept the default wrapping (75, recommended by RFC2425).

Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it imports vCards (it treats them as raw new-line characters), nor wraps long lines on export. This is mostly a cosmetic problem, but wrapping can be disabled by setting width to 0, if desired.

FIXME - breaks round-trip encoding, need to change this to not wrap fields that are already wrapped.



242
243
244
245
246
247
248
249
250
251
# File 'lib/vcard/field.rb', line 242

def encode(width=nil)
  width = 75 unless width
  l = @line
  # Wrap to width, unless width is zero.
  if width > 0
    l = l.gsub(/.{#{width},#{width}}/) { |m| m + "\n " }
  end
  # Make sure it's terminated with no more than a single NL.
  l.gsub(/\s*\z/, "") + "\n"
end

#encodingObject

The value of the ENCODING parameter, if present, or nil if not present.



405
406
407
408
409
410
411
412
413
414
415
# File 'lib/vcard/field.rb', line 405

def encoding
  e = param("ENCODING")

  if e
    if e.length > 1
      raise ::Vcard::InvalidEncodingError, "multi-valued param 'ENCODING' (#{e})"
    end
    e = e.first.upcase
  end
  e
end

#groupObject

The group, if present, or nil if not present.



261
262
263
# File 'lib/vcard/field.rb', line 261

def group
  @group
end

#group=(group) ⇒ Object

Set the group of this field to group.



504
505
506
507
# File 'lib/vcard/field.rb', line 504

def group=(group)
  mutate(group, @name, @params, @value)
  group
end

#group?(group) ⇒ Boolean

Is the #group of this field group? Group names are case insensitive. A group of nil matches if the field has no group.

Returns:

  • (Boolean)


341
342
343
# File 'lib/vcard/field.rb', line 341

def group?(group)
  @group.casecmp(group) == 0
end

#kindObject

The type of the value, as specified by the VALUE parameter, nil if unspecified.



419
420
421
422
423
424
425
426
427
428
# File 'lib/vcard/field.rb', line 419

def kind
  v = param("VALUE")
  if v
    if v.size > 1
      raise ::Vcard::InvalidEncodingError, "multi-valued param 'VALUE' (#{values})"
    end
    v = v.first.downcase
  end
  v
end

#kind?(kind) ⇒ Boolean

Is the value of this field of type kind? RFC2425 allows the type of a fields value to be encoded in the VALUE parameter. Don’t rely on its presence, they aren’t required, and usually aren’t bothered with. In cases where the kind of value might vary (an iCalendar DTSTART can be either a date or a date-time, for example), you are more likely to see the kind of value specified explicitly.

The value types defined by RFC 2425 are:

  • uri:

  • text:

  • date: a list of 1 or more dates

  • time: a list of 1 or more times

  • date-time: a list of 1 or more date-times

  • integer:

  • boolean:

  • float:

Returns:

  • (Boolean)


361
362
363
# File 'lib/vcard/field.rb', line 361

def kind?(kind)
  self.kind.casecmp(kind) == 0
end

#nameObject

The name.



256
257
258
# File 'lib/vcard/field.rb', line 256

def name
  @name
end

#name?(name) ⇒ Boolean

Is the #name of this Field name? Names are case insensitive.

Returns:

  • (Boolean)


335
336
337
# File 'lib/vcard/field.rb', line 335

def name?(name)
  @name.to_s.casecmp(name) == 0
end

#pnamesObject Also known as: params

An Array of all the param names.



266
267
268
# File 'lib/vcard/field.rb', line 266

def pnames
  @params.keys
end

#pref=(ispref) ⇒ Object

Set whether a field is marked as preferred. See #pref?



389
390
391
392
393
394
395
# File 'lib/vcard/field.rb', line 389

def pref=(ispref)
  if ispref
    pvalue_iadd("TYPE", "PREF")
  else
    pvalue_idel("TYPE", "PREF")
  end
end

#pref?Boolean

Is this field marked as preferred? A vCard field is preferred if #type?(“PREF”). This method is not necessarily meaningful for non-vCard profiles.

Returns:

  • (Boolean)


384
385
386
# File 'lib/vcard/field.rb', line 384

def pref?
  type? "PREF"
end

#pvalue(name) ⇒ Object

The first value of the param name, nil if there is no such param, the param has no value, or the first param value is zero-length.



275
276
277
278
279
280
281
282
283
284
# File 'lib/vcard/field.rb', line 275

def pvalue(name)
  v = pvalues( name )
  if v
    v = v.first
  end
  if v
    v = nil unless v.length > 0
  end
  v
end

#pvalue_iadd(pname, pvalue) ⇒ Object

Add pvalue to the param pname‘s value. The values are treated as a set so duplicate values won’t occur, and String values are case insensitive. See Field.create() for a description of pvalue.



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/vcard/field.rb', line 549

def pvalue_iadd(pname, pvalue)
  pname = pname.upcase

  # Get a uniq set, where strings are compared case-insensitively.
  values = [ pvalue, @params[pname] ].flatten.compact
  values = values.collect do |v|
    if v.respond_to? :to_str
      v = v.to_str.upcase
    end
    v
  end
  values.uniq!

  h = @params.dup

  h[pname] = values

  mutate(@group, @name, h, @value)
  values
end

#pvalue_idel(pname, pvalue) ⇒ Object

Delete pvalue from the param pname‘s value. The values are treated as a set so duplicate values won’t occur, and String values are case insensitive. pvalue must be a single String or Symbol.



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/vcard/field.rb', line 573

def pvalue_idel(pname, pvalue)
  pname = pname.upcase
  if pvalue.respond_to? :to_str
    pvalue = pvalue.to_str.downcase
  end

  # Get a uniq set, where strings are compared case-insensitively.
  values = [ nil, @params[pname] ].flatten.compact
  values = values.collect do |v|
    if v.respond_to? :to_str
      v = v.to_str.downcase
    end
    v
  end
  values.uniq!
  values.delete pvalue

  h = @params.dup

  h[pname] = values

  mutate(@group, @name, h, @value)
  values
end

#pvalues(name) ⇒ Object Also known as: param, []

The Array of all values of the param name, nil if there is no such param, [] if the param has no values. If the Field isn’t frozen, the Array is mutable.



289
290
291
# File 'lib/vcard/field.rb', line 289

def pvalues(name)
  @params[name.upcase]
end

#text=(text) ⇒ Object

Convert value to text, then assign.

TODO - unimplemented



519
520
# File 'lib/vcard/field.rb', line 519

def text=(text)
end

#to_dateObject

The value as an array of Date objects (all times and dates in RFC2425 are lists, even where it might not make sense, such as a birthday).

The field value may be a list of either DATE or DATE-TIME values, decoding is tried first as a DATE-TIME, then as a DATE, if neither works an InvalidEncodingError will be raised.



473
474
475
476
477
478
479
480
481
482
483
# File 'lib/vcard/field.rb', line 473

def to_date
  ::Vcard.decode_date_time_list(value).collect do |d|
    # We get [ year, month, day, hour, min, sec, usec, tz ]
    Date.new(d[0], d[1], d[2])
  end
rescue ::Vcard::InvalidEncodingError
  ::Vcard.decode_date_list(value).collect do |d|
    # We get [ year, month, day ]
    Date.new(*d)
  end
end

#to_textObject

The value as text. Text can have escaped newlines, commas, and escape characters, this method will strip them, if present.

In theory, #value could also do this, but it would need to know that the value is of type “TEXT”, and often for text values the “VALUE” parameter is not present, so knowledge of the expected type of the field is required from the decoder.



492
493
494
# File 'lib/vcard/field.rb', line 492

def to_text
  ::Vcard.decode_text(value)
end

#to_timeObject

The value as an array of Time objects (all times and dates in RFC2425 are lists, even where it might not make sense, such as a birthday). The time will be UTC if marked as so (with a timezone of “Z”), and in localtime otherwise.

TODO - support timezone offsets

TODO - if year is before 1970, this won’t work… but some people are generating calendars saying Canada Day started in 1753! That’s just wrong! So, what to do? I add a message saying what the year is that breaks, so they at least know that its ridiculous! I think I need my own DateTime variant.



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/vcard/field.rb', line 442

def to_time
  ::Vcard.decode_date_time_list(value).collect do |d|
    # We get [ year, month, day, hour, min, sec, usec, tz ]
    begin
      if(d.pop == "Z")
        Time.gm(*d)
      else
        Time.local(*d)
      end
    rescue ArgumentError => e
      raise ::Vcard::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
    end
  end
rescue ::Vcard::InvalidEncodingError
  ::Vcard.decode_date_list(value).collect do |d|
    # We get [ year, month, day ]
    begin
      Time.gm(*d)
    rescue ArgumentError => e
      raise ::Vcard::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
    end
  end
end

#type?(type) ⇒ Boolean

Is one of the values of the TYPE parameter of this field type? The type parameter values are case insensitive. False if there is no TYPE parameter.

TYPE parameters are used for general categories, such as distinguishing between an email address used at home or at work.

Returns:

  • (Boolean)


371
372
373
374
375
376
377
378
379
# File 'lib/vcard/field.rb', line 371

def type?(type)
  type = type.to_str

  types = param("TYPE")

  if types
    types = types.detect { |t| t.casecmp(type) == 0 }
  end
end

#valid?Boolean

Returns:

  • (Boolean)


187
188
189
# File 'lib/vcard/field.rb', line 187

def valid?
  @valid
end

#valueObject

The decoded value.

The encoding specified by the #encoding, if any, is stripped.

Note: Both the RFC 2425 encoding param (“b”, meaning base-64) and the vCard 2.1 encoding params (“base64”, “quoted-printable”, “8bit”, and “7bit”) are supported.

FIXME:

  • should use the VALUE parameter

  • should also take a default value type, so it can be converted if VALUE parameter is not present.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/vcard/field.rb', line 318

def value
  case encoding
  when nil, "8BIT", "7BIT" then @value

    # Hack - if the base64 lines started with 2 SPC chars, which is invalid,
    # there will be extra spaces in @value. Since no SPC chars show up in
    # b64 encodings, they can be safely stripped out before unpacking.
  when "B", "BASE64"       then @value.gsub(" ", "").unpack("m*").first

  when "QUOTED-PRINTABLE"  then @value.unpack("M*").first

  else
    raise ::Vcard::InvalidEncodingError, "unrecognized encoding (#{encoding})"
  end
end

#value=(value) ⇒ Object

Set the value of this field to value. Valid values are as in Field.create().



511
512
513
514
# File 'lib/vcard/field.rb', line 511

def value=(value)
  mutate(@group, @name, @params, value)
  value
end

#value?(value) ⇒ Boolean

Is the value of this field value? The check is case insensitive. FIXME - it shouldn’t be insensitive, make a #casevalue? method.

Returns:

  • (Boolean)


399
400
401
# File 'lib/vcard/field.rb', line 399

def value?(value)
  @value.casecmp(value) == 0
end

#value_rawObject

The undecoded value, see value.



497
498
499
# File 'lib/vcard/field.rb', line 497

def value_raw
  @value
end