mhc 1.1.1 → 1.2.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 (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