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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JcardJcal
4
+ VERSION = "0.1.0"
5
+ 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"
@@ -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: []