mhc 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/COPYRIGHT +28 -0
- data/Gemfile +8 -0
- data/README.org +209 -0
- data/Rakefile +13 -0
- data/bin/mhc +312 -0
- data/emacs/Cask +25 -0
- data/emacs/Makefile +58 -0
- data/emacs/mhc-calendar.el +1723 -0
- data/emacs/mhc-calfw.el +135 -0
- data/emacs/mhc-compat.el +90 -0
- data/emacs/mhc-date.el +642 -0
- data/emacs/mhc-day.el +149 -0
- data/emacs/mhc-db.el +158 -0
- data/emacs/mhc-draft.el +211 -0
- data/emacs/mhc-e21.el +167 -0
- data/emacs/mhc-face.el +236 -0
- data/emacs/mhc-file.el +224 -0
- data/emacs/mhc-guess.el +648 -0
- data/emacs/mhc-header.el +176 -0
- data/emacs/mhc-logic.el +563 -0
- data/emacs/mhc-message.el +130 -0
- data/emacs/mhc-minibuf.el +466 -0
- data/emacs/mhc-misc.el +248 -0
- data/emacs/mhc-mua.el +260 -0
- data/emacs/mhc-parse.el +286 -0
- data/emacs/mhc-process.el +35 -0
- data/emacs/mhc-ps.el +1174 -0
- data/emacs/mhc-record.el +201 -0
- data/emacs/mhc-schedule.el +202 -0
- data/emacs/mhc-summary.el +763 -0
- data/emacs/mhc-sync.el +158 -0
- data/emacs/mhc-vars.el +149 -0
- data/emacs/mhc.el +1114 -0
- data/icons/Anniversary.xbm +6 -0
- data/icons/Anniversary.xpm +27 -0
- data/icons/Birthday.xbm +6 -0
- data/icons/Birthday.xpm +25 -0
- data/icons/Business.xbm +6 -0
- data/icons/Business.xpm +24 -0
- data/icons/CheckBox.xbm +6 -0
- data/icons/CheckBox.xpm +24 -0
- data/icons/CheckedBox.xbm +6 -0
- data/icons/CheckedBox.xpm +25 -0
- data/icons/Conflict.xbm +6 -0
- data/icons/Conflict.xpm +22 -0
- data/icons/Date.xbm +6 -0
- data/icons/Date.xpm +29 -0
- data/icons/Holiday.xbm +6 -0
- data/icons/Holiday.xpm +25 -0
- data/icons/Link.xbm +6 -0
- data/icons/Link.xpm +25 -0
- data/icons/Other.xbm +6 -0
- data/icons/Other.xpm +28 -0
- data/icons/Party.xbm +6 -0
- data/icons/Party.xpm +23 -0
- data/icons/Private.xbm +6 -0
- data/icons/Private.xpm +26 -0
- data/icons/Recurrence.xbm +6 -0
- data/icons/Recurrence.xpm +98 -0
- data/icons/Vacation.xbm +6 -0
- data/icons/Vacation.xpm +26 -0
- data/lib/mhc.rb +45 -0
- data/lib/mhc/builder.rb +64 -0
- data/lib/mhc/caldav.rb +304 -0
- data/lib/mhc/calendar.rb +106 -0
- data/lib/mhc/command.rb +13 -0
- data/lib/mhc/command/cache.rb +14 -0
- data/lib/mhc/command/completions.rb +108 -0
- data/lib/mhc/command/init.rb +133 -0
- data/lib/mhc/command/scan.rb +33 -0
- data/lib/mhc/command/sync.rb +22 -0
- data/lib/mhc/config.rb +229 -0
- data/lib/mhc/converter.rb +330 -0
- data/lib/mhc/datastore.rb +164 -0
- data/lib/mhc/date_enumerator.rb +274 -0
- data/lib/mhc/date_frame.rb +124 -0
- data/lib/mhc/date_helper.rb +49 -0
- data/lib/mhc/etag.rb +68 -0
- data/lib/mhc/event.rb +396 -0
- data/lib/mhc/formatter.rb +312 -0
- data/lib/mhc/logger.rb +94 -0
- data/lib/mhc/modifier.rb +149 -0
- data/lib/mhc/occurrence.rb +94 -0
- data/lib/mhc/occurrence_enumerator.rb +113 -0
- data/lib/mhc/property_value.rb +33 -0
- data/lib/mhc/property_value/date.rb +190 -0
- data/lib/mhc/property_value/integer.rb +15 -0
- data/lib/mhc/property_value/list.rb +41 -0
- data/lib/mhc/property_value/period.rb +49 -0
- data/lib/mhc/property_value/range.rb +100 -0
- data/lib/mhc/property_value/recurrence_condition.rb +272 -0
- data/lib/mhc/property_value/text.rb +11 -0
- data/lib/mhc/property_value/time.rb +45 -0
- data/lib/mhc/query.rb +210 -0
- data/lib/mhc/sync.rb +46 -0
- data/lib/mhc/sync/driver.rb +108 -0
- data/lib/mhc/sync/status.rb +70 -0
- data/lib/mhc/sync/status_manager.rb +142 -0
- data/lib/mhc/sync/strategy.rb +233 -0
- data/lib/mhc/sync/syncinfo.rb +98 -0
- data/lib/mhc/templates/config.yml.erb +142 -0
- data/lib/mhc/version.rb +4 -0
- data/lib/mhc/webdav.rb +319 -0
- data/mhc.gemspec +24 -0
- data/samples/DOT.mhc-config.yml +116 -0
- data/samples/japanese-holidays.mhcc +153 -0
- data/samples/mhc-completions.zsh +11 -0
- data/spec/mhc_spec.rb +682 -0
- data/spec/spec_helper.rb +9 -0
- data/xpm/close.xpm +18 -0
- data/xpm/delete.xpm +19 -0
- data/xpm/exit.xpm +18 -0
- data/xpm/month.xpm +18 -0
- data/xpm/next.xpm +18 -0
- data/xpm/next2.xpm +18 -0
- data/xpm/next_year.xpm +18 -0
- data/xpm/open.xpm +19 -0
- data/xpm/prev.xpm +18 -0
- data/xpm/prev2.xpm +18 -0
- data/xpm/prev_year.xpm +18 -0
- data/xpm/save.xpm +19 -0
- data/xpm/today.xpm +18 -0
- metadata +214 -0
data/lib/mhc/event.rb
ADDED
@@ -0,0 +1,396 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
### event.rb
|
4
|
+
##
|
5
|
+
## Author: Yoshinari Nomura <nom@quickhack.net>
|
6
|
+
##
|
7
|
+
## Created: 1999/07/16
|
8
|
+
## Revised: $Date: 2008-10-08 03:22:37 $
|
9
|
+
##
|
10
|
+
|
11
|
+
module Mhc
|
12
|
+
# Mhc::Event defines a simple representation of calendar events.
|
13
|
+
# It looks like a RFC822 message with some X- headers to represent event properties:
|
14
|
+
# * X-SC-Subject:
|
15
|
+
# * X-SC-Location:
|
16
|
+
# * X-SC-Day:
|
17
|
+
# * X-SC-Time:
|
18
|
+
# * X-SC-Category:
|
19
|
+
# * X-SC-Recurrence-Tag:
|
20
|
+
# * X-SC-Mission-Tag:
|
21
|
+
# * X-SC-Cond:
|
22
|
+
# * X-SC-Duration:
|
23
|
+
# * X-SC-Alarm:
|
24
|
+
# * X-SC-Record-Id:
|
25
|
+
# * X-SC-Sequence:
|
26
|
+
#
|
27
|
+
class Event
|
28
|
+
################################################################
|
29
|
+
## initializers
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
clear
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse(string)
|
36
|
+
return new.parse(string)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_file(path, lazy = true)
|
40
|
+
return new.parse_file(path, lazy)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_file(path, lazy = true)
|
44
|
+
STDOUT.puts "parsing #{File.expand_path(path)}" if $MHC_DEBUG
|
45
|
+
|
46
|
+
clear
|
47
|
+
header, body = nil, nil
|
48
|
+
|
49
|
+
File.open(path, "r") do |file|
|
50
|
+
header = file.gets("\n\n")
|
51
|
+
body = file.gets(nil) unless lazy
|
52
|
+
end
|
53
|
+
|
54
|
+
@path = path if lazy
|
55
|
+
parse_header(header)
|
56
|
+
self.description = body if body
|
57
|
+
return self
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse(string)
|
61
|
+
clear
|
62
|
+
header, body = string.scrub.split(/\n\n/, 2)
|
63
|
+
|
64
|
+
parse_header(header)
|
65
|
+
self.description = body
|
66
|
+
return self
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.new_from_ics(ics_string)
|
70
|
+
Mhc::Converter::IcalendarImporter.parse_ics(ics_string)
|
71
|
+
end
|
72
|
+
|
73
|
+
def path
|
74
|
+
return @path
|
75
|
+
end
|
76
|
+
################################################################
|
77
|
+
## access methods to each property.
|
78
|
+
|
79
|
+
## alarm
|
80
|
+
def alarm
|
81
|
+
return @alarm ||= Mhc::PropertyValue::Period.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def alarm=(string)
|
85
|
+
return @alarm = alarm.parse(string)
|
86
|
+
end
|
87
|
+
|
88
|
+
## category
|
89
|
+
def categories
|
90
|
+
return @categories ||=
|
91
|
+
Mhc::PropertyValue::List.new(Mhc::PropertyValue::Text)
|
92
|
+
end
|
93
|
+
|
94
|
+
def categories=(string)
|
95
|
+
return @categories = categories.parse(string)
|
96
|
+
end
|
97
|
+
|
98
|
+
## description
|
99
|
+
def description
|
100
|
+
unless @description
|
101
|
+
@description = Mhc::PropertyValue::Text.new
|
102
|
+
|
103
|
+
if lazy? && File.file?(@path)
|
104
|
+
File.open(@path, "r") do |file|
|
105
|
+
file.gets("\n\n") # discard header.
|
106
|
+
@description.parse(file.gets(nil))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return @description
|
111
|
+
end
|
112
|
+
alias_method :body, :description
|
113
|
+
|
114
|
+
def description=(string)
|
115
|
+
return @description = description.parse(string)
|
116
|
+
end
|
117
|
+
|
118
|
+
## location
|
119
|
+
def location
|
120
|
+
return @location ||= Mhc::PropertyValue::Text.new
|
121
|
+
end
|
122
|
+
|
123
|
+
def location=(string)
|
124
|
+
return @location = location.parse(string)
|
125
|
+
end
|
126
|
+
|
127
|
+
## record-id
|
128
|
+
def record_id
|
129
|
+
return @record_id ||= Mhc::PropertyValue::Text.new
|
130
|
+
end
|
131
|
+
|
132
|
+
def record_id=(string)
|
133
|
+
return @record_id = record_id.parse(string)
|
134
|
+
end
|
135
|
+
|
136
|
+
def uid
|
137
|
+
record_id.to_s
|
138
|
+
end
|
139
|
+
|
140
|
+
## subject
|
141
|
+
def subject
|
142
|
+
return @subject ||= Mhc::PropertyValue::Text.new
|
143
|
+
end
|
144
|
+
|
145
|
+
def subject=(string)
|
146
|
+
return @subject = subject.parse(string)
|
147
|
+
end
|
148
|
+
|
149
|
+
## date list is a list of date range
|
150
|
+
def dates
|
151
|
+
return @dates ||=
|
152
|
+
Mhc::PropertyValue::List.new(Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date.new))
|
153
|
+
end
|
154
|
+
|
155
|
+
def dates=(string)
|
156
|
+
string = string.split.select {|s| /^!/ !~ s}.join(" ")
|
157
|
+
return @dates = dates.parse(string)
|
158
|
+
end
|
159
|
+
|
160
|
+
def obsolete_dates=(string)
|
161
|
+
# STDERR.print "Obsolete X-SC-Date: header.\n"
|
162
|
+
if /(\d+)\s+([A-Z][a-z][a-z])\s+(\d+)\s+(\d\d:\d\d)/ =~ string
|
163
|
+
dd, mm, yy, hhmm = $1.to_i, $2, $3.to_i + 1900, $4
|
164
|
+
mm = ("JanFebMarAprMayJunJulAugSepOctNovDec".index(mm)) / 3 + 1
|
165
|
+
@dates = dates.parse("%04d%02d%02d" % [yy, mm, dd])
|
166
|
+
if hhmm and hhmm != '00:00'
|
167
|
+
@time_range = time_range.parse(hhmm)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def exceptions
|
173
|
+
return @exceptions ||=
|
174
|
+
Mhc::PropertyValue::List.new(Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date.new, "!"))
|
175
|
+
end
|
176
|
+
|
177
|
+
def exceptions=(string)
|
178
|
+
string = string.split.select {|s| /^!/ =~ s}.map{|s| s[1..-1]}.join(" ")
|
179
|
+
return @exceptions = exceptions.parse(string)
|
180
|
+
end
|
181
|
+
|
182
|
+
## time
|
183
|
+
def time_range
|
184
|
+
return @time_range ||=
|
185
|
+
Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Time)
|
186
|
+
end
|
187
|
+
|
188
|
+
def time_range=(string)
|
189
|
+
@time_range = time_range.parse(string)
|
190
|
+
return @time_range
|
191
|
+
end
|
192
|
+
|
193
|
+
## duration
|
194
|
+
def duration
|
195
|
+
return @duration ||=
|
196
|
+
Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date)
|
197
|
+
end
|
198
|
+
|
199
|
+
def duration=(string)
|
200
|
+
return @duration = duration.parse(string)
|
201
|
+
end
|
202
|
+
|
203
|
+
## recurrence condition
|
204
|
+
def recurrence_condition
|
205
|
+
return @cond ||= Mhc::PropertyValue::RecurrenceCondition.new
|
206
|
+
end
|
207
|
+
|
208
|
+
def recurrence_condition=(string)
|
209
|
+
return @cond = recurrence_condition.parse(string)
|
210
|
+
end
|
211
|
+
|
212
|
+
## recurrence-tag
|
213
|
+
def recurrence_tag
|
214
|
+
return @recurrence_tag ||= Mhc::PropertyValue::Text.new
|
215
|
+
end
|
216
|
+
|
217
|
+
def recurrence_tag=(string)
|
218
|
+
return @recurrence_tag = recurrence_tag.parse(string)
|
219
|
+
end
|
220
|
+
|
221
|
+
## mission-tag
|
222
|
+
def mission_tag
|
223
|
+
return @mission_tag ||= Mhc::PropertyValue::Text.new
|
224
|
+
end
|
225
|
+
|
226
|
+
def mission_tag=(string)
|
227
|
+
return @mission_tag = mission_tag.parse(string)
|
228
|
+
end
|
229
|
+
|
230
|
+
## sequence
|
231
|
+
def sequence
|
232
|
+
return @sequence ||= Mhc::PropertyValue::Integer.new.parse("0")
|
233
|
+
end
|
234
|
+
|
235
|
+
def sequence=(string)
|
236
|
+
return @sequence = sequence.parse(string.to_s)
|
237
|
+
end
|
238
|
+
|
239
|
+
def occurrences(range:nil)
|
240
|
+
Mhc::OccurrenceEnumerator.new(self, dates, exceptions, recurrence_condition, duration, range)
|
241
|
+
end
|
242
|
+
|
243
|
+
def etag
|
244
|
+
return "#{uid.to_s}-#{sequence.to_s}"
|
245
|
+
end
|
246
|
+
|
247
|
+
def recurring?
|
248
|
+
not recurrence_condition.empty?
|
249
|
+
end
|
250
|
+
|
251
|
+
def allday?
|
252
|
+
time_range.blank?
|
253
|
+
end
|
254
|
+
|
255
|
+
def range
|
256
|
+
min0, max0 = Mhc::PropertyValue::Date.parse("19000101"),
|
257
|
+
Mhc::PropertyValue::Date.parse("99991231")
|
258
|
+
|
259
|
+
if recurring?
|
260
|
+
min, max = min0, max0
|
261
|
+
else
|
262
|
+
min, max = dates.min, dates.max
|
263
|
+
min = min.respond_to?(:first) ? min.first : min0
|
264
|
+
max = max.respond_to?(:last) ? max.last : max0
|
265
|
+
end
|
266
|
+
min = duration.first if duration.first && duration.first > min
|
267
|
+
max = duration.last if duration.last && duration.last < max
|
268
|
+
|
269
|
+
return min..max if min && max && min <= max
|
270
|
+
|
271
|
+
STDERR.puts "Warn: invalid date range? #{self.uid}"
|
272
|
+
return min0..max0
|
273
|
+
end
|
274
|
+
|
275
|
+
################################################################
|
276
|
+
### dump
|
277
|
+
|
278
|
+
def dump
|
279
|
+
non_xsc_header = @non_xsc_header.to_s.sub(/\n+\z/, "")
|
280
|
+
non_xsc_header += "\n" if non_xsc_header != ""
|
281
|
+
|
282
|
+
body = description.to_mhc_string
|
283
|
+
body += "\n" if body != "" && body !~ /\n\z/
|
284
|
+
|
285
|
+
return dump_header + non_xsc_header + "\n" + body
|
286
|
+
end
|
287
|
+
|
288
|
+
def dump_header
|
289
|
+
return "X-SC-Subject: #{subject.to_mhc_string}\n" +
|
290
|
+
"X-SC-Location: #{location.to_mhc_string}\n" +
|
291
|
+
"X-SC-Day: " + "#{dates.to_mhc_string} #{exceptions.to_mhc_string}".strip + "\n" +
|
292
|
+
"X-SC-Time: #{time_range.to_mhc_string}\n" +
|
293
|
+
"X-SC-Category: #{categories.to_mhc_string}\n" +
|
294
|
+
"X-SC-Mission-Tag: #{mission_tag.to_mhc_string}\n" +
|
295
|
+
"X-SC-Recurrence-Tag: #{recurrence_tag.to_mhc_string}\n" +
|
296
|
+
"X-SC-Cond: #{recurrence_condition.to_mhc_string}\n" +
|
297
|
+
"X-SC-Duration: #{duration.to_mhc_string}\n" +
|
298
|
+
"X-SC-Alarm: #{alarm.to_mhc_string}\n" +
|
299
|
+
"X-SC-Record-Id: #{record_id.to_mhc_string}\n" +
|
300
|
+
"X-SC-Sequence: #{sequence.to_mhc_string}\n"
|
301
|
+
end
|
302
|
+
|
303
|
+
alias_method :to_mhc_string, :dump
|
304
|
+
|
305
|
+
################################################################
|
306
|
+
### converter
|
307
|
+
|
308
|
+
def to_ics
|
309
|
+
Mhc::Converter::Icalendar.new.to_ics(self)
|
310
|
+
end
|
311
|
+
|
312
|
+
def to_icalendar
|
313
|
+
Mhc::Converter::Icalendar.new.to_icalendar(self)
|
314
|
+
end
|
315
|
+
|
316
|
+
def to_ics_string
|
317
|
+
Mhc::Converter::Icalendar.new.to_ics_string(self)
|
318
|
+
end
|
319
|
+
|
320
|
+
################################################################
|
321
|
+
private
|
322
|
+
|
323
|
+
def lazy?
|
324
|
+
return !@path.nil?
|
325
|
+
end
|
326
|
+
|
327
|
+
def clear
|
328
|
+
@alarm, @categories, @description, @location = [nil]*4
|
329
|
+
@record_id, @subject = [nil]*2
|
330
|
+
@dates, @exceptions, @time_range, @duration, @cond, @oc = [nil]*6
|
331
|
+
@non_xsc_header, @path = [nil]*2
|
332
|
+
return self
|
333
|
+
end
|
334
|
+
|
335
|
+
def parse_header_full(string)
|
336
|
+
xsc, @non_xsc_header = separate_header(string)
|
337
|
+
parse_xsc_header(xsc)
|
338
|
+
return self
|
339
|
+
end
|
340
|
+
|
341
|
+
def parse_header(string)
|
342
|
+
hash = {}
|
343
|
+
string.scan(/^x-sc-([^:]++):[ \t]*([^\n]*(?:\n[ \t]+[^\n]*)*)/i) do |key, val|
|
344
|
+
hash[key.downcase] = val.gsub("\n", " ").strip
|
345
|
+
end
|
346
|
+
parse_xsc_header(hash)
|
347
|
+
return self
|
348
|
+
end
|
349
|
+
|
350
|
+
def parse_xsc_header(hash)
|
351
|
+
hash.each do |key, val|
|
352
|
+
case key
|
353
|
+
when "day" ; self.dates = val ; self.exceptions = val
|
354
|
+
when "date" ; self.obsolete_dates = val
|
355
|
+
when "subject" ; self.subject = val
|
356
|
+
when "location" ; self.location = val
|
357
|
+
when "time" ; self.time_range = val
|
358
|
+
when "duration" ; self.duration = val
|
359
|
+
when "category" ; self.categories = val
|
360
|
+
when "mission-tag" ; self.mission_tag = val
|
361
|
+
when "recurrence-tag" ; self.recurrence_tag = val
|
362
|
+
when "cond" ; self.recurrence_condition = val
|
363
|
+
when "alarm" ; self.alarm = val
|
364
|
+
when "record-id" ; self.record_id = val
|
365
|
+
when "sequence" ; self.sequence = val
|
366
|
+
else
|
367
|
+
# raise NotImplementedError, "X-SC-#{key.capitalize}"
|
368
|
+
# STDERR.print "Obsolete: X-SC-#{key.capitalize}\n"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
return self
|
372
|
+
end
|
373
|
+
|
374
|
+
## return: X-SC-* headers as a hash and
|
375
|
+
## non-X-SC-* headers as one string.
|
376
|
+
def separate_header(header)
|
377
|
+
xsc, non_xsc, xsc_key = {}, "", nil
|
378
|
+
|
379
|
+
header.split("\n").each do |line|
|
380
|
+
if line =~ /^X-SC-([^:]+):(.*)/i
|
381
|
+
xsc_key = $1.downcase
|
382
|
+
xsc[xsc_key] = $2.to_s.strip
|
383
|
+
|
384
|
+
elsif line =~ /^\s/ && xsc_key
|
385
|
+
xsc[xsc_key] += " " + line
|
386
|
+
|
387
|
+
else
|
388
|
+
xsc_key = nil
|
389
|
+
non_xsc += line + "\n"
|
390
|
+
end
|
391
|
+
end
|
392
|
+
return [xsc, non_xsc]
|
393
|
+
end
|
394
|
+
|
395
|
+
end # class Event
|
396
|
+
end # module Mhc
|
@@ -0,0 +1,312 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Mhc
|
4
|
+
class FormatterNameError < StandardError; end
|
5
|
+
|
6
|
+
class Formatter
|
7
|
+
def self.build(formatter:, date_range:, **options)
|
8
|
+
case formatter.to_sym
|
9
|
+
when :text
|
10
|
+
Text.new(date_range: date_range, options:options)
|
11
|
+
when :mail
|
12
|
+
Mail.new(date_range: date_range, options:options)
|
13
|
+
when :orgtable
|
14
|
+
OrgTable.new(date_range: date_range, options:options)
|
15
|
+
when :emacs
|
16
|
+
Emacs.new(date_range: date_range, options:options)
|
17
|
+
when :icalendar
|
18
|
+
ICalendar.new(date_range: date_range, options:options)
|
19
|
+
when :calfw
|
20
|
+
SymbolicExpression.new(date_range: date_range, options:options)
|
21
|
+
when :howm
|
22
|
+
Howm.new(date_range: date_range, options:options)
|
23
|
+
else
|
24
|
+
raise FormatterNameError.new("Unknown format: #{formatter} (#{formatter.class})")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Base
|
29
|
+
def initialize(date_range:, options:nil)
|
30
|
+
@date_range = date_range
|
31
|
+
@options = options
|
32
|
+
@occurrences, @events, @items = [], [], {}
|
33
|
+
@event_hash = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(occurrence)
|
37
|
+
event = occurrence.event
|
38
|
+
@occurrences << occurrence
|
39
|
+
@events << event unless @event_hash[event]
|
40
|
+
@event_hash[event] = true
|
41
|
+
|
42
|
+
@items[occurrence.date] ||= []
|
43
|
+
@items[occurrence.date] << occurrence
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
context = {:items => @items}.merge(@options)
|
48
|
+
prepare(context)
|
49
|
+
string = format_header(context) + format_body(context) + format_footer(context)
|
50
|
+
teardown(context)
|
51
|
+
return string
|
52
|
+
end
|
53
|
+
|
54
|
+
################################################################
|
55
|
+
private
|
56
|
+
|
57
|
+
def prepare(context); end
|
58
|
+
def teardown(context); end
|
59
|
+
|
60
|
+
def pad_empty_dates
|
61
|
+
@date_range.each do |date|
|
62
|
+
@items[date] ||= []
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def expand_multiple_days_occurrences
|
67
|
+
@occurrences.each do |oc|
|
68
|
+
next if oc.oneday?
|
69
|
+
((oc.first + 1) .. oc.last).each do |date|
|
70
|
+
@items[date] ||= []
|
71
|
+
@items[date] << oc
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_header(context); ""; end
|
77
|
+
def format_footer(context); ""; end
|
78
|
+
def format_day_header(context, date); ""; end
|
79
|
+
def format_day_footer(context, date); ""; end
|
80
|
+
|
81
|
+
def format_body(context)
|
82
|
+
context[:number] = 0
|
83
|
+
@items.keys.sort.map{|date| format_day(context, date, @items[date]) }.join
|
84
|
+
end
|
85
|
+
|
86
|
+
def format_day(context, date, items)
|
87
|
+
string = format_day_header(context, date)
|
88
|
+
|
89
|
+
items = sort_items_in_day(items)
|
90
|
+
items.each_with_index do |occurrence, count|
|
91
|
+
context[:number] += 1
|
92
|
+
context[:number_in_day] = count + 1
|
93
|
+
string += format_item(context, date, occurrence)
|
94
|
+
end
|
95
|
+
|
96
|
+
return string + format_day_footer(context, date)
|
97
|
+
end
|
98
|
+
|
99
|
+
def format_item(context, date, item)
|
100
|
+
format("%s%-11s %s%s\n",
|
101
|
+
format_item_header(context, date, item),
|
102
|
+
item.time_range.to_mhc_string.toutf8,
|
103
|
+
item.subject.to_s.toutf8,
|
104
|
+
append(enclose(item.location)).toutf8
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_item_header(context, date, item)
|
109
|
+
if context[:number_in_day] == 1
|
110
|
+
date.strftime("%Y/%m/%d %a ")
|
111
|
+
else
|
112
|
+
" " * 15
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
################################################################
|
117
|
+
## helpers
|
118
|
+
def append(item, separator = " ")
|
119
|
+
return "" if item.to_s.empty?
|
120
|
+
return separator + item.to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
def prepend(item, separator = " ")
|
124
|
+
return "" if item.to_s.empty?
|
125
|
+
return item.to_s + separator
|
126
|
+
end
|
127
|
+
|
128
|
+
def enclose(item, bracket = "[]")
|
129
|
+
return "" if item.to_s.empty?
|
130
|
+
return bracket[0] + item.to_s + bracket[1]
|
131
|
+
end
|
132
|
+
|
133
|
+
# sort occurrences in a day
|
134
|
+
# make sure all-day occurrences are prior to others
|
135
|
+
def sort_items_in_day(items)
|
136
|
+
items.sort do |a,b|
|
137
|
+
sign_a = a.allday? ? 0 : 1
|
138
|
+
sign_b = b.allday? ? 0 : 1
|
139
|
+
|
140
|
+
if sign_a != sign_b
|
141
|
+
sign_a - sign_b
|
142
|
+
else
|
143
|
+
a <=> b
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class Text < Base
|
150
|
+
def prepare(context)
|
151
|
+
expand_multiple_days_occurrences
|
152
|
+
end
|
153
|
+
end # class Text
|
154
|
+
|
155
|
+
class Mail < Base
|
156
|
+
private
|
157
|
+
|
158
|
+
def format_header(context)
|
159
|
+
mail_address = context[:mailto].to_s
|
160
|
+
subject = format("MHC schedule report (%s--%s)", *context[:items].keys.minmax)
|
161
|
+
header = "To: #{mail_address}\n"
|
162
|
+
header += "From: #{append(mail_address, "secretary-of-")}\n"
|
163
|
+
header += "Subject: #{subject}\n"
|
164
|
+
header += "Content-Type: Text/Plain; charset=utf-8\n"
|
165
|
+
header += "Content-Transfer-Encoding: 8bit\n"
|
166
|
+
header += "\n"
|
167
|
+
header += format("* mhc %s--%s\n", *context[:items].keys.minmax)
|
168
|
+
end
|
169
|
+
end # class Mail
|
170
|
+
|
171
|
+
class SymbolicExpression < Base
|
172
|
+
private
|
173
|
+
|
174
|
+
def format_header(context); "("; end
|
175
|
+
def format_footer(context); "(periods #{@periods}))\n"; end
|
176
|
+
|
177
|
+
def format_day_header(context, date)
|
178
|
+
date.strftime("((%2m %2d %Y) . (")
|
179
|
+
end
|
180
|
+
|
181
|
+
def format_item(context, date, item)
|
182
|
+
unless item.oneday?
|
183
|
+
format_multiple_days_item(context, date, item)
|
184
|
+
return ""
|
185
|
+
end
|
186
|
+
format_item_line(item)
|
187
|
+
end
|
188
|
+
|
189
|
+
def format_multiple_days_item(context, date, item)
|
190
|
+
@periods ||= ""
|
191
|
+
@periods += item.first.strftime("((%2m %2d %Y) ") +
|
192
|
+
item.last.strftime(" (%2m %2d %Y) ") +
|
193
|
+
format_item_line(item) + ') '
|
194
|
+
end
|
195
|
+
|
196
|
+
def format_day_footer(context, date); ")) "; end
|
197
|
+
|
198
|
+
def format_item_line(item)
|
199
|
+
'"' +
|
200
|
+
format("%s%s%s",
|
201
|
+
prepend(item.time_range.first.to_s).toutf8,
|
202
|
+
item.subject.to_s.toutf8,
|
203
|
+
append(enclose(item.location)).toutf8).gsub(/[\"\\]/, '\\\\\&') +
|
204
|
+
'" '
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class Emacs < SymbolicExpression
|
210
|
+
private
|
211
|
+
|
212
|
+
def prepare(context)
|
213
|
+
expand_multiple_days_occurrences
|
214
|
+
end
|
215
|
+
|
216
|
+
def format_header(context); "("; end
|
217
|
+
def format_footer(context); ")\n"; end
|
218
|
+
|
219
|
+
def format_day_header(context, date)
|
220
|
+
# (DAYS_FROM_EPOC . [year month day wday holiday-p (
|
221
|
+
format("(%d . [%d %d %d %d nil (", date.absolute_from_epoch, date.year, date.month, date.day, date.wday)
|
222
|
+
end
|
223
|
+
|
224
|
+
def format_item(context, date, item)
|
225
|
+
# [ RECORD CONDITION SUBJECT LOCATION (TIMEB . TIMEE) ALARM CATEGORIES PRIORITY REGION RECURRENCE-TAG]
|
226
|
+
format("[(%s . [%s nil nil]) nil %s %s (%s . %s) %s (%s) nil nil %s]",
|
227
|
+
elisp_string(item.path.to_s),
|
228
|
+
elisp_string(item.uid.to_s),
|
229
|
+
elisp_string(item.subject),
|
230
|
+
elisp_string(item.location),
|
231
|
+
(item.time_range.first ? (item.time_range.first.to_i / 60) : "nil"),
|
232
|
+
(item.time_range.last ? (item.time_range.last.to_i / 60) : "nil"),
|
233
|
+
elisp_string(item.alarm.to_s),
|
234
|
+
item.categories.map{|c| elisp_string(c.to_s.downcase)}.join(" "),
|
235
|
+
elisp_string(item.recurrence_tag))
|
236
|
+
end
|
237
|
+
|
238
|
+
def format_day_footer(context, date)
|
239
|
+
")]) "
|
240
|
+
end
|
241
|
+
|
242
|
+
def elisp_string(string)
|
243
|
+
'"' + string.to_s.toutf8.gsub(/[\"\\]/, '\\\\\&') + '"'
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class ICalendar < Base
|
248
|
+
private
|
249
|
+
|
250
|
+
def format_body(context)
|
251
|
+
ical = RiCal.Calendar
|
252
|
+
ical.prodid = Mhc::PRODID
|
253
|
+
@events.each do |event|
|
254
|
+
ical.events << event.to_icalendar
|
255
|
+
end
|
256
|
+
return ical.to_s
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
class OrgTable < Base
|
261
|
+
private
|
262
|
+
|
263
|
+
def format_header(context)
|
264
|
+
format("* mhc %s--%s\n", *context[:items].keys.minmax)
|
265
|
+
end
|
266
|
+
|
267
|
+
def format_item(context, date, item)
|
268
|
+
# | No | Mission | Recurrence | Subject | Path | Date |
|
269
|
+
format(" | %4d | %s | %s | %s | %s | [%04d-%02d-%02d%s] |\n",
|
270
|
+
context[:number],
|
271
|
+
item.mission_tag.to_s.toutf8,
|
272
|
+
item.recurrence_tag.to_s.toutf8,
|
273
|
+
item.subject.to_s.toutf8,
|
274
|
+
item.path.to_s,
|
275
|
+
date.year, date.month, date.mday,
|
276
|
+
append(item.time_range.to_s))
|
277
|
+
end
|
278
|
+
end # class OrgTable
|
279
|
+
|
280
|
+
class Howm < Base
|
281
|
+
private
|
282
|
+
|
283
|
+
def format_header(context)
|
284
|
+
format("= mhc %s--%s\n", *context[:items].keys.minmax)
|
285
|
+
end
|
286
|
+
|
287
|
+
def format_item(context, date, item)
|
288
|
+
string = format("[%04d-%02d-%02d %5s]%1s %s\n",
|
289
|
+
date.year, date.month, date.mday,
|
290
|
+
item.time_range.first.to_s,
|
291
|
+
mark_todo(item.categories.to_mhc_string),
|
292
|
+
item.subject)
|
293
|
+
if item.description.to_s != ""
|
294
|
+
string += item.description.to_s.gsub(/^/, " ") + "\n"
|
295
|
+
end
|
296
|
+
return string
|
297
|
+
end
|
298
|
+
|
299
|
+
def mark_todo(category)
|
300
|
+
case category
|
301
|
+
when /done/i
|
302
|
+
"."
|
303
|
+
when /todo/i
|
304
|
+
"+"
|
305
|
+
else
|
306
|
+
"@"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end # class Howm
|
310
|
+
|
311
|
+
end # module Formatter
|
312
|
+
end # module Mhc
|