jcard_jcal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/SECURITY.md +10 -0
- data/data/ical_properties.yml +52 -0
- data/data/vcard_properties.yml +42 -0
- data/lib/jcal.rb +30 -0
- data/lib/jcard.rb +28 -0
- data/lib/jcard_jcal/component.rb +72 -0
- data/lib/jcard_jcal/content_line/param_codec.rb +48 -0
- data/lib/jcard_jcal/content_line/reader.rb +86 -0
- data/lib/jcard_jcal/content_line/text_codec.rb +56 -0
- data/lib/jcard_jcal/content_line/writer.rb +57 -0
- data/lib/jcard_jcal/converter.rb +229 -0
- data/lib/jcard_jcal/errors.rb +21 -0
- data/lib/jcard_jcal/property.rb +113 -0
- data/lib/jcard_jcal/registry.rb +83 -0
- data/lib/jcard_jcal/value_types.rb +358 -0
- data/lib/jcard_jcal/version.rb +5 -0
- data/lib/jcard_jcal.rb +21 -0
- data/sig/jcard_jcal.rbs +123 -0
- metadata +86 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "uri"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "content_line/text_codec"
|
|
7
|
+
|
|
8
|
+
module JCardJCal
|
|
9
|
+
# Converts scalar property values; structured properties are handled by the converters.
|
|
10
|
+
module ValueTypes
|
|
11
|
+
TYPES = %w[text uri cal-address date time date-time date-and-or-time timestamp boolean integer float
|
|
12
|
+
utc-offset language-tag duration period recur binary unknown].freeze
|
|
13
|
+
MAX_BINARY_SIZE = 1_048_576
|
|
14
|
+
WEEKDAYS = %w[MO TU WE TH FR SA SU].freeze
|
|
15
|
+
FREQUENCIES = %w[SECONDLY MINUTELY HOURLY DAILY WEEKLY MONTHLY YEARLY].freeze
|
|
16
|
+
NUMERIC_RULES = %w[count interval bysecond byminute byhour bymonthday byyearday byweekno bymonth bysetpos].freeze
|
|
17
|
+
RULES = (NUMERIC_RULES + %w[freq until byday wkst rscale skip]).freeze
|
|
18
|
+
DURATION = /\A[+-]?P(?:\d+W|\d+D(?:T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))?|T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))\z/i
|
|
19
|
+
GRANDFATHERED_LANGUAGES = %w[en-gb-oed i-ami i-bnn i-default i-enochian i-hak i-klingon i-lux
|
|
20
|
+
i-mingo i-navajo i-pwn i-tao i-tay i-tsu sgn-be-fr sgn-be-nl sgn-ch-de
|
|
21
|
+
art-lojban cel-gaulish no-bok no-nyn zh-guoyu zh-hakka zh-min zh-min-nan zh-xiang].freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def from_text(value, type, format: :vcard, max_binary_size: MAX_BINARY_SIZE)
|
|
26
|
+
string!(value)
|
|
27
|
+
type = type.to_s.downcase
|
|
28
|
+
converted = case type
|
|
29
|
+
when "text" then ContentLine::TextCodec.decode(value)
|
|
30
|
+
when "uri", "cal-address" then format == :vcard ? value.gsub("\\,", ",") : value
|
|
31
|
+
when "boolean"
|
|
32
|
+
invalid!(type) unless /\A(?:TRUE|FALSE)\z/i.match?(value)
|
|
33
|
+
value.casecmp?("TRUE")
|
|
34
|
+
when "integer"
|
|
35
|
+
invalid!(type) unless /\A[+-]?\d+\z/.match?(value)
|
|
36
|
+
value.to_i
|
|
37
|
+
when "float"
|
|
38
|
+
invalid!(type) unless /\A[+-]?\d+(?:\.\d+)?\z/.match?(value)
|
|
39
|
+
value.to_f
|
|
40
|
+
when "date", "time", "date-time", "date-and-or-time", "timestamp", "utc-offset"
|
|
41
|
+
temporal(value, type, format, true)
|
|
42
|
+
when "period"
|
|
43
|
+
value.split("/", -1).map.with_index do |part, i|
|
|
44
|
+
i == 1 && /\A[+-]?P/i.match?(part) ? part : temporal(part, "date-time", :ical, true)
|
|
45
|
+
end
|
|
46
|
+
when "recur" then parse_recur(value)
|
|
47
|
+
else value
|
|
48
|
+
end
|
|
49
|
+
from_json(converted, type, format: format, max_binary_size: max_binary_size)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def from_json(value, type, format: :vcard, max_binary_size: MAX_BINARY_SIZE)
|
|
53
|
+
raise ArgumentError, "format must be :vcard or :ical" unless %i[vcard ical].include?(format)
|
|
54
|
+
|
|
55
|
+
type = type.to_s.downcase
|
|
56
|
+
case type
|
|
57
|
+
when "boolean"
|
|
58
|
+
invalid!(type) unless [true, false].include?(value)
|
|
59
|
+
when "integer", "float"
|
|
60
|
+
invalid!(type) unless (value.is_a?(Integer) || value.is_a?(Float)) && value.finite?
|
|
61
|
+
invalid!(type) if type == "integer" && format == :ical && !(value == value.to_i && (-2_147_483_648..2_147_483_647).cover?(value))
|
|
62
|
+
when "period" then validate_period(value)
|
|
63
|
+
when "recur" then validate_recur(value)
|
|
64
|
+
else
|
|
65
|
+
string!(value)
|
|
66
|
+
invalid!(type) if value.match?(/[\x00-\x08\x0b-\x1f\x7f]/) || (type != "text" && value.include?("\n"))
|
|
67
|
+
case type
|
|
68
|
+
when "date", "time", "date-time", "date-and-or-time", "timestamp", "utc-offset"
|
|
69
|
+
temporal(value, type, format, false)
|
|
70
|
+
when "uri", "cal-address" then validate_uri(value)
|
|
71
|
+
when "language-tag" then validate_language(value)
|
|
72
|
+
when "duration" then invalid!(type) unless DURATION.match?(value)
|
|
73
|
+
when "binary" then validate_binary(value, max_binary_size)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_json(value, type, **)
|
|
80
|
+
from_json(value, type, **)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_text(value, type, format: :vcard, max_binary_size: MAX_BINARY_SIZE)
|
|
84
|
+
type = type.to_s.downcase
|
|
85
|
+
from_json(value, type, format: format, max_binary_size: max_binary_size)
|
|
86
|
+
case type
|
|
87
|
+
when "text" then ContentLine::TextCodec.encode(value)
|
|
88
|
+
when "uri", "cal-address" then format == :vcard ? value.gsub(",") { "\\," } : value
|
|
89
|
+
when "boolean" then value ? "TRUE" : "FALSE"
|
|
90
|
+
when "integer" then value.to_i.to_s
|
|
91
|
+
when "float" then decimal(value)
|
|
92
|
+
when "date", "time", "date-time", "date-and-or-time", "timestamp", "utc-offset"
|
|
93
|
+
temporal(value, type, format, false)
|
|
94
|
+
when "period"
|
|
95
|
+
value.map.with_index do |part, i|
|
|
96
|
+
i == 1 && /\A[+]?P/i.match?(part) ? part : temporal(part, "date-time", :ical, false)
|
|
97
|
+
end.join("/")
|
|
98
|
+
when "recur"
|
|
99
|
+
value.map do |key, part|
|
|
100
|
+
part = temporal(part, part.include?("T") ? "date-time" : "date", :ical, false) if key == "until"
|
|
101
|
+
parts = Array(part).map { |item| NUMERIC_RULES.include?(key) && item.is_a?(Numeric) ? item.to_i : item }
|
|
102
|
+
"#{key.upcase}=#{parts.join(",")}"
|
|
103
|
+
end.join(";")
|
|
104
|
+
else value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def temporal(value, type, format, to_json)
|
|
109
|
+
case type
|
|
110
|
+
when "date" then date(value, format, to_json)
|
|
111
|
+
when "time" then time(value, format, to_json)
|
|
112
|
+
when "utc-offset" then offset(value, format, to_json)
|
|
113
|
+
when "date-and-or-time"
|
|
114
|
+
if value.start_with?("T")
|
|
115
|
+
"T#{time(value[1..], format, to_json)}"
|
|
116
|
+
elsif value.include?("T")
|
|
117
|
+
temporal(value, "date-time", format, to_json)
|
|
118
|
+
else
|
|
119
|
+
date(value, format, to_json)
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
parts = value.split("T", -1)
|
|
123
|
+
invalid!(type) unless parts.length == 2
|
|
124
|
+
day, clock = parts
|
|
125
|
+
full = type == "timestamp" || format == :ical
|
|
126
|
+
converted_date = date(day, format, to_json, full: full, no_reduced: true)
|
|
127
|
+
converted_time = time(clock, format, to_json, full: full, no_truncated: true)
|
|
128
|
+
"#{converted_date}T#{converted_time}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def date(value, format, to_json, full: false, no_reduced: false)
|
|
133
|
+
expression = to_json ? /\A(?:(\d{4})(\d{2})(\d{2})|--(\d{2})(\d{2}))\z/ : /\A(?:(\d{4})-(\d{2})-(\d{2})|--(\d{2})-(\d{2}))\z/
|
|
134
|
+
match = expression.match(value)
|
|
135
|
+
if match
|
|
136
|
+
year, month, day = match[1] ? match.captures[0, 3] : [nil, match[4], match[5]]
|
|
137
|
+
invalid!("date") if (full || format == :ical) && year.nil?
|
|
138
|
+
invalid!("date") unless Date.valid_date?((year || "2000").to_i, month.to_i, day.to_i, Date::GREGORIAN)
|
|
139
|
+
prefix = year || "--"
|
|
140
|
+
return to_json ? "#{prefix}#{"-" if year}#{month}-#{day}" : "#{prefix}#{month}#{day}"
|
|
141
|
+
end
|
|
142
|
+
invalid!("date") if full || format == :ical
|
|
143
|
+
case value
|
|
144
|
+
when /\A---(\d{2})\z/
|
|
145
|
+
invalid!("date") unless (1..31).cover?(Regexp.last_match(1).to_i)
|
|
146
|
+
when /\A(?:\d{4}-|--)(\d{2})\z/
|
|
147
|
+
invalid!("date") if no_reduced || !(1..12).cover?(Regexp.last_match(1).to_i)
|
|
148
|
+
when /\A\d{4}\z/ then invalid!("date") if no_reduced
|
|
149
|
+
else invalid!("date")
|
|
150
|
+
end
|
|
151
|
+
value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def time(value, format, to_json, full: false, no_truncated: false)
|
|
155
|
+
expression = if to_json
|
|
156
|
+
/\A(\d{2}(?:\d{2}){0,2}|-\d{2}(?:\d{2})?|--\d{2})(Z|[+-]\d{2}(?:\d{2})?)?\z/
|
|
157
|
+
else
|
|
158
|
+
/\A(\d{2}(?::\d{2}){0,2}|-\d{2}(?::\d{2})?|--\d{2})(Z|[+-]\d{2}(?::\d{2})?)?\z/
|
|
159
|
+
end
|
|
160
|
+
match = expression.match(value)
|
|
161
|
+
invalid!("time") unless match
|
|
162
|
+
clock, zone = match.captures
|
|
163
|
+
prefix = clock[/\A-*/]
|
|
164
|
+
units = clock.delete(":-").scan(/../).map(&:to_i)
|
|
165
|
+
invalid!("time") if (full || format == :ical) && (units.length != 3 || !prefix.empty?)
|
|
166
|
+
invalid!("time") if no_truncated && !prefix.empty?
|
|
167
|
+
limits = [23, 59, 60].drop(prefix.length)
|
|
168
|
+
invalid!("time") unless units.each_with_index.all? { |unit, i| unit <= limits[i] }
|
|
169
|
+
if zone && zone != "Z"
|
|
170
|
+
invalid!("time") if format == :ical
|
|
171
|
+
zone = offset(zone, :vcard, to_json)
|
|
172
|
+
end
|
|
173
|
+
"#{prefix}#{units.map { |unit| unit.to_s.rjust(2, "0") }.join(to_json ? ":" : "")}#{zone}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def offset(value, format, to_json)
|
|
177
|
+
expression = to_json ? /\A([+-])(\d{2})(\d{2})?(\d{2})?\z/ : /\A([+-])(\d{2})(?::(\d{2}))?(?::(\d{2}))?\z/
|
|
178
|
+
match = expression.match(value)
|
|
179
|
+
invalid!("utc-offset") unless match
|
|
180
|
+
sign, hour, minute, second = match.captures
|
|
181
|
+
invalid!("utc-offset") if hour.to_i > 23 || minute.to_i > 59 || second.to_i > 59
|
|
182
|
+
invalid!("utc-offset") if format == :vcard && second
|
|
183
|
+
invalid!("utc-offset") if format == :ical && (!minute || (sign == "-" && [hour, minute, second].all? { |v| v.to_i.zero? }))
|
|
184
|
+
"#{sign}#{[hour, minute, second].compact.join(to_json ? ":" : "")}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_period(value)
|
|
188
|
+
invalid!("period") unless value.is_a?(Array) && value.length == 2 && value.all?(String)
|
|
189
|
+
value.each { |part| string!(part) }
|
|
190
|
+
start, finish = value
|
|
191
|
+
temporal(start, "date-time", :ical, false)
|
|
192
|
+
if /\A[+-]?P/i.match?(finish)
|
|
193
|
+
invalid!("period duration") unless DURATION.match?(finish) && !finish.start_with?("-") && finish.scan(/\d+/).any? { |n| n.to_i.positive? }
|
|
194
|
+
else
|
|
195
|
+
temporal(finish, "date-time", :ical, false)
|
|
196
|
+
invalid!("period end") if start.end_with?("Z") == finish.end_with?("Z") && finish <= start
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def parse_recur(value)
|
|
201
|
+
value.split(";", -1).each_with_object({}) do |part, result|
|
|
202
|
+
key, data = part.split("=", 2)
|
|
203
|
+
key = key.to_s.downcase
|
|
204
|
+
invalid!("recur") if !data || data.empty? || result.key?(key) || !RULES.include?(key)
|
|
205
|
+
values = data.split(",", -1)
|
|
206
|
+
values.map! do |item|
|
|
207
|
+
if NUMERIC_RULES.include?(key) && !(key == "bymonth" && /\A\d+L\z/i.match?(item))
|
|
208
|
+
invalid!("recur #{key}") unless /\A[+-]?\d+\z/.match?(item)
|
|
209
|
+
item.to_i
|
|
210
|
+
elsif key == "until"
|
|
211
|
+
temporal(item, item.include?("T") ? "date-time" : "date", :ical, true)
|
|
212
|
+
else
|
|
213
|
+
item
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
result[key] = values.length == 1 ? values.first : values
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_recur(value)
|
|
221
|
+
invalid!("recur") unless value.is_a?(Hash) && value.keys.all? { |key| RULES.include?(key) }
|
|
222
|
+
invalid!("recur freq") unless value["freq"].is_a?(String) && FREQUENCIES.include?(value["freq"].upcase)
|
|
223
|
+
invalid!("recur count/until") if value.key?("count") && value.key?("until")
|
|
224
|
+
invalid!("recur skip") if value.key?("skip") && !value.key?("rscale")
|
|
225
|
+
value.each do |key, data|
|
|
226
|
+
if key.start_with?("by")
|
|
227
|
+
parts = data.is_a?(Array) ? data : [data]
|
|
228
|
+
invalid!("recur #{key}") if parts.empty?
|
|
229
|
+
parts.each { |part| validate_rule_part(key, part, value) }
|
|
230
|
+
else
|
|
231
|
+
validate_rule_part(key, data, value)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
frequency = value["freq"].upcase
|
|
235
|
+
invalid!("recur bymonthday") if frequency == "WEEKLY" && value.key?("bymonthday")
|
|
236
|
+
invalid!("recur byyearday") if %w[DAILY WEEKLY MONTHLY].include?(frequency) && value.key?("byyearday")
|
|
237
|
+
invalid!("recur byweekno") if frequency != "YEARLY" && value.key?("byweekno")
|
|
238
|
+
invalid!("recur bysetpos") if value.key?("bysetpos") && value.keys.none? { |key| key.start_with?("by") && key != "bysetpos" }
|
|
239
|
+
return unless Array(value["byday"]).any? { |day| /\A[+-]?\d/.match?(day) }
|
|
240
|
+
|
|
241
|
+
invalid!("recur byday") unless %w[MONTHLY YEARLY].include?(frequency)
|
|
242
|
+
invalid!("recur byday") if frequency == "YEARLY" && value.key?("byweekno")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def validate_rule_part(key, part, rule)
|
|
246
|
+
# ponytail: calendar-specific RSCALE ranges need a calendar engine when semantic validation is added.
|
|
247
|
+
scaled = rule.key?("rscale") && !rule["rscale"].to_s.casecmp?("GREGORIAN")
|
|
248
|
+
valid = case key
|
|
249
|
+
when "freq" then part.is_a?(String) && FREQUENCIES.include?(part.upcase)
|
|
250
|
+
when "wkst" then part.is_a?(String) && WEEKDAYS.include?(part.upcase)
|
|
251
|
+
when "rscale" then part.is_a?(String) && /\A[a-z0-9-]+\z/i.match?(part)
|
|
252
|
+
when "skip" then part.is_a?(String) && %w[OMIT BACKWARD FORWARD].include?(part.upcase)
|
|
253
|
+
when "until"
|
|
254
|
+
string!(part)
|
|
255
|
+
temporal(part, part.include?("T") ? "date-time" : "date", :ical, false)
|
|
256
|
+
when "byday"
|
|
257
|
+
match = /\A([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)\z/i.match(part) if part.is_a?(String)
|
|
258
|
+
match && (!match[1] || (1..(scaled ? 99 : 53)).cover?(match[1].to_i.abs))
|
|
259
|
+
when "bymonth"
|
|
260
|
+
if part.is_a?(String)
|
|
261
|
+
scaled && /\A\d{1,2}L\z/i.match?(part) && part.to_i.positive?
|
|
262
|
+
else
|
|
263
|
+
integral?(part) && (1..(scaled ? 99 : 12)).cover?(part)
|
|
264
|
+
end
|
|
265
|
+
when "count", "interval" then integral?(part) && part.positive?
|
|
266
|
+
when "bysecond" then integral?(part) && (0..60).cover?(part)
|
|
267
|
+
when "byminute" then integral?(part) && (0..59).cover?(part)
|
|
268
|
+
when "byhour" then integral?(part) && (0..23).cover?(part)
|
|
269
|
+
when "bymonthday" then integral?(part) && (1..(scaled ? 99 : 31)).cover?(part.abs)
|
|
270
|
+
when "byweekno" then integral?(part) && (1..(scaled ? 99 : 53)).cover?(part.abs)
|
|
271
|
+
when "byyearday", "bysetpos" then integral?(part) && (1..(scaled ? 999 : 366)).cover?(part.abs)
|
|
272
|
+
end
|
|
273
|
+
invalid!("recur #{key}") unless valid
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def integral?(value)
|
|
277
|
+
(value.is_a?(Integer) || value.is_a?(Float)) && value.finite? && value == value.to_i
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def validate_uri(value)
|
|
281
|
+
invalid!("uri") unless value.ascii_only? && !value.match?(/%(?![0-9a-f]{2})/i) && URI.parse(value).absolute?
|
|
282
|
+
rescue URI::InvalidURIError
|
|
283
|
+
invalid!("uri")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def validate_language(value)
|
|
287
|
+
return if GRANDFATHERED_LANGUAGES.include?(value.downcase)
|
|
288
|
+
return if /\Ax(?:-[a-z0-9]{1,8})+\z/i.match?(value)
|
|
289
|
+
|
|
290
|
+
pattern = /\A(?:[a-z]{2,3}(?:-[a-z]{3}){0,3}|[a-z]{4}|[a-z]{5,8})
|
|
291
|
+
(?:-[a-z]{4})?(?:-(?:[a-z]{2}|\d{3}))?(?:-(?:[a-z0-9]{5,8}|\d[a-z0-9]{3}))*
|
|
292
|
+
(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?\z/ix
|
|
293
|
+
invalid!("language-tag") unless pattern.match?(value)
|
|
294
|
+
singletons = value.downcase.split("-").take_while { |part| part != "x" }.select { |part| part.length == 1 }
|
|
295
|
+
invalid!("language-tag") unless singletons.uniq.length == singletons.length
|
|
296
|
+
variants = value.downcase.split("-").drop(1).take_while { |part| part.length != 1 }
|
|
297
|
+
.grep(/\A(?:[a-z0-9]{5,8}|\d[a-z0-9]{3})\z/)
|
|
298
|
+
invalid!("language-tag") unless variants.uniq.length == variants.length
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def validate_binary(value, limit)
|
|
302
|
+
validate_binary_size(value, limit)
|
|
303
|
+
invalid!("binary") unless %r{\A(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\z}.match?(value)
|
|
304
|
+
padding = if value.end_with?("==")
|
|
305
|
+
2
|
|
306
|
+
else
|
|
307
|
+
(value.end_with?("=") ? 1 : 0)
|
|
308
|
+
end
|
|
309
|
+
invalid!("binary padding") if padding == 2 && !"AQgw".include?(value[-3])
|
|
310
|
+
invalid!("binary padding") if padding == 1 && !"AEIMQUYcgkosw048".include?(value[-2])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def validate_binary_size(value, limit)
|
|
314
|
+
raise ArgumentError, "max_binary_size must be a nonnegative integer" unless limit.is_a?(Integer) && limit >= 0
|
|
315
|
+
return unless value.is_a?(String)
|
|
316
|
+
|
|
317
|
+
padding = if value.end_with?("==")
|
|
318
|
+
2
|
|
319
|
+
else
|
|
320
|
+
(value.end_with?("=") ? 1 : 0)
|
|
321
|
+
end
|
|
322
|
+
return unless value.bytesize > ((limit + 2) / 3) * 4 || ((value.bytesize / 4) * 3) - padding > limit
|
|
323
|
+
|
|
324
|
+
raise LimitError, "binary size limit exceeded"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def decimal(value)
|
|
328
|
+
string = value.to_s
|
|
329
|
+
return string unless string.include?("e")
|
|
330
|
+
|
|
331
|
+
mantissa, exponent = string.split("e")
|
|
332
|
+
sign = mantissa.start_with?("-") ? "-" : ""
|
|
333
|
+
whole, fraction = mantissa.delete_prefix("-").split(".")
|
|
334
|
+
digits = whole + fraction.to_s
|
|
335
|
+
point = whole.length + exponent.to_i
|
|
336
|
+
result = if point <= 0
|
|
337
|
+
"0.#{"0" * -point}#{digits}"
|
|
338
|
+
elsif point >= digits.length
|
|
339
|
+
digits + ("0" * (point - digits.length))
|
|
340
|
+
else
|
|
341
|
+
"#{digits[0, point]}.#{digits[point..]}"
|
|
342
|
+
end
|
|
343
|
+
result = result.sub(/0+\z/, "").delete_suffix(".") if result.include?(".")
|
|
344
|
+
sign + result
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def string!(value)
|
|
348
|
+
unless value.is_a?(String) && value.valid_encoding? && value.encoding.ascii_compatible? &&
|
|
349
|
+
value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
350
|
+
invalid!("string")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def invalid!(type)
|
|
355
|
+
raise ValueError, "invalid #{type} value"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
data/lib/jcard_jcal.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jcard_jcal/version"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module JcardJcal
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
JCardJCal = JcardJcal
|
|
10
|
+
|
|
11
|
+
require_relative "jcard_jcal/errors"
|
|
12
|
+
require_relative "jcard_jcal/content_line/text_codec"
|
|
13
|
+
require_relative "jcard_jcal/content_line/reader"
|
|
14
|
+
require_relative "jcard_jcal/content_line/writer"
|
|
15
|
+
require_relative "jcard_jcal/value_types"
|
|
16
|
+
require_relative "jcard_jcal/registry"
|
|
17
|
+
require_relative "jcard_jcal/property"
|
|
18
|
+
require_relative "jcard_jcal/component"
|
|
19
|
+
require_relative "jcard_jcal/converter"
|
|
20
|
+
require_relative "jcard"
|
|
21
|
+
require_relative "jcal"
|
data/sig/jcard_jcal.rbs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module JcardJcal
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
type json_value = nil | bool | Integer | Float | String | Array[json_value] | Hash[String, json_value]
|
|
5
|
+
type parameters = Hash[String, String | Array[String]]
|
|
6
|
+
type property_data = Array[json_value]
|
|
7
|
+
type card_data = [String, Array[property_data]]
|
|
8
|
+
type calendar_data = [String, Array[property_data], Array[calendar_data]]
|
|
9
|
+
type component_data = card_data | calendar_data
|
|
10
|
+
type metadata = Hash[String, json_value]
|
|
11
|
+
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
attr_reader line_number: Integer?
|
|
14
|
+
attr_reader pointer: String?
|
|
15
|
+
def initialize: (String message, ?line_number: Integer?, ?pointer: String?) -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ParseError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ValidationError < Error
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ValueError < ValidationError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class LimitError < ValueError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ConversionError < Error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Registry
|
|
34
|
+
UNKNOWN: metadata
|
|
35
|
+
def initialize: (?Hash[String, metadata] entries) -> void
|
|
36
|
+
def initialize_copy: (Registry other) -> void
|
|
37
|
+
def []: (String | Symbol name) -> metadata
|
|
38
|
+
def register: (String | Symbol name, type: String | Symbol, ?types: Array[String | Symbol]?,
|
|
39
|
+
?multiple: bool, ?structured: bool, **json_value) -> self
|
|
40
|
+
def freeze: () -> self
|
|
41
|
+
def self.load: (String filename) -> instance
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Property
|
|
45
|
+
attr_reader warnings: Array[Error]
|
|
46
|
+
def initialize: (property_data data, ?registry: Registry?, ?strict: bool, ?pointer: String,
|
|
47
|
+
?line_number: Integer?, ?max_binary_size: Integer) -> void
|
|
48
|
+
def name: () -> String
|
|
49
|
+
def params: () -> parameters
|
|
50
|
+
def type: () -> String
|
|
51
|
+
def values: () -> Array[json_value]
|
|
52
|
+
def group: () -> String?
|
|
53
|
+
def to_a: () -> property_data
|
|
54
|
+
def validate!: () -> self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Component
|
|
58
|
+
attr_reader name: String
|
|
59
|
+
attr_reader warnings: Array[Error]
|
|
60
|
+
def initialize: (String name, Array[Property] properties, ?Array[Component] components,
|
|
61
|
+
?warnings: Array[Error], ?registry: Registry?, ?max_binary_size: Integer) -> void
|
|
62
|
+
def properties: (?String? name) -> Array[Property]
|
|
63
|
+
def property: (String name) -> Property?
|
|
64
|
+
def components: (?String? name) -> Array[Component]
|
|
65
|
+
def to_a: () -> component_data
|
|
66
|
+
def to_json: (*untyped args) -> String
|
|
67
|
+
def validate!: () -> self
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module JCardJCal = JcardJcal
|
|
72
|
+
|
|
73
|
+
module JCard
|
|
74
|
+
class Registry < JcardJcal::Registry
|
|
75
|
+
DEFAULT: Registry
|
|
76
|
+
def self.default: () -> Registry
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Property < JcardJcal::Property
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Card < JcardJcal::Component
|
|
83
|
+
def properties: (?String? name) -> Array[Property]
|
|
84
|
+
def property: (String name) -> Property?
|
|
85
|
+
def to_a: () -> JcardJcal::card_data
|
|
86
|
+
def to_vcard: () -> String
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.parse: (String source, ?registry: JcardJcal::Registry?, ?strict: bool,
|
|
90
|
+
?max_depth: Integer, ?max_properties: Integer, ?max_line_length: Integer,
|
|
91
|
+
?max_binary_size: Integer, ?max_input_size: Integer) -> Card
|
|
92
|
+
def self.from_vcard: (String source, ?registry: JcardJcal::Registry?, ?strict: bool,
|
|
93
|
+
?max_depth: Integer, ?max_properties: Integer, ?max_line_length: Integer,
|
|
94
|
+
?max_binary_size: Integer, ?max_input_size: Integer) -> Array[Card]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
module JCal
|
|
98
|
+
class Registry < JcardJcal::Registry
|
|
99
|
+
DEFAULT: Registry
|
|
100
|
+
def self.default: () -> Registry
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class Property < JcardJcal::Property
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class Component < JcardJcal::Component
|
|
107
|
+
def properties: (?String? name) -> Array[Property]
|
|
108
|
+
def property: (String name) -> Property?
|
|
109
|
+
def components: (?String? name) -> Array[Component]
|
|
110
|
+
def to_a: () -> JcardJcal::calendar_data
|
|
111
|
+
def to_ical: () -> String
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class Calendar < Component
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.parse: (String source, ?registry: JcardJcal::Registry?, ?strict: bool,
|
|
118
|
+
?max_depth: Integer, ?max_properties: Integer, ?max_line_length: Integer,
|
|
119
|
+
?max_binary_size: Integer, ?max_input_size: Integer) -> Calendar
|
|
120
|
+
def self.from_ical: (String source, ?registry: JcardJcal::Registry?, ?strict: bool,
|
|
121
|
+
?max_depth: Integer, ?max_properties: Integer, ?max_line_length: Integer,
|
|
122
|
+
?max_binary_size: Integer, ?max_input_size: Integer) -> Calendar
|
|
123
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jcard_jcal
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.19.4
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 2.19.4
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
description: Read, validate, and write jCard (RFC 7095) and jCal (RFC 7265), including
|
|
33
|
+
structured values, recurrence rules, and unknown extensions.
|
|
34
|
+
email:
|
|
35
|
+
- t.yudai92@gmail.com
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- SECURITY.md
|
|
44
|
+
- data/ical_properties.yml
|
|
45
|
+
- data/vcard_properties.yml
|
|
46
|
+
- lib/jcal.rb
|
|
47
|
+
- lib/jcard.rb
|
|
48
|
+
- lib/jcard_jcal.rb
|
|
49
|
+
- lib/jcard_jcal/component.rb
|
|
50
|
+
- lib/jcard_jcal/content_line/param_codec.rb
|
|
51
|
+
- lib/jcard_jcal/content_line/reader.rb
|
|
52
|
+
- lib/jcard_jcal/content_line/text_codec.rb
|
|
53
|
+
- lib/jcard_jcal/content_line/writer.rb
|
|
54
|
+
- lib/jcard_jcal/converter.rb
|
|
55
|
+
- lib/jcard_jcal/errors.rb
|
|
56
|
+
- lib/jcard_jcal/property.rb
|
|
57
|
+
- lib/jcard_jcal/registry.rb
|
|
58
|
+
- lib/jcard_jcal/value_types.rb
|
|
59
|
+
- lib/jcard_jcal/version.rb
|
|
60
|
+
- sig/jcard_jcal.rbs
|
|
61
|
+
homepage: https://github.com/ydah/jcard_jcal
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
allowed_push_host: https://rubygems.org
|
|
66
|
+
source_code_uri: https://github.com/ydah/jcard_jcal
|
|
67
|
+
changelog_uri: https://github.com/ydah/jcard_jcal/blob/main/CHANGELOG.md
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.3.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.16
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Convert vCard and iCalendar to and from jCard and jCal
|
|
86
|
+
test_files: []
|