mhc 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/bin/mhc +66 -3
  3. data/emacs/Cask +1 -1
  4. data/emacs/mhc-date.el +1 -1
  5. data/emacs/mhc-day.el +1 -1
  6. data/emacs/mhc-db.el +57 -38
  7. data/emacs/mhc-draft.el +36 -22
  8. data/emacs/mhc-face.el +1 -1
  9. data/emacs/mhc-header.el +20 -1
  10. data/emacs/mhc-minibuf.el +12 -7
  11. data/emacs/mhc-parse.el +1 -1
  12. data/emacs/mhc-process.el +26 -9
  13. data/emacs/mhc-ps.el +1 -1
  14. data/emacs/mhc-schedule.el +5 -2
  15. data/emacs/mhc-summary.el +31 -12
  16. data/emacs/mhc-vars.el +15 -2
  17. data/emacs/mhc.el +50 -24
  18. data/lib/mhc.rb +3 -1
  19. data/lib/mhc/builder.rb +5 -1
  20. data/lib/mhc/calendar.rb +5 -1
  21. data/lib/mhc/command/cache.rb +5 -4
  22. data/lib/mhc/converter.rb +3 -2
  23. data/lib/mhc/datastore.rb +52 -13
  24. data/lib/mhc/date_enumerator.rb +2 -2
  25. data/lib/mhc/event.rb +42 -21
  26. data/lib/mhc/formatter.rb +17 -312
  27. data/lib/mhc/formatter/base.rb +125 -0
  28. data/lib/mhc/formatter/emacs.rb +47 -0
  29. data/lib/mhc/formatter/howm.rb +35 -0
  30. data/lib/mhc/formatter/icalendar.rb +17 -0
  31. data/lib/mhc/formatter/json.rb +27 -0
  32. data/lib/mhc/formatter/mail.rb +20 -0
  33. data/lib/mhc/formatter/org_table.rb +24 -0
  34. data/lib/mhc/formatter/symbolic_expression.rb +42 -0
  35. data/lib/mhc/formatter/text.rb +29 -0
  36. data/lib/mhc/occurrence.rb +27 -5
  37. data/lib/mhc/occurrence_enumerator.rb +1 -1
  38. data/lib/mhc/property_value.rb +6 -0
  39. data/lib/mhc/property_value/date.rb +23 -14
  40. data/lib/mhc/property_value/date_time.rb +19 -0
  41. data/lib/mhc/property_value/integer.rb +5 -1
  42. data/lib/mhc/property_value/list.rb +7 -6
  43. data/lib/mhc/property_value/period.rb +3 -1
  44. data/lib/mhc/property_value/range.rb +1 -1
  45. data/lib/mhc/property_value/time.rb +8 -1
  46. data/lib/mhc/version.rb +1 -1
  47. data/spec/mhc_spec.rb +83 -0
  48. metadata +13 -3
@@ -257,8 +257,8 @@ module Mhc
257
257
  def each
258
258
  head, tail = range
259
259
  @range_list.each do |date_range|
260
- break if date_range.first > tail
261
- next if date_range.last < head
260
+ break if date_range.first.to_date > tail
261
+ next if date_range.last.to_date < head
262
262
  yield date_range
263
263
  end
264
264
  end
@@ -40,6 +40,10 @@ module Mhc
40
40
  return new.parse_file(path, lazy)
41
41
  end
42
42
 
43
+ def self.validate(string)
44
+ return new.validate(string)
45
+ end
46
+
43
47
  def parse_file(path, lazy = true)
44
48
  STDOUT.puts "parsing #{File.expand_path(path)}" if $MHC_DEBUG
45
49
 
@@ -325,6 +329,17 @@ module Mhc
325
329
  Mhc::Converter::Icalendar.new.to_ics_string(self)
326
330
  end
327
331
 
332
+ def validate(string)
333
+ header, _ = string.scrub.split(/\n\n/, 2)
334
+ errors = parse_header(header)
335
+
336
+ errors << ["no subject"] if subject.empty?
337
+ errors << ["no record-id"] if record_id.empty?
338
+ errors << ["no effective date specified"] if dates.empty? && recurrence_condition.empty?
339
+
340
+ return errors
341
+ end
342
+
328
343
  ################################################################
329
344
  private
330
345
 
@@ -348,35 +363,41 @@ module Mhc
348
363
 
349
364
  def parse_header(string)
350
365
  hash = {}
351
- string.scan(/^x-sc-([^:]++):[ \t]*([^\n]*(?:\n[ \t]+[^\n]*)*)/i) do |key, val|
366
+ string.scrub.scan(/^x-sc-([^:]++):[ \t]*([^\n]*(?:\n[ \t]+[^\n]*)*)/i) do |key, val|
352
367
  hash[key.downcase] = val.gsub("\n", " ").strip
353
368
  end
354
- parse_xsc_header(hash)
355
- return self
369
+ return parse_xsc_header(hash)
356
370
  end
357
371
 
358
372
  def parse_xsc_header(hash)
373
+ errors = []
359
374
  hash.each do |key, val|
360
- case key
361
- when "day" ; self.dates = val ; self.exceptions = val
362
- when "date" ; self.obsolete_dates = val
363
- when "subject" ; self.subject = val
364
- when "location" ; self.location = val
365
- when "time" ; self.time_range = val
366
- when "duration" ; self.duration = val
367
- when "category" ; self.categories = val
368
- when "mission-tag" ; self.mission_tag = val
369
- when "recurrence-tag" ; self.recurrence_tag = val
370
- when "cond" ; self.recurrence_condition = val
371
- when "alarm" ; self.alarm = val
372
- when "record-id" ; self.record_id = val
373
- when "sequence" ; self.sequence = val
374
- else
375
- # raise NotImplementedError, "X-SC-#{key.capitalize}"
376
- # STDERR.print "Obsolete: X-SC-#{key.capitalize}\n"
375
+ next if val.empty?
376
+ begin
377
+ case key
378
+ when "day" ; self.dates = val
379
+ ; self.exceptions = val
380
+ when "date" ; self.obsolete_dates = val
381
+ when "subject" ; self.subject = val
382
+ when "location" ; self.location = val
383
+ when "time" ; self.time_range = val
384
+ when "duration" ; self.duration = val
385
+ when "category" ; self.categories = val
386
+ when "mission-tag" ; self.mission_tag = val
387
+ when "recurrence-tag" ; self.recurrence_tag = val
388
+ when "cond" ; self.recurrence_condition = val
389
+ when "alarm" ; self.alarm = val
390
+ when "record-id" ; self.record_id = val
391
+ when "sequence" ; self.sequence = val
392
+ else
393
+ raise Mhc::PropertyValue::ParseError,
394
+ "invalid X-SC-#{key.capitalize} header"
395
+ end
396
+ rescue Mhc::PropertyValue::ParseError => e
397
+ errors << [e, key]
377
398
  end
378
399
  end
379
- return self
400
+ return errors
380
401
  end
381
402
 
382
403
  ## return: X-SC-* headers as a hash and
@@ -1,9 +1,8 @@
1
- # -*- coding: utf-8 -*-
2
-
3
1
  module Mhc
4
- class FormatterNameError < StandardError; end
5
-
6
2
  class Formatter
3
+
4
+ class NameError < StandardError; end
5
+
7
6
  def self.build(formatter:, date_range:, **options)
8
7
  case formatter.to_sym
9
8
  when :text
@@ -15,323 +14,29 @@ module Mhc
15
14
  when :emacs
16
15
  Emacs.new(date_range: date_range, options:options)
17
16
  when :icalendar
18
- ICalendar.new(date_range: date_range, options:options)
17
+ Icalendar.new(date_range: date_range, options:options)
19
18
  when :calfw
20
19
  SymbolicExpression.new(date_range: date_range, options:options)
21
20
  when :howm
22
21
  Howm.new(date_range: date_range, options:options)
23
22
  when :json
24
- FullCalendar.new(date_range: date_range, options:options)
23
+ Json.new(date_range: date_range, options:options)
25
24
  else
26
- raise FormatterNameError.new("Unknown format: #{formatter} (#{formatter.class})")
27
- end
28
- end
29
-
30
- class Base
31
- def initialize(date_range:, options:nil)
32
- @date_range = date_range
33
- @options = options
34
- @occurrences, @events, @items = [], [], {}
35
- @event_hash = {}
36
- end
37
-
38
- def <<(occurrence)
39
- event = occurrence.event
40
- @occurrences << occurrence
41
- @events << event unless @event_hash[event]
42
- @event_hash[event] = true
43
-
44
- @items[occurrence.date] ||= []
45
- @items[occurrence.date] << occurrence
46
- end
47
-
48
- def to_s
49
- context = {:items => @items}.merge(@options)
50
- prepare(context)
51
- string = format_header(context) + format_body(context) + format_footer(context)
52
- teardown(context)
53
- return string
54
- end
55
-
56
- ################################################################
57
- private
58
-
59
- def prepare(context); end
60
- def teardown(context); end
61
-
62
- def pad_empty_dates
63
- @date_range.each do |date|
64
- @items[date] ||= []
65
- end
66
- end
67
-
68
- def expand_multiple_days_occurrences
69
- @occurrences.each do |oc|
70
- next if oc.oneday?
71
- ((oc.first + 1) .. oc.last).each do |date|
72
- @items[date] ||= []
73
- @items[date] << oc
74
- end
75
- end
76
- end
77
-
78
- def format_header(context); ""; end
79
- def format_footer(context); ""; end
80
- def format_day_header(context, date, is_holiday); ""; end
81
- def format_day_footer(context, date); ""; end
82
-
83
- def format_body(context)
84
- context[:number] = 0
85
- @items.keys.sort.map{|date| format_day(context, date, @items[date]) }.join
86
- end
87
-
88
- def format_day(context, date, items)
89
- string = format_day_header(context, date, items.any?{|e| e.holiday?})
90
-
91
- items = sort_items_in_day(items)
92
- items.each_with_index do |occurrence, count|
93
- context[:number] += 1
94
- context[:number_in_day] = count + 1
95
- string += format_item(context, date, occurrence)
96
- end
97
-
98
- return string + format_day_footer(context, date)
99
- end
100
-
101
- def format_item(context, date, item)
102
- format("%s%-11s %s%s\n",
103
- format_item_header(context, date, item),
104
- item.time_range.to_mhc_string.toutf8,
105
- item.subject.to_s.toutf8,
106
- append(enclose(item.location)).toutf8
107
- )
108
- end
109
-
110
- def format_item_header(context, date, item)
111
- if context[:number_in_day] == 1
112
- date.strftime("%Y/%m/%d %a ")
113
- else
114
- " " * 15
115
- end
116
- end
117
-
118
- ################################################################
119
- ## helpers
120
- def append(item, separator = " ")
121
- return "" if item.to_s.empty?
122
- return separator + item.to_s
123
- end
124
-
125
- def prepend(item, separator = " ")
126
- return "" if item.to_s.empty?
127
- return item.to_s + separator
128
- end
129
-
130
- def enclose(item, bracket = "[]")
131
- return "" if item.to_s.empty?
132
- return bracket[0] + item.to_s + bracket[1]
133
- end
134
-
135
- # sort occurrences in a day
136
- # make sure all-day occurrences are prior to others
137
- def sort_items_in_day(items)
138
- items.sort do |a,b|
139
- sign_a = a.allday? ? 0 : 1
140
- sign_b = b.allday? ? 0 : 1
141
-
142
- if sign_a != sign_b
143
- sign_a - sign_b
144
- else
145
- a <=> b
146
- end
147
- end
25
+ raise Formatter::NameError.new("Unknown format: #{formatter} (#{formatter.class})")
148
26
  end
149
27
  end
150
28
 
151
- class Text < Base
152
- def prepare(context)
153
- expand_multiple_days_occurrences
154
- end
155
- end # class Text
156
-
157
- class Mail < Base
158
- private
159
-
160
- def format_header(context)
161
- mail_address = context[:mailto].to_s
162
- subject = format("MHC schedule report (%s--%s)", *context[:items].keys.minmax)
163
- header = "To: #{mail_address}\n"
164
- header += "From: #{append(mail_address, "secretary-of-")}\n"
165
- header += "Subject: #{subject}\n"
166
- header += "Content-Type: Text/Plain; charset=utf-8\n"
167
- header += "Content-Transfer-Encoding: 8bit\n"
168
- header += "\n"
169
- header += format("* mhc %s--%s\n", *context[:items].keys.minmax)
170
- end
171
- end # class Mail
172
-
173
- class SymbolicExpression < Base
174
- private
175
-
176
- def format_header(context); "("; end
177
- def format_footer(context); "(periods #{@periods}))\n"; end
29
+ dir = File.dirname(__FILE__) + "/formatter"
178
30
 
179
- def format_day_header(context, date, is_holiday)
180
- date.strftime("((%2m %2d %Y) . (")
181
- end
182
-
183
- def format_item(context, date, item)
184
- unless item.oneday?
185
- format_multiple_days_item(context, date, item)
186
- return ""
187
- end
188
- format_item_line(item)
189
- end
190
-
191
- def format_multiple_days_item(context, date, item)
192
- @periods ||= ""
193
- @periods += item.first.strftime("((%2m %2d %Y) ") +
194
- item.last.strftime(" (%2m %2d %Y) ") +
195
- format_item_line(item) + ') '
196
- end
197
-
198
- def format_day_footer(context, date); ")) "; end
199
-
200
- def format_item_line(item)
201
- '"' +
202
- format("%s%s%s",
203
- prepend(item.time_range.first.to_s).toutf8,
204
- item.subject.to_s.toutf8,
205
- append(enclose(item.location)).toutf8).gsub(/[\"\\]/, '\\\\\&') +
206
- '" '
207
-
208
- end
209
- end
210
-
211
- class Emacs < SymbolicExpression
212
- private
213
-
214
- def prepare(context)
215
- expand_multiple_days_occurrences
216
- end
217
-
218
- def format_header(context); "("; end
219
- def format_footer(context); ")\n"; end
220
-
221
- def format_day_header(context, date, is_holiday)
222
- # (DAYS_FROM_EPOC . [year month day wday holiday-p (
223
- format("(%d . [%d %d %d %d #{is_holiday ? 't' : 'nil'} (", date.absolute_from_epoch, date.year, date.month, date.day, date.wday)
224
- end
225
-
226
- def format_item(context, date, item)
227
- # [ RECORD CONDITION SUBJECT LOCATION (TIMEB . TIMEE) ALARM CATEGORIES PRIORITY REGION RECURRENCE-TAG]
228
- format("[(%s . [%s nil nil]) nil %s %s (%s . %s) %s (%s) nil nil %s]",
229
- elisp_string(item.path.to_s),
230
- elisp_string(item.uid.to_s),
231
- elisp_string(item.subject),
232
- elisp_string(item.location),
233
- (item.time_range.first ? (item.time_range.first.to_i / 60) : "nil"),
234
- (item.time_range.last ? (item.time_range.last.to_i / 60) : "nil"),
235
- elisp_string(item.alarm.to_s),
236
- item.categories.map{|c| elisp_string(c.to_s.downcase)}.join(" "),
237
- elisp_string(item.recurrence_tag))
238
- end
239
-
240
- def format_day_footer(context, date)
241
- ")]) "
242
- end
243
-
244
- def elisp_string(string)
245
- '"' + string.to_s.toutf8.gsub(/[\"\\]/, '\\\\\&') + '"'
246
- end
247
- end
248
-
249
- class ICalendar < Base
250
- private
251
-
252
- def format_body(context)
253
- ical = RiCal.Calendar
254
- ical.prodid = Mhc::PRODID
255
- @events.each do |event|
256
- ical.events << event.to_icalendar
257
- end
258
- return ical.to_s
259
- end
260
- end
261
-
262
- class OrgTable < Base
263
- private
264
-
265
- def format_header(context)
266
- format("* mhc %s--%s\n", *context[:items].keys.minmax)
267
- end
268
-
269
- def format_item(context, date, item)
270
- # | No | Mission | Recurrence | Subject | Path | Date |
271
- format(" | %4d | %s | %s | %s | %s | [%04d-%02d-%02d%s] |\n",
272
- context[:number],
273
- item.mission_tag.to_s.toutf8,
274
- item.recurrence_tag.to_s.toutf8,
275
- item.subject.to_s.toutf8,
276
- item.path.to_s,
277
- date.year, date.month, date.mday,
278
- append(item.time_range.to_s))
279
- end
280
- end # class OrgTable
281
-
282
- class Howm < Base
283
- private
284
-
285
- def format_header(context)
286
- format("= mhc %s--%s\n", *context[:items].keys.minmax)
287
- end
288
-
289
- def format_item(context, date, item)
290
- string = format("[%04d-%02d-%02d %5s]%1s %s\n",
291
- date.year, date.month, date.mday,
292
- item.time_range.first.to_s,
293
- mark_todo(item.categories.to_mhc_string),
294
- item.subject)
295
- if item.description.to_s != ""
296
- string += item.description.to_s.gsub(/^/, " ") + "\n"
297
- end
298
- return string
299
- end
300
-
301
- def mark_todo(category)
302
- case category
303
- when /done/i
304
- "."
305
- when /todo/i
306
- "+"
307
- else
308
- "@"
309
- end
310
- end
311
- end # class Howm
312
-
313
- class FullCalendar < Base
314
- require "json"
315
-
316
- def format_body(context)
317
- events = []
318
- @occurrences.each do |oc|
319
- class_name = []
320
- class_name += oc.categories.map{|c| "mhc-category-#{c.to_s.downcase}"}
321
- class_name << (oc.allday? ? "mhc-allday" : "mhc-time-range")
322
-
323
- events << {
324
- id: oc.record_id,
325
- allDay: oc.allday?,
326
- title: oc.subject,
327
- start: oc.dtstart.iso8601,
328
- end: oc.dtend.iso8601,
329
- className: class_name
330
- }
331
- end
332
- return events.to_json
333
- end
334
- end # class FullCalendar
31
+ autoload :Base, "#{dir}/base.rb"
32
+ autoload :Emacs, "#{dir}/emacs.rb"
33
+ autoload :Howm, "#{dir}/howm.rb"
34
+ autoload :Icalendar, "#{dir}/icalendar.rb"
35
+ autoload :Json, "#{dir}/json.rb"
36
+ autoload :Mail, "#{dir}/mail.rb"
37
+ autoload :OrgTable, "#{dir}/org_table.rb"
38
+ autoload :SymbolicExpression, "#{dir}/symbolic_expression.rb"
39
+ autoload :Text, "#{dir}/text.rb"
335
40
 
336
- end # module Formatter
41
+ end # class Formatter
337
42
  end # module Mhc