mhc 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4522630272ecdc9d44c9b994b2b31f8e36278abb0004708ec0e39d63e5d16792
4
- data.tar.gz: cbcd079db569d336eec4eb87f21e5598cd3735b04f0fef6ffbbc8b7fdb583067
3
+ metadata.gz: b0b65ae5e1dd5f504de2eeb59f280eff7ef0422133520006e9814ab52510e9ac
4
+ data.tar.gz: '0168de3d55867bf8356a02cf9be81d3ecd199b01f64d5c2cdc5f6835a0605fdd'
5
5
  SHA512:
6
- metadata.gz: 1f052c78047e59e8638248ef56928f2ee3e85ae544b959d294c6900afb06868cb305a4a23b1ae85a35d963b870dbb0ec950c4c1d5cd9ea25d9ffe07334be5cd8
7
- data.tar.gz: 874b007618cfd8be8a49ba0fab497ba38f6a36edfedbedf0ec72bb983c6c5ceff67a52c2b6138a62ced23fb0586e5f644eca018e2d9b963a8d79666100b94b9a
6
+ metadata.gz: d4533cef977bafd8491f1f3bbb709049dfa17b7ca70f0286381aaba60ffb51a096d34b28d6cb7239e1a5f932729237dce1d931332c1830983357ec99196f75e5
7
+ data.tar.gz: 40dbdfcdcacf2ac4f274db2897bfa91f25a58ebb0d80c4b705d9134efef3e2fafa7e1ec51a743817e075e7a0f015b53262a6c3c1f915400ce5e0dd5dad31727d
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ Style/RedundantReturn:
2
+ Enabled: false
3
+ Style/CommentedKeyword:
4
+ Enabled: false
5
+ Style/Documentation:
6
+ Enabled: false
7
+ Style/RedundantSelf:
8
+ Enabled: false
9
+ Style/StringLiterals:
10
+ Enabled: false
11
+ Metrics/MethodLength:
12
+ Enabled: false
13
+ Metrics/ClassLength:
14
+ Enabled: false
data/README.org CHANGED
@@ -127,6 +127,7 @@
127
127
  | =M= | Open pointed article to edit |
128
128
  | =D= | Delete pointed article |
129
129
  | =C= | Copy article temporally as a reusable template |
130
+ | =R= | Copy article like =C= using completing-read |
130
131
  | =Y= | Same as =E= but use the template stored by =C= |
131
132
 
132
133
  3) Draft Buffer
@@ -149,6 +150,7 @@
149
150
  X-SC-Time: 18:00-21:00
150
151
  X-SC-Category: Private Party
151
152
  X-SC-Cond:
153
+ X-SC-Recurrence-Tag: HomeParty
152
154
  X-SC-Duration:
153
155
  X-SC-Record-Id: C34D89F5-27FA-4243-AC6C-168D8D243D9A
154
156
  X-SC-Sequence: 0
@@ -241,6 +243,40 @@
241
243
  of 2015-07-15 (Wed) and inclusion of 2015-08-01 (Sat).
242
244
  #+END_EXAMPLE
243
245
 
246
+ ** Headers for Grouping
247
+ *** X-SC-Category
248
+ =X-SC-Category:= is a space separated list of category
249
+ It is useful for selective display both in CLI:
250
+ : mhc scan today --category=Private
251
+ and Emacs UI:
252
+ : M-x mhc-set-default-category
253
+ : Default Category: Private && !Party
254
+
255
+ Also useful sync with Google Calendar. See [[https://github.com/yoshinari-nomura/mhc/blob/master/samples/DOT.mhc-config.yml][mhc-config.yml]] for
256
+ details.
257
+
258
+ *** X-SC-Recurrence-Tag
259
+ =X-SC-Recurrence-Tag:= is a tag for bundling multiple event articles as one recurring group.
260
+
261
+ MHC allows flexible description of repeating events using
262
+ =X-SC-Cond:=, =X-SC-Duration:=, or =X-SC-Day:=. However, we know
263
+ this is not sufficient.
264
+
265
+ For example, in my experience, some monthly meetings do not have
266
+ distinct recurring patterns such like "Third Wednesday of each
267
+ month." Instead, the next date is fixed by coordination during the meeting.
268
+
269
+ In such cases, it is difficult to mark these events as a series of related events.
270
+ That's why we need =X-SC-Recurrence-Tag:=
271
+
272
+ Using =X-SC-Recurrence-Tag:=, MHC suggests what event should be arranged.
273
+ : mhc stuck_recurrences
274
+ will tell you to make the next appointment.
275
+
276
+ For example, if you have "X-SC-Recurrence-Tag: Dentist" in your
277
+ articles of past dentist events, mhc will point out that you
278
+ forgot to make your next dentist appointment.
279
+
244
280
  * INFORMATION FOR DEVELOPERS
245
281
  ** INSTALL for developers:
246
282
  1) Install rbenv + ruby-build
@@ -326,6 +362,7 @@
326
362
  (setq load-path
327
363
  (cons "~/src/mhc/emacs" load-path))
328
364
  (autoload 'mhc "mhc" "Message Harmonized Calendar system." t)
365
+ (autoload 'mhc-import "mhc" "Import a schedule." t)
329
366
 
330
367
  ;; M-x mhc
331
368
  #+END_SRC
data/bin/mhc CHANGED
@@ -72,9 +72,10 @@ class MhcCLI < Thor
72
72
  register_option :repository, :desc => "Set MHC top directory", :banner => "DIRECTORY"
73
73
  register_option :calendar, :desc => "Set source CALENDAR"
74
74
  register_option :category, :desc => "Pick items only in CATEGORY"
75
- register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json)
75
+ register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json html)
76
76
  register_option :search, :desc => "Search items by complex expression"
77
77
  register_option :dry_run, :desc => "Perform a trial run with no changes made", :type => :boolean
78
+ register_option :verbose, :desc => "Show verbose message", :type => :boolean
78
79
 
79
80
  ################################################################
80
81
  # command name mappings
@@ -113,7 +114,9 @@ class MhcCLI < Thor
113
114
  Mhc::Command::Cache.new(builder.datastore)
114
115
  end
115
116
 
117
+ ################################################################
116
118
  # Command: todo
119
+ ################################################################
117
120
  desc "todo", "List Todo entries in MHC calendar"
118
121
 
119
122
  named_option :repository
@@ -131,7 +134,8 @@ class MhcCLI < Thor
131
134
  search_range = nil
132
135
  end
133
136
  next if task.in_category?("done") && !options[:show_all]
134
- todos << task.occurrences(range: search_range).first
137
+ task_first = task.occurrences(range: search_range).first
138
+ todos << task_first if task_first
135
139
  end
136
140
  todos.each.sort{|a, b| a.dtstart <=> b.dtstart}.each do |t|
137
141
  deadline = t.dtstart
@@ -168,6 +172,38 @@ class MhcCLI < Thor
168
172
  Mhc::Command::Completions.new(help, global_options, command, config)
169
173
  end
170
174
 
175
+ ################################################################
176
+ # Command: compose
177
+ ################################################################
178
+ desc "compose", "Create new draft and open it in your editor"
179
+
180
+ long_desc <<-LONGDESC
181
+ Create a new draft and open it in your editor.
182
+ To select your favorite editor,
183
+ set environment variable MHC_EDITOR or EDITOR.
184
+ LONGDESC
185
+
186
+ def compose
187
+ require 'securerandom'
188
+ ev = Mhc::Event.new
189
+ ev.record_id = SecureRandom.uuid.upcase
190
+ path = builder.datastore.create(ev, true)
191
+ puts "Create draft #{path}"
192
+ editor = ENV["MHC_EDITOR"] || ENV["EDITOR"] || "vi"
193
+ system(editor, path)
194
+
195
+ while add(path) != 0
196
+ if ask("Retry?") =~ /^[Yy]/
197
+ system(editor, path)
198
+ else
199
+ puts "Abort..."
200
+ exit(1)
201
+ end
202
+ end
203
+ puts "Successfully registered."
204
+ exit(0)
205
+ end
206
+
171
207
  ################################################################
172
208
  # Command: config
173
209
  ################################################################
@@ -179,6 +215,40 @@ class MhcCLI < Thor
179
215
  puts Mhc::Converter::Emacs.new.to_emacs(config.get_value(name))
180
216
  end
181
217
 
218
+ ################################################################
219
+ # Command: add
220
+ ################################################################
221
+ desc "add FILE", "Add event FILE to repository"
222
+
223
+ named_option :repository
224
+
225
+ def add(file)
226
+ full_path = File.expand_path(file)
227
+
228
+ unless File.exist?(full_path)
229
+ puts Mhc::Converter::Emacs.new.to_emacs("No such file #{file}.")
230
+ return 1
231
+ end
232
+
233
+ event_string = File.open(full_path).read
234
+ errors = Mhc::Event.validate(event_string)
235
+
236
+ if errors.empty?
237
+ ev = Mhc::Event.parse(event_string)
238
+ calendar.add(ev)
239
+ return 0
240
+ else
241
+ string = ""
242
+ errors.each do |err, key|
243
+ string += "#{err.to_s.capitalize}"
244
+ string += " in X-SC-#{key.capitalize}" if key
245
+ string += ".\n"
246
+ end
247
+ puts string
248
+ return 1
249
+ end
250
+ end
251
+
182
252
  ################################################################
183
253
  # Command: init
184
254
  ################################################################
@@ -188,6 +258,55 @@ class MhcCLI < Thor
188
258
  Mhc::Command::Init.new(top_dir, options[:config] || DEFAULT_CONFIG_PATH, ENV["MHC_TZID"])
189
259
  end
190
260
 
261
+ ################################################################
262
+ # Command: stuck_recurrences
263
+ ################################################################
264
+ desc "stuck_recurrences", "List stuck recurrences in MHC calendar"
265
+
266
+ named_option :repository
267
+ named_option :verbose
268
+ named_option :format
269
+
270
+ # * stuck_recurrences
271
+ #
272
+ # For all "living recurrences", if no next event has been set in
273
+ # the "future", show the information on the last time.
274
+ #
275
+ # + "living recurrences" ::
276
+ # + There is a last-time evnet within a year and a half.
277
+ # + "future" :: ten years from now.
278
+ #
279
+ def stuck_recurrences(format = options[:format])
280
+ today = Mhc::PropertyValue::Date.today
281
+
282
+ # Seek -1.5y to +10y
283
+ search_range = (today - ((365 * 5).to_i)) ..
284
+ (today + ((365 * 10).to_i))
285
+
286
+ recurrences = {}
287
+
288
+ # Get all recurrences in search_range
289
+ calendar.occurrences(search_range).each do |oc|
290
+ rtag = oc.recurrence_tag.to_s
291
+ next if rtag.empty?
292
+ recurrences[rtag] ||= []
293
+ recurrences[rtag] << oc
294
+ end
295
+
296
+ formatter = Mhc::Formatter.build(formatter: format || :text, date_range: search_range)
297
+
298
+ recurrences.each do |rtag, recurrence|
299
+ if options[:verbose]
300
+ rec_put_debug(recurrence)
301
+ else
302
+ formatter << recurrence.last if rec_stuck?(recurrence)
303
+ end
304
+ end
305
+ str = formatter.to_s
306
+ return str if format == :json || options[:verbose]
307
+ puts str
308
+ end # stuck_recurrences
309
+
191
310
  ################################################################
192
311
  # Command: scan
193
312
  ################################################################
@@ -222,6 +341,9 @@ class MhcCLI < Thor
222
341
  named_option :calendar, :category, :format, :repository, :search
223
342
 
224
343
  def scan(range)
344
+ if options[:format].to_s == 'html'
345
+ $stuck_recurrences_json = stuck_recurrences(:json)
346
+ end
225
347
  begin
226
348
  Mhc::Command::Scan.new(calendar, range, **symbolize_keys(options))
227
349
  rescue Mhc::PropertyValue::ParseError, Mhc::Formatter::NameError, Mhc::Query::ParseError => e
@@ -326,6 +448,80 @@ class MhcCLI < Thor
326
448
 
327
449
  private
328
450
 
451
+ def rec_put_debug(recurrence)
452
+ ol = recurrence.last
453
+
454
+ today = Mhc::PropertyValue::Date.today
455
+ last_occurrence = recurrence.last
456
+ last_occurrence_date = last_occurrence.dtstart.to_date
457
+ # Ignore living recurrences
458
+ return false if last_occurrence_date > today
459
+
460
+ status = if rec_stuck?(recurrence) then " TODO" else "" end
461
+ print "*#{status} #{ol.recurrence_tag}#{status} "
462
+
463
+ print format("%s %s",
464
+ ol.dtstart.strftime("%Y-%m-%d %a"),
465
+ ol.subject)
466
+
467
+ print format(" (length: %d, max: %d, min: %d, avr: %d)\n",
468
+ recurrence.length,
469
+ *rec_stat(recurrence))
470
+
471
+ pr = recurrence.first
472
+ recurrence.each do |oc|
473
+ puts format(" %s %s (%d)",
474
+ oc.dtstart.strftime("%Y-%m-%d %a"),
475
+ oc.subject,
476
+ (oc.dtstart.to_date - pr.dtstart.to_date).to_i
477
+ )
478
+ pr = oc
479
+ end
480
+ end
481
+
482
+ # Judge if a recurrence is stuck or not. recurrence is a set of occurrences.
483
+ def rec_stuck?(recurrence, today = Mhc::PropertyValue::Date.today)
484
+ last_occurrence = recurrence.last
485
+ last_occurrence_date = last_occurrence.dtstart.to_date
486
+ max_interval, _min_interval, avr_interval = rec_stat(recurrence)
487
+
488
+ # Not stuck if scheduled in the future dates
489
+ return false if last_occurrence_date > today
490
+
491
+ # Stuck if last event occurred within 90 days, but average
492
+ # interval is less than 60 days.
493
+ if (today - last_occurrence_date).to_i < 90 && avr_interval < 60
494
+ return true
495
+ end
496
+
497
+ # Not stuck if the number of days between the last event and today
498
+ # does not reach 80% of the maximum interval.
499
+ return false if (today - last_occurrence_date).to_i < max_interval * 4 / 5
500
+
501
+ # Not stuck if the number of days between the last event and today
502
+ # is more than 30 days or 1.5 years beyond the maximum interval.
503
+ # It would be no more recurring.
504
+ return false if (today - last_occurrence_date).to_i > [max_interval + 30, 365*1.5].min
505
+
506
+ # Otherwise, stuck
507
+ return true
508
+ end
509
+
510
+ # Return statistics on recurrences: max/min/average interval.
511
+ def rec_stat(recurrence)
512
+ # Calcurate all intervals of the recurrence
513
+ intervals = recurrence.each_cons(2).map{|ocs|
514
+ (ocs[1].dtstart.to_date - ocs[0].dtstart.to_date).to_i
515
+ }.reject{|interval| interval < 1}
516
+
517
+ # Assume annual event if the recurrence is one-shot.
518
+ intervals = [365] if intervals.empty?
519
+
520
+ return [intervals.max,
521
+ intervals.min,
522
+ intervals.reduce(0, :+) / intervals.length]
523
+ end
524
+
329
525
  def exit_on_error(&block)
330
526
  begin
331
527
  yield if block_given?
data/emacs/Cask CHANGED
@@ -5,7 +5,7 @@
5
5
  (source org)
6
6
  (source melpa)
7
7
 
8
- (package "mhc" "1.2.4" "Message Harmonized Calendaring system") ;; MHC_VERSION
8
+ (package "mhc" "1.2.5" "Message Harmonized Calendaring system") ;; MHC_VERSION
9
9
 
10
10
  (files "mhc.el" "mhc-*.el")
11
11
 
data/emacs/mhc-db.el CHANGED
@@ -34,6 +34,12 @@ If optional SEARCH is non-nil returned value is clipped by search string."
34
34
  (if category (format " --category=%s" category) "")
35
35
  (if search (format " --search='%s'" search) ""))))
36
36
 
37
+ (defun mhc-db-stuck-recurrences ()
38
+ "List stuck recurrences in MHC calendar."
39
+ (mhc-db-flatten
40
+ (mhc-process-send-command
41
+ (format "stuck_recurrences --format=emacs"))))
42
+
37
43
  (defun mhc-db-scan-flat (begin-date end-date &optional nosort category search)
38
44
  "Scan MHC database from BEGIN-DATE to END-DATE.
39
45
  Unlike `mhc-db-scan`, returned value is not grouped by date.
@@ -42,19 +48,48 @@ For example:
42
48
  If optional NOSORT is non-nil, returned value is not sort.
43
49
  If optional CATEGORY is non-nil, returned value is clipped by category.
44
50
  If optional SEARCH is non-nil returned value is clipped by search string."
45
- (let ((dayinfo-list (mhc-db-scan begin-date end-date nosort category search)))
46
- (apply 'append
47
- (mapcar (lambda (dayinfo)
48
- (let ((date (mhc-day-date dayinfo))
49
- (schedules (mhc-day-schedules dayinfo)))
50
- (mapcar (lambda (sch) (cons date sch)) schedules)))
51
- dayinfo-list))))
51
+ (mhc-db-flatten (mhc-db-scan begin-date end-date nosort category search)))
52
+
53
+ (defun mhc-db-flatten (dayinfo-list)
54
+ "Flatten DAYINFO-LIST scanned from `mhc-db-scan`.
55
+ Unlike `mhc-db-scan`, returned value is not grouped by date."
56
+ (apply 'append
57
+ (mapcar (lambda (dayinfo)
58
+ (let ((date (mhc-day-date dayinfo))
59
+ (schedules (mhc-day-schedules dayinfo)))
60
+ (mapcar (lambda (sch) (cons date sch)) schedules)))
61
+ dayinfo-list)))
52
62
 
53
63
  (defun mhc-db-search (&rest query)
54
64
  (let ((b (mhc-date-new 1970 1 1))
55
65
  (e (mhc-date-yy+ (mhc-date-now) 10)))
56
66
  (mhc-db-scan b e nil nil (mhc-db/query-to-search-string query))))
57
67
 
68
+ (defun mhc-db-past-occurrences (&optional days)
69
+ (let ((now (mhc-date-now)))
70
+ (nreverse
71
+ (mhc-db-scan-flat (mhc-date- now (or days 732)) now))))
72
+
73
+ (defun mhc-db-make-completion-list (occurrences)
74
+ (delq nil
75
+ (mapcar
76
+ (lambda (occurrence)
77
+ (let ((sch (cdr occurrence)))
78
+ (if (not (string= (mhc-record-name (mhc-schedule-record sch)) ""))
79
+ (cons (mhc-db-real-to-display occurrence) occurrence))))
80
+ occurrences)))
81
+
82
+ (defun mhc-db-real-to-display (occurrence)
83
+ (let* ((date (car occurrence))
84
+ (sch (cdr occurrence))
85
+ (location (mhc-schedule-location sch)))
86
+ (concat
87
+ (mhc-date-format date "%04d/%02d/%02d" yy mm dd)
88
+ (format " %11s " (mhc-schedule-time-as-string sch))
89
+ (mhc-schedule-subject sch)
90
+ (if (and location (not (string= location "")))
91
+ (format " [%s]" location)))))
92
+
58
93
  (defun mhc-db/quote-string (string)
59
94
  (format "\"%s\"" string))
60
95
 
data/emacs/mhc-summary.el CHANGED
@@ -294,9 +294,10 @@ If optional argument FOR-DRAFT is non-nil, Hilight message as draft message."
294
294
  (set-visited-file-name nil)
295
295
  ;; (rename-buffer (file-name-nondirectory file) 'unique)
296
296
  ;; (run-hooks 'mhc-calendar-view-file-hook)
297
+ (let ((buffer-read-only nil))
298
+ (mhc-message-mode))
297
299
  (set-buffer-modified-p nil)
298
300
  (setq buffer-read-only t)
299
- (mhc-message-mode)
300
301
  (mhc-message-set-file-name file)
301
302
  )))
302
303
 
@@ -633,6 +634,7 @@ If BANNER is set, it is printed on the horizontal line."
633
634
  (define-key mhc-summary-mode-map "E" 'mhc-edit)
634
635
  (define-key mhc-summary-mode-map "M" 'mhc-modify)
635
636
  (define-key mhc-summary-mode-map "C" 'mhc-reuse-copy)
637
+ (define-key mhc-summary-mode-map "R" 'mhc-reuse-past-event)
636
638
  (define-key mhc-summary-mode-map "Y" 'mhc-reuse-create)
637
639
 
638
640
  (define-key mhc-summary-mode-map "n" 'mhc-summary-display-next)
data/emacs/mhc-vars.el CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  ;;; Constants:
21
- (defconst mhc-version "mhc 1.2.4") ;; MHC_VERSION
21
+ (defconst mhc-version "mhc 1.2.5") ;; MHC_VERSION
22
22
 
23
23
 
24
24
  ;;; Configration Variables:
data/emacs/mhc.el CHANGED
@@ -3,7 +3,7 @@
3
3
  ;; Description: Message Harmonized Calendaring system.
4
4
  ;; Author: Yoshinari Nomura <nom@quickhack.net>
5
5
  ;; Created: 1994-07-04
6
- ;; Version: 1.2.4
6
+ ;; Version: 1.2.5
7
7
  ;; Keywords: calendar
8
8
  ;; URL: http://www.quickhack.net/mhc
9
9
  ;; Package-Requires: ((calfw "20150703"))
@@ -19,7 +19,8 @@
19
19
  ;;
20
20
  ;; (setq load-path
21
21
  ;; (cons "~/src/mhc/emacs" load-path))
22
- ;; (autoload 'mhc "mhc")
22
+ ;; (autoload 'mhc "mhc" nil t)
23
+ ;; (autoload 'mhc-import "mhc" nil t)
23
24
  ;;
24
25
  ;; and M-x mhc
25
26
 
@@ -325,10 +326,11 @@
325
326
  ret))))
326
327
 
327
328
  (defun mhc-expr-compile (string)
328
- (byte-compile
329
- `(lambda (schedule)
330
- ,(mhc-expr-parse string)
331
- )))
329
+ (let ((lexical-binding nil))
330
+ (byte-compile
331
+ `(lambda (schedule)
332
+ ,(mhc-expr-parse string)
333
+ ))))
332
334
 
333
335
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
334
336
  ;;
@@ -796,6 +798,7 @@ Returns t if the importation was succeeded."
796
798
  :group 'mhc
797
799
  :type 'boolean)
798
800
 
801
+ ;;;###autoload
799
802
  (defun mhc-import (&optional get-original)
800
803
  "Import a schedule from the current article.
801
804
  The default action of this command is to import a schedule from the
@@ -912,6 +915,27 @@ the default action of this command is changed to the latter."
912
915
  (message "%s is copied." (mhc-record-subject-as-string record)))
913
916
  (message "No file here."))))
914
917
 
918
+ (defun mhc-reuse-past-event ()
919
+ "Select past schedule and copy to template."
920
+ (interactive)
921
+ (let* ((completion-ignore-case t)
922
+ (read-file-name-completion-ignore-case t)
923
+ (read-buffer-completion-ignore-case t)
924
+ (case-fold-search t)
925
+ (collection
926
+ (mhc-db-make-completion-list
927
+ (mhc-db-past-occurrences)))
928
+ (completion-table
929
+ (lambda (string pred action)
930
+ (if (eq action 'metadata)
931
+ '(metadata (display-sort-function . identity)
932
+ (cycle-sort-function . identity))
933
+ (complete-with-action action collection string pred))))
934
+ (key (completing-read "Select occurrence: " completion-table nil t))
935
+ (sch (cdr (cdr (assoc key collection)))))
936
+ (mhc-reuse-copy
937
+ (mhc-record-name (mhc-schedule-record sch)))))
938
+
915
939
  (defun mhc-modify ()
916
940
  "Modify the current schedule."
917
941
  (interactive)
data/lib/mhc/calendar.rb CHANGED
@@ -23,6 +23,22 @@ module Mhc
23
23
  @datastore.entries(category: "todo")
24
24
  end
25
25
 
26
+ def recurrences(rec, &scope_block)
27
+ @datastore.entries(recurrence: rec)
28
+ end
29
+
30
+ def recurrence_tags
31
+ hash = {}
32
+ @datastore.entries.each do |ev|
33
+ tag = ev.recurrence_tag.to_s
34
+ next if tag == ""
35
+ next if hash[tag]
36
+
37
+ yield tag
38
+ hash[tag] = ev
39
+ end
40
+ end
41
+
26
42
  def occurrences(date_range, &scope_block)
27
43
  ocs = []
28
44
  @datastore.entries(range: date_range).each do |event|
@@ -34,6 +50,10 @@ module Mhc
34
50
  return ocs.sort
35
51
  end
36
52
 
53
+ def add(event)
54
+ @datastore.update(event)
55
+ end
56
+
37
57
  ################################################################
38
58
  ## for sync manager
39
59
 
data/lib/mhc/config.rb CHANGED
@@ -52,7 +52,11 @@ module Mhc
52
52
  end
53
53
 
54
54
  def self.create_from_yaml_string(yaml_string, filename = nil)
55
- hash = YAML.load(yaml_string, filename) || {}
55
+ hash = if YAML.respond_to?(:unsafe_load)
56
+ YAML.unsafe_load(yaml_string, filename: filename)
57
+ else
58
+ YAML.load(yaml_string, filename)
59
+ end || {}
56
60
  return new(hash)
57
61
  end
58
62
 
data/lib/mhc/converter.rb CHANGED
@@ -131,15 +131,86 @@ module Mhc
131
131
  end
132
132
  end
133
133
 
134
+ # https://github.com/rubyredrick/ri_cal
135
+ def value_part(unit, diff) # :nodoc:
136
+ (diff == 0) ? nil : "#{diff}#{unit}"
137
+ end
138
+
139
+ # https://github.com/rubyredrick/ri_cal
140
+ def from_datetimes(start, finish, sign = '') # :nodoc:
141
+ if start > finish
142
+ from_datetimes(finish, start, '-')
143
+ else
144
+ diff = finish - start
145
+ days_diff = diff.to_i
146
+ hours = (diff - days_diff) * 24
147
+ hour_diff = hours.to_i
148
+ minutes = (hours - hour_diff) * 60
149
+ min_diff = minutes.to_i
150
+ seconds = (minutes - min_diff) * 60
151
+ sec_diff = seconds.to_i
152
+
153
+ day_part = value_part('D',days_diff)
154
+ hour_part = value_part('H', hour_diff)
155
+ min_part = value_part('M', min_diff)
156
+ sec_part = value_part('S', sec_diff)
157
+ t_part = (hour_diff.abs + min_diff.abs + sec_diff.abs) == 0 ? "" : "T"
158
+ "P#{day_part}#{t_part}#{hour_part}#{min_part}#{sec_part}"
159
+ end
160
+ end
161
+
162
+ # Convert ~X-SC-Days:~ to RFC5545 3.8.2 ~RDATE~
163
+ #
164
+ # RDATE allows three types of format: PERIOD, DATE, DATE-TIME.
165
+ # + DATE :: 20140301
166
+ # + DATE-TIME :: 20140301T123000
167
+ # + PERIOD :: 20140301T123000/20140301T143000 or 20140301T123000/PT2H
168
+ #
169
+ # NOTE: *Google calenadr does not accept PERIOD*
170
+ # That's why we cannot make X-SC-Day: have different duration-period for each occurrence.
171
+ #
134
172
  def rdates(event)
135
173
  return nil if event.dates.empty?
136
- ocs = Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).map {|oc| oc.dtstart}
174
+ # Get all RDATE candidates from X-SC-Date
175
+ ocs = Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).map do |oc|
176
+ if oc.dtstart.respond_to?(:hour)
177
+ # duration = from_datetimes(oc.dtstart, oc.dtend)
178
+
179
+ # 20140301T123000/20140301T143000
180
+ dtstart = IcalendarImporter.tz_convert(oc.dtstart, dst_tzid: 'UTC').strftime("%Y%m%dT%H%M00Z")
181
+ dtend = IcalendarImporter.tz_convert(oc.dtend, dst_tzid: 'UTC').strftime("%Y%m%dT%H%M00Z")
182
+
183
+ dtstart + "/" + dtend
184
+ # dtstart + "/" + duration
185
+ else
186
+ oc.dtstart
187
+ end
188
+ end
189
+
190
+ # Check if all in X-SC-Date should be converted into RDATE,
191
+ # because the first occurrence of X-SC-Date might be dedicated
192
+ # to DTSTART in some particular case.
193
+ #
137
194
  if event.recurring?
195
+ # Use all values of X-SC-Date as RDATE, because DTSTART
196
+ # should be calculated from recurrence-rule (X-SC-Cond and
197
+ # X-SC-Duration).
138
198
  ocs
139
199
  else
140
- ocs = ocs[1..-1]
141
- return nil if ocs.empty?
200
+ # This is Workaround for avoiding the serious bug in Android.
201
+ # Android badly ignores DTSTART-DTEND if RDATE is set.
202
+ # As a result, the first occurence is vanished.
203
+ # So, I set the first occurence twice; in RDATE and DTSTART.
204
+ return nil if ocs[1..-1].empty?
142
205
  return ocs
206
+
207
+ # This is what I wanted to do:
208
+ # Remove the first occurence of X-SC-Date, because the first
209
+ # value should be dedicated to DTSTART.
210
+ #
211
+ # ocs = ocs[1..-1]
212
+ # return nil if ocs.empty?
213
+ # return ocs
143
214
  end
144
215
  end
145
216
 
data/lib/mhc/datastore.rb CHANGED
@@ -25,7 +25,7 @@ module Mhc
25
25
  Dir.foreach(".") do |ent|
26
26
  parse_mhcc(ent).each {|ev|
27
27
  next if category && !ev.in_category?(category)
28
- next if recurrence && !ev.in_recurrence?(recurrence)
28
+ next if recurrence && !(ev.recurrence_tag.to_s == recurrence)
29
29
  yielder << ev
30
30
  } if /\.mhcc$/ =~ ent
31
31
  next unless /\.mhc$/ =~ ent
@@ -52,22 +52,25 @@ module Mhc
52
52
  return Event.parse_file(path)
53
53
  end
54
54
 
55
- def create(event)
55
+ def create(event, draft = false)
56
56
  if find_by_uid(event.uid)
57
57
  raise "Already exist uid:#{uid} in #{@basedir}"
58
58
  end
59
+ path = uid_to_path(event.uid, draft)
59
60
  File.open(path, "w") do |f|
60
61
  f.write(event.dump)
61
62
  end
63
+ return path.to_s
62
64
  end
63
65
 
64
- def update(event)
65
- unless path = uid_to_path(event.uid)
66
+ def update(event, draft = false)
67
+ unless path = uid_to_path(event.uid, draft)
66
68
  raise "Not found uid:#{uid} in #{@basedir}"
67
69
  end
68
70
  File.open(path, "w") do |f|
69
71
  f.write(event.dump)
70
72
  end
73
+ return path.to_s
71
74
  end
72
75
 
73
76
  def delete(uid_or_event)
@@ -105,7 +108,8 @@ module Mhc
105
108
  return Dir.glob(glob).first
106
109
  end
107
110
 
108
- def uid_to_path(uid)
111
+ def uid_to_path(uid, draft = false)
112
+ return @basedir + ('draft/' + uid + '.mhc') if draft
109
113
  return @basedir + ('spool/' + uid + '.mhc')
110
114
  end
111
115
 
@@ -0,0 +1,32 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Html < Base
4
+ TEMPLATE_DIR = File.expand_path("../../templates", __FILE__)
5
+
6
+ def initialize(date_range:, options:)
7
+ super(date_range: date_range, options: options)
8
+ @json_formatter = Json.new(date_range: date_range, options: options)
9
+ end
10
+
11
+ def <<(occurrence)
12
+ @json_formatter << occurrence
13
+ end
14
+
15
+ private
16
+
17
+ def format_header(context)
18
+ require "erb"
19
+ template_path = File.expand_path("full-calendar.html.erb", TEMPLATE_DIR)
20
+ @template = ERB.new(File.open(template_path).read, nil, "-")
21
+ return ""
22
+ end
23
+
24
+ def format_body(context)
25
+ env = Struct.new(:json_event_array, :tzid)
26
+ mhc = env.new(@json_formatter.to_s, Mhc.default_tzid) # used in ERB template
27
+ @template.result(binding)
28
+ end
29
+
30
+ end # class Html
31
+ end # class Formatter
32
+ end # module Mhc
data/lib/mhc/formatter.rb CHANGED
@@ -21,6 +21,8 @@ module Mhc
21
21
  Howm.new(date_range: date_range, options:options)
22
22
  when :json
23
23
  Json.new(date_range: date_range, options:options)
24
+ when :html
25
+ Html.new(date_range: date_range, options:options)
24
26
  else
25
27
  raise Formatter::NameError.new("Unknown format: #{formatter} (#{formatter.class})")
26
28
  end
@@ -37,6 +39,7 @@ module Mhc
37
39
  autoload :OrgTable, "#{dir}/org_table.rb"
38
40
  autoload :SymbolicExpression, "#{dir}/symbolic_expression.rb"
39
41
  autoload :Text, "#{dir}/text.rb"
42
+ autoload :Html, "#{dir}/html.rb"
40
43
 
41
44
  end # class Formatter
42
45
  end # module Mhc
@@ -0,0 +1,417 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+
8
+ <title>Calendar</title>
9
+
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.9.0/fullcalendar.min.css" />
11
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
12
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" />
13
+
14
+ <style media="screen" type="text/css">
15
+ .container {
16
+ // max-width: 100%;
17
+ width: auto;
18
+ }
19
+
20
+ .ui-draggable {
21
+ cursor: grab;
22
+ }
23
+
24
+ a {
25
+ cursor: pointer;
26
+ }
27
+
28
+ .fc-sun {
29
+ color:#gray;
30
+ background-color: #fff0f0;
31
+ }
32
+
33
+ .fc-sat {
34
+ color:#gray;
35
+ background-color: #f0f0ff;
36
+ }
37
+
38
+
39
+
40
+ #calendar .mhc-allday {
41
+ background-color: lightblue;
42
+ border-color: lightblue;
43
+ color: black;
44
+ }
45
+
46
+ #calendar .mhc-time-range {
47
+ background-color: #f7f7ff;
48
+ border-color: #f7f7ff;
49
+ color: black;
50
+ }
51
+
52
+ #calendar .mhc-time-range:hover {
53
+ background-color: aliceblue;
54
+ border-color: lavender;
55
+ color: black;
56
+ }
57
+
58
+ #calendar .mhc-category-holiday {
59
+ background-color: orangered;
60
+ border-color: orangered;
61
+ color: white;
62
+ }
63
+
64
+ #calendar .mhc-category-birthday {
65
+ background-color: #ffffe8;
66
+ border-color: #ffffe8;
67
+ color: black;
68
+ }
69
+
70
+
71
+
72
+
73
+ #side-calendar .mhc-category-birthday {
74
+ display: none;
75
+ }
76
+
77
+
78
+
79
+
80
+ #stuck-events .fc-list-heading {
81
+ display: none;
82
+ }
83
+
84
+ </style>
85
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
86
+ <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.js"></script>
87
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.js"></script>
88
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.js"></script>
89
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.9.0/fullcalendar.js"></script>
90
+
91
+ </head>
92
+ <body>
93
+ <!--
94
+ <div class="navmenu navmenu-default navmenu-fixed-left offcanvas">
95
+ <a class="navmenu-brand" href="#">brand</a>
96
+ <ul class="nav navmenu-nav">
97
+ <li><a href="#">Menu text 1</a></li>
98
+ <li><a href="#">Menu text 2</a></li>
99
+ <li><a href="#">Menu text 3</a></li>
100
+ </ul>
101
+ </div>
102
+ <div class="navbar navbar-default navbar-fixed-top">
103
+ <button type="button" class="navbar-toggle" data-toggle="offcanvas" data-target=".navmenu" data-canvas="body">
104
+ <span class="icon-bar"></span>
105
+ <span class="icon-bar"></span>
106
+ <span class="icon-bar"></span>
107
+ </button>
108
+ </div>
109
+ -->
110
+
111
+ <div class="container">
112
+ <div class="row">
113
+
114
+ <div class="col-xs-3">
115
+ <ul class="nav nav-tabs">
116
+ <li class="active"><a data-toggle="tab" href="#side-calendar">Past Events</a></li>
117
+ <li><a data-toggle="tab" href="#stuck-events">Stuck Events</a></li>
118
+ </ul>
119
+ <div class="tab-content">
120
+ <div id="side-calendar" class="tab-pane fade in active"></div>
121
+ <div id="stuck-events" class="tab-pane fade"></div>
122
+ </div>
123
+ </div><!-- col-xs-3 -->
124
+
125
+ <div class="col-xs-9" id="calendar"></div> <!-- col-xs-9 -->
126
+ </div><!-- row -->
127
+ </div><!-- /.container -->
128
+
129
+ <script>
130
+ function create_uuid() {
131
+ var d = +new Date();
132
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
133
+ .replace(/[xy]/g, function(c) {
134
+ var r = (d + Math.random() * 16) % 16 | 0;
135
+ return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16).toUpperCase();
136
+ });
137
+ }
138
+
139
+ function max_height_for(element) {
140
+ var top = $(element).position().top;
141
+ return $(window).height() - top - 7;
142
+ }
143
+
144
+ function adjust_height(element) {
145
+ var height = max_height_for(element);
146
+ $(element).fullCalendar('option', 'height', height);
147
+ }
148
+
149
+ var json_event_array = [<%= mhc.json_event_array %>];
150
+
151
+ $(document).ready(function() {
152
+
153
+ $('#side-calendar').fullCalendar({
154
+ defaultView: 'listMonth',
155
+ header: {
156
+ left: '',
157
+ center: 'title',
158
+ right: ''
159
+ },
160
+ height: max_height_for('#side-calendar'),
161
+ editable: true,
162
+ eventResourceEditable: true,
163
+ eventLimit: true,
164
+ selectable: true,
165
+ droppable: true,
166
+ timezone: '<%= mhc.tzid %>',
167
+ timeFormat: "HH:mm",
168
+ displayEventEnd: false,
169
+ allDayText: "",
170
+ month: 'HH:mm',
171
+ week: 'HH:mm',
172
+ day: 'HH:mm',
173
+ // eventSources: [<%= $stuck_recurrences_json %>],
174
+ eventSources: json_event_array,
175
+ XeventDragStart: function(calEvent, jsEvent, ui, view) {
176
+ alert('Drag Event: ' + calEvent.title);
177
+ alert('Event ID: ' + calEvent.id);
178
+ alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
179
+ alert('View: ' + view.name);
180
+ // change the border color just for fun
181
+ $(this).css('border-color', 'red');
182
+ },
183
+ viewRender: function(view, element) {
184
+ $('tr.fc-list-item').each(function() {
185
+ var duration;
186
+ var seg = $(this).data('fc-seg');
187
+ var myevent = seg.footprint.getEventLegacy();
188
+ if (myevent.end) {
189
+ var minutes = moment.duration(myevent.end.diff(myevent.start)).asMinutes();
190
+ var days = Math.floor(minutes/(24*60));
191
+ minutes -= days * 24 * 60;
192
+ var hours = Math.floor(minutes/60);
193
+ minutes -= hours * 60;
194
+ duration = days + "." + hours + ":" + minutes;
195
+ } else {
196
+ duration = "0.00:00";
197
+ }
198
+ if (myevent.title == "母の日") {
199
+ // alert('My Event: ' + myevent.title);
200
+ // alert('Duration: ' + duration);
201
+ }
202
+ var uuid = create_uuid();
203
+ // alert('Seg: ' + seg);
204
+ $(this).data('event', {
205
+ title: myevent.title,
206
+ allDay: myevent.allDay,
207
+ start: myevent.start.format("HH:mm"),
208
+ duration: duration,
209
+ stick: true,
210
+ className: myevent.className
211
+ });
212
+ });
213
+
214
+ $('tr.fc-list-item').draggable({
215
+ appendTo: "#calendar",
216
+ cursorAt: {top: 5, left: 5},
217
+ iframeFix: true,
218
+ refreshPositions: true,
219
+ scroll: false,
220
+ cursor: "move",
221
+ revert: true, // will cause the event to go back to its
222
+ revertDuration: 0, // original position after the drag
223
+ helper: "clone",
224
+ zIndex: 100,
225
+ opacity: 0.35,
226
+
227
+ start: function(event, ui) {
228
+ }
229
+ });
230
+ if(view.name == 'listMonth') {
231
+ adjust_height('#side-calendar');
232
+ }
233
+ },
234
+ windowResize: function(view) {
235
+ if(view.name == 'listMonth') {
236
+ adjust_height('#side-calendar');
237
+ }
238
+ }
239
+ });
240
+
241
+ $('#stuck-events').fullCalendar({
242
+ defaultView: 'listall',
243
+ Xheader: {
244
+ left: '',
245
+ center: '',
246
+ right: ''
247
+ },
248
+ header: false,
249
+ views: {
250
+ listall: {
251
+ type: 'list',
252
+ duration: { days: 3650},
253
+ dayCount: 30
254
+ }
255
+ },
256
+ height: max_height_for('#side-calendar'), // XXX
257
+ editable: true,
258
+ eventResourceEditable: true,
259
+ eventLimit: true,
260
+ selectable: true,
261
+ droppable: true,
262
+ timezone: '<%= mhc.tzid %>',
263
+ timeFormat: " ",
264
+ allDayText: "",
265
+ month: 'HH:mm',
266
+ week: 'HH:mm',
267
+ day: 'HH:mm',
268
+ eventSources: [<%= $stuck_recurrences_json %>],
269
+ XeventDragStart: function(calEvent, jsEvent, ui, view) {
270
+ alert('Drag Event: ' + calEvent.title);
271
+ alert('Event ID: ' + calEvent.id);
272
+ alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
273
+ alert('View: ' + view.name);
274
+ // change the border color just for fun
275
+ $(this).css('border-color', 'red');
276
+ },
277
+ viewRender: function(view, element) {
278
+ $('tr.fc-list-item').each(function() {
279
+ var duration;
280
+ var seg = $(this).data('fc-seg');
281
+ var myevent = seg.footprint.getEventLegacy();
282
+ if (myevent.end) {
283
+ var minutes = moment.duration(myevent.end.diff(myevent.start)).asMinutes();
284
+ var days = Math.floor(minutes/(24*60));
285
+ minutes -= days * 24 * 60;
286
+ var hours = Math.floor(minutes/60);
287
+ minutes -= hours * 60;
288
+ duration = days + "." + hours + ":" + minutes;
289
+ } else {
290
+ duration = "0.00:00";
291
+ }
292
+ if (myevent.title == "母の日") {
293
+ // alert('My Event: ' + myevent.title);
294
+ // alert('Duration: ' + duration);
295
+ }
296
+ var uuid = create_uuid();
297
+ // alert('Seg: ' + seg);
298
+ $(this).data('event', {
299
+ title: myevent.title,
300
+ allDay: myevent.allDay,
301
+ start: myevent.start.format("HH:mm"),
302
+ duration: duration,
303
+ stick: true,
304
+ className: myevent.className
305
+ });
306
+ });
307
+
308
+ $('tr.fc-list-item').draggable({
309
+ appendTo: "#calendar",
310
+ cursorAt: {top: 5, left: 5},
311
+ iframeFix: true,
312
+ refreshPositions: true,
313
+ scroll: false,
314
+ cursor: "move",
315
+ revert: true, // will cause the event to go back to its
316
+ revertDuration: 0, // original position after the drag
317
+ helper: "clone",
318
+ zIndex: 100,
319
+ opacity: 0.35,
320
+
321
+ start: function(event, ui) {
322
+ }
323
+ });
324
+ if(view.name == 'listMonth') {
325
+ adjust_height('#stuck-events');
326
+ }
327
+ },
328
+ windowResize: function(view) {
329
+ if(view.name == 'listMonth') {
330
+ adjust_height('#stuck-events');
331
+ }
332
+ }
333
+ });
334
+
335
+ $('#calendar').fullCalendar({
336
+ header: {
337
+ left: 'prev,next today',
338
+ center: 'title',
339
+ right: 'month,agendaWeek,agendaDay,listMonth'
340
+ },
341
+ firstDay: 1,
342
+ height: max_height_for('#calendar'),
343
+ editable: true,
344
+ eventResourceEditable: true,
345
+ eventLimit: true,
346
+ selectable: true,
347
+ droppable: true,
348
+
349
+ // drop: Event ではない draggable からドロップされたとき.
350
+ // draggable が event として認識できないようなときでも発火する
351
+ drop: function(date) {
352
+ // alert("Dropped on " + date.format());
353
+ },
354
+
355
+ // eventReceive: Event ではない draggable からドロップされたとき
356
+ // draggable が event として認識可能な property を持っている場合だけ発火する
357
+ eventReceive: function(event) {
358
+ event.id = create_uuid(); // XXX! これがうまくいかない
359
+ $('#calendar').fullCalendar('updateEvent', event);
360
+ // alert("Drop event from ourter world! ID: " + event.id);
361
+ },
362
+
363
+ // eventDrop: 既に登録されている event を変更したとき
364
+ eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
365
+ // alert("eventDrop:" + event.title);
366
+ // alert("eventDrop:" + event.id);
367
+ },
368
+
369
+ // return (this.eventInstance || this.eventDef).toLegacy()
370
+ eventClick: function(calEvent, jsEvent, view) {
371
+ var yesno = prompt('Do you delete ' + calEvent.title + '?');
372
+ // alert('Event ID: ' + calEvent.id);
373
+ if (yesno == "yes"){
374
+ $('#calendar').fullCalendar('removeEvents', [calEvent.id]);
375
+ }
376
+ // alert('This Event: ' + $(this).getEventLegacy().title);
377
+ //alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
378
+ //alert('View: ' + view.name);
379
+ // change the border color just for fun
380
+ // $(this).css('border-color', 'red');
381
+ },
382
+
383
+ timezone: '<%= mhc.tzid %>',
384
+ timeFormat: "HH:mm",
385
+ allDayText: "",
386
+ month: 'HH:mm',
387
+ week: 'HH:mm',
388
+ day: 'HH:mm',
389
+ eventSources: json_event_array,
390
+ // eventSources: [<%= mhc.json_event_array %>],
391
+ viewRender: function(view, element) {
392
+ if(view.name == 'month') {
393
+ adjust_height('#calendar');
394
+ adjust_height('#stuck-events');
395
+ }
396
+ var currentDate = $('#calendar').fullCalendar('getDate');
397
+ // alert("The current date of the calendar is " + currentDate.format());
398
+ $('#side-calendar').fullCalendar('gotoDate', currentDate.subtract(1, 'y'));
399
+ $('#stuck-events').fullCalendar('gotoDate', currentDate.subtract(5, 'y'));
400
+ },
401
+ windowResize: function(view) {
402
+ if(view.name == 'month') {
403
+ adjust_height('#calendar');
404
+ adjust_height('#stuck-events');
405
+ }
406
+ }
407
+ }) // $('#calendar')
408
+
409
+ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
410
+ var target = $(e.target).attr("href") // activated tab
411
+ adjust_height(target);
412
+ });
413
+
414
+ });
415
+ </script>
416
+ </body>
417
+ </html>
data/lib/mhc/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Mhc
2
- VERSION = "1.2.4"
2
+ VERSION = "1.2.5"
3
3
  PRODID = "-//Quickhack.net//MHC #{Mhc::VERSION}//EN"
4
4
  end
data/mhc.gemspec CHANGED
@@ -23,7 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
24
  spec.require_paths = ["lib"]
25
25
 
26
- spec.add_runtime_dependency "thor", ">= 0.19.1"
26
+ spec.add_runtime_dependency "thor", ">= 1.2.0"
27
+ spec.add_runtime_dependency "rexml", ">= 3.2.4"
27
28
  spec.add_runtime_dependency "ri_cal", ">= 0.8.8"
28
29
  spec.add_runtime_dependency "tzinfo", ">= 1.2.2"
29
30
  spec.add_runtime_dependency "tzinfo-data", ">= 1.2015.4"
@@ -31,14 +31,14 @@ X-SC-Record-Id: AC1B378C-BAFE-4B6E-AF9C-78C2377E8AA4
31
31
  X-SC-Subject: 天皇誕生日
32
32
  X-SC-Category: Holiday Japanese
33
33
  X-SC-Cond: 29 Apr
34
- X-SC-Duration: 19480429-19980429
34
+ X-SC-Duration: 19490429-19880429
35
35
  X-SC-Record-Id: B78EAAEE-9963-4573-9EC7-0879F8940AEE
36
36
 
37
37
  X-SC-Subject: みどりの日
38
38
  X-SC-Category: Holiday Japanese
39
39
  X-SC-Cond: 29 Apr
40
40
  X-SC-Duration: 19890429-20060429
41
- X-SC-Record-Id: B78EAAEE-9963-4573-9EC7-0879F8940AEE
41
+ X-SC-Record-Id: 8C05BF80-006C-4FA7-8CF1-5F1C18CB8641
42
42
 
43
43
  X-SC-Subject: 昭和の日
44
44
  X-SC-Category: Holiday Japanese
@@ -186,12 +186,12 @@ X-SC-Day: 19730430 19730924 19740506 19740916 19741104 19751124
186
186
  20180430 20180924 20181224 20190506 20190812 20191104 20200224 20200506
187
187
  20210809
188
188
  20230102 20240212 20240506 20240812 20240923 20241104 20250224 20250506
189
- 20251124 20260506 20270322 20290212 20290430 20290924 20291224
189
+ 20251124 20260506 20270322 20290212 20290430 20290924
190
190
  20300506 20300812 20301104 20310224 20310506 20311124 20330321 20340102
191
- 20350212 20350430 20350924 20351224 20360506 20361124 20370506
192
- 20400102 20400430 20401224 20410506 20410812 20411104 20420224 20420506
191
+ 20350212 20350430 20350924 20360506 20361124 20370506
192
+ 20400102 20400430 20410506 20410812 20411104 20420224 20420506
193
193
  20421124 20430506 20440321 20450102 20460212 20460430 20460924
194
- 20461224 20470506 20470812 20471104 20480224 20480506 20500321
194
+ 20470506 20470812 20471104 20480224 20480506 20500321
195
195
  X-SC-Category: Holiday Japanese
196
196
  X-SC-Record-Id: E2D696DF-EB49-44D7-B06C-812B10BD99A2
197
197
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mhc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshinari Nomura
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-13 00:00:00.000000000 Z
11
+ date: 2024-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.19.1
19
+ version: 1.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.19.1
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rexml
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.4
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: ri_cal
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -104,6 +118,7 @@ extra_rdoc_files: []
104
118
  files:
105
119
  - ".gitignore"
106
120
  - ".rspec"
121
+ - ".rubocop.yml"
107
122
  - ".travis.yml"
108
123
  - COPYRIGHT
109
124
  - Gemfile
@@ -187,6 +202,7 @@ files:
187
202
  - lib/mhc/formatter/base.rb
188
203
  - lib/mhc/formatter/emacs.rb
189
204
  - lib/mhc/formatter/howm.rb
205
+ - lib/mhc/formatter/html.rb
190
206
  - lib/mhc/formatter/icalendar.rb
191
207
  - lib/mhc/formatter/json.rb
192
208
  - lib/mhc/formatter/mail.rb
@@ -215,6 +231,7 @@ files:
215
231
  - lib/mhc/sync/strategy.rb
216
232
  - lib/mhc/sync/syncinfo.rb
217
233
  - lib/mhc/templates/config.yml.erb
234
+ - lib/mhc/templates/full-calendar.html.erb
218
235
  - lib/mhc/version.rb
219
236
  - lib/mhc/webdav.rb
220
237
  - mhc.gemspec
@@ -255,7 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
272
  - !ruby/object:Gem::Version
256
273
  version: '0'
257
274
  requirements: []
258
- rubygems_version: 3.1.4
275
+ rubygems_version: 3.2.22
259
276
  signing_key:
260
277
  specification_version: 4
261
278
  summary: Message Harmonized Calendaring