mhc 1.2.4 → 1.2.5

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