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.
- 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
|