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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. 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