mhc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. metadata +214 -0
@@ -0,0 +1,11 @@
1
+ module Mhc
2
+ module PropertyValue
3
+ class Text < Base
4
+ require "nkf"
5
+ def to_mhc_string
6
+ return NKF.nkf("-w", @value.to_s)
7
+ end
8
+ alias_method :to_s, :to_mhc_string
9
+ end # class Text
10
+ end # module PropertyValue
11
+ end # module Mhc
@@ -0,0 +1,45 @@
1
+ class DateTime
2
+ alias_method :to_mhc_string, :to_s
3
+ end
4
+
5
+ module Mhc
6
+ module PropertyValue
7
+ class Time < Base
8
+ include Comparable
9
+
10
+ def parse(string)
11
+ if /^(\d+):(\d+)$/ =~ string
12
+ @sec = ($1.to_i) * 3600 + ($2.to_i) * 60
13
+ end
14
+ return self
15
+ end
16
+
17
+ def days; (@sec ) / 86400 ;end
18
+ def hour; (@sec % 86400) / 3600 ;end
19
+ def minute; (@sec % 3600) / 60 ;end
20
+
21
+ def <=>(o)
22
+ return @sec <=> o.to_i
23
+ end
24
+
25
+ def to_mhc_string
26
+ return format("%02d:%02d", hour, minute)
27
+ end
28
+
29
+ alias_method :to_s, :to_mhc_string
30
+
31
+ def to_i
32
+ return @sec
33
+ end
34
+
35
+ def to_a
36
+ return [hour, minute]
37
+ end
38
+
39
+ def to_datetime(date = Mhc::PropertyValue::Date.new(1970, 1, 2))
40
+ date = date + days
41
+ time = ::DateTime.new(date.year, date.month, date.day, hour, minute, 0, DateTime.now.zone) # make local DateTime
42
+ end
43
+ end
44
+ end # module PropertyValue
45
+ end # module Mhc
data/lib/mhc/query.rb ADDED
@@ -0,0 +1,210 @@
1
+ module Mhc
2
+ class Query
3
+ def initialize(query_string)
4
+ @expression = Expression.new(Context.new(query_string))
5
+ @query_string = query_string
6
+ end
7
+
8
+ def to_proc
9
+ return @expression.to_proc
10
+ end
11
+
12
+ def to_s
13
+ @query_string.to_s
14
+ end
15
+
16
+ class ParseError < StandardError; end
17
+
18
+ #
19
+ # Expression :: Term ('|' Term)*
20
+ #
21
+ class Expression
22
+ def initialize(context)
23
+ @terms = [Term.new(context)]
24
+ @terms << Term.new(context) while context.eat_if(:orop)
25
+ end
26
+
27
+ def to_proc
28
+ @procs = @terms.map(&:to_proc)
29
+ return lambda {|ev| @procs.any? {|p| p.call(ev)}}
30
+ end
31
+ end # class Expression
32
+
33
+ #
34
+ # Term :: Factor ('&' Factor)*
35
+ #
36
+ class Term
37
+ def initialize(context)
38
+ @factors = [Factor.new(context)]
39
+ @factors << Factor.new(context) while context.eat_if(:andop)
40
+ end
41
+
42
+ def to_proc
43
+ @procs = @factors.map(&:to_proc)
44
+ return lambda {|ev| @procs.all? {|p| p.call(ev)}}
45
+ end
46
+ end # class Term
47
+
48
+ #
49
+ # Factor :: '!'* ( '(' Expression ')' || RelationalExpression )
50
+ #
51
+ class Factor
52
+ def initialize(context)
53
+ @expected_value = true
54
+ @expected_value = !@expected_value while context.eat_if(:negop)
55
+
56
+ if context.eat_if(:lparen)
57
+ @value = Expression.new(context)
58
+ context.expect(:rparen)
59
+ else
60
+ @value = RelationalExpression.new(context)
61
+ end
62
+ end
63
+
64
+ def to_proc
65
+ @proc = @value.to_proc
66
+ return lambda {|ev| @proc.call(ev) == @expected_value}
67
+ end
68
+ end # class Factor
69
+
70
+ #
71
+ # RelationalExpression :: Symbol Operator (Argument || '[' Argument Argument* ']')
72
+ #
73
+ class RelationalExpression
74
+ KEYWORDS = [:subject, :category, :body, :location, :recurrence_tag]
75
+
76
+ def initialize(context)
77
+ @name = context.expect(:symbol).value.downcase.to_sym
78
+ raise ParseError, "unknown keyword '#{@name}'" unless KEYWORDS.member?(@name)
79
+
80
+ context.expect(:sepop) # Currently, operator is only ":"
81
+
82
+ @arguments = []
83
+ if context.eat_if(:lbracket)
84
+ loop do
85
+ @arguments << Argument.new(context)
86
+ break if context.eat_if(:rbracket)
87
+ end
88
+ else
89
+ @arguments << Argument.new(context)
90
+ end
91
+ end
92
+
93
+ def to_proc
94
+ case @name
95
+ when :category
96
+ @arguments = @arguments.map{|arg| arg.value.downcase}
97
+ return lambda {|ev| !(ev.categories.map{|c| c.to_s.downcase} & @arguments).empty?}
98
+ when :recurrence_tag
99
+ @arguments = @arguments.map{|arg| arg.value.downcase}
100
+ return lambda {|ev| !!@arguments.find{|v| ev.send(@name).to_s.downcase.toutf8 == v}}
101
+ else
102
+ @arguments = @arguments.map{|arg| Regexp.quote(arg.value)}
103
+ return lambda {|ev| !!@arguments.find{|v| ev.send(@name).to_s.toutf8.match(v)}}
104
+ end
105
+ end
106
+ end # class RelationalExpression
107
+
108
+ #
109
+ # Argument :: Symbol || String
110
+ #
111
+ class Argument
112
+ def initialize(context)
113
+ token = context.expect(:symbol, :string)
114
+ @type = token.type
115
+ @value = token.value
116
+ end
117
+
118
+ def value
119
+ case @type
120
+ when :string
121
+ @value[1..-2]
122
+ else
123
+ @value
124
+ end
125
+ end
126
+ end # class Argument
127
+
128
+ class Context
129
+ TOKENS = {
130
+ symbol: /[a-zA-Z_][a-zA-Z_\d]*/,
131
+ string: /"(?:[^"\\]|\\.)*"/,
132
+ negop: /!/,
133
+ andop: /&/,
134
+ orop: /\|/,
135
+ sepop: /:/,
136
+ lparen: /\(/,
137
+ rparen: /\)/,
138
+ lbracket: /\[/,
139
+ rbracket: /\]/
140
+ }.map{|type,regexp| "(?<#{type}>#{regexp})"}.join("|")
141
+
142
+ TOKEN_REGEXP = Regexp.new('^\s*(' + TOKENS + ')')
143
+
144
+ def initialize(string)
145
+ @tokens = tokenize(string)
146
+ end
147
+
148
+ def eat_if(*expected_types)
149
+ expected_types.each do |expected_type|
150
+ if @tokens.first and @tokens.first.type == expected_type
151
+ return @tokens.shift
152
+ end
153
+ end
154
+ return nil
155
+ end
156
+
157
+ def expect(*expected_types)
158
+ token = eat_if(*expected_types) and return token
159
+ raise ParseError, "#{expected_types.map(&:upcase).join(' or ')} expected before #{@tokens.first.value rescue 'END'}"
160
+ end
161
+
162
+ def debug_dump
163
+ @tokens.map{|token| "#{token.type} => #{token.value}"}.join(", ")
164
+ end
165
+
166
+ private
167
+
168
+ def tokenize(string)
169
+ tokens = []
170
+
171
+ loop do
172
+ token, string = get_token(string)
173
+ break if token.nil?
174
+ tokens << token
175
+ end
176
+
177
+ raise ParseError, "can not tokenize '#{string}'" unless string.length == 0
178
+ return tokens
179
+ end
180
+
181
+ def get_token(string)
182
+ if match = TOKEN_REGEXP.match(string)
183
+ name = match.names.find{|name| match[name]}
184
+ value = match[name]
185
+ remain = match.post_match.strip
186
+ return [Token.new(name, value), remain]
187
+ end
188
+ return [nil, string]
189
+ end
190
+ end # class Context
191
+
192
+ class Token
193
+ attr_reader :type, :value
194
+
195
+ def initialize(type, string)
196
+ @type, @value = type.to_sym, string
197
+ end
198
+ end # class Token
199
+
200
+ class Test
201
+ attr_reader :categories, :subject
202
+
203
+ def initialize(categories = [], subject = "", body = "")
204
+ @categories = categories
205
+ @subject = subject
206
+ @body = body
207
+ end
208
+ end
209
+ end
210
+ end
data/lib/mhc/sync.rb ADDED
@@ -0,0 +1,46 @@
1
+ ################################################################
2
+ #
3
+ # We assume a CalDAV server as a remote side.
4
+ # CalDAV has no ability to provide such information.
5
+ # It only provides ETag mechanism, which is defined
6
+ # in HTTP protocol (see rfc2616).
7
+ #
8
+ # So, we have to maintain a ETag cache (replica) on local and
9
+ # manage difference between the cache and the remote.
10
+ # Using the ETag information:
11
+ #
12
+ # (1) get uid-etag list via PROPFIND (WebDav) method
13
+ #
14
+ # make a set: R_SET = [(r_uid, r_etag)..]
15
+ # r_uid: unique id of a remote article.
16
+ # r_etag: corresponding etag of r_uid.
17
+ #
18
+ # (2) get uid-etag list via local cache.
19
+ #
20
+ # make a set: L_SET = [(l_uid, l_etag)..]
21
+ #
22
+ # (3) for each uid: [uid| (uid, etag) <- L_SET + R_SET]
23
+ #
24
+ # (A) if (uid, _) is missed in L_SET
25
+ # -> SET_MARK(uid, M)
26
+ #
27
+ # (B) if (uid, _) is missed in R_SET
28
+ # -> SET_MARK(uid, D)
29
+ #
30
+ # (C) if (uid, _) exists in both R_SET and L_SET
31
+ # if l_etag != r_etag
32
+ # -> SET_MARK(uid, M)
33
+ # if l_etag == r_etag
34
+ # -> SET_MARK(uid, N)
35
+ #
36
+ module Mhc
37
+ module Sync
38
+ dir = File.dirname(__FILE__) + "/sync"
39
+
40
+ autoload :Driver, "#{dir}/driver.rb"
41
+ autoload :Status, "#{dir}/status.rb"
42
+ autoload :StatusManager, "#{dir}/status_manager.rb"
43
+ autoload :Strategy, "#{dir}/strategy.rb"
44
+ autoload :Log, "#{dir}/syncinfo.rb"
45
+ end # module Sync
46
+ end # module Mhc
@@ -0,0 +1,108 @@
1
+ module Mhc
2
+ module Sync
3
+ ##
4
+ # Sync Driver takes two calendar databases to sync.
5
+ #
6
+ # Each record in calendar has to respond to:
7
+ # * Record#unmodified?
8
+ # * Record#deleted?
9
+ # * Record#etag
10
+ # * Record#etag=
11
+ # * Record#ex_etag
12
+ #
13
+ class Driver
14
+ def initialize(db1, db2, strategy)
15
+ @db1 = db1
16
+ @db2 = db2
17
+ @strategy = Strategy::Factory.create(strategy)
18
+ end
19
+
20
+ def sync_all(dry_run = false, max_count = 50)
21
+ list_cache = uid_list
22
+
23
+ items = count_sync_items(list_cache)
24
+ if items > max_count
25
+ STDERR.print "Too many (#{items}) articles to sync... abort\n"
26
+ return false
27
+ end
28
+
29
+ list_cache.each do |uid|
30
+ sync(uid, dry_run)
31
+ end
32
+
33
+ return true
34
+ end
35
+
36
+ private
37
+
38
+ def count_sync_items(sync_uid_list)
39
+ sync_uid_list.map{|uid| sync(uid, true, true)}.count{|s| s != :ignore}
40
+ end
41
+
42
+ def sync(uid, dry_run = false, quiet = false)
43
+ info1 = @db1.syncinfo(uid)
44
+ info2 = @db2.syncinfo(uid)
45
+
46
+ unless @strategy.whatnow(info1, info2) == :ignore or quiet
47
+ STDERR.print "ABOUT#{dry_run ? '(DRY_RUN)' : ''} #{uid} => #{@strategy.whatnow(info1, info2)} "
48
+ STDERR.print "(#{info1.sync_status} vs #{info2.sync_status})\n"
49
+ end
50
+ return @strategy.whatnow(info1, info2) if dry_run
51
+
52
+ case @strategy.whatnow(info1, info2)
53
+ when :ignore
54
+ #ignore(side1, side2)
55
+ when :conflict
56
+ merge(uid, @db1, @db2)
57
+ when :delete1
58
+ delete(uid, @db1, @db2)
59
+ when :delete2
60
+ delete(uid, @db2, @db1)
61
+ when :copy1_to_2
62
+ copy(uid, @db1, @db2)
63
+ when :copy2_to_1
64
+ copy(uid, @db2, @db1)
65
+ when :overwrite1_to_2
66
+ copy(uid, @db1, @db2, :overwrite)
67
+ when :overwrite2_to_1
68
+ copy(uid, @db2, @db1, :overwrite)
69
+ end
70
+ end
71
+
72
+ def uid_list
73
+ (@db1.uid_list + @db2.uid_list).sort.uniq
74
+ end
75
+
76
+ def merge(uid, db1, db2)
77
+ # Not yet implemented
78
+ s1 = db1.get(uid)
79
+ s2 = db2.get(uid)
80
+ STDERR.print("Conflict: UID=#{uid} ... did nothing.\n")
81
+ end
82
+
83
+
84
+ def delete(uid, db, db2)
85
+ info = db.syncinfo(uid)
86
+ info2 = db2.syncinfo(uid)
87
+ if db.delete(uid)
88
+ info.mark_synced(nil)
89
+ info2.mark_synced(nil)
90
+ end
91
+ end
92
+
93
+ def copy(uid, db1, db2, overwrite = false)
94
+ ev = db1.get(uid)
95
+ STDERR.print "COPYING:#{overwrite ? ' (overwrite)' : ''} #{ev.uid}\n"
96
+
97
+ db2.delete(uid) if overwrite
98
+
99
+ if new_info = db2.put(ev, overwrite)
100
+ db1.syncinfo(uid).mark_synced(ev.etag)
101
+ db2.syncinfo(uid).mark_synced(new_info.etag)
102
+ else
103
+ STDERR.print "COPY: failed.\n"
104
+ end
105
+ end
106
+ end # class Driver
107
+ end # module Sync
108
+ end # module Mhc
@@ -0,0 +1,70 @@
1
+ module Mhc
2
+ module Sync
3
+ ##
4
+ # status
5
+ #
6
+ class Status
7
+ def initialize(uid, manager, wrapped_record = nil)
8
+ @uid, @manager, @wrapped_record = uid, manager, wrapped_record
9
+ end
10
+
11
+ def uid
12
+ return @uid
13
+ end
14
+
15
+ def etag
16
+ @manager.etag(@uid)
17
+ end
18
+
19
+ def ex_etag
20
+ @manager.ex_etag(@uid)
21
+ end
22
+
23
+ def sync_status
24
+ return :norecord if !etag and !ex_etag
25
+ return :created if etag and !ex_etag
26
+ return :deleted if !etag and ex_etag
27
+ return :unmodified if etag == ex_etag
28
+ return :modified if etag != ex_etag
29
+ end
30
+
31
+ def modified?
32
+ sync_status == :created or sync_status == :modified
33
+ end
34
+
35
+ def unmodified?
36
+ sync_status == :unmodified
37
+ end
38
+
39
+ def norecord?
40
+ sync_status == :norecord
41
+ end
42
+
43
+ def deleted?
44
+ sync_status == :deleted
45
+ end
46
+
47
+ def mark_synced(etag = self.etag)
48
+ @manager.mark_synced(uid, etag)
49
+ return self
50
+ end
51
+
52
+ ### as a calendar DB redord
53
+ def to_ics_string
54
+ # LastNote or mhc is assumed.
55
+ if @wrapped_record.respond_to?(:to_ics_string)
56
+ result = @wrapped_record.to_ics_string
57
+ return result
58
+ end
59
+
60
+ # HTTP::Response from caldav server is assumed.
61
+ if @wrapped_record.respond_to?(:body)
62
+ return @wrapped_record.body
63
+ end
64
+
65
+ return nil # Nil or unsupport object class. XXX donot put.
66
+ end
67
+
68
+ end # class Status
69
+ end # module Sync
70
+ end # module Mhc