mhc 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/bin/mhc +66 -3
- data/emacs/Cask +1 -1
- data/emacs/mhc-date.el +1 -1
- data/emacs/mhc-day.el +1 -1
- data/emacs/mhc-db.el +57 -38
- data/emacs/mhc-draft.el +36 -22
- data/emacs/mhc-face.el +1 -1
- data/emacs/mhc-header.el +20 -1
- data/emacs/mhc-minibuf.el +12 -7
- data/emacs/mhc-parse.el +1 -1
- data/emacs/mhc-process.el +26 -9
- data/emacs/mhc-ps.el +1 -1
- data/emacs/mhc-schedule.el +5 -2
- data/emacs/mhc-summary.el +31 -12
- data/emacs/mhc-vars.el +15 -2
- data/emacs/mhc.el +50 -24
- data/lib/mhc.rb +3 -1
- data/lib/mhc/builder.rb +5 -1
- data/lib/mhc/calendar.rb +5 -1
- data/lib/mhc/command/cache.rb +5 -4
- data/lib/mhc/converter.rb +3 -2
- data/lib/mhc/datastore.rb +52 -13
- data/lib/mhc/date_enumerator.rb +2 -2
- data/lib/mhc/event.rb +42 -21
- data/lib/mhc/formatter.rb +17 -312
- data/lib/mhc/formatter/base.rb +125 -0
- data/lib/mhc/formatter/emacs.rb +47 -0
- data/lib/mhc/formatter/howm.rb +35 -0
- data/lib/mhc/formatter/icalendar.rb +17 -0
- data/lib/mhc/formatter/json.rb +27 -0
- data/lib/mhc/formatter/mail.rb +20 -0
- data/lib/mhc/formatter/org_table.rb +24 -0
- data/lib/mhc/formatter/symbolic_expression.rb +42 -0
- data/lib/mhc/formatter/text.rb +29 -0
- data/lib/mhc/occurrence.rb +27 -5
- data/lib/mhc/occurrence_enumerator.rb +1 -1
- data/lib/mhc/property_value.rb +6 -0
- data/lib/mhc/property_value/date.rb +23 -14
- data/lib/mhc/property_value/date_time.rb +19 -0
- data/lib/mhc/property_value/integer.rb +5 -1
- data/lib/mhc/property_value/list.rb +7 -6
- data/lib/mhc/property_value/period.rb +3 -1
- data/lib/mhc/property_value/range.rb +1 -1
- data/lib/mhc/property_value/time.rb +8 -1
- data/lib/mhc/version.rb +1 -1
- data/spec/mhc_spec.rb +83 -0
- metadata +13 -3
data/lib/mhc/date_enumerator.rb
CHANGED
@@ -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
|
data/lib/mhc/event.rb
CHANGED
@@ -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
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
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
|
400
|
+
return errors
|
380
401
|
end
|
381
402
|
|
382
403
|
## return: X-SC-* headers as a hash and
|
data/lib/mhc/formatter.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
23
|
+
Json.new(date_range: date_range, options:options)
|
25
24
|
else
|
26
|
-
raise
|
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
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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 #
|
41
|
+
end # class Formatter
|
337
42
|
end # module Mhc
|