Class: Icalendar::CalendarParser

Inherits:
Base show all
Defined in:
lib/icalendar/calendar_parser.rb

Constant Summary collapse

NAME =

1*(ALPHA / DIGIT / “=”)

'[-a-z0-9]+'
QSTR =

<“> <Any character except CTLs, DQUOTE> <”>

'"[^"]*"'
PTEXT =

*<Any character except CTLs, DQUOTE, “;”, “:”, “,”>

'[^";:,]*'
PVALUE =

param-value = ptext / quoted-string

"#{PTEXT}|#{QSTR}"
LINE =

Contentline

"(#{NAME})([^:]*)\:(.*)"
PARAM =

param = name “=” param-value *(“,” param-value) Note: v2.1 allows a type or encoding param-value to appear without the type= or the encoding=. This is hideous, but we try and support it, if there # is no “=”, then $2 will be “”, and we will treat it as a v2.1 param.

";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
DATE =

date = date-fullyear [“-”] date-month [“-”] date-mday date-fullyear = 4 DIGIT date-month = 2 DIGIT date-mday = 2 DIGIT

'(\d\d\d\d)-?(\d\d)-?(\d\d)'
TIME =

time = time-hour [“:”] time-minute [“:”] time-second [time-secfrac] [time-zone] time-hour = 2 DIGIT time-minute = 2 DIGIT time-second = 2 DIGIT time-secfrac = “,” 1*DIGIT time-zone = “Z” / time-numzone time-numzome = sign time-hour [“:”] time-minute

'(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'

Instance Method Summary collapse

Constructor Details

#initialize(src) ⇒ CalendarParser

Returns a new instance of CalendarParser.



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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/icalendar/calendar_parser.rb', line 38

def initialize(src)
  @@logger.info("New Calendar Parser")
  
  # Define the next line method different depending on whether
  # this is a string or an IO object so we can be efficient about
  # parsing large files...

  # Just do the unfolding work in one shot if its a whole string
  if src.respond_to?(:split)
    unfolded = []

    # Split into an array of lines, then unfold those into a new array
    src.split(/\r?\n/).each do |line|

      # If it's a continuation line, add it to the last.
      # If it's an empty line, drop it from the input.
      if( line =~ /^[ \t]/ )
        unfolded << unfolded.pop + line[1, line.size-1]
      elsif( line =~ /^$/ )
      else
        unfolded << line
      end
    end

    @lines = unfolded
    @index = 0

    # Now that we are unfolded we can just iterate through the array.
    # Dynamically define next line for a string.
    def next_line
      if @index == @lines.size
        return nil
      else
        line = @lines[@index]
        @index += 1
        return line
      end
    end

    # If its a file we need to read and unfold on the go to save from reading
    # large amounts of data into memory.
  elsif src.respond_to?(:gets)
    @file = src
    @prev_line = src.gets
    if !@prev_line.nil?
      @prev_line.chomp!
    end
    
    # Dynamically define next line for an IO object
    def next_line
      line = @prev_line
      
      if line.nil? 
        return nil 
      end

      # Loop through until we get to a non-continuation line...
      loop do
        nextLine = @file.gets
        if !nextLine.nil?
          nextLine.chomp!
        end
        
        # If it's a continuation line, add it to the last.
        # If it's an empty line, drop it from the input.
        if( nextLine =~ /^[ \t]/ )
          line << nextLine[1, nextLine.size]
        elsif( nextLine =~ /^$/ )
        else
          @prev_line = nextLine
          break
        end
      end
      line
    end
  else
    raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
  end
end

Instance Method Details

#parseObject

Parse the calendar into an object representation



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/icalendar/calendar_parser.rb', line 119

def parse
  calendars = []
  
  # Outer loop for Calendar objects
  while (line = next_line)
    fields = parse_line(line)
    
    # Just iterate through until we find the beginning of a calendar object
    if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
      cal = parse_calendar
      calendars << cal
    end
  end
  
  calendars
end

#parse_calendar(component = Calendar.new) ⇒ Object

Parse a single VCALENDAR object – This should consist of the PRODID, VERSION, option METHOD & CALSCALE, and then one or more calendar components: VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE



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
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/icalendar/calendar_parser.rb', line 140

def parse_calendar(component = Calendar.new)
  while (line = next_line)
    fields = parse_line(line)

    name = fields[:name]
    
    # Although properties are supposed to come before components, we should
    # be able to handle them in any order...
    if name == "END"
      break
    elsif name == "BEGIN" # New component
      case(fields[:value])
      when "VEVENT"
        component.events << parse_calendar(Event.new)
      when "VTODO"
        component.todos << parse_calendar(Todo.new)
      when "VJOURNAL"
        component.journals << parse_calendar(Journal.new)
      when "VFREEBUSY"
        component.freebusys << parse_calendar(Freebusy.new)
      when "VTIMEZONE"
        component.timezones << parse_calendar(Timezone.new)
      when "VALARM"
        component.alarms << parse_calendar(Alarm.new)
      end
    else # If its not a component then it should be properties...
      
      # Just set the properties ourselves so that the parser can still
      # parse invalid files...
      @@logger.debug("Setting #{name} => #{fields[:value]}")
      component.properties[name] = fields[:value]
      if not fields[:params].empty?
        component.property_params[name] = fields[:params]
      end

      # This will generate the correctly formed calls to the dynamic method
      # handler.
#          component.send("#{name}=", fields[:value])
#          component.send("#{name}_params=", fields[:params]) unless fields[:params].empty?
    end
  end
  
  component
end

#parse_line(line) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/icalendar/calendar_parser.rb', line 185

def parse_line(line)
  unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
    raise "Invalid line in calendar string!"
  end

  name = $1.upcase # The case insensitive part is upcased for easier comparison...
  paramslist = $2
  value = $3
  
  params = {}
  
  # Collect the params, if any.
  if paramslist.size > 1
    
    # v3.0 and v2.1 params
    paramslist.scan( %r{#{PARAM}}i ) do
      
      # param names are case-insensitive, and multi-valued
      pname = $1
      pvals = $3
      
      # v2.1 pvals 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 == ""
        pvals = $1
        case $1
        when /quoted-printable/i
          pname = 'encoding'
          
        when /base64/i
          pname = 'encoding'
          
        else
          pname = 'type'
        end
      end
      
      unless params.key? pname
        params[pname] = []
      end
      pvals.scan( %r{(#{PVALUE})} ) do
        if $1.size > 0
          params[pname] << $1
        end
      end
    end
  end
  
  {:name => name, :params => params, :value => value}
end