├── test ├── parser │ ├── newline_junk.json │ ├── single_empty_vcalendar.json │ ├── single_empty_vcalendar.ics │ ├── values.ics │ ├── newline_junk.ics │ ├── tzid_with_gmt.ics │ ├── time.ics │ ├── quoted_params.ics │ ├── integer.ics │ ├── float.ics │ ├── period.ics │ ├── boolean.ics │ ├── utc_offset.ics │ ├── values.json │ ├── multiple_root_components.ics │ ├── unfold_properties.ics │ ├── utc_offset.json │ ├── dates.ics │ ├── tzid_with_gmt.json │ ├── base64.ics │ ├── recur.ics │ ├── period.json │ ├── quoted_params.json │ ├── component.ics │ ├── float.json │ ├── integer.json │ ├── multiple_root_components.json │ ├── multivalue.ics │ ├── time.json │ ├── rfc.ics │ ├── unfold_properties.json │ ├── boolean.json │ ├── base64.json │ ├── component.json │ ├── rfc.json │ ├── dates.json │ ├── recur.json │ ├── vcard_author.vcf │ ├── multivalue.json │ ├── property_params.ics │ ├── vcard.vcf │ ├── vcard_author.json │ ├── vcard3.vcf │ ├── vcard.json │ ├── property_params.json │ └── vcard3.json ├── acceptance │ ├── blank_description_test.js │ ├── forced_types_test.js │ ├── utc_negative_zero_test.js │ ├── daily_recuring_test.js │ └── google_birthday_test.js ├── performance │ ├── parse_test.js │ ├── iterator_test.js │ ├── time_test.js │ └── rrule_test.js ├── binary_test.js ├── support │ ├── factory.js │ └── performance.js ├── vcard_time_test.js ├── timezone_service_test.js ├── component_parser_test.js ├── utc_offset_test.js ├── duration_test.js ├── helper_test.js ├── stringify_test.js ├── helper.js ├── parse_test.js ├── recur_expansion_test.js └── timezone_test.js ├── tools └── ICALTester │ ├── support │ ├── .gitignore │ └── libical-recur.c │ ├── rules.json │ ├── Makefile │ ├── README.md │ ├── compare.js │ └── lib │ └── ICALTester.js ├── samples ├── blank_line_end.ics ├── blank_line_mid.ics ├── timezones │ ├── America │ │ ├── Atikokan.ics │ │ ├── New_York.ics │ │ ├── Los_Angeles.ics │ │ └── Denver.ics │ └── Makebelieve │ │ ├── RRULE_UNTIL_test.ics │ │ ├── RDATE_utc_test.ics │ │ └── RDATE_test.ics ├── utc_negative_zero.ics ├── only_dtstart_date.ics ├── only_dtstart_time.ics ├── duration_instead_of_dtend.ics ├── minimal.ics ├── parserv2.ics ├── forced_types.ics ├── multiple_rrules.ics ├── blank_description.ics ├── daily_recur.ics ├── day_long_recur_yearly.ics ├── recur_instances_finite.ics ├── recur_instances.ics └── google_birthday.ics ├── .codeclimate.yml ├── .editorconfig ├── .gitignore ├── CODE_OF_CONDUCT.md ├── bower.json ├── .travis.yml ├── tasks ├── timezones.js ├── travis.js └── tests.js ├── CHANGELOG.md ├── package.json ├── lib └── ical │ ├── timezone_service.js │ ├── component_parser.js │ ├── binary.js │ ├── utc_offset.js │ ├── vcard_time.js │ └── period.js ├── CONTRIBUTING.md ├── sandbox └── validator.html ├── README.md └── Gruntfile.js /test/parser/newline_junk.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", [], []] 2 | -------------------------------------------------------------------------------- /test/parser/single_empty_vcalendar.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", [], []] 2 | -------------------------------------------------------------------------------- /tools/ICALTester/support/.gitignore: -------------------------------------------------------------------------------- 1 | *.dSYM 2 | libical-recur 3 | -------------------------------------------------------------------------------- /samples/blank_line_end.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | END:VCALENDAR 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/parser/single_empty_vcalendar.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | END:VCALENDAR 3 | -------------------------------------------------------------------------------- /test/parser/values.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | LOCATION: 3 | END:VCALENDAR 4 | -------------------------------------------------------------------------------- /test/parser/newline_junk.ics: -------------------------------------------------------------------------------- 1 | 2 | 3 | BEGIN:VCALENDAR 4 | END:VCALENDAR 5 | 6 | 7 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - "build/*" 5 | -------------------------------------------------------------------------------- /samples/blank_line_mid.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | COMMENT:This blank line is invalid 3 | 4 | END:VCALENDAR 5 | -------------------------------------------------------------------------------- /test/parser/tzid_with_gmt.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | DTSTART;TZID="(GMT +01:00)":20111028T160000 3 | END:VCALENDAR 4 | -------------------------------------------------------------------------------- /test/parser/time.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-TIME;VALUE=TIME:230000 3 | X-TIMEZ;VALUE=TIME:230000Z 4 | END:VCALENDAR 5 | 6 | -------------------------------------------------------------------------------- /test/parser/quoted_params.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-FOO;BAR=";hidden=value";FOO=baz:realvalue 3 | yep 4 | END:VCALENDAR 5 | -------------------------------------------------------------------------------- /test/parser/integer.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-INTEGER;VALUE=INTEGER:105 3 | X-INVALID;VALUE=INTEGER:foobar 4 | END:VCALENDAR 5 | 6 | -------------------------------------------------------------------------------- /test/parser/float.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-FLOAT;VALUE=FLOAT:10.35 3 | X-INVALID-FLOAT;VALUE=FLOAT:my foo! 4 | END:VCALENDAR 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_size = 2 7 | indent_style = space 8 | -------------------------------------------------------------------------------- /test/parser/period.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z, 3 | 19960404T010000/PT3H 4 | END:VCALENDAR 5 | 6 | -------------------------------------------------------------------------------- /test/parser/boolean.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-TRUE;VALUE=BOOLEAN:TRUE 3 | X-FALSE;VALUE=BOOLEAN:FALSE 4 | X-MAYBE;VALUE=BOOLEAN:MAYBE 5 | END:VCALENDAR 6 | -------------------------------------------------------------------------------- /test/parser/utc_offset.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | X-OFFSET;VALUE=UTC-OFFSET:-0500 3 | X-OFFSET-SECONDS;VALUE=UTC-OFFSET:+055001 4 | END:VCALENDAR 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/parser/values.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "location", 5 | {}, 6 | "text", 7 | "" 8 | ] 9 | ], 10 | [] 11 | ] 12 | -------------------------------------------------------------------------------- /test/parser/multiple_root_components.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | DTSTAMP:20140401T010101Z 3 | END:VCALENDAR 4 | BEGIN:VCALENDAR 5 | DTSTAMP:20140401T020202Z 6 | END:VCALENDAR 7 | -------------------------------------------------------------------------------- /test/parser/unfold_properties.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | ATTENDEE;CN=Person;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRU 3 | E:mailto:person@foo.com 4 | END:VCALENDAR 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.pyc 4 | tools/tzurl 5 | tools/libical 6 | zoneinfo 7 | api 8 | ghpages-stage 9 | /validator.html 10 | coverage/ 11 | build/ 12 | -------------------------------------------------------------------------------- /test/parser/utc_offset.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | ["x-offset", {}, "utc-offset", "-05:00"], 4 | ["x-offset-seconds", {}, "utc-offset", "+05:50:01"] 5 | ], 6 | [] 7 | ] 8 | -------------------------------------------------------------------------------- /test/parser/dates.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | DTSTART:20120901T130000Z 3 | DTEND;VALUE=DATE:20120901 4 | RDATE:20131210 5 | X-MYDATE;VALUE=DATE:20120901 6 | X-MYDATETIME;VALUE=DATE-TIME:20120901T130000 7 | END:VCALENDAR 8 | -------------------------------------------------------------------------------- /test/parser/tzid_with_gmt.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "dtstart", 5 | { "tzid": "(GMT +01:00)" }, 6 | "date-time", 7 | "2011-10-28T16:00:00" 8 | ] 9 | ], 10 | [] 11 | ] 12 | -------------------------------------------------------------------------------- /test/parser/base64.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY;X-BASE 3 | 64-PARAM=UGFyYW1ldGVyCg=:WW91IHJlYWxseSBzcGVudCB0aGUgdGltZS 4 | B0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg= 5 | END:VCALENDAR 6 | -------------------------------------------------------------------------------- /test/parser/recur.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | RRULE:FREQ=MONTHLY;BYMONTH=1,3 3 | RRULE:FREQ=MONTHLY;BYMONTH=2 4 | RRULE:FREQ=DAILY;UNTIL=20121011 5 | RRULE:FREQ=DAILY;UNTIL=20121011T121314 6 | RRULE:FREQ=DAILY;UNTIL=20121011T121314Z 7 | END:VCALENDAR 8 | -------------------------------------------------------------------------------- /test/parser/period.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "rdate", 5 | {}, 6 | "period", 7 | ["1996-04-03T02:00:00Z", "1996-04-03T04:00:00Z"], 8 | ["1996-04-04T01:00:00", "PT3H"] 9 | ] 10 | ], 11 | [] 12 | ] 13 | -------------------------------------------------------------------------------- /test/parser/quoted_params.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "x-foo", 5 | { 6 | "bar": ";hidden=value", 7 | "foo": "baz" 8 | }, 9 | "unknown", 10 | "realvalue yep" 11 | ] 12 | ], 13 | [] 14 | ] 15 | -------------------------------------------------------------------------------- /test/parser/component.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | BEGIN:VEVENT 3 | SUMMARY:foo \\n 4 | bar 5 | BEGIN:VALARM 6 | SUMMARY:escaped\, comma and\; semicolon\nnewline 7 | END:VALARM 8 | END:VEVENT 9 | BEGIN:VEVENT 10 | SUMMARY:another 11 | END:VEVENT 12 | END:VCALENDAR 13 | -------------------------------------------------------------------------------- /test/parser/float.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "x-float", 5 | {}, 6 | "float", 7 | 10.35 8 | ], 9 | [ 10 | "x-invalid-float", 11 | {}, 12 | "float", 13 | 0.0 14 | ] 15 | ], 16 | [] 17 | ] 18 | -------------------------------------------------------------------------------- /test/parser/integer.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "x-integer", 5 | {}, 6 | "integer", 7 | 105 8 | ], 9 | [ 10 | "x-invalid", 11 | {}, 12 | "integer", 13 | 0 14 | ] 15 | ], 16 | [] 17 | ] 18 | -------------------------------------------------------------------------------- /test/parser/multiple_root_components.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["vcalendar", 3 | [ ["dtstamp", {}, "date-time", "2014-04-01T01:01:01Z" ] ], 4 | [] 5 | ], 6 | ["vcalendar", 7 | [ ["dtstamp", {}, "date-time", "2014-04-01T02:02:02Z" ] ], 8 | [] 9 | ] 10 | ] 11 | -------------------------------------------------------------------------------- /test/parser/multivalue.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | CATEGORIES:foo,blue\, fish,woot 3 | GEO:10.10;10.05 4 | REQUEST-STATUS:3.1;Invalid property value;DTSTART:96-Apr-01 5 | RDATE;VALUE=DATE:20121001,20121002,20121003 6 | EXDATE:20120901T130000,20120905T130000 7 | END:VCALENDAR 8 | -------------------------------------------------------------------------------- /test/parser/time.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "x-time", 5 | {}, 6 | "time", 7 | "23:00:00" 8 | ], 9 | [ 10 | "x-timez", 11 | {}, 12 | "time", 13 | "23:00:00Z" 14 | ] 15 | 16 | ], 17 | [] 18 | ] 19 | -------------------------------------------------------------------------------- /test/parser/rfc.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | CALSCALE:GREGORIAN 3 | PRODID:-//Example Inc.//Example Calendar//EN 4 | VERSION:2.0 5 | BEGIN:VEVENT 6 | DTSTAMP:20080205T191224Z 7 | DTSTART;VALUE=DATE:20081006 8 | SUMMARY:Planning meeting 9 | UID:4088E990AD89CB3DBB484909 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /test/parser/unfold_properties.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "attendee", 5 | { 6 | "cn": "Person", 7 | "role": "REQ-PARTICIPANT", 8 | "partstat": "ACCEPTED", 9 | "rsvp": "TRUE" 10 | }, 11 | "cal-address", 12 | "mailto:person@foo.com" 13 | ] 14 | ], 15 | [] 16 | ] 17 | -------------------------------------------------------------------------------- /test/parser/boolean.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "x-true", 5 | {}, 6 | "boolean", 7 | true 8 | ], 9 | [ 10 | "x-false", 11 | {}, 12 | "boolean", 13 | false 14 | ], 15 | [ 16 | "x-maybe", 17 | {}, 18 | "boolean", 19 | false 20 | ] 21 | ], 22 | [] 23 | ] 24 | -------------------------------------------------------------------------------- /test/parser/base64.json: -------------------------------------------------------------------------------- 1 | [ 2 | "vcalendar", 3 | [ 4 | [ 5 | "attach", 6 | { 7 | "fmttype": "text/plain", 8 | "encoding": "BASE64", 9 | "x-base64-param": "UGFyYW1ldGVyCg=" 10 | }, 11 | "binary", 12 | "WW91IHJlYWxseSBzcGVudCB0aGUgdGltZSB0byBiYXNlNjQgZGVjb2RlIHRoaXM/Cg=" 13 | ] 14 | ], 15 | [] 16 | ] 17 | -------------------------------------------------------------------------------- /samples/timezones/America/Atikokan.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2012h//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:America/Atikokan 6 | X-LIC-LOCATION:America/Atikokan 7 | BEGIN:STANDARD 8 | TZOFFSETFROM:-0500 9 | TZOFFSETTO:-0500 10 | TZNAME:EST 11 | DTSTART:19700101T000000 12 | END:STANDARD 13 | END:VTIMEZONE 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /test/acceptance/blank_description_test.js: -------------------------------------------------------------------------------- 1 | testSupport.requireICAL(); 2 | 3 | suite('ics - blank description', function() { 4 | var icsData; 5 | 6 | testSupport.defineSample('blank_description.ics', function(data) { 7 | icsData = data; 8 | }); 9 | 10 | test('summary', function() { 11 | // just verify it can parse blank lines 12 | var result = ICAL.parse(icsData); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/parser/component.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [], 3 | [ 4 | [ 5 | "vevent", 6 | [["summary", {}, "text", "foo \\nbar"]], 7 | [ 8 | [ 9 | "valarm", 10 | [["summary", {}, "text", "escaped, comma and; semicolon\nnewline"]], 11 | [] 12 | ] 13 | ] 14 | ], 15 | [ 16 | "vevent", 17 | [["summary", {}, "text", "another"]], 18 | [] 19 | ] 20 | ] 21 | ] 22 | -------------------------------------------------------------------------------- /test/performance/parse_test.js: -------------------------------------------------------------------------------- 1 | perfCompareSuite('ICAL parse/stringify', function(perf, ICAL) { 2 | 3 | var icsData; 4 | var parsed; 5 | testSupport.defineSample('parserv2.ics', function(data) { 6 | icsData = data; 7 | parsed = ICAL.parse(icsData); 8 | }); 9 | 10 | perf.test('#parse', function() { 11 | var data = ICAL.parse(icsData); 12 | }); 13 | 14 | perf.test('#stringify', function() { 15 | ICAL.stringify(parsed); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/parser/rfc.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | ["calscale", {}, "text", "GREGORIAN"], 4 | ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], 5 | ["version", {}, "text", "2.0"] 6 | ], 7 | [ 8 | ["vevent", 9 | [ 10 | ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], 11 | ["dtstart", {}, "date", "2008-10-06"], 12 | ["summary", {}, "text", "Planning meeting"], 13 | ["uid", {}, "text", "4088E990AD89CB3DBB484909"] 14 | ], 15 | [] 16 | ] 17 | ] 18 | ] 19 | -------------------------------------------------------------------------------- /tools/ICALTester/rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "freq": "DAILY" }, 3 | { "freq": "DAILY", "byday": ["MO", "TU", "WE", "TH", "FR"] }, 4 | { "freq": "WEEKLY" }, 5 | { "freq": "WEEKLY", "interval": 2 }, 6 | { "freq": "MONTHLY" }, 7 | { "freq": "YEARLY" }, 8 | 9 | { "freq": "WEEKLY", "byday": "%" }, 10 | 11 | { "freq": "MONTHLY", "bymonthday": "%" }, 12 | { "freq": "MONTHLY", "byday": "%" }, 13 | 14 | { "freq": "YEARLY", "bymonthday": "%", "bymonth": "%" }, 15 | { "freq": "YEARLY", "byday": "%", "bymonth": "%" } 16 | ] 17 | -------------------------------------------------------------------------------- /samples/timezones/America/New_York.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2012h//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:America/New_York 6 | X-LIC-LOCATION:America/New_York 7 | BEGIN:DAYLIGHT 8 | TZOFFSETFROM:-0500 9 | TZOFFSETTO:-0400 10 | TZNAME:EDT 11 | DTSTART:19700308T020000 12 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 13 | END:DAYLIGHT 14 | BEGIN:STANDARD 15 | TZOFFSETFROM:-0400 16 | TZOFFSETTO:-0500 17 | TZNAME:EST 18 | DTSTART:19701101T020000 19 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 20 | END:STANDARD 21 | END:VTIMEZONE 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /test/parser/dates.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "dtstart", 5 | {}, 6 | "date-time", 7 | "2012-09-01T13:00:00Z" 8 | ], 9 | [ 10 | "dtend", 11 | {}, 12 | "date", 13 | "2012-09-01" 14 | ], 15 | [ 16 | "rdate", 17 | {}, 18 | "date", 19 | "2013-12-10" 20 | ], 21 | [ 22 | "x-mydate", 23 | {}, 24 | "date", 25 | "2012-09-01" 26 | ], 27 | [ 28 | "x-mydatetime", 29 | {}, 30 | "date-time", 31 | "2012-09-01T13:00:00" 32 | ] 33 | ], 34 | [] 35 | ] 36 | -------------------------------------------------------------------------------- /samples/timezones/America/Los_Angeles.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2012h//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:America/Los_Angeles 6 | X-LIC-LOCATION:America/Los_Angeles 7 | BEGIN:DAYLIGHT 8 | TZOFFSETFROM:-0800 9 | TZOFFSETTO:-0700 10 | TZNAME:PDT 11 | DTSTART:19700308T020000 12 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 13 | END:DAYLIGHT 14 | BEGIN:STANDARD 15 | TZOFFSETFROM:-0700 16 | TZOFFSETTO:-0800 17 | TZNAME:PST 18 | DTSTART:19701101T020000 19 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 20 | END:STANDARD 21 | END:VTIMEZONE 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /test/acceptance/forced_types_test.js: -------------------------------------------------------------------------------- 1 | testSupport.requireICAL(); 2 | 3 | suite('ics test', function() { 4 | var icsData; 5 | 6 | testSupport.defineSample('forced_types.ics', function(data) { 7 | icsData = data; 8 | }); 9 | 10 | test('force type', function() { 11 | // just verify it can parse forced types 12 | var result = ICAL.parse(icsData); 13 | var component = new ICAL.Component(result); 14 | var vevent = component.getFirstSubcomponent( 15 | 'vevent' 16 | ); 17 | 18 | var start = vevent.getFirstPropertyValue('dtstart'); 19 | 20 | assert.isTrue(start.isDate, 'is date type'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /samples/timezones/Makebelieve/RRULE_UNTIL_test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//ical.js//NONSGML Makebelieve//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Makebelieve/RRULE_UNTIL 6 | X-LIC-LOCATION:Makebelieve/RRULE_UNTIL 7 | BEGIN:STANDARD 8 | TZOFFSETFROM:-0400 9 | TZOFFSETTO:-0500 10 | TZNAME:RRULE_UNTIL_standard 11 | DTSTART:19700101T020000Z 12 | RRULE:FREQ=YEARLY;INTERVAL=5;UNTIL=19800101T020000Z 13 | END:STANDARD 14 | BEGIN:DAYLIGHT 15 | TZOFFSETFROM:-0500 16 | TZOFFSETTO:-0400 17 | TZNAME:RDATE_UNTIL_daylight 18 | DTSTART:19750101T020000 19 | RRULE:FREQ=YEARLY;INTERVAL=5;UNTIL=19850101T020000Z 20 | END:DAYLIGHT 21 | END:VTIMEZONE 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /test/binary_test.js: -------------------------------------------------------------------------------- 1 | suite('ICAL.Binary', function() { 2 | var subject; 3 | 4 | setup(function() { 5 | subject = new ICAL.Binary(); 6 | }); 7 | 8 | test('setEncodedValue', function() { 9 | subject.setEncodedValue('bananas'); 10 | assert.equal(subject.decodeValue(), 'bananas'); 11 | assert.equal(subject.value, 'YmFuYW5hcw=='); 12 | 13 | subject.setEncodedValue('apples'); 14 | assert.equal(subject.decodeValue(), 'apples'); 15 | assert.equal(subject.value, 'YXBwbGVz'); 16 | }); 17 | 18 | test('null values', function() { 19 | subject.setEncodedValue(null); 20 | assert.equal(subject.decodeValue(), null); 21 | assert.equal(subject.value, null); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /samples/utc_negative_zero.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Zimbra-Calendar-Provider 4 | BEGIN:VTIMEZONE 5 | TZID:Etc/GMT 6 | BEGIN:STANDARD 7 | DTSTART:19710101T000000 8 | TZOFFSETTO:-0000 9 | TZOFFSETFROM:-0000 10 | TZNAME:GMT 11 | END:STANDARD 12 | END:VTIMEZONE 13 | BEGIN:VEVENT 14 | UID:d118e997-3683-4552-8fe8-57c641f1f179 15 | SUMMARY:And another 16 | ORGANIZER;CN=Sahaja Lal:mailto:calmozilla1@yahoo.com 17 | DTSTART;TZID=Etc/GMT:20120821T210000 18 | DTEND;TZID=Etc/GMT:20120821T213000 19 | STATUS:CONFIRMED 20 | CLASS:PUBLIC 21 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 22 | TRANSP:OPAQUE 23 | X-MICROSOFT-DISALLOW-COUNTER:TRUE 24 | DTSTAMP:20120817T032509Z 25 | SEQUENCE:0 26 | END:VEVENT 27 | END:VCALENDAR 28 | -------------------------------------------------------------------------------- /test/parser/recur.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "rrule", 5 | {}, 6 | "recur", 7 | { "freq": "MONTHLY", "bymonth": [1,3] } 8 | ], 9 | [ 10 | "rrule", 11 | {}, 12 | "recur", 13 | { "freq": "MONTHLY", "bymonth": 2 } 14 | ], 15 | [ 16 | "rrule", 17 | {}, 18 | "recur", 19 | { "freq": "DAILY", "until": "2012-10-11" } 20 | ], 21 | [ 22 | "rrule", 23 | {}, 24 | "recur", 25 | { "freq": "DAILY", "until": "2012-10-11T12:13:14" } 26 | ], 27 | [ 28 | "rrule", 29 | {}, 30 | "recur", 31 | { "freq": "DAILY", "until": "2012-10-11T12:13:14Z" } 32 | ] 33 | ], 34 | [] 35 | ] 36 | -------------------------------------------------------------------------------- /test/parser/vcard_author.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:4.0 3 | FN:Simon Perreault 4 | N:Perreault;Simon;;;ing. jr,M.Sc. 5 | BDAY:--0203 6 | ANNIVERSARY:20090808T1430-0500 7 | GENDER:M 8 | LANG;PREF=1:fr 9 | LANG;PREF=2:en 10 | ORG;TYPE=work:Viagenie 11 | ADR;TYPE=work:;Suite D2-630;2875 Laurier; 12 | Quebec;QC;G1V 2M2;Canada 13 | TEL;VALUE=uri;TYPE="work,voice";PREF=1:tel:+1-418-656-9254;ext=102 14 | TEL;VALUE=uri;TYPE="work,cell,voice,video,text":tel:+1-418-262-6501 15 | EMAIL;TYPE=work:simon.perreault@viagenie.ca 16 | GEO;TYPE=work:geo:46.772673\,-71.282945 17 | KEY;TYPE=work;VALUE=uri: 18 | http://www.viagenie.ca/simon.perreault/simon.asc 19 | TZ;VALUE=utc-offset:-0500 20 | URL;TYPE=home:http://nomis80.org 21 | END:VCARD 22 | -------------------------------------------------------------------------------- /samples/timezones/Makebelieve/RDATE_utc_test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//ical.js//NONSGML Makebelieve//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Makebelieve/RDATE_as_date_utc 6 | X-LIC-LOCATION:Makebelieve/RDATE_as_date_utc 7 | BEGIN:STANDARD 8 | TZOFFSETFROM:-0400 9 | TZOFFSETTO:-0500 10 | TZNAME:RDATE_as_date_utc_standard 11 | DTSTART:19700101T020000Z 12 | RDATE:19700101T020000 13 | RDATE;VALUE=DATE:19800101 14 | RDATE:19900101T070000Z 15 | END:STANDARD 16 | BEGIN:DAYLIGHT 17 | TZOFFSETFROM:-0500 18 | TZOFFSETTO:-0400 19 | TZNAME:RDATE_as_date_utc_daylight 20 | DTSTART:19750101T020000Z 21 | RDATE:19750101T020000 22 | RDATE;VALUE=DATE:19850101 23 | RDATE:19950101T070000Z 24 | END:DAYLIGHT 25 | END:VTIMEZONE 26 | END:VCALENDAR 27 | -------------------------------------------------------------------------------- /samples/timezones/Makebelieve/RDATE_test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//ical.js//NONSGML Makebelieve//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Makebelieve/RDATE_as_date 6 | X-LIC-LOCATION:Makebelieve/RDATE_as_date 7 | BEGIN:STANDARD 8 | TZOFFSETFROM:-0400 9 | TZOFFSETTO:-0500 10 | TZNAME:RDATE_as_date_standard 11 | DTSTART:19700101T020000 12 | RDATE:19700101T020000 13 | RDATE;VALUE=DATE:19800101 14 | RDATE:19900101T070000Z 15 | END:STANDARD 16 | BEGIN:DAYLIGHT 17 | TZOFFSETFROM:-0500 18 | TZOFFSETTO:-0400 19 | TZNAME:RDATE_as_date_daylight 20 | DTSTART:19750101T020000 21 | RDATE:19750101T020000 22 | RDATE;VALUE=DATE:19850101 23 | RDATE:19950101T070000Z 24 | END:DAYLIGHT 25 | END:VTIMEZONE 26 | END:VCALENDAR 27 | -------------------------------------------------------------------------------- /test/parser/multivalue.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "categories", 5 | {}, 6 | "text", 7 | "foo", 8 | "blue, fish", 9 | "woot" 10 | ], 11 | [ 12 | "geo", 13 | {}, 14 | "float", 15 | [10.10, 10.05] 16 | ], 17 | [ 18 | "request-status", 19 | {}, 20 | "text", 21 | ["3.1", "Invalid property value", "DTSTART:96-Apr-01"] 22 | ], 23 | [ 24 | "rdate", 25 | {}, 26 | "date", 27 | "2012-10-01", 28 | "2012-10-02", 29 | "2012-10-03" 30 | ], 31 | [ 32 | "exdate", 33 | {}, 34 | "date-time", 35 | "2012-09-01T13:00:00", 36 | "2012-09-05T13:00:00" 37 | ] 38 | ], 39 | [] 40 | ] 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /test/performance/iterator_test.js: -------------------------------------------------------------------------------- 1 | perfCompareSuite('iterator', function(perf, ICAL) { 2 | 3 | var icsData; 4 | 5 | testSupport.defineSample('parserv2.ics', function(data) { 6 | icsData = data; 7 | }); 8 | 9 | var parsed; 10 | var comp; 11 | var tz; 12 | var std; 13 | var rrule; 14 | 15 | suiteSetup(function() { 16 | parsed = ICAL.parse(icsData); 17 | comp = new ICAL.Component(parsed); 18 | tz = comp.getFirstSubcomponent('vtimezone'); 19 | std = tz.getFirstSubcomponent('standard'); 20 | rrule = std.getFirstPropertyValue('rrule'); 21 | }); 22 | 23 | perf.test('timezone iterator & first iteration', function() { 24 | var iterator = rrule.iterator(std.getFirstPropertyValue('dtstart')); 25 | iterator.next(); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/acceptance/utc_negative_zero_test.js: -------------------------------------------------------------------------------- 1 | testSupport.requireICAL(); 2 | 3 | suite('ics - negative zero', function() { 4 | var icsData; 5 | 6 | testSupport.defineSample('utc_negative_zero.ics', function(data) { 7 | icsData = data; 8 | }); 9 | 10 | test('summary', function() { 11 | var result = ICAL.parse(icsData); 12 | var component = new ICAL.Component(result); 13 | var vtimezone = component.getFirstSubcomponent( 14 | 'vtimezone' 15 | ); 16 | 17 | var standard = vtimezone.getFirstSubcomponent( 18 | 'standard' 19 | ); 20 | 21 | var props = standard.getAllProperties(); 22 | var offset = props[1].getFirstValue(); 23 | 24 | assert.equal( 25 | offset.factor, 26 | -1, 27 | 'offset' 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/parser/property_params.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | ATTENDEE;DELEGATED-TO="mailto:foo7@bar","mailto:foo8@bar";CN="Foo, Bar":mai 3 | lto:foo1@bar 4 | ATTENDEE;DELEGATED-TO="mailto:foo7@bar","mailto:foo8@bar";CN="Foo; Bar":mai 5 | lto:foo2@bar 6 | ATTENDEE;CN="Foo, Bar":mailto:foo3@bar 7 | ATTENDEE;CN="Foo; Bar":mailto:foo4@bar 8 | ATTENDEE;DELEGATED-TO="mailto:foo7@bar";CN="Foo, Bar":mailto:foo5@bar 9 | ATTENDEE;DELEGATED-TO="mailto:foo7@bar";CN="Foo; Bar":mailto:foo6@bar 10 | ATTENDEE;ROLE="REQ-PARTICIPANT;foo";DELEGATED-FROM="mailto:bar@baz.com";PAR 11 | TSTAT=ACCEPTED;RSVP=TRUE:mailto:foo@bar.com 12 | X-FOO;PARAM1=VAL1:FOO;BAR 13 | X-FOO2;PARAM1=VAL1;PARAM2=VAL2:FOO;BAR 14 | X-BAR;PARAM1="VAL1:FOO":BAZ;BAR 15 | X-BAZ;PARAM1="VAL1:FOO";PARAM2=VAL2:BAZ;BAR 16 | X-BAZ2;PARAM1=VAL1;PARAM2="VAL2:FOO":BAZ;BAR 17 | END:VCALENDAR 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ical.js", 3 | "version": "1.3.0", 4 | "homepage": "https://github.com/mozilla-comm/ical.js", 5 | "authors": [ 6 | "Philipp Kewisch ", 7 | "Github Contributors (https://github.com/mozilla-comm/ical.js/graphs/contributors)" 8 | ], 9 | "description": "Javascript parser for ics (rfc5545) and vcard (rfc6350) data", 10 | "main": "build/ical.js", 11 | "moduleType": [ 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "calendar", 17 | "iCalendar", 18 | "jCal", 19 | "vCard", 20 | "jCard", 21 | "parser" 22 | ], 23 | "license": "MPL-2.0", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tools", 30 | "zoneinfo", 31 | "coverage", 32 | "junk" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "10" 5 | - "8" 6 | before_install: 7 | - yarn global add grunt-cli 8 | - yarn global add karma-cli 9 | - echo -e "Host *\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 10 | - eval `ssh-agent -s` 11 | - '[ -n "$GITHUB_SSH_KEY" ] && echo "$GITHUB_SSH_KEY" | base64 -d | ssh-add - || true' 12 | install: yarn --frozen-lockfile 13 | script: grunt test-ci -v 14 | sudo: false 15 | env: 16 | global: 17 | - secure: MUB0jpyCJiYqrXQS6cwtLv1jyaDR30naRyHDBu+tLoyZNBTRWvlwrW75SvKEVwFI0JWqgUKRcwixsQFKRnKhZe+nD5zC56hxTtZc7Zw7m+mwpLHFJg5tfw8OAimd1Bvku9g0mig2kvjipxSsOMQsqXC7WfqESrTD2NMVdymp/ik= 18 | - secure: 0nJbWUF7NqGrDzX2de7SUx0YyHd0n2IXNmKIYRktEQiKe1omqeXi5SfIMzCHSmUGX3Ez2F2L5cVDbwob3pydFcK7KxpHp4JhW71JXis1sFvvaNR9lRglL7IFYRsqZz25esJlzS44B+Qr3mQCZLz77liDSX3mnmLmy14XECgB+DE= 19 | -------------------------------------------------------------------------------- /test/acceptance/daily_recuring_test.js: -------------------------------------------------------------------------------- 1 | testSupport.requireICAL(); 2 | 3 | suite('ics - blank description', function() { 4 | var icsData; 5 | 6 | testSupport.defineSample('daily_recur.ics', function(data) { 7 | icsData = data; 8 | }); 9 | 10 | test('summary', function() { 11 | // just verify it can parse blank lines 12 | var result = ICAL.parse(icsData); 13 | var component = new ICAL.Component(result); 14 | var vevent = component.getFirstSubcomponent( 15 | 'vevent' 16 | ); 17 | 18 | var recur = vevent.getFirstPropertyValue( 19 | 'rrule' 20 | ); 21 | 22 | var start = vevent.getFirstPropertyValue( 23 | 'dtstart' 24 | ); 25 | 26 | var iter = recur.iterator(start); 27 | var limit = 10; 28 | while (limit) { 29 | iter.next(); 30 | limit--; 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tools/ICALTester/Makefile: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # Portions Copyright (C) Philipp Kewisch, 2015 5 | 6 | LIBICAL_BASE ?= $(CURDIR)/../libical 7 | LIBICAL_BUILD = $(LIBICAL_BASE)/build 8 | LDFLAGS = -L $(LIBICAL_BUILD)/lib -lical 9 | CFLAGS = -I $(LIBICAL_BUILD)/src -g 10 | 11 | LIBICAL_SOURCES = support/libical-recur.c 12 | LIBICAL_PROG = support/libical-recur 13 | 14 | all: libical 15 | 16 | libical: $(LIBICAL_BUILD) 17 | $(CC) -o $(LIBICAL_PROG) $(LIBICAL_SOURCES) $(LDFLAGS) $(CFLAGS) 18 | 19 | clean: 20 | $(RM) -r $(LIBICAL_BASE) 21 | 22 | run: 23 | node index.js 24 | 25 | $(LIBICAL_BUILD): 26 | cd $(dir $(LIBICAL_BASE)) && git clone https://github.com/libical/libical 27 | mkdir $(LIBICAL_BUILD) 28 | cd $(LIBICAL_BUILD) && cmake .. && make 29 | -------------------------------------------------------------------------------- /samples/timezones/America/Denver.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//custom/thing 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:America/Denver 6 | BEGIN:DAYLIGHT 7 | TZOFFSETFROM:-0700 8 | TZOFFSETTO:-0600 9 | TZNAME:MDT 10 | DTSTART:20070311T020000 11 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 12 | END:DAYLIGHT 13 | BEGIN:STANDARD 14 | TZOFFSETFROM:-0600 15 | TZOFFSETTO:-0700 16 | TZNAME:MST 17 | DTSTART:20071104T020000 18 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 19 | END:STANDARD 20 | BEGIN:DAYLIGHT 21 | TZOFFSETFROM:-0700 22 | TZOFFSETTO:-0600 23 | TZNAME:MDT 24 | DTSTART:19180331T020000 25 | RDATE:20030406T020000 26 | RDATE:20040404T020000 27 | RDATE:20050403T020000 28 | RDATE:20060402T020000 29 | END:DAYLIGHT 30 | BEGIN:STANDARD 31 | TZOFFSETFROM:-0600 32 | TZOFFSETTO:-0700 33 | TZNAME:MST 34 | DTSTART:19181027T020000 35 | RDATE:20031026T020000 36 | RDATE:20041031T020000 37 | RDATE:20051030T020000 38 | RDATE:20061029T020000 39 | END:STANDARD 40 | END:VTIMEZONE 41 | END:VCALENDAR 42 | -------------------------------------------------------------------------------- /test/support/factory.js: -------------------------------------------------------------------------------- 1 | if (typeof(testSupport) === 'undefined') { 2 | this.testSupport = {}; 3 | } 4 | 5 | testSupport.factory = { 6 | vcalComp: function() { 7 | return new ICAL.Component({ 8 | type: 'COMPONENT', 9 | name: 'VCALENDAR' 10 | }, null); 11 | }, 12 | 13 | veventComp: function() { 14 | return new ICAL.Component(this.vevent( 15 | this.propUUID() 16 | )); 17 | }, 18 | 19 | vevent: function(props) { 20 | if (typeof(props) === 'undefined') { 21 | props = []; 22 | } else if (!(props instanceof Array)) { 23 | props = [props]; 24 | } 25 | 26 | return { 27 | type: 'COMPONENT', 28 | name: 'VEVENT', 29 | value: props 30 | }; 31 | }, 32 | 33 | propUUID: function(uuid) { 34 | if (typeof(uuid) === 'undefined') { 35 | uuid = 'uuid-value'; 36 | } 37 | 38 | return { 39 | name: 'UID', 40 | value: [uuid], 41 | type: 'TEXT' 42 | }; 43 | } 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /samples/only_dtstart_date.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@example.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;VALUE=DATE:20120630 27 | DTSTAMP:20120724T212411Z 28 | UID:dn4vrfmfn5p05roahsopg57h48@example.com 29 | CREATED:20120724T212411Z 30 | DESCRIPTION: 31 | LAST-MODIFIED:20120724T212411Z 32 | LOCATION: 33 | SEQUENCE:0 34 | STATUS:CONFIRMED 35 | SUMMARY:Really long event name thing 36 | TRANSP:OPAQUE 37 | END:VEVENT 38 | END:VCALENDAR 39 | -------------------------------------------------------------------------------- /samples/only_dtstart_time.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@example.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;TZID=America/Los_Angeles:20120630T060000 27 | DTSTAMP:20120724T212411Z 28 | UID:dn4vrfmfn5p05roahsopg57h48@example.com 29 | CREATED:20120724T212411Z 30 | DESCRIPTION: 31 | LAST-MODIFIED:20120724T212411Z 32 | LOCATION: 33 | SEQUENCE:0 34 | STATUS:CONFIRMED 35 | SUMMARY:Really long event name thing 36 | TRANSP:OPAQUE 37 | END:VEVENT 38 | END:VCALENDAR -------------------------------------------------------------------------------- /samples/duration_instead_of_dtend.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@example.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;TZID=America/Los_Angeles:20120630T060000 27 | DURATION:P1D 28 | DTSTAMP:20120724T212411Z 29 | UID:dn4vrfmfn5p05roahsopg57h48@example.com 30 | CREATED:20120724T212411Z 31 | DESCRIPTION: 32 | LAST-MODIFIED:20120724T212411Z 33 | LOCATION: 34 | SEQUENCE:0 35 | STATUS:CONFIRMED 36 | SUMMARY:Really long event name thing 37 | TRANSP:OPAQUE 38 | END:VEVENT 39 | END:VCALENDAR 40 | -------------------------------------------------------------------------------- /samples/minimal.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@gmail.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;TZID=America/Los_Angeles:20120630T060000 27 | DTEND;TZID=America/Los_Angeles:20120630T070000 28 | DTSTAMP:20120724T212411Z 29 | UID:dn4vrfmfn5p05roahsopg57h48@google.com 30 | CREATED:20120724T212411Z 31 | DESCRIPTION: 32 | LAST-MODIFIED:20120724T212411Z 33 | LOCATION: 34 | SEQUENCE:0 35 | STATUS:CONFIRMED 36 | SUMMARY:Really long event name thing 37 | TRANSP:OPAQUE 38 | END:VEVENT 39 | END:VCALENDAR 40 | -------------------------------------------------------------------------------- /samples/parserv2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Zimbra-Calendar-Provider 4 | BEGIN:VTIMEZONE 5 | TZID:America/Los_Angeles 6 | BEGIN:STANDARD 7 | DTSTART:19710101T020000 8 | TZOFFSETTO:-0800 9 | TZOFFSETFROM:-0700 10 | RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 11 | TZNAME:PST 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:19710101T020000 15 | TZOFFSETTO:-0700 16 | TZOFFSETFROM:-0800 17 | RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 18 | TZNAME:PDT 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:44c10eaa-db0b-4223-8653-cf2b63f26326 23 | RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR 24 | SUMMARY:Calendar 25 | DESCRIPTION:desc 26 | ATTENDEE;CN=XXX;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRU 27 | E:mailto:foo@bar.com 28 | ATTENDEE;CN=XXXX;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TR 29 | UE:mailto:x@bar.com 30 | ORGANIZER;CN=foobar:mailto:x@bar.com 31 | DTSTART;TZID=America/Los_Angeles:20120911T103000 32 | DTEND;TZID=America/Los_Angeles:20120911T110000 33 | STATUS:CONFIRMED 34 | CLASS:PUBLIC 35 | TRANSP:OPAQUE 36 | LAST-MODIFIED:20120911T184851Z 37 | DTSTAMP:20120911T184851Z 38 | SEQUENCE:1 39 | BEGIN:VALARM 40 | ACTION:DISPLAY 41 | TRIGGER;RELATED=START:-PT5M 42 | DESCRIPTION:Reminder 43 | END:VALARM 44 | END:VEVENT 45 | END:VCALENDAR 46 | -------------------------------------------------------------------------------- /tools/ICALTester/support/libical-recur.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char *argv[]) { 11 | struct icalrecurrencetype recur; 12 | icalrecur_iterator *ritr; 13 | struct icaltimetype dtstart, next; 14 | int howmany = 1; 15 | 16 | if (argc < 4) { 17 | printf("Usage: libical-recur \n"); 18 | exit(1); 19 | } 20 | 21 | howmany = atoi(argv[3]); 22 | dtstart = icaltime_from_string(argv[2]); 23 | recur = icalrecurrencetype_from_string(argv[1]); 24 | ritr = icalrecur_iterator_new(recur, dtstart); 25 | 26 | if (ritr) { 27 | for (next = icalrecur_iterator_next(ritr); 28 | howmany > 0 && !icaltime_is_null_time(next); 29 | next = icalrecur_iterator_next(ritr), howmany--) { 30 | printf("%s\n", icaltime_as_ical_string_r(next)); 31 | } 32 | } else { 33 | printf("Error: %d\n", icalerrno); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/acceptance/google_birthday_test.js: -------------------------------------------------------------------------------- 1 | suite('google birthday events', function() { 2 | var icsData; 3 | 4 | testSupport.defineSample('google_birthday.ics', function(data) { 5 | icsData = data; 6 | }); 7 | 8 | test('expanding malformatted recurring event', function(done) { 9 | // just verify it can parse forced types 10 | var parser = new ICAL.ComponentParser(); 11 | var primary; 12 | var exceptions = []; 13 | 14 | var expectedDates = [ 15 | new Date(2012, 11, 10), 16 | new Date(2013, 11, 10), 17 | new Date(2014, 11, 10) 18 | ]; 19 | 20 | parser.onevent = function(event) { 21 | if (event.isRecurrenceException()) { 22 | exceptions.push(event); 23 | } else { 24 | primary = event; 25 | } 26 | }; 27 | 28 | parser.oncomplete = function() { 29 | exceptions.forEach(function(item) { 30 | primary.relateException(item); 31 | }); 32 | 33 | var iter = primary.iterator(); 34 | var next; 35 | var dates = []; 36 | while ((next = iter.next())) { 37 | dates.push(next.toJSDate()); 38 | } 39 | 40 | assert.deepEqual( 41 | dates, 42 | expectedDates 43 | ); 44 | 45 | done(); 46 | }; 47 | 48 | parser.process(icsData); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /samples/forced_types.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@gmail.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;VALUE=DATE:20120904 27 | DTEND;VALUE=DATE:20120905 28 | DTSTAMP:20120905T084734Z 29 | UID:redgrb1l0aju5edm6h0s102eu4@google.com 30 | CREATED:20120905T084734Z 31 | DESCRIPTION: 32 | LAST-MODIFIED:20120905T084734Z 33 | LOCATION: 34 | SEQUENCE:0 35 | STATUS:CONFIRMED 36 | SUMMARY:Event 37 | TRANSP:TRANSPARENT 38 | BEGIN:VALARM 39 | ACTION:EMAIL 40 | DESCRIPTION:This is an event reminder 41 | SUMMARY:Alarm notification 42 | ATTENDEE:mailto:calmozilla1@gmail.com 43 | TRIGGER;VALUE=DATE-TIME:20120903T233000Z 44 | END:VALARM 45 | BEGIN:VALARM 46 | ACTION:DISPLAY 47 | DESCRIPTION:This is an event reminder 48 | END:VALARM 49 | END:VEVENT 50 | END:VCALENDAR 51 | -------------------------------------------------------------------------------- /samples/multiple_rrules.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Zimbra-Calendar-Provider 4 | BEGIN:VTIMEZONE 5 | TZID:America/Los_Angeles 6 | BEGIN:STANDARD 7 | DTSTART:19710101T020000 8 | TZOFFSETTO:-0800 9 | TZOFFSETFROM:-0700 10 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU 11 | TZNAME:PST 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:19710101T020000 15 | TZOFFSETTO:-0700 16 | TZOFFSETFROM:-0800 17 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU 18 | TZNAME:PDT 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:1334F9B7-6136-444E-A58D-472564C6AA73 23 | RRULE:FREQ=WEEKLY;UNTIL=20120730T065959Z 24 | RRULE:FREQ=MONTHLY;BYDAY=SU;UNTIL=20120730T065959Z 25 | SUMMARY:sahaja <> frashed 26 | DESCRIPTION:weekly 1on1 27 | ATTENDEE;CN=James Lal;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS 28 | -ACTION;RSVP=TRUE:mailto:jlal@mozilla.com 29 | ORGANIZER;CN=Faramarz Rashed:mailto:frashed@mozilla.com 30 | DTSTART;TZID=America/Los_Angeles:20120326T110000 31 | DTEND;TZID=America/Los_Angeles:20120326T113000 32 | STATUS:CONFIRMED 33 | CLASS:PUBLIC 34 | TRANSP:OPAQUE 35 | LAST-MODIFIED:20120326T161522Z 36 | DTSTAMP:20120730T165637Z 37 | SEQUENCE:9 38 | BEGIN:VALARM 39 | ACTION:DISPLAY 40 | TRIGGER;RELATED=START:-PT5M 41 | DESCRIPTION:Reminder 42 | END:VALARM 43 | END:VEVENT 44 | END:VCALENDAR 45 | 46 | -------------------------------------------------------------------------------- /samples/blank_description.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@gmail.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;TZID=America/Los_Angeles:20120630T060000 27 | DTEND;TZID=America/Los_Angeles:20120630T070000 28 | DTSTAMP:20120724T212411Z 29 | UID:dn4vrfmfn5p05roahsopg57h48@google.com 30 | CREATED:20120724T212411Z 31 | DESCRIPTION: 32 | LAST-MODIFIED:20120724T212411Z 33 | LOCATION: 34 | SEQUENCE:0 35 | STATUS:CONFIRMED 36 | SUMMARY:Really long event name thing 37 | TRANSP:OPAQUE 38 | BEGIN:VALARM 39 | ACTION:EMAIL 40 | DESCRIPTION:This is an event reminder 41 | SUMMARY:Alarm notification 42 | ATTENDEE:mailto:calmozilla1@gmail.com 43 | TRIGGER:-P0DT0H30M0S 44 | END:VALARM 45 | BEGIN:VALARM 46 | ACTION:DISPLAY 47 | DESCRIPTION:This is an event reminder 48 | TRIGGER:-P0DT0H30M0S 49 | END:VALARM 50 | END:VEVENT 51 | END:VCALENDAR 52 | -------------------------------------------------------------------------------- /samples/daily_recur.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@gmail.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;TZID=America/Los_Angeles:20120801T050000 27 | DTEND;TZID=America/Los_Angeles:20120801T060000 28 | RRULE:FREQ=DAILY 29 | DTSTAMP:20120803T221236Z 30 | UID:tgh9qho17b07pk2n2ji3gluans@google.com 31 | CREATED:20120803T221236Z 32 | DESCRIPTION: 33 | LAST-MODIFIED:20120803T221236Z 34 | LOCATION: 35 | SEQUENCE:0 36 | STATUS:CONFIRMED 37 | SUMMARY:Every day recurring 38 | TRANSP:OPAQUE 39 | BEGIN:VALARM 40 | ACTION:EMAIL 41 | DESCRIPTION:This is an event reminder 42 | SUMMARY:Alarm notification 43 | ATTENDEE:mailto:calmozilla1@gmail.com 44 | TRIGGER:-P0DT0H30M0S 45 | END:VALARM 46 | BEGIN:VALARM 47 | ACTION:DISPLAY 48 | DESCRIPTION:This is an event reminder 49 | TRIGGER:-P0DT0H30M0S 50 | END:VALARM 51 | END:VEVENT 52 | END:VCALENDAR 53 | -------------------------------------------------------------------------------- /samples/day_long_recur_yearly.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:calmozilla1@gmail.com 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | BEGIN:VTIMEZONE 8 | TZID:America/Los_Angeles 9 | X-LIC-LOCATION:America/Los_Angeles 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0800 12 | TZOFFSETTO:-0700 13 | TZNAME:PDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0700 19 | TZOFFSETTO:-0800 20 | TZNAME:PST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART;VALUE=DATE:20120803 27 | DTEND;VALUE=DATE:20120804 28 | RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR 29 | DTSTAMP:20120803T221306Z 30 | UID:4pfh824gvims850j0gar361t04@google.com 31 | CREATED:20120803T221306Z 32 | DESCRIPTION: 33 | LAST-MODIFIED:20120803T221306Z 34 | LOCATION: 35 | SEQUENCE:0 36 | STATUS:CONFIRMED 37 | SUMMARY:Day Long Event 38 | TRANSP:TRANSPARENT 39 | BEGIN:VALARM 40 | ACTION:EMAIL 41 | DESCRIPTION:This is an event reminder 42 | SUMMARY:Alarm notification 43 | ATTENDEE:mailto:calmozilla1@gmail.com 44 | TRIGGER;VALUE=DATE-TIME:20120802T233000Z 45 | END:VALARM 46 | BEGIN:VALARM 47 | ACTION:DISPLAY 48 | DESCRIPTION:This is an event reminder 49 | TRIGGER;VALUE=DATE-TIME:20120802T233000Z 50 | END:VALARM 51 | END:VEVENT 52 | END:VCALENDAR 53 | -------------------------------------------------------------------------------- /test/parser/vcard.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:4.0 3 | ADR;TYPE=work:pobox;apt;street;city;state;zipcode;country 4 | ANNIVERSARY:19960415 5 | BDAY:--0203 6 | CALADRURI:http://example.com/calendar/jdoe 7 | CALURI;MEDIATYPE=text/calendar:ftp://ftp.example.com/calA.ics 8 | CLIENTPIDMAP:1;urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b 9 | EMAIL;TYPE=work:jqpublic@xyz.example.com 10 | FBURL;MEDIATYPE=text/calendar:ftp://example.com/busy/project-a.ifb 11 | FN:J. Doe 12 | GENDER:M;Fellow 13 | GEO:geo:37.386013\,-122.082932 14 | IMPP;PREF=1:xmpp:alice@example.com 15 | KEY:http://www.example.com/keys/jdoe.cer 16 | KIND:individual 17 | LANG;PREF=1:fr 18 | LOGO:http://www.example.com/pub/logos/abccorp.jpg 19 | MEMBER:urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af 20 | N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P. 21 | NICKNAME;TYPE=work:Boss 22 | NOTE:This fax number is operational 0800 to 1715 EST\, Mon-Fri 23 | ORG:ABC\, Inc.;North American Division;Marketing 24 | PHOTO:http://www.example.com/pub/photos/jqpublic.gif 25 | RELATED;TYPE=friend:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 26 | REV:19951031T222710Z 27 | ROLE:Project Leader 28 | SOUND:CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com 29 | SOURCE:ldap://ldap.example.com/cn=Babs%20Jensen\,%20o=Babsco\,%20c=US 30 | TEL;VALUE=uri;TYPE=home:tel:+33-01-23-45-67 31 | TITLE:Research Scientist 32 | TZ;VALUE=utc-offset:-0500 33 | XML: 34 | END:VCARD 35 | -------------------------------------------------------------------------------- /tools/ICALTester/README.md: -------------------------------------------------------------------------------- 1 | ICALTester 2 | ========== 3 | 4 | This is a simple program to compare various ICAL recurrence implementations 5 | with ICAL.js. It generates random rules based on the format defined in 6 | rules.json and runs them through the various libraries. 7 | 8 | Running 9 | ------- 10 | 11 | The usage goes as follows: 12 | 13 | ```bash 14 | $ node compare.js rules.json ./support/libical-recur 15 | ``` 16 | 17 | The first argument to compare.js is the path to the rules.json described 18 | further down. An example file is provided. The second argument is the path to a 19 | binary executed for comparison. The binary should be able to take arguments as 20 | in the following example and expects the same output: 21 | 22 | ```bash 23 | # Usage: ./support/libical-recur 24 | $ ./support/libical-recur "FREQ=MONTHLY;BYDAY=1FR,3SU" "2014-11-11T08:00:00" 10 25 | 20141116T080000 26 | 20141205T080000 27 | 20141221T080000 28 | 20150102T080000 29 | 20150118T080000 30 | 20150206T080000 31 | 20150215T080000 32 | 20150306T080000 33 | 20150315T080000 34 | 20150403T080000 35 | ``` 36 | 37 | The libical-recur binary can be built using the provided Makefile. 38 | 39 | rules.json 40 | ---------- 41 | 42 | The format is the same as what can be passed to ICAL.Recur.fromData(), with one 43 | addition. If the value is `%`, the tester generates a random rule value. 44 | 45 | Example: 46 | 47 | ```json 48 | [ 49 | { "freq": "MONTHLY", "bymonthday": "%" } 50 | ] 51 | ``` 52 | 53 | Possible Result: 54 | 55 | ``` 56 | RRULE:FREQ=MONTHLY;BYMONTHDAY=1,15,17,20,31 57 | ``` 58 | -------------------------------------------------------------------------------- /test/parser/vcard_author.json: -------------------------------------------------------------------------------- 1 | ["vcard", 2 | [ 3 | ["version", {}, "text", "4.0"], 4 | ["fn", {}, "text", "Simon Perreault"], 5 | ["n", 6 | {}, 7 | "text", 8 | ["Perreault", "Simon", "", "", ["ing. jr", "M.Sc."]] 9 | ], 10 | ["bday", {}, "date-and-or-time", "--02-03"], 11 | ["anniversary", 12 | {}, 13 | "date-and-or-time", 14 | "2009-08-08T14:30-05:00" 15 | ], 16 | ["gender", {}, "text", "M"], 17 | ["lang", { "pref": "1" }, "language-tag", "fr"], 18 | ["lang", { "pref": "2" }, "language-tag", "en"], 19 | ["org", { "type": "work" }, "text", "Viagenie"], 20 | ["adr", 21 | { "type": "work" }, 22 | "text", 23 | [ 24 | "", 25 | "Suite D2-630", 26 | "2875 Laurier", 27 | "Quebec", 28 | "QC", 29 | "G1V 2M2", 30 | "Canada" 31 | ] 32 | ], 33 | ["tel", 34 | { "type": ["work", "voice"], "pref": "1" }, 35 | "uri", 36 | "tel:+1-418-656-9254;ext=102" 37 | ], 38 | ["tel", 39 | { "type": ["work", "cell", "voice", "video", "text"] }, 40 | "uri", 41 | "tel:+1-418-262-6501" 42 | ], 43 | ["email", 44 | { "type": "work" }, 45 | "text", 46 | "simon.perreault@viagenie.ca" 47 | ], 48 | ["geo", { "type": "work" }, "uri", "geo:46.772673,-71.282945"], 49 | ["key", 50 | { "type": "work" }, 51 | "uri", 52 | "http://www.viagenie.ca/simon.perreault/simon.asc" 53 | ], 54 | ["tz", {}, "utc-offset", "-05:00"], 55 | ["url", { "type": "home" }, "uri", "http://nomis80.org"] 56 | ], 57 | [] 58 | ] 59 | -------------------------------------------------------------------------------- /samples/recur_instances_finite.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Zimbra-Calendar-Provider 4 | BEGIN:VTIMEZONE 5 | TZID:America/Los_Angeles 6 | BEGIN:STANDARD 7 | DTSTART:19710101T020000 8 | TZOFFSETTO:-0800 9 | TZOFFSETFROM:-0700 10 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU 11 | TZNAME:PST 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:19710101T020000 15 | TZOFFSETTO:-0700 16 | TZOFFSETFROM:-0800 17 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU 18 | TZNAME:PDT 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VEVENT 22 | UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868 23 | DESCRIPTION:IAM FOO 24 | RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU;UNTIL=20121231T100000 25 | SUMMARY:Crazy Event Thingy! 26 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sahaja 27 | Lal;X-NUM-GUESTS=0:mailto:calmozilla1@gmail.com 28 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ja 29 | mes@lightsofapollo.com;X-NUM-GUESTS=0:mailto:james@lightsofapollo.com 30 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ia 31 | m.revelation@gmail.com;X-NUM-GUESTS=0:mailto:iam.revelation@gmail.com 32 | LOCATION:PLACE 33 | ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com 34 | DTSTART;TZID=America/Los_Angeles:20121002T100000 35 | DTEND;TZID=America/Los_Angeles:20121002T103000 36 | STATUS:CONFIRMED 37 | CLASS:PUBLIC 38 | TRANSP:OPAQUE 39 | LAST-MODIFIED:20120912T171506Z 40 | DTSTAMP:20120912T171506Z 41 | SEQUENCE:0 42 | RDATE;TZID=America/Los_Angeles:20121110T100000 43 | RDATE;TZID=America/Los_Angeles:20121105T100000 44 | BEGIN:VALARM 45 | ACTION:DISPLAY 46 | TRIGGER;RELATED=START:-PT5M 47 | DESCRIPTION:Reminder 48 | END:VALARM 49 | END:VEVENT 50 | END:VCALENDAR 51 | -------------------------------------------------------------------------------- /tasks/timezones.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var path = require('path'); 5 | var child_process = require('child_process'); 6 | 7 | var OLSON_DB_REMOTE = 'http://www.iana.org/time-zones/repository/releases/tzdata%s.tar.gz'; 8 | var TZURL_DIR = process.env.TZURL_DIR || path.join(__dirname, '..', 'tools', 'tzurl') 9 | var OLSON_DIR = process.env.OLSON_DIR || path.join(TZURL_DIR, 'olson'); 10 | 11 | module.exports = function(grunt) { 12 | grunt.registerTask('timezones', 'Get Olson timezone data', function() { 13 | var olsonversion = grunt.option('olsondb'); 14 | if (!olsonversion) { 15 | olsonversion = (new Date()).getFullYear() + "a"; 16 | grunt.fail.warn('Need to specify --olsondb=, e.g. ' + olsonversion); 17 | return; 18 | } 19 | 20 | if (grunt.file.isDir(TZURL_DIR)) { 21 | grunt.log.ok('Using existing tzurl installation'); 22 | } else { 23 | grunt.log.ok('Retrieving tzurl from svn'); 24 | child_process.execSync('svn export -r40 http://tzurl.googlecode.com/svn/trunk/ ' + TZURL_DIR); 25 | } 26 | 27 | if (grunt.file.isDir(OLSON_DIR)) { 28 | grunt.log.ok('Using olson database from ' + OLSON_DIR); 29 | } else { 30 | var url = util.format(OLSON_DB_REMOTE, olsonversion); 31 | grunt.log.ok('Downloading ' + url); 32 | grunt.file.mkdir(OLSON_DIR); 33 | child_process.execSync('wget ' + url + ' -O - | tar xz -C ' + OLSON_DIR); 34 | } 35 | 36 | grunt.log.ok('Building tzurl tool'); 37 | child_process.execSync('make -C "' + TZURL_DIR + '" OLSON_DIR="' + OLSON_DIR + '"'); 38 | 39 | grunt.log.ok('Running vzic'); 40 | child_process.execSync(path.join(TZURL_DIR, 'vzic')); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /test/performance/time_test.js: -------------------------------------------------------------------------------- 1 | perfCompareSuite('ICAL.Time', function(perf, ICAL) { 2 | 3 | perf.test('subtract date', function() { 4 | var time = new ICAL.Time({ 5 | year: 2012, 6 | month: 1, 7 | day: 1, 8 | hour: 10, 9 | minute: 3 10 | }); 11 | 12 | var time2 = new ICAL.Time({ 13 | year: 2012, 14 | month: 10, 15 | day: 1, 16 | hour: 1, 17 | minute: 55 18 | }); 19 | 20 | time.subtractDate(time2); 21 | }); 22 | 23 | var dur = new ICAL.Duration({ 24 | days: 3, 25 | hour: 3, 26 | minutes: 3 27 | }); 28 | 29 | perf.test('add duration', function() { 30 | var time = new ICAL.Time({ 31 | year: 2012, 32 | month: 1, 33 | day: 32, 34 | seconds: 1 35 | }); 36 | 37 | time.addDuration(dur); 38 | 39 | // to trigger normalization 40 | time.year; 41 | }); 42 | 43 | perf.test('create and clone time', function() { 44 | var time = new ICAL.Time({ 45 | year: 2012, 46 | month: 1, 47 | day: 32, 48 | seconds: 1 49 | }); 50 | 51 | if (time.day !== 1) { 52 | throw new Error('test sanity fails for .day'); 53 | } 54 | 55 | if (time.month !== 2) { 56 | throw new Error('test sanity fails for .month'); 57 | } 58 | 59 | time.clone(); 60 | }); 61 | 62 | var _time = new ICAL.Time({ 63 | year: 2012, 64 | month: 1, 65 | day: 32, 66 | seconds: 1 67 | }); 68 | 69 | perf.test('toUnixTime', function() { 70 | _time.toUnixTime(); 71 | }); 72 | 73 | perf.test('dayOfWeek', function() { 74 | _time.dayOfWeek(); 75 | }); 76 | 77 | perf.test('weekNumber', function() { 78 | _time.weekNumber(); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /tasks/travis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.config.set("travis", { 6 | branch: process.env.TRAVIS_BRANCH, 7 | leader: (process.env.TRAVIS_JOB_NUMBER || "").substr(-2) == ".1", 8 | commit: process.env.TRAVIS_COMMIT, 9 | pullrequest: (process.env.TRAVIS_PULL_REQUEST || "false") == "false" ? null : process.env.TRAVIS_PULL_REQUEST, 10 | secure: process.env.TRAVIS_SECURE_ENV_VARS == "true", 11 | tag: process.env.TRAVIS_TAG 12 | }); 13 | 14 | function registerCITask(name, descr, cond) { 15 | grunt.registerTask(name, function(/* ...tasks */) { 16 | var task = Array.prototype.join.call(arguments, ":"); 17 | grunt.config.requires("travis"); 18 | var travis = grunt.config.get("travis"); 19 | 20 | if (cond(travis, task)) { 21 | grunt.task.run(task); 22 | } else { 23 | grunt.log.ok('Skipping ' + task + ', not on ' + descr); 24 | } 25 | }); 26 | } 27 | 28 | grunt.registerTask('run-with-env', function(/* env, ...tasks */) { 29 | var env = arguments[0]; 30 | var task = Array.prototype.slice.call(arguments, 1).join(":"); 31 | 32 | if (process.env[env]) { 33 | grunt.task.run(task); 34 | } else { 35 | grunt.fail.warn('Cannot run ' + task + ', environment ' + env + ' not available'); 36 | } 37 | }); 38 | 39 | registerCITask('run-on-master-leader', 'branch master leader', function(travis) { 40 | return travis.branch == 'master' && !travis.pullrequest && 41 | travis.secure && travis.leader; 42 | }); 43 | 44 | registerCITask('run-on-leader', 'build leader', function(travis) { 45 | return travis.leader; 46 | }); 47 | 48 | registerCITask('run-on-pullrequest', 'pull request', function(travis) { 49 | return travis.pullrequest; 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /test/performance/rrule_test.js: -------------------------------------------------------------------------------- 1 | perfCompareSuite('rrule', function(perf, ICAL) { 2 | 3 | var start; 4 | var occurrences; 5 | 6 | suiteSetup(function() { 7 | start = ICAL.Time.fromString("2015-01-01T12:00:00"); 8 | occurrences = 50; 9 | }); 10 | 11 | 12 | // These are common rules that can be created in the UI of various clients. 13 | // At the moment we will just check getting 50 occurrences with INTERVAL=1. 14 | // Checking COUNT, UNTIL and a higher INTERVAL could be applied to any rule, 15 | // which would be quite a lot of combinations. Therefore those rules are just 16 | // checked once. 17 | [ 18 | // COUNT, UNTIL and INTERVAL 19 | "FREQ=DAILY;COUNT=50", 20 | "FREQ=DAILY;UNTIL=2015-02-20T12:00:00", 21 | "FREQ=DAILY;INTERVAL=7", 22 | 23 | // Lightning rules 24 | "FREQ=DAILY", 25 | 26 | "FREQ=WEEKLY", 27 | "FREQ=WEEKLY;BYDAY=MO,WE,FR", 28 | 29 | "FREQ=MONTHLY", 30 | "FREQ=MONTHLY;BYMONTHDAY=1,15,31", 31 | "FREQ=MONTHLY;BYMONTHDAY=-1", 32 | "FREQ=MONTHLY;BYDAY=FR", 33 | "FREQ=MONTHLY;BYDAY=-1SU", 34 | "FREQ=MONTHLY;BYDAY=3MO", 35 | 36 | "FREQ=YEARLY", 37 | "FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=11", 38 | "FREQ=YEARLY;BYDAY=4TH;BYMONTH=11", 39 | 40 | "FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,SA,SU;BYMONTH=11", 41 | "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=11", 42 | "FREQ=YEARLY;BYDAY=4TH;BYMONTH=11", 43 | 44 | // Apple iCal rules 45 | "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=3", 46 | "FREQ=MONTHLY;BYDAY=SA,SU;BYMONTH=11;BYSETPOS=-1" 47 | 48 | ].forEach(function(rulestring) { 49 | perf.test(rulestring, function() { 50 | var rrule = ICAL.Recur.fromString(rulestring); 51 | var iter = rrule.iterator(start); 52 | for (var i = 0; i < occurrences; i++) { 53 | iter.next(); 54 | } 55 | }); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.3.0] - 2018-11-09 6 | 7 | ## [1.2.2] - 2016-07-20 8 | 9 | ## [1.2.1] - 2016-04-14 10 | 11 | ## [1.2.0] - 2016-04-13 12 | 13 | ## [1.1.2] - 2015-07-07 14 | 15 | ## [1.1.1] - 2015-06-01 16 | 17 | ## [1.1.0] - 2015-06-01 18 | 19 | ## [1.0.4] - 2015-05-23 20 | 21 | ## [1.0.3] - 2015-05-22 22 | 23 | ## [1.0.2] - 2015-05-22 24 | 25 | ## [1.0.1] - 2015-05-22 26 | 27 | ## [1.0.0] - 2015-05-22 28 | 29 | ## [0.0.6] - 2014-09-08 30 | 31 | ## [0.0.5] - 2014-09-03 32 | 33 | ## [0.0.4] - 2014-09-03 34 | 35 | ## [0.0.3] - 2014-09-03 36 | 37 | ## 0.0.1 - 2014-05-23 38 | 39 | [Unreleased]: https://github.com/mozilla-comm/ical.js/compare/v1.3.0...HEAD 40 | [1.3.0]: https://github.com/mozilla-comm/ical.js/compare/v1.2.2...v1.3.0 41 | [1.2.2]: https://github.com/mozilla-comm/ical.js/compare/v1.2.1...v1.2.2 42 | [1.2.1]: https://github.com/mozilla-comm/ical.js/compare/v1.2.0...v1.2.1 43 | [1.2.0]: https://github.com/mozilla-comm/ical.js/compare/v1.1.2...v1.2.0 44 | [1.1.2]: https://github.com/mozilla-comm/ical.js/compare/v1.1.1...v1.1.2 45 | [1.1.1]: https://github.com/mozilla-comm/ical.js/compare/v1.1.0...v1.1.1 46 | [1.1.0]: https://github.com/mozilla-comm/ical.js/compare/v1.0.4...v1.1.0 47 | [1.0.4]: https://github.com/mozilla-comm/ical.js/compare/v1.0.3...v1.0.4 48 | [1.0.3]: https://github.com/mozilla-comm/ical.js/compare/v1.0.2...v1.0.3 49 | [1.0.2]: https://github.com/mozilla-comm/ical.js/compare/v1.0.1...v1.0.2 50 | [1.0.1]: https://github.com/mozilla-comm/ical.js/compare/v1.0.0...v1.0.1 51 | [1.0.0]: https://github.com/mozilla-comm/ical.js/compare/v0.0.6...v1.0.0 52 | [0.0.6]: https://github.com/mozilla-comm/ical.js/compare/v0.0.5...v0.0.6 53 | [0.0.5]: https://github.com/mozilla-comm/ical.js/compare/v0.0.4...v0.0.5 54 | [0.0.4]: https://github.com/mozilla-comm/ical.js/compare/v0.0.3...v0.0.4 55 | [0.0.3]: https://github.com/mozilla-comm/ical.js/compare/v0.0.1...v0.0.3 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ical.js", 3 | "version": "1.3.0", 4 | "author": "Philipp Kewisch", 5 | "contributors": [ 6 | "Github Contributors (https://github.com/mozilla-comm/ical.js/graphs/contributors)" 7 | ], 8 | "description": "Javascript parser for ics (rfc5545) and vcard (rfc6350) data", 9 | "main": "build/ical.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mozilla-comm/ical.js.git" 13 | }, 14 | "keywords": [ 15 | "calendar", 16 | "iCalendar", 17 | "jCal", 18 | "vCard", 19 | "jCard", 20 | "parser" 21 | ], 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "benchmark": "^2.1.4", 25 | "biased-opener": "^0.2.8", 26 | "chai": "^4.2.0", 27 | "coveralls": "^3.0.3", 28 | "grunt": "^1.0.4", 29 | "grunt-concurrent": "^3.0.0", 30 | "grunt-contrib-concat": "^1.0.1", 31 | "grunt-contrib-uglify": "^4.0.1", 32 | "grunt-coveralls": "^2.0.0", 33 | "grunt-gh-pages": "^3.1.0", 34 | "grunt-jsdoc": "^2.4.0", 35 | "grunt-karma": "^3.0.2", 36 | "grunt-mocha-cli": "^5.0.0", 37 | "grunt-mocha-istanbul": "^5.0.2", 38 | "grunt-release": "^0.14.0", 39 | "gruntify-eslint": "^5.0.0", 40 | "istanbul": "^0.4.5", 41 | "karma": "^4.1.0", 42 | "karma-chai": "^0.1.0", 43 | "karma-mocha": "^1.3.0", 44 | "karma-sauce-launcher": "^2.0.2", 45 | "karma-spec-reporter": "^0.0.32", 46 | "minami": "^1.2.3", 47 | "mocha": "^7.0.1" 48 | }, 49 | "resolutions": { 50 | "karma-sauce-launcher/selenium-webdriver": "4.0.0-alpha.1" 51 | }, 52 | "license": "MPL-2.0", 53 | "engine": { 54 | "node": ">=8" 55 | }, 56 | "scripts": { 57 | "test": "grunt test-node" 58 | }, 59 | "files": [ 60 | "build/ical.js", 61 | "build/ical.min.js", 62 | "lib/ical/*.js" 63 | ], 64 | "saucelabs": { 65 | "SL_Chrome": { 66 | "base": "SauceLabs", 67 | "browserName": "chrome" 68 | }, 69 | "SL_Firefox": { 70 | "base": "SauceLabs", 71 | "browserName": "firefox" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/parser/vcard3.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | FN:Mr. John Q. Public\, Esq. 4 | N:Public;John;Quinlan;Mr.;Esq. 5 | N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P. 6 | NICKNAME:Robbie 7 | NICKNAME:Jim,Jimmie 8 | PHOTO;VALUE=uri:http://www.abc.com/pub/photos 9 | /jqpublic.gif 10 | PHOTO;ENCODING=b;TYPE=JPEG:SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBp 11 | bWFnZSBqdXN0IGEgdGVzdC4= 12 | BDAY:1996-04-15 13 | BDAY:1953-10-15T23:10:00Z 14 | BDAY:1987-09-27T08:30:00-06:00 15 | ADR;TYPE=dom,home,postal,parcel:;;123 Main 16 | Street;Any Town;CA;91921-1234 17 | LABEL;TYPE=dom,home,postal,parcel:Mr.John Q. Public\, Esq.\n 18 | Mail Drop: TNE QB\n123 Main Street\nAny Town\, CA 91921-1234 19 | \nU.S.A. 20 | TEL;TYPE=work,voice,pref,msg:+1-213-555-1234 21 | EMAIL;TYPE=internet:jqpublic@xyz.dom1.com 22 | EMAIL;TYPE=internet:jdoe@isp.net 23 | EMAIL;TYPE=internet,pref:jane_doe@abc.com 24 | MAILER:PigeonMail 2.1 25 | TZ:-05:00 26 | TZ;VALUE=text:-05:00\; EST\; Raleigh/North America 27 | \;This example has a single value\, not a structure text value. 28 | GEO:37.386013;-122.082932 29 | TITLE:Director\, Research and Development 30 | ROLE:Programmer 31 | LOGO;VALUE=uri:http://www.abc.com/pub/logos/abccorp.jpg 32 | LOGO;ENCODING=b;TYPE=JPEG:SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBp 33 | bWFnZSBqdXN0IGEgdGVzdC4= 34 | AGENT;VALUE=uri: 35 | CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com 36 | AGENT:BEGIN:VCARD\nFN:Susan Thomas\nTEL:+1-919-555- 37 | 1234\nEMAIL\;INTERNET:sthomas@host.com\nEND:VCARD\n 38 | ORG:ABC\, Inc.;North American Division;Marketing 39 | CATEGORIES:TRAVEL AGENT 40 | CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY 41 | NOTE:This fax number is operational 0800 to 1715 42 | EST\, Mon-Fri. 43 | PRODID:-//ONLINE DIRECTORY//NONSGML Version 1//EN 44 | REV:1995-10-31T22:27:10Z 45 | REV:1997-11-15 46 | SORT-STRING:Harten 47 | SOUND;TYPE=BASIC;VALUE=uri:CID:JOHNQPUBLIC.part8. 48 | 19960229T080000.xyzMail@host1.com 49 | SOUND;TYPE=BASIC;ENCODING=b:VGhlcmUgaXMgbm8gc291bmQgaW4gc3BhY2U= 50 | CLASS:PUBLIC 51 | CLASS:PRIVATE 52 | CLASS:CONFIDENTIAL 53 | KEY;ENCODING=b:Tm90IHRoZSBrZXkgeW91IGFyZSBsb29raW5nIGZvcg== 54 | END:VCARD 55 | -------------------------------------------------------------------------------- /test/parser/vcard.json: -------------------------------------------------------------------------------- 1 | [ 2 | "vcard", 3 | [ 4 | [ "version", {}, "text", "4.0" ], 5 | [ "adr", { "type": "work" }, "text", [ "pobox", "apt", "street", "city", "state", "zipcode", "country" ] ], 6 | [ "anniversary", {}, "date-and-or-time", "1996-04-15" ], 7 | [ "bday", {}, "date-and-or-time", "--02-03" ], 8 | [ "caladruri", {}, "uri", "http://example.com/calendar/jdoe" ], 9 | [ "caluri", { "mediatype": "text/calendar" }, "uri", "ftp://ftp.example.com/calA.ics" ], 10 | [ "clientpidmap", {}, "text", [ "1", "urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b" ] ], 11 | [ "email", { "type": "work" }, "text", "jqpublic@xyz.example.com" ], 12 | [ "fburl", { "mediatype": "text/calendar" }, "uri", "ftp://example.com/busy/project-a.ifb" ], 13 | [ "fn", {}, "text", "J. Doe" ], 14 | [ "gender", {}, "text", [ "M", "Fellow" ] ], 15 | [ "geo", {}, "uri", "geo:37.386013,-122.082932" ], 16 | [ "impp", { "pref": "1" }, "uri", "xmpp:alice@example.com" ], 17 | [ "key", {}, "uri", "http://www.example.com/keys/jdoe.cer" ], 18 | [ "kind", {}, "text", "individual" ], 19 | [ "lang", { "pref": "1" }, "language-tag", "fr" ], 20 | [ "logo", {}, "uri", "http://www.example.com/pub/logos/abccorp.jpg" ], 21 | [ "member", {}, "uri", "urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af" ], 22 | [ "n", {}, "text", [ "Stevenson", "John", ["Philip", "Paul"], "Dr.", [ "Jr.", "M.D.", "A.C.P." ] ] ], 23 | [ "nickname", { "type": "work" }, "text", "Boss" ], 24 | [ "note", {}, "text", "This fax number is operational 0800 to 1715 EST, Mon-Fri" ], 25 | [ "org", {}, "text", [ "ABC, Inc.", "North American Division", "Marketing" ] ], 26 | [ "photo", {}, "uri", "http://www.example.com/pub/photos/jqpublic.gif" ], 27 | [ "related", { "type": "friend" }, "uri", "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" ], 28 | [ "rev", {}, "timestamp", "1995-10-31T22:27:10Z" ], 29 | [ "role", {}, "text", "Project Leader" ], 30 | [ "sound", {}, "uri", "CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com" ], 31 | [ "source", {}, "uri", "ldap://ldap.example.com/cn=Babs%20Jensen,%20o=Babsco,%20c=US" ], 32 | [ "tel", { "type": "home" }, "uri", "tel:+33-01-23-45-67" ], 33 | [ "title", {}, "text", "Research Scientist" ], 34 | [ "tz", {}, "utc-offset", "-05:00" ], 35 | [ "xml", {}, "text", "" ] 36 | ], 37 | [] 38 | ] 39 | -------------------------------------------------------------------------------- /test/parser/property_params.json: -------------------------------------------------------------------------------- 1 | ["vcalendar", 2 | [ 3 | [ 4 | "attendee", 5 | { 6 | "delegated-to": [ 7 | "mailto:foo7@bar", 8 | "mailto:foo8@bar" 9 | ], 10 | "cn": "Foo, Bar" 11 | }, 12 | "cal-address", 13 | "mailto:foo1@bar" 14 | ], 15 | [ 16 | "attendee", 17 | { 18 | "delegated-to": [ 19 | "mailto:foo7@bar", 20 | "mailto:foo8@bar" 21 | ], 22 | "cn": "Foo; Bar" 23 | }, 24 | "cal-address", 25 | "mailto:foo2@bar" 26 | ], 27 | [ 28 | "attendee", 29 | { 30 | "cn": "Foo, Bar" 31 | }, 32 | "cal-address", 33 | "mailto:foo3@bar" 34 | ], 35 | [ 36 | "attendee", 37 | { 38 | "cn": "Foo; Bar" 39 | }, 40 | "cal-address", 41 | "mailto:foo4@bar" 42 | ], 43 | [ 44 | "attendee", 45 | { 46 | "delegated-to": "mailto:foo7@bar", 47 | "cn": "Foo, Bar" 48 | }, 49 | "cal-address", 50 | "mailto:foo5@bar" 51 | ], 52 | [ 53 | "attendee", 54 | { 55 | "delegated-to": "mailto:foo7@bar", 56 | "cn": "Foo; Bar" 57 | }, 58 | "cal-address", 59 | "mailto:foo6@bar" 60 | ], 61 | [ 62 | "attendee", 63 | { 64 | "role": "REQ-PARTICIPANT;foo", 65 | "delegated-from": "mailto:bar@baz.com", 66 | "partstat": "ACCEPTED", 67 | "rsvp": "TRUE" 68 | }, 69 | "cal-address", 70 | "mailto:foo@bar.com" 71 | ], 72 | [ 73 | "x-foo", 74 | { 75 | "param1": "VAL1" 76 | }, 77 | "unknown", 78 | "FOO;BAR" 79 | ], 80 | [ 81 | "x-foo2", 82 | { 83 | "param1": "VAL1", 84 | "param2": "VAL2" 85 | }, 86 | "unknown", 87 | "FOO;BAR" 88 | ], 89 | [ 90 | "x-bar", 91 | { 92 | "param1": "VAL1:FOO" 93 | }, 94 | "unknown", 95 | "BAZ;BAR" 96 | ], 97 | [ 98 | "x-baz", 99 | { 100 | "param1": "VAL1:FOO", 101 | "param2": "VAL2" 102 | }, 103 | "unknown", 104 | "BAZ;BAR" 105 | ], 106 | [ 107 | "x-baz2", 108 | { 109 | "param1": "VAL1", 110 | "param2": "VAL2:FOO" 111 | }, 112 | "unknown", 113 | "BAZ;BAR" 114 | ] 115 | ], 116 | [] 117 | ] 118 | -------------------------------------------------------------------------------- /test/vcard_time_test.js: -------------------------------------------------------------------------------- 1 | suite('vcard time', function() { 2 | // Lots of things are also covered in the design test 3 | 4 | suite('initialization', function() { 5 | test('default icaltype', function() { 6 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-01'); 7 | assert.equal(subject.icaltype, 'date-and-or-time'); 8 | }); 9 | 10 | test('clone', function() { 11 | var orig = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-02T03:04:05-08:00', 'date-time'); 12 | var subject = orig.clone(); 13 | 14 | orig.day++; 15 | orig.month++; 16 | orig.year++; 17 | orig.hour++; 18 | orig.minute++; 19 | orig.second++; 20 | orig.zone = ICAL.Timezone.utcTimezone; 21 | 22 | assert.equal(orig.toString(), '2016-02-03T04:05:06Z'); 23 | assert.equal(subject.toString(), '2015-01-02T03:04:05-08:00'); 24 | assert.equal(subject.icaltype, 'date-time'); 25 | assert.equal(subject.zone.toString(), '-08:00'); 26 | }); 27 | }); 28 | 29 | suite('#utcOffset', function() { 30 | testSupport.useTimezones('America/New_York'); 31 | 32 | test('floating and utc', function() { 33 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-02T03:04:05', 'date-time'); 34 | subject.zone = ICAL.Timezone.utcTimezone; 35 | assert.equal(subject.utcOffset(), 0); 36 | 37 | subject.zone = ICAL.Timezone.localTimezone; 38 | assert.equal(subject.utcOffset(), 0); 39 | }); 40 | test('ICAL.UtcOffset', function() { 41 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-02T03:04:05-08:00', 'date-time'); 42 | assert.equal(subject.utcOffset(), -28800); 43 | }); 44 | test('Olson timezone', function() { 45 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-02T03:04:05'); 46 | subject.zone = ICAL.TimezoneService.get('America/New_York'); 47 | assert.equal(subject.utcOffset(), -18000); 48 | }); 49 | }); 50 | 51 | suite('#toString', function() { 52 | testSupport.useTimezones('America/New_York'); 53 | 54 | test('invalid icaltype', function() { 55 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-01', 'ballparkfigure'); 56 | assert.isNull(subject.toString()); 57 | }); 58 | test('invalid timezone', function() { 59 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-01T01:01:01'); 60 | subject.zone = null; 61 | assert.equal(subject.toString(), '2015-01-01T01:01:01'); 62 | }); 63 | test('Olson timezone', function() { 64 | var subject = ICAL.VCardTime.fromDateAndOrTimeString('2015-01-02T03:04:05'); 65 | subject.zone = ICAL.TimezoneService.get('America/New_York'); 66 | assert.equal(subject.toString(), '2015-01-02T03:04:05-05:00'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /lib/ical/timezone_service.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 5 | 6 | 7 | /** 8 | * This symbol is further described later on 9 | * @ignore 10 | */ 11 | ICAL.TimezoneService = (function() { 12 | var zones; 13 | 14 | /** 15 | * @classdesc 16 | * Singleton class to contain timezones. Right now its all manual registry in 17 | * the future we may use this class to download timezone information or handle 18 | * loading pre-expanded timezones. 19 | * 20 | * @namespace 21 | * @alias ICAL.TimezoneService 22 | */ 23 | var TimezoneService = { 24 | get count() { 25 | return Object.keys(zones).length; 26 | }, 27 | 28 | reset: function() { 29 | zones = Object.create(null); 30 | var utc = ICAL.Timezone.utcTimezone; 31 | 32 | zones.Z = utc; 33 | zones.UTC = utc; 34 | zones.GMT = utc; 35 | }, 36 | 37 | /** 38 | * Checks if timezone id has been registered. 39 | * 40 | * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 41 | * @return {Boolean} False, when not present 42 | */ 43 | has: function(tzid) { 44 | return !!zones[tzid]; 45 | }, 46 | 47 | /** 48 | * Returns a timezone by its tzid if present. 49 | * 50 | * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 51 | * @return {?ICAL.Timezone} The timezone, or null if not found 52 | */ 53 | get: function(tzid) { 54 | return zones[tzid]; 55 | }, 56 | 57 | /** 58 | * Registers a timezone object or component. 59 | * 60 | * @param {String=} name 61 | * The name of the timezone. Defaults to the component's TZID if not 62 | * passed. 63 | * @param {ICAL.Component|ICAL.Timezone} zone 64 | * The initialized zone or vtimezone. 65 | */ 66 | register: function(name, timezone) { 67 | if (name instanceof ICAL.Component) { 68 | if (name.name === 'vtimezone') { 69 | timezone = new ICAL.Timezone(name); 70 | name = timezone.tzid; 71 | } 72 | } 73 | 74 | if (timezone instanceof ICAL.Timezone) { 75 | zones[name] = timezone; 76 | } else { 77 | throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component'); 78 | } 79 | }, 80 | 81 | /** 82 | * Removes a timezone by its tzid from the list. 83 | * 84 | * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 85 | * @return {?ICAL.Timezone} The removed timezone, or null if not registered 86 | */ 87 | remove: function(tzid) { 88 | return (delete zones[tzid]); 89 | } 90 | }; 91 | 92 | // initialize defaults 93 | TimezoneService.reset(); 94 | 95 | return TimezoneService; 96 | }()); 97 | -------------------------------------------------------------------------------- /samples/recur_instances.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Zimbra-Calendar-Provider 4 | BEGIN:VTIMEZONE 5 | TZID:America/Los_Angeles 6 | BEGIN:STANDARD 7 | DTSTART:19710101T020000 8 | TZOFFSETTO:-0800 9 | TZOFFSETFROM:-0700 10 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU 11 | TZNAME:PST 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:19710101T020000 15 | TZOFFSETTO:-0700 16 | TZOFFSETFROM:-0800 17 | RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU 18 | TZNAME:PDT 19 | END:DAYLIGHT 20 | END:VTIMEZONE 21 | BEGIN:VTIMEZONE 22 | X-INVALID-TIMEZONE:TRUE 23 | END:VTIMEZONE 24 | BEGIN:VEVENT 25 | UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868 26 | DESCRIPTION:IAM FOO 27 | RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1TU 28 | SUMMARY:Crazy Event Thingy! 29 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sahaja 30 | Lal;X-NUM-GUESTS=0:mailto:calmozilla1@gmail.com 31 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ja 32 | mes@lightsofapollo.com;X-NUM-GUESTS=0:mailto:james@lightsofapollo.com 33 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=ia 34 | m.revelation@gmail.com;X-NUM-GUESTS=0:mailto:iam.revelation@gmail.com 35 | LOCATION:PLACE 36 | ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com 37 | DTSTART;TZID=America/Los_Angeles:20121002T100000 38 | DTEND;TZID=America/Los_Angeles:20121002T103000 39 | STATUS:CONFIRMED 40 | CLASS:PUBLIC 41 | TRANSP:OPAQUE 42 | LAST-MODIFIED:20120912T171506Z 43 | DTSTAMP:20120912T171506Z 44 | SEQUENCE:0 45 | RDATE;TZID=America/Los_Angeles:20121105T100000 46 | RDATE;TZID=America/Los_Angeles:20121110T100000,20121130T100000 47 | EXDATE;TZID=America/Los_Angeles:20130402T100000 48 | EXDATE;TZID=America/Los_Angeles:20121204T100000 49 | EXDATE;TZID=America/Los_Angeles:20130205T100000 50 | BEGIN:VALARM 51 | ACTION:DISPLAY 52 | TRIGGER;RELATED=START:-PT5M 53 | DESCRIPTION:Reminder 54 | END:VALARM 55 | END:VEVENT 56 | BEGIN:VEVENT 57 | UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868 58 | SUMMARY:Crazy Event Thingy! 59 | DESCRIPTION:I HAZ CHANGED! 60 | ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com 61 | DTSTART;TZID=America/Los_Angeles:20121002T150000 62 | DTEND;TZID=America/Los_Angeles:20121002T153000 63 | STATUS:CONFIRMED 64 | CLASS:PUBLIC 65 | TRANSP:OPAQUE 66 | RECURRENCE-ID;TZID=America/Los_Angeles:20121002T100000 67 | LAST-MODIFIED:20120912T171540Z 68 | DTSTAMP:20120912T171540Z 69 | SEQUENCE:1 70 | BEGIN:VALARM 71 | ACTION:DISPLAY 72 | TRIGGER;RELATED=START:-PT5M 73 | DESCRIPTION:Reminder 74 | END:VALARM 75 | END:VEVENT 76 | BEGIN:VEVENT 77 | UID:623c13c0-6c2b-45d6-a12b-c33ad61c4868 78 | SUMMARY:Crazy Event Thingy! 79 | ORGANIZER;CN=James Lal:mailto:jlal@mozilla.com 80 | DTSTART;TZID=America/Los_Angeles:20121106T200000 81 | DTEND;TZID=America/Los_Angeles:20121106T203000 82 | STATUS:CONFIRMED 83 | CLASS:PUBLIC 84 | TRANSP:OPAQUE 85 | RECURRENCE-ID:20121105T180000Z 86 | LAST-MODIFIED:20120912T171820Z 87 | DTSTAMP:20120912T171820Z 88 | SEQUENCE:1 89 | BEGIN:VALARM 90 | ACTION:DISPLAY 91 | TRIGGER;RELATED=START:-PT5M 92 | DESCRIPTION:Reminder 93 | END:VALARM 94 | END:VEVENT 95 | BEGIN:X-UNKNOWN 96 | END:X-UNKNOWN 97 | END:VCALENDAR 98 | -------------------------------------------------------------------------------- /samples/google_birthday.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | X-WR-CALNAME:Contacts' birthdays and events 6 | X-WR-TIMEZONE:America/Los_Angeles 7 | X-WR-CALDESC:Your contacts' birthdays and anniversaries 8 | BEGIN:VEVENT 9 | DTSTART;VALUE=DATE:20141210 10 | DTEND;VALUE=DATE:20141211 11 | RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1 12 | RDATE:20131210Z 13 | RDATE:20121210Z 14 | DTSTAMP:20121207T183041Z 15 | UID:2014_BIRTHDAY_79d389868f96182e@google.com 16 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac 17 | ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct 18 | m6abj3dtmg@virtual 19 | CLASS:PUBLIC 20 | CREATED:20121207T183041Z 21 | LAST-MODIFIED:20121207T183041Z 22 | SEQUENCE:1 23 | STATUS:CONFIRMED 24 | SUMMARY:PErson #2's birthday 25 | TRANSP:OPAQUE 26 | END:VEVENT 27 | BEGIN:VEVENT 28 | DTSTART;VALUE=DATE:20121210 29 | DTEND;VALUE=DATE:20121211 30 | DTSTAMP:20121207T183041Z 31 | UID:BIRTHDAY_79d389868f96182e@google.com 32 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac 33 | ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct 34 | m6abj3dtmg@virtual 35 | X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i 36 | mages/cake.gif 37 | X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip 38 | RECURRENCE-ID;VALUE=DATE:20121210 39 | CLASS:PUBLIC 40 | CREATED:20121207T183041Z 41 | DESCRIPTION:Today is PErson #2's birthday! 42 | LAST-MODIFIED:20121207T183041Z 43 | SEQUENCE:1 44 | STATUS:CONFIRMED 45 | SUMMARY:PErson #2's birthday 46 | TRANSP:OPAQUE 47 | END:VEVENT 48 | BEGIN:VEVENT 49 | DTSTART;VALUE=DATE:20131210 50 | DTEND;VALUE=DATE:20131211 51 | DTSTAMP:20121207T183041Z 52 | UID:BIRTHDAY_79d389868f96182e@google.com 53 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac 54 | ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct 55 | m6abj3dtmg@virtual 56 | X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i 57 | mages/cake.gif 58 | X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip 59 | RECURRENCE-ID;VALUE=DATE:20131210 60 | CLASS:PUBLIC 61 | CREATED:20121207T183041Z 62 | DESCRIPTION:Today is PErson #2's birthday! 63 | LAST-MODIFIED:20121207T183041Z 64 | SEQUENCE:1 65 | STATUS:CONFIRMED 66 | SUMMARY:PErson #2's birthday 67 | TRANSP:OPAQUE 68 | END:VEVENT 69 | BEGIN:VEVENT 70 | DTSTART;VALUE=DATE:20141210 71 | DTEND;VALUE=DATE:20141211 72 | DTSTAMP:20121207T183041Z 73 | UID:BIRTHDAY_79d389868f96182e@google.com 74 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Contac 75 | ts;X-NUM-GUESTS=0:mailto:4dhmurjkc5hn8sq0ctp6utbg5pr2sor1dhimsp31e8n6errfct 76 | m6abj3dtmg@virtual 77 | X-GOOGLE-CALENDAR-CONTENT-ICON:https://calendar.google.com/googlecalendar/i 78 | mages/cake.gif 79 | X-GOOGLE-CALENDAR-CONTENT-DISPLAY:chip 80 | RECURRENCE-ID;VALUE=DATE:20141210 81 | CLASS:PUBLIC 82 | CREATED:20121207T183041Z 83 | DESCRIPTION:Today is PErson #2's birthday! 84 | LAST-MODIFIED:20121207T183041Z 85 | SEQUENCE:1 86 | STATUS:CONFIRMED 87 | SUMMARY:PErson #2's birthday 88 | TRANSP:OPAQUE 89 | END:VEVENT 90 | END:VCALENDAR 91 | -------------------------------------------------------------------------------- /test/timezone_service_test.js: -------------------------------------------------------------------------------- 1 | suite('timezone_service', function() { 2 | var icsData; 3 | testSupport.defineSample('timezones/America/Los_Angeles.ics', function(data) { 4 | icsData = data; 5 | }); 6 | 7 | var subject; 8 | setup(function() { 9 | subject = ICAL.TimezoneService; 10 | subject.reset(); 11 | }); 12 | 13 | teardown(function() { 14 | subject.reset(); 15 | }); 16 | 17 | test('utc zones', function() { 18 | var zones = ['Z', 'UTC', 'GMT']; 19 | zones.forEach(function(tzid) { 20 | assert.ok(subject.has(tzid), tzid + ' should exist'); 21 | assert.equal(subject.get(tzid), ICAL.Timezone.utcTimezone); 22 | }); 23 | }); 24 | 25 | test('#reset', function() { 26 | var name = 'ZFOO'; 27 | subject.register(name, ICAL.Timezone.utcTimezone); 28 | assert.isTrue(subject.has(name), 'should have set ' + name); 29 | 30 | subject.reset(); 31 | assert.isFalse(subject.has(name), 'removes ' + name + ' after reset'); 32 | 33 | assert.equal(subject.count, 3); 34 | }); 35 | 36 | suite('register zones', function() { 37 | test('when it does not exist', function() { 38 | var name = 'test'; 39 | assert.isFalse(subject.has(name)); 40 | 41 | assert.equal(subject.count, 3); 42 | subject.register(name, ICAL.Timezone.localTimezone); 43 | assert.equal(subject.count, 4); 44 | assert.isTrue(subject.has(name), 'is present after set'); 45 | assert.equal( 46 | subject.get(name), 47 | ICAL.Timezone.localTimezone 48 | ); 49 | 50 | subject.remove(name); 51 | assert.isFalse(subject.has(name), 'can remove zones'); 52 | }); 53 | 54 | test('with invalid type', function() { 55 | assert.throws(function() { 56 | subject.register('zzz', 'fff'); 57 | }, "timezone must be ICAL.Timezone"); 58 | }); 59 | test('with only invalid component', function() { 60 | assert.throws(function() { 61 | var comp = new ICAL.Component('vtoaster'); 62 | subject.register(comp); 63 | }, "timezone must be ICAL.Timezone"); 64 | }); 65 | 66 | test('override', function() { 67 | // don't do this but you can if you want to shoot 68 | // yourself in the foot. 69 | assert.equal(subject.count, 3); 70 | subject.register('Z', ICAL.Timezone.localTimezone); 71 | 72 | assert.equal( 73 | subject.get('Z'), 74 | ICAL.Timezone.localTimezone 75 | ); 76 | assert.equal(subject.count, 3); 77 | }); 78 | 79 | test('using a component', function() { 80 | var parsed = ICAL.parse(icsData); 81 | var comp = new ICAL.Component(parsed); 82 | var vtimezone = comp.getFirstSubcomponent('vtimezone'); 83 | var tzid = vtimezone.getFirstPropertyValue('tzid'); 84 | 85 | assert.equal(subject.count, 3); 86 | subject.register(vtimezone); 87 | assert.equal(subject.count, 4); 88 | 89 | assert.isTrue(subject.has(tzid), 'successfully registed with component'); 90 | 91 | var zone = subject.get(tzid); 92 | 93 | assert.instanceOf(zone, ICAL.Timezone); 94 | assert.equal(zone.tzid, tzid); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Woohoo, a new contributor! 2 | ========================== 3 | Thank you so much for looking into ical.js. With your work you are doing good 4 | by making it easier to process calendar data on the web. 5 | 6 | To give you a feeling about what you are dealing with, ical.js was originally 7 | created as a replacement for [libical], meant to be used in [Lightning], the 8 | calendaring extension to Thunderbird. Using binary components in Mozilla 9 | extensions often leads to compatibility issues so a pure JavaScript 10 | implementation was needed. It was also used in the Firefox OS calendaring 11 | application. 12 | 13 | Work on the library prompted creating some standards around it. One of them is 14 | jCal ([rfc7265]), an alternative text format for iCalendar data using JSON. The 15 | other document is jCard ([rfc7095]), which is the counterpart for vCard data. 16 | 17 | Pull Requests 18 | ------------- 19 | In general we are happy about any form of contribution to ical.js. Note however 20 | that since the library is used in at least one larger projects, drastic changes 21 | to the API should be discussed in an issue beforehand. If you have a bug fix 22 | that doesn't affect the API or just adds methods and you don't want to waste 23 | time discussing it, feel free to just send a pull request and we'll see. 24 | 25 | Also, you should check for linter errors and run the tests using `grunt 26 | linters` and `grunt test-node`. See the next section for details on tests. As 27 | they take a while, you can skip the performance tests using `grunt 28 | test-node:unit` and `grunt test-node:acceptance`, but if you are uncertain if 29 | your change may affect performance, you should run all tests. 30 | 31 | Currently the team working on ical.js consists of a very small number of 32 | voluntary contributors. If you don't get a reply in a timely manner please 33 | don't feel turned down. If you are getting impatient with us, go ahead and send 34 | one or more reminders via email or comment. 35 | 36 | Tests 37 | ----- 38 | To make sure there are no regressions, we use unit testing and continuous 39 | integration via Travis. Sending a pull request with a unit test for the bug you 40 | are fixing or feature you are adding will greatly improve the speed of 41 | reviewing and the pull request being merged. Please read the page on [running 42 | tests] in the wiki to set these up and make sure everything passes. 43 | 44 | License 45 | ------- 46 | ical.js is licensed under the [Mozilla Public License], version 2.0. 47 | 48 | Last words 49 | ---------- 50 | If you have any questions please don't hesitate to get in touch. You can leave 51 | a comment on an issue, send [@kewisch] an email, or for ad-hoc questions contact 52 | `Fallen` on [irc.mozilla.org]. 53 | 54 | [libical]: https://github.com/libical/libical/ 55 | [Lightning]: http://www.mozilla.org/projects/calendar/ 56 | [rfc7095]: https://tools.ietf.org/html/rfc7095 57 | [rfc7265]: https://tools.ietf.org/html/rfc7265 58 | [running tests]: https://github.com/mozilla-comm/ical.js/wiki/Running-Tests 59 | [irc.mozilla.org]: irc://irc.mozilla.org/#calendar 60 | [@kewisch]: https://github.com/kewisch/ 61 | [Mozilla Public License]: https://www.mozilla.org/MPL/2.0/ 62 | -------------------------------------------------------------------------------- /tools/ICALTester/compare.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2015 */ 5 | 6 | var fs = require('fs'); 7 | var spawn = require('child_process').spawn; 8 | 9 | var diff = require('difflet')({ indent: true, comments: true }); 10 | var Tester = require('./lib/ICALTester'); 11 | var ICAL = require('../..'); 12 | 13 | function setupHandlers(binPath) { 14 | Tester.addHandler("icaljs", function(rule, dtstart, max, callback) { 15 | var iter = rule.iterator(dtstart); 16 | var occ = 0; 17 | var start = new Date(); 18 | 19 | var results = []; 20 | (function loop() { 21 | var next, diff; 22 | 23 | if (++occ > max) { 24 | return callback(results); 25 | } 26 | 27 | try { 28 | next = iter.next(); 29 | } catch (e) { 30 | return callback(e.message || e); 31 | } 32 | 33 | if (next) { 34 | results.push(next.toICALString()); 35 | } else { 36 | return callback(results); 37 | } 38 | 39 | diff = (new Date() - start) / 1000; 40 | if (diff > Tester.MAX_EXECUTION_TIME) { 41 | return callback("Maximum execution time exceeded"); 42 | } 43 | 44 | setImmediate(loop); 45 | })(); 46 | }); 47 | 48 | Tester.addHandler("other", function(rule, dtstart, max, callback) { 49 | var results = []; 50 | var ptimer = null; 51 | var recur = spawn(binPath, [rule.toString(), dtstart.toICALString(), max]); 52 | 53 | recur.stdout.on('data', function(data) { 54 | Array.prototype.push.apply(results, data.toString().split("\n").slice(0, -1)); 55 | }); 56 | 57 | recur.on('close', function(code) { 58 | if (ptimer) { 59 | clearTimeout(ptimer); 60 | } 61 | 62 | if (code === null) { 63 | callback("Maximum execution time exceeded"); 64 | } else if (code !== 0) { 65 | callback("Execution error: " + code); 66 | } else { 67 | callback(null, results); 68 | } 69 | }); 70 | 71 | ptimer = setTimeout(function() { 72 | ptimer = null; 73 | recur.kill(); 74 | }, Tester.MAX_EXECUTION_TIME); 75 | }); 76 | } 77 | 78 | function usage(message) { 79 | if (message) { 80 | console.log("Error: " + message); 81 | } 82 | console.log("Usage: ICALTester rules.json /path/to/binary"); 83 | process.exit(1); 84 | } 85 | 86 | function main() { 87 | if (process.argv.length < 4) { 88 | usage(); 89 | } 90 | 91 | var rulesFile = fs.statSync(process.argv[2]) && process.argv[2]; 92 | var binPath = fs.statSync(process.argv[3]) && process.argv[3]; 93 | var ruleData = JSON.parse(fs.readFileSync(rulesFile)); 94 | 95 | var dtstart = ICAL.Time.fromString("2014-11-11T08:00:00"); 96 | var max = 10; 97 | 98 | setupHandlers(binPath); 99 | 100 | Tester.run(ruleData, dtstart, max, function(err, results) { 101 | console.log(diff.compare(results.other, results.icaljs)); 102 | }); 103 | } 104 | 105 | main(); 106 | -------------------------------------------------------------------------------- /tasks/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.registerTask('performance-update', function(version) { 5 | function copyMaster(callback) { 6 | grunt.util.spawn({ 7 | cmd: 'git', 8 | args: ['show', 'master:build/ical.js'] 9 | }, function(error, result, code) { 10 | grunt.file.write(filepath, header + result.stdout + footer); 11 | callback(); 12 | }); 13 | } 14 | 15 | var filepath = 'build/benchmark/ical_' + version + '.js'; 16 | var header = "var ICAL_" + version + " = (function() { var ICAL = {};\n" + 17 | "if (typeof global !== 'undefined') global.ICAL_" + version + " = ICAL;\n"; 18 | var footer = "\nreturn ICAL; }());"; 19 | 20 | if (!version) { 21 | grunt.fail.fatal('Need to specify build version name or "upstream" as parameter'); 22 | } else if (version == "upstream") { 23 | var done = this.async(); 24 | grunt.util.spawn({ 25 | cmd: 'git', 26 | args: ['diff', '--shortstat'], 27 | }, function(error, result, code) { 28 | if (result.stdout.length) { 29 | grunt.log.ok('There are git changes, also comparing against master branch'); 30 | copyMaster(done); 31 | } else { 32 | grunt.util.spawn({ 33 | cmd: 'git', 34 | args: ['symbolic-ref', 'HEAD'] 35 | }, function(error, result, code) { 36 | var branch = result.stdout.replace('refs/heads/', ''); 37 | if (branch == 'master') { 38 | grunt.log.ok('No git changes, not comparing against master branch'); 39 | grunt.file.delete(filepath); 40 | done(); 41 | } else { 42 | grunt.log.ok('Not on master, also comparing against master branch'); 43 | copyMaster(done); 44 | } 45 | }); 46 | } 47 | }); 48 | } else { 49 | grunt.file.copy('build/ical.js', filepath, { 50 | process: function(contents) { 51 | return header + contents + footer; 52 | } 53 | }); 54 | grunt.log.ok('Successfully created ' + filepath); 55 | } 56 | }); 57 | 58 | grunt.registerTask('test-node', function(arg) { 59 | if (!arg || arg == 'performance') { 60 | grunt.task.run('performance-update:upstream'); 61 | } 62 | 63 | if (grunt.option('debug')) { 64 | var done = this.async(); 65 | var open = require('biased-opener'); 66 | open('http://127.0.0.1:8080/debug?port=5858', { 67 | preferredBrowsers : ['chrome', 'chromium', 'opera'] 68 | }, function(err, okMsg) { 69 | if (err) { 70 | // unable to launch one of preferred browsers for some reason 71 | console.log(err.message); 72 | console.log('Please open the URL manually in Chrome/Chromium/Opera or similar browser'); 73 | } 74 | done(); 75 | }); 76 | grunt.task.run('concurrent:' + (arg || "all")); 77 | } else if (arg) { 78 | grunt.task.run('mochacli:' + arg); 79 | } else { 80 | grunt.task.run('mochacli:performance'); 81 | grunt.task.run('mochacli:acceptance'); 82 | grunt.task.run('mochacli:unit'); 83 | } 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /test/parser/vcard3.json: -------------------------------------------------------------------------------- 1 | [ 2 | "vcard", 3 | [ 4 | [ "version", {}, "text", "3.0" ], 5 | [ "fn", {}, "text", "Mr. John Q. Public, Esq." ], 6 | [ "n", {}, "text", [ "Public", "John", "Quinlan", "Mr.", "Esq." ] ], 7 | [ "n", {}, "text", [ "Stevenson", "John", [ "Philip", "Paul" ], "Dr.", [ "Jr.", "M.D.", "A.C.P." ] ] ], 8 | [ "nickname", {}, "text", "Robbie" ], 9 | [ "nickname", {}, "text", "Jim", "Jimmie" ], 10 | [ "photo", {}, "uri", "http://www.abc.com/pub/photos/jqpublic.gif" ], 11 | [ "photo", { "encoding": "b", "type": "JPEG" }, "binary", "SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBpbWFnZSBqdXN0IGEgdGVzdC4=" ], 12 | [ "bday", {}, "date", "1996-04-15" ], 13 | [ "bday", {}, "date-time", "1953-10-15T23:10:00Z" ], 14 | [ "bday", {}, "date-time", "1987-09-27T08:30:00-06:00" ], 15 | [ "adr", { "type": [ "dom", "home", "postal", "parcel" ] }, "text", ["", "", "123 Main Street", "Any Town", "CA", "91921-1234" ] ], 16 | [ "label", { "type": [ "dom", "home", "postal", "parcel" ] }, "text", "Mr.John Q. Public, Esq.\nMail Drop: TNE QB\n123 Main Street\nAny Town, CA 91921-1234\nU.S.A." ], 17 | [ "tel", { "type": [ "work", "voice", "pref", "msg" ] }, "phone-number", "+1-213-555-1234" ], 18 | [ "email", { "type": "internet" }, "text", "jqpublic@xyz.dom1.com" ], 19 | [ "email", { "type": "internet" }, "text", "jdoe@isp.net" ], 20 | [ "email", { "type": [ "internet", "pref" ] }, "text", "jane_doe@abc.com" ], 21 | [ "mailer", {}, "text", "PigeonMail 2.1" ], 22 | [ "tz", {}, "utc-offset", "-05:00" ], 23 | [ "tz", {}, "text", "-05:00; EST; Raleigh/North America;This example has a single value, not a structure text value." ], 24 | [ "geo", {}, "float", [37.386013, -122.082932 ] ], 25 | [ "title", {}, "text", "Director, Research and Development" ], 26 | [ "role", {}, "text", "Programmer" ], 27 | [ "logo", {}, "uri", "http://www.abc.com/pub/logos/abccorp.jpg" ], 28 | [ "logo", { "encoding": "b", "type": "JPEG" }, "binary", "SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBpbWFnZSBqdXN0IGEgdGVzdC4=" ], 29 | [ "agent", {}, "uri", "CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com" ], 30 | [ "agent", {}, "vcard", "BEGIN:VCARD\nFN:Susan Thomas\nTEL:+1-919-555-1234\nEMAIL;INTERNET:sthomas@host.com\nEND:VCARD\n" ], 31 | [ "org", {}, "text", ["ABC, Inc.", "North American Division", "Marketing" ] ], 32 | [ "categories", {}, "text", "TRAVEL AGENT" ], 33 | [ "categories", {}, "text", "INTERNET", "IETF", "INDUSTRY", "INFORMATION TECHNOLOGY" ], 34 | [ "note", {}, "text", "This fax number is operational 0800 to 1715 EST, Mon-Fri." ], 35 | [ "prodid", {}, "text", "-//ONLINE DIRECTORY//NONSGML Version 1//EN" ], 36 | [ "rev", {}, "date-time", "1995-10-31T22:27:10Z" ], 37 | [ "rev", {}, "date", "1997-11-15" ], 38 | [ "sort-string", {}, "text", "Harten" ], 39 | [ "sound", { "type": "BASIC" }, "uri", "CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@host1.com" ], 40 | [ "sound", { "type": "BASIC", "encoding": "b" }, "binary", "VGhlcmUgaXMgbm8gc291bmQgaW4gc3BhY2U=" ], 41 | [ "class", {}, "text", "PUBLIC" ], 42 | [ "class", {}, "text", "PRIVATE" ], 43 | [ "class", {}, "text", "CONFIDENTIAL" ], 44 | [ "key", { "encoding": "b" }, "binary", "Tm90IHRoZSBrZXkgeW91IGFyZSBsb29raW5nIGZvcg==" ] 45 | ], 46 | [] 47 | ] 48 | -------------------------------------------------------------------------------- /test/support/performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define a performance suite... 3 | */ 4 | (function(globals) { 5 | 6 | var VERSIONS = ['latest', 'previous', 'upstream']; 7 | 8 | function Context(bench, options) { 9 | this.bench = bench; 10 | 11 | if (options) { 12 | for (var key in options) { 13 | this[key] = options[key]; 14 | } 15 | } 16 | 17 | } 18 | 19 | Context.prototype = { 20 | prefix: '', 21 | icalVersion: 'latest', 22 | 23 | loadICAL: function(callback) { 24 | if (this.icalObject) { 25 | callback(this.icalObject); 26 | } else if (this.icalVersion) { 27 | if (this.icalVersion == "latest") { 28 | this.icalObject = globals.ICAL; 29 | callback(this.icalObject); 30 | } else { 31 | try { 32 | var self = this; 33 | testSupport.requireBenchmarkBuild(this.icalVersion, function(lib) { 34 | self.icalObject = lib; 35 | if (!self.icalObject) { 36 | console.log('Version ICAL_' + self.icalVersion + ' not found, skipping'); 37 | } 38 | callback(self.icalObject); 39 | }); 40 | } catch (e) { 41 | console.log('Version ICAL_' + this.icalVersion + ' not found, skipping'); 42 | this.icalObject = null; 43 | callback(null); 44 | } 45 | } 46 | } 47 | }, 48 | 49 | test: function(name, test) { 50 | var context = this; 51 | 52 | this.bench.add( 53 | this.prefix + name, 54 | test 55 | ); 56 | }, 57 | 58 | compare: function(suite) { 59 | VERSIONS.forEach(function(versionName) { 60 | var context = new Context(this.bench, { 61 | icalVersion: versionName, 62 | prefix: versionName + ': ' 63 | }); 64 | 65 | context.loadICAL(function(ICAL) { 66 | if (ICAL) { 67 | suite(context, ICAL); 68 | } 69 | }); 70 | }, this); 71 | } 72 | }; 73 | 74 | function perfSuite(name, scope) { 75 | var bench; 76 | if (testSupport.isNode) { 77 | bench = new (require('benchmark').Suite)(); 78 | } else { 79 | bench = new Benchmark.Suite(); 80 | } 81 | 82 | var context = new Context(bench); 83 | 84 | /** 85 | * This is somewhat lame because it requires you to manually 86 | * check the results (visually via console output) to actually 87 | * see what the performance results are... 88 | * 89 | * The intent is to define a nicer API while using our existing tools. 90 | * Later we will improve on the tooling to make this a bit more automatic. 91 | */ 92 | suite(name, function() { 93 | scope.call(this, context); 94 | 95 | test('benchmark', function(done) { 96 | this.timeout(0); 97 | // quick formatting hack 98 | console.log(); 99 | 100 | bench.on('cycle', function(event) { 101 | console.log(String(event.target)); 102 | }); 103 | 104 | bench.on('complete', function(event) { 105 | done(); 106 | }); 107 | 108 | bench.run(); 109 | }); 110 | }); 111 | } 112 | 113 | function perfCompareSuite(name, scope) { 114 | perfSuite(name, function(perf) { 115 | perf.compare(scope); 116 | }); 117 | } 118 | 119 | globals.perfSuite = perfSuite; 120 | globals.perfCompareSuite = perfCompareSuite; 121 | 122 | }( 123 | (typeof window !== 'undefined') ? window : global 124 | )); 125 | -------------------------------------------------------------------------------- /test/component_parser_test.js: -------------------------------------------------------------------------------- 1 | suite('component_parser', function() { 2 | var subject; 3 | var icsData; 4 | 5 | testSupport.defineSample('recur_instances.ics', function(data) { 6 | icsData = data; 7 | }); 8 | 9 | suite('#process', function() { 10 | var events = []; 11 | var timezones = []; 12 | 13 | function eventEquals(a, b, msg) { 14 | if (!a) 15 | throw new Error('actual is falsy'); 16 | 17 | if (!b) 18 | throw new Error('expected is falsy'); 19 | 20 | if (a instanceof ICAL.Event) { 21 | a = a.component; 22 | } 23 | 24 | if (b instanceof ICAL.Event) { 25 | b = b.component; 26 | } 27 | 28 | assert.deepEqual(a.toJSON(), b.toJSON(), msg); 29 | } 30 | 31 | function setupProcess(options) { 32 | setup(function(done) { 33 | events.length = 0; 34 | timezones.length = 0; 35 | 36 | subject = new ICAL.ComponentParser(options); 37 | 38 | subject.onrecurrenceexception = function(item) { 39 | exceptions.push(item); 40 | }; 41 | 42 | subject.onevent = function(event) { 43 | events.push(event); 44 | } 45 | 46 | subject.ontimezone = function(tz) { 47 | timezones.push(tz); 48 | } 49 | 50 | subject.oncomplete = function() { 51 | done(); 52 | } 53 | 54 | subject.process(ICAL.parse(icsData)); 55 | }); 56 | } 57 | 58 | suite('without events', function() { 59 | setupProcess({ parseEvent: false }); 60 | 61 | test('parse result', function() { 62 | assert.lengthOf(events, 0); 63 | assert.lengthOf(timezones, 1); 64 | 65 | var tz = timezones[0]; 66 | assert.instanceOf(tz, ICAL.Timezone); 67 | assert.equal(tz.tzid, 'America/Los_Angeles'); 68 | }); 69 | 70 | }); 71 | 72 | suite('with events', function() { 73 | setupProcess(); 74 | 75 | test('parse result', function() { 76 | var component = new ICAL.Component(ICAL.parse(icsData)); 77 | var list = component.getAllSubcomponents('vevent'); 78 | 79 | var expectedEvents = []; 80 | 81 | list.forEach(function(item) { 82 | expectedEvents.push(new ICAL.Event(item)); 83 | }); 84 | 85 | assert.instanceOf(expectedEvents[0], ICAL.Event); 86 | 87 | eventEquals(events[0], expectedEvents[0]); 88 | eventEquals(events[1], expectedEvents[1]); 89 | eventEquals(events[2], expectedEvents[2]); 90 | }); 91 | }); 92 | 93 | suite('without parsing timezones', function() { 94 | setupProcess({ parseTimezone: false }); 95 | 96 | test('parse result', function() { 97 | assert.lengthOf(timezones, 0); 98 | assert.lengthOf(events, 3); 99 | }); 100 | }); 101 | 102 | suite('alternate input', function() { 103 | test('parsing component from string', function(done) { 104 | var subject = new ICAL.ComponentParser(); 105 | subject.oncomplete = function() { 106 | assert.lengthOf(events, 3); 107 | done(); 108 | } 109 | subject.process(icsData); 110 | }); 111 | test('parsing component from component', function(done) { 112 | var subject = new ICAL.ComponentParser(); 113 | subject.oncomplete = function() { 114 | assert.lengthOf(events, 3); 115 | done(); 116 | } 117 | var comp = new ICAL.Component(ICAL.parse(icsData)); 118 | subject.process(comp); 119 | }); 120 | }); 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /sandbox/validator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | 83 | 84 | 85 |
86 |

iCalendar / jCal Validator

87 |

This validator form takes either iCalendar or jCal data. iCalendar data will be parsed to jCal and re-serialized to iCalendar. Similarly, jCal data will be parsed to iCalendar and re-serialized to jCal.

88 |
89 |
90 |

91 |
92 |

 93 |       
 98 |       
102 |     
103 | 104 | 105 | -------------------------------------------------------------------------------- /test/utc_offset_test.js: -------------------------------------------------------------------------------- 1 | suite('ICAL.UtcOffset', function() { 2 | test('#clone', function() { 3 | var subject = new ICAL.UtcOffset({ hours: 5, minutes: 6 }); 4 | assert.equal(subject.toString(), "+05:06"); 5 | 6 | var cloned = subject.clone(); 7 | subject.hours = 6; 8 | 9 | assert.equal(cloned.toString(), "+05:06"); 10 | assert.equal(subject.toString(), "+06:06"); 11 | }); 12 | 13 | test('#toICALString', function() { 14 | var subject = new ICAL.UtcOffset({ hours: 5, minutes: 6 }); 15 | assert.equal(subject.toString(), "+05:06"); 16 | assert.equal(subject.toICALString(), "+0506"); 17 | }); 18 | 19 | suite('#normalize', function() { 20 | test('minute overflow', function() { 21 | assert.hasProperties(new ICAL.UtcOffset({ 22 | minutes: 120 23 | }), { 24 | hours: 2, minutes: 0, factor: 1 25 | }); 26 | }); 27 | test('minutes underflow', function() { 28 | assert.hasProperties(new ICAL.UtcOffset({ 29 | minutes: -120 30 | }), { 31 | hours: 2, minutes: 0, factor: -1 32 | }); 33 | }); 34 | test('minutes underflow with hours', function() { 35 | assert.hasProperties(new ICAL.UtcOffset({ 36 | hours: 2, 37 | minutes: -120 38 | }), { 39 | hours: 0, minutes: 0, factor: 1 40 | }); 41 | }); 42 | test('hours overflow', function() { 43 | assert.hasProperties(new ICAL.UtcOffset({ 44 | hours: 15, 45 | minutes: 30 46 | }), { 47 | hours: 11, minutes: 30, factor: -1 48 | }); 49 | }); 50 | test('hours underflow', function() { 51 | assert.hasProperties(new ICAL.UtcOffset({ 52 | hours: 13, 53 | minutes: 30, 54 | factor: -1 55 | }), { 56 | hours: 13, minutes: 30, factor: 1 57 | }); 58 | }); 59 | test('hours double underflow', function() { 60 | assert.hasProperties(new ICAL.UtcOffset({ 61 | hours: 40, 62 | minutes: 30, 63 | factor: -1 64 | }), { 65 | hours: 13, minutes: 30, factor: 1 66 | }); 67 | }); 68 | test('negative zero utc offset', function() { 69 | assert.hasProperties(new ICAL.UtcOffset({ 70 | hours: 0, 71 | minutes: 0, 72 | factor: -1 73 | }), { 74 | hours: 0, minutes: 0, factor: -1 75 | }); 76 | 77 | }); 78 | }); 79 | 80 | suite('#compare', function() { 81 | test('greater', function() { 82 | var a = new ICAL.UtcOffset({ hours: 5, minutes: 1 }); 83 | var b = new ICAL.UtcOffset({ hours: 5, minutes: 0 }); 84 | assert.equal(a.compare(b), 1); 85 | }); 86 | test('equal', function() { 87 | var a = new ICAL.UtcOffset({ hours: 15, minutes: 0 }); 88 | var b = new ICAL.UtcOffset({ hours: -12, minutes: 0 }); 89 | assert.equal(a.compare(b), 0); 90 | }); 91 | test('equal zero', function() { 92 | var a = new ICAL.UtcOffset({ hours: 0, minutes: 0, factor: -1 }); 93 | var b = new ICAL.UtcOffset({ hours: 0, minutes: 0 }); 94 | assert.equal(a.compare(b), 0); 95 | }); 96 | test('less than', function() { 97 | var a = new ICAL.UtcOffset({ hours: 5, minutes: 0 }); 98 | var b = new ICAL.UtcOffset({ hours: 5, minutes: 1 }); 99 | assert.equal(a.compare(b), -1); 100 | }); 101 | }); 102 | 103 | suite('from/toSeconds', function() { 104 | test('static', function() { 105 | var subject = ICAL.UtcOffset.fromSeconds(3661); 106 | assert.equal(subject.toString(), '+01:01'); 107 | assert.equal(subject.toSeconds(), 3660); 108 | }); 109 | test('instance', function() { 110 | var subject = ICAL.UtcOffset.fromSeconds(3661); 111 | subject.fromSeconds(-7321); 112 | assert.equal(subject.toString(), '-02:02'); 113 | assert.equal(subject.toSeconds(), -7320); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/duration_test.js: -------------------------------------------------------------------------------- 1 | suite('ical/duration', function() { 2 | test('#clone', function() { 3 | var subject = new ICAL.Duration.fromData({ 4 | weeks: 1, 5 | days: 2, 6 | hours: 3, 7 | minutes: 4, 8 | seconds: 5, 9 | isNegative: true 10 | }); 11 | 12 | var expected = { 13 | weeks: 1, 14 | days: 2, 15 | hours: 3, 16 | minutes: 4, 17 | seconds: 5, 18 | isNegative: true 19 | }; 20 | 21 | var expected2 = { 22 | weeks: 6, 23 | days: 7, 24 | hours: 8, 25 | minutes: 9, 26 | seconds: 10, 27 | isNegative: true 28 | }; 29 | 30 | var subject2 = subject.clone(); 31 | assert.hasProperties(subject, expected, 'base object unchanged'); 32 | assert.hasProperties(subject2, expected, 'cloned object unchanged'); 33 | 34 | for (var k in expected2) { 35 | subject2[k] = expected2[k]; 36 | } 37 | 38 | assert.hasProperties(subject, expected, 'base object unchanged'); 39 | assert.hasProperties(subject2, expected2, 'cloned object changed'); 40 | }); 41 | 42 | test('#reset', function() { 43 | var expected = { 44 | weeks: 1, 45 | days: 2, 46 | hours: 3, 47 | minutes: 4, 48 | seconds: 5, 49 | isNegative: true 50 | }; 51 | var subject = new ICAL.Duration(expected); 52 | assert.hasProperties(subject, expected); 53 | 54 | subject.reset(); 55 | 56 | assert.hasProperties(subject, { 57 | weeks: 0, 58 | days: 0, 59 | hours: 0, 60 | minutes: 0, 61 | seconds: 0, 62 | isNegative: false 63 | }); 64 | 65 | assert.equal(subject.toString(), "PT0S"); 66 | }); 67 | 68 | suite('#normalize', function() { 69 | function verify(name, str, data) { 70 | test(name, function() { 71 | var subject = new ICAL.Duration(); 72 | for (var k in data) { 73 | subject[k] = data[k]; 74 | } 75 | subject.normalize(); 76 | assert.equal(subject.toString(), str); 77 | assert.equal(subject.toICALString(), str); 78 | }); 79 | } 80 | 81 | verify('weeks and day => days', 'P50D', { 82 | weeks: 7, 83 | days: 1 84 | }); 85 | verify('days => week' , 'P2W', { 86 | days: 14 87 | }); 88 | verify('days and weeks => week' , 'P4W', { 89 | weeks: 2, 90 | days: 14 91 | }); 92 | verify('seconds => everything', 'P1DT1H1M1S', { 93 | seconds: 86400 + 3600 + 60 + 1 94 | }); 95 | }); 96 | 97 | suite("#compare", function() { 98 | function verify(str, a, b, cmp) { 99 | test(str, function() { 100 | var dur_a = new ICAL.Duration.fromString(a); 101 | var dur_b = new ICAL.Duration.fromString(b); 102 | assert.equal(dur_a.compare(dur_b), cmp); 103 | }); 104 | } 105 | 106 | verify('a>b', 'PT3H', 'PT1S', 1); 107 | verify('a> 18 & 0x3f; 88 | h2 = bits >> 12 & 0x3f; 89 | h3 = bits >> 6 & 0x3f; 90 | h4 = bits & 0x3f; 91 | 92 | // use hexets to index into b64, and append result to encoded string 93 | tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); 94 | } while (i < data.length); 95 | 96 | enc = tmp_arr.join(''); 97 | 98 | var r = data.length % 3; 99 | 100 | return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); 101 | 102 | }, 103 | 104 | _b64_decode: function base64_decode(data) { 105 | // http://kevin.vanzonneveld.net 106 | // + original by: Tyler Akins (http://rumkin.com) 107 | // + improved by: Thunder.m 108 | // + input by: Aman Gupta 109 | // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 110 | // + bugfixed by: Onno Marsman 111 | // + bugfixed by: Pellentesque Malesuada 112 | // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 113 | // + input by: Brett Zamir (http://brett-zamir.me) 114 | // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 115 | // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); 116 | // * returns 1: 'Kevin van Zonneveld' 117 | // mozilla has this native 118 | // - but breaks in 2.0.0.12! 119 | //if (typeof this.window['btoa'] == 'function') { 120 | // return btoa(data); 121 | //} 122 | var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 123 | "abcdefghijklmnopqrstuvwxyz0123456789+/="; 124 | var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, 125 | ac = 0, 126 | dec = "", 127 | tmp_arr = []; 128 | 129 | if (!data) { 130 | return data; 131 | } 132 | 133 | data += ''; 134 | 135 | do { // unpack four hexets into three octets using index points in b64 136 | h1 = b64.indexOf(data.charAt(i++)); 137 | h2 = b64.indexOf(data.charAt(i++)); 138 | h3 = b64.indexOf(data.charAt(i++)); 139 | h4 = b64.indexOf(data.charAt(i++)); 140 | 141 | bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; 142 | 143 | o1 = bits >> 16 & 0xff; 144 | o2 = bits >> 8 & 0xff; 145 | o3 = bits & 0xff; 146 | 147 | if (h3 == 64) { 148 | tmp_arr[ac++] = String.fromCharCode(o1); 149 | } else if (h4 == 64) { 150 | tmp_arr[ac++] = String.fromCharCode(o1, o2); 151 | } else { 152 | tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); 153 | } 154 | } while (i < data.length); 155 | 156 | dec = tmp_arr.join(''); 157 | 158 | return dec; 159 | }, 160 | 161 | /** 162 | * The string representation of this value 163 | * @return {String} 164 | */ 165 | toString: function() { 166 | return this.value; 167 | } 168 | }; 169 | 170 | /** 171 | * Creates a binary value from the given string. 172 | * 173 | * @param {String} aString The binary value string 174 | * @return {ICAL.Binary} The binary value instance 175 | */ 176 | Binary.fromString = function(aString) { 177 | return new Binary(aString); 178 | }; 179 | 180 | return Binary; 181 | }()); 182 | -------------------------------------------------------------------------------- /lib/ical/utc_offset.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 5 | 6 | 7 | /** 8 | * This symbol is further described later on 9 | * @ignore 10 | */ 11 | ICAL.UtcOffset = (function() { 12 | 13 | /** 14 | * @classdesc 15 | * This class represents the "duration" value type, with various calculation 16 | * and manipulation methods. 17 | * 18 | * @class 19 | * @alias ICAL.UtcOffset 20 | * @param {Object} aData An object with members of the utc offset 21 | * @param {Number=} aData.hours The hours for the utc offset 22 | * @param {Number=} aData.minutes The minutes in the utc offset 23 | * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 24 | */ 25 | function UtcOffset(aData) { 26 | this.fromData(aData); 27 | } 28 | 29 | UtcOffset.prototype = { 30 | 31 | /** 32 | * The hours in the utc-offset 33 | * @type {Number} 34 | */ 35 | hours: 0, 36 | 37 | /** 38 | * The minutes in the utc-offset 39 | * @type {Number} 40 | */ 41 | minutes: 0, 42 | 43 | /** 44 | * The sign of the utc offset, 1 for positive offset, -1 for negative 45 | * offsets. 46 | * @type {Number} 47 | */ 48 | factor: 1, 49 | 50 | /** 51 | * The type name, to be used in the jCal object. 52 | * @constant 53 | * @type {String} 54 | * @default "utc-offset" 55 | */ 56 | icaltype: "utc-offset", 57 | 58 | /** 59 | * Returns a clone of the utc offset object. 60 | * 61 | * @return {ICAL.UtcOffset} The cloned object 62 | */ 63 | clone: function() { 64 | return ICAL.UtcOffset.fromSeconds(this.toSeconds()); 65 | }, 66 | 67 | /** 68 | * Sets up the current instance using members from the passed data object. 69 | * 70 | * @param {Object} aData An object with members of the utc offset 71 | * @param {Number=} aData.hours The hours for the utc offset 72 | * @param {Number=} aData.minutes The minutes in the utc offset 73 | * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 74 | */ 75 | fromData: function(aData) { 76 | if (aData) { 77 | for (var key in aData) { 78 | /* istanbul ignore else */ 79 | if (aData.hasOwnProperty(key)) { 80 | this[key] = aData[key]; 81 | } 82 | } 83 | } 84 | this._normalize(); 85 | }, 86 | 87 | /** 88 | * Sets up the current instance from the given seconds value. The seconds 89 | * value is truncated to the minute. Offsets are wrapped when the world 90 | * ends, the hour after UTC+14:00 is UTC-12:00. 91 | * 92 | * @param {Number} aSeconds The seconds to convert into an offset 93 | */ 94 | fromSeconds: function(aSeconds) { 95 | var secs = Math.abs(aSeconds); 96 | 97 | this.factor = aSeconds < 0 ? -1 : 1; 98 | this.hours = ICAL.helpers.trunc(secs / 3600); 99 | 100 | secs -= (this.hours * 3600); 101 | this.minutes = ICAL.helpers.trunc(secs / 60); 102 | return this; 103 | }, 104 | 105 | /** 106 | * Convert the current offset to a value in seconds 107 | * 108 | * @return {Number} The offset in seconds 109 | */ 110 | toSeconds: function() { 111 | return this.factor * (60 * this.minutes + 3600 * this.hours); 112 | }, 113 | 114 | /** 115 | * Compare this utc offset with another one. 116 | * 117 | * @param {ICAL.UtcOffset} other The other offset to compare with 118 | * @return {Number} -1, 0 or 1 for less/equal/greater 119 | */ 120 | compare: function icaltime_compare(other) { 121 | var a = this.toSeconds(); 122 | var b = other.toSeconds(); 123 | return (a > b) - (b > a); 124 | }, 125 | 126 | _normalize: function() { 127 | // Range: 97200 seconds (with 1 hour inbetween) 128 | var secs = this.toSeconds(); 129 | var factor = this.factor; 130 | while (secs < -43200) { // = UTC-12:00 131 | secs += 97200; 132 | } 133 | while (secs > 50400) { // = UTC+14:00 134 | secs -= 97200; 135 | } 136 | 137 | this.fromSeconds(secs); 138 | 139 | // Avoid changing the factor when on zero seconds 140 | if (secs == 0) { 141 | this.factor = factor; 142 | } 143 | }, 144 | 145 | /** 146 | * The iCalendar string representation of this utc-offset. 147 | * @return {String} 148 | */ 149 | toICALString: function() { 150 | return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString()); 151 | }, 152 | 153 | /** 154 | * The string representation of this utc-offset. 155 | * @return {String} 156 | */ 157 | toString: function toString() { 158 | return (this.factor == 1 ? "+" : "-") + 159 | ICAL.helpers.pad2(this.hours) + ':' + 160 | ICAL.helpers.pad2(this.minutes); 161 | } 162 | }; 163 | 164 | /** 165 | * Creates a new {@link ICAL.UtcOffset} instance from the passed string. 166 | * 167 | * @param {String} aString The string to parse 168 | * @return {ICAL.Duration} The created utc-offset instance 169 | */ 170 | UtcOffset.fromString = function(aString) { 171 | // -05:00 172 | var options = {}; 173 | //TODO: support seconds per rfc5545 ? 174 | options.factor = (aString[0] === '+') ? 1 : -1; 175 | options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2)); 176 | options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2)); 177 | 178 | return new ICAL.UtcOffset(options); 179 | }; 180 | 181 | /** 182 | * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds 183 | * value. 184 | * 185 | * @param {Number} aSeconds The number of seconds to convert 186 | */ 187 | UtcOffset.fromSeconds = function(aSeconds) { 188 | var instance = new UtcOffset(); 189 | instance.fromSeconds(aSeconds); 190 | return instance; 191 | }; 192 | 193 | return UtcOffset; 194 | }()); 195 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var isNode = typeof(window) === 'undefined'; 4 | 5 | if (!isNode) { 6 | window.navigator; 7 | } 8 | 9 | // lazy defined navigator causes global leak warnings... 10 | 11 | testSupport = { 12 | isNode: (typeof(window) === 'undefined'), 13 | isKarma: (typeof(window) !== 'undefined' && typeof window.__karma__ !== 'undefined') 14 | }; 15 | 16 | function loadChai(chai) { 17 | chai.config.includeStack = true; 18 | assert = chai.assert; 19 | assert.hasProperties = function chai_hasProperties(given, props, msg) { 20 | msg = (typeof(msg) === 'undefined') ? '' : msg + ': '; 21 | 22 | if (props instanceof Array) { 23 | props.forEach(function(prop) { 24 | assert.ok( 25 | (prop in given), 26 | msg + 'given should have "' + prop + '" property' 27 | ); 28 | }); 29 | } else { 30 | for (var key in props) { 31 | assert.deepEqual( 32 | given[key], 33 | props[key], 34 | msg + ' property equality for (' + key + ') ' 35 | ); 36 | } 37 | } 38 | }; 39 | } 40 | 41 | // Load chai, and the extra libs we include 42 | if (testSupport.isNode) { 43 | loadChai(require('chai')); 44 | } else if (window.chai) { 45 | loadChai(window.chai); 46 | } else { 47 | require('/node_modules/chai/chai.js', function() { 48 | loadChai(window.chai); 49 | }); 50 | } 51 | 52 | /* cross require */ 53 | testSupport.requireICAL = function() { 54 | var files = [ 55 | 'helpers', 56 | 'recur_expansion', 57 | 'event', 58 | 'component_parser', 59 | 'design', 60 | 'parse', 61 | 'stringify', 62 | 'component', 63 | 'property', 64 | 'utc_offset', 65 | 'binary', 66 | 'period', 67 | 'duration', 68 | 'timezone', 69 | 'timezone_service', 70 | 'time', 71 | 'vcard_time', 72 | 'recur', 73 | 'recur_iterator' 74 | ]; 75 | 76 | files.forEach(function(file) { 77 | testSupport.require('/lib/ical/' + file + '.js'); 78 | }); 79 | }; 80 | 81 | /** 82 | * Requires a benchmark build. 83 | * 84 | * @param {String} version of the build (see build/benchmark/*) 85 | * @param {Function} optional callback called on completion 86 | */ 87 | testSupport.requireBenchmarkBuild = function(version, callback) { 88 | testSupport.require('/build/benchmark/ical_' + version + '.js', callback); 89 | }; 90 | 91 | testSupport.require = function cross_require(file, callback) { 92 | if (!(/\.js$/.test(file))) { 93 | file += '.js'; 94 | } 95 | 96 | if (testSupport.isNode) { 97 | var lib = require(__dirname + '/../' + file); 98 | if (typeof(callback) !== 'undefined') { 99 | callback(lib); 100 | } 101 | } else { 102 | window.require(file, callback); 103 | } 104 | }; 105 | 106 | /** 107 | * Registers a given timezone from samples with the timezone service. 108 | * 109 | * @param {String} zone like "America/Los_Angeles". 110 | * @param {Function} callback fired when load/register is complete. 111 | */ 112 | testSupport.registerTimezone = function(zone, callback) { 113 | if (!this._timezones) { 114 | this._timezones = Object.create(null); 115 | } 116 | 117 | var ics = this._timezones[zone]; 118 | 119 | function register(ics) { 120 | var parsed = ICAL.parse(ics); 121 | var calendar = new ICAL.Component(parsed); 122 | var vtimezone = calendar.getFirstSubcomponent('vtimezone'); 123 | 124 | var zone = new ICAL.Timezone(vtimezone); 125 | 126 | ICAL.TimezoneService.register(vtimezone); 127 | } 128 | 129 | if (ics) { 130 | setTimeout(function() { 131 | callback(null, register(ics)); 132 | }, 0); 133 | } else { 134 | var path = 'samples/timezones/' + zone + '.ics'; 135 | testSupport.load(path, function(err, data) { 136 | if (err) { 137 | callback(err); 138 | } 139 | var zone = register(data); 140 | this._timezones[zone] = data; 141 | 142 | callback(null, register(data)); 143 | }.bind(this)); 144 | } 145 | }; 146 | 147 | /** 148 | * Registers a timezones for a given suite of tests. Uses suiteSetup to stage 149 | * and will use suiteTeardown to purge all timezones for clean tests... 150 | * 151 | * Please note that you should only call this once per suite, otherwise the 152 | * teardown function will reset the service while the parent suite will still 153 | * need them. 154 | */ 155 | testSupport.useTimezones = function(zones) { 156 | let allZones = Array.prototype.slice.call(arguments); 157 | 158 | suiteTeardown(function() { 159 | // to ensure clean tests 160 | ICAL.TimezoneService.reset(); 161 | }); 162 | 163 | suiteSetup(function(done) { 164 | function zoneDone() { 165 | if (--remaining == 0) { 166 | done(); 167 | } 168 | } 169 | 170 | // By default, Z/UTC/GMT are already registered 171 | if (ICAL.TimezoneService.count > 3) { 172 | throw new Error("Can only register zones once"); 173 | } 174 | 175 | var remaining = allZones.length; 176 | allZones.forEach(function(zone) { 177 | testSupport.registerTimezone(zone, zoneDone); 178 | }); 179 | }); 180 | }; 181 | 182 | /** 183 | * @param {String} path relative to root (/) of project. 184 | * @param {Function} callback [err, contents]. 185 | */ 186 | testSupport.load = function(path, callback) { 187 | if (testSupport.isNode) { 188 | var root = __dirname + '/../'; 189 | require('fs').readFile(root + path, 'utf8', function(err, contents) { 190 | callback(err, contents); 191 | }); 192 | } else { 193 | var path = '/' + path; 194 | if (testSupport.isKarma) { 195 | path = '/base/' + path.replace(/^\//, ''); 196 | } 197 | var xhr = new XMLHttpRequest(); 198 | xhr.open('GET', path, true); 199 | xhr.onreadystatechange = function() { 200 | if (xhr.readyState === 4) { 201 | if (xhr.status !== 200) { 202 | callback(new Error('file not found or other error', xhr)); 203 | } else { 204 | callback(null, xhr.responseText); 205 | } 206 | } 207 | }; 208 | xhr.send(null); 209 | } 210 | }; 211 | 212 | testSupport.defineSample = function(file, cb) { 213 | suiteSetup(function(done) { 214 | testSupport.load('samples/' + file, function(err, data) { 215 | if (err) { 216 | done(err); 217 | } 218 | cb(data); 219 | done(); 220 | }); 221 | }); 222 | }; 223 | 224 | testSupport.lib = function(lib, callback) { 225 | testSupport.require('/lib/ical/' + lib, callback); 226 | }; 227 | 228 | testSupport.helper = function(lib) { 229 | testSupport.require('/test/support/' + lib); 230 | }; 231 | 232 | if (!testSupport.isKarma) { 233 | testSupport.require('/node_modules/benchmark/benchmark.js'); 234 | testSupport.require('/test/support/performance.js'); 235 | 236 | // Load it here so its pre-loaded in all suite blocks... 237 | testSupport.requireICAL(); 238 | } 239 | 240 | }()); 241 | -------------------------------------------------------------------------------- /lib/ical/vcard_time.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2015 */ 5 | 6 | "use strict"; 7 | 8 | (function() { 9 | 10 | /** 11 | * Describes a vCard time, which has slight differences to the ICAL.Time. 12 | * Properties can be null if not specified, for example for dates with 13 | * reduced accuracy or truncation. 14 | * 15 | * Note that currently not all methods are correctly re-implemented for 16 | * VCardTime. For example, comparison will have undefined results when some 17 | * members are null. 18 | * 19 | * Also, normalization is not yet implemented for this class! 20 | * 21 | * @alias ICAL.VCardTime 22 | * @class 23 | * @extends {ICAL.Time} 24 | * @param {Object} data The data for the time instance 25 | * @param {Number=} data.year The year for this date 26 | * @param {Number=} data.month The month for this date 27 | * @param {Number=} data.day The day for this date 28 | * @param {Number=} data.hour The hour for this date 29 | * @param {Number=} data.minute The minute for this date 30 | * @param {Number=} data.second The second for this date 31 | * @param {ICAL.Timezone|ICAL.UtcOffset} zone The timezone to use 32 | * @param {String} icaltype The type for this date/time object 33 | */ 34 | ICAL.VCardTime = function(data, zone, icaltype) { 35 | this.wrappedJSObject = this; 36 | var time = this._time = Object.create(null); 37 | 38 | time.year = null; 39 | time.month = null; 40 | time.day = null; 41 | time.hour = null; 42 | time.minute = null; 43 | time.second = null; 44 | 45 | this.icaltype = icaltype || "date-and-or-time"; 46 | 47 | this.fromData(data, zone); 48 | }; 49 | ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ { 50 | 51 | /** 52 | * The class identifier. 53 | * @constant 54 | * @type {String} 55 | * @default "vcardtime" 56 | */ 57 | icalclass: "vcardtime", 58 | 59 | /** 60 | * The type name, to be used in the jCal object. 61 | * @type {String} 62 | * @default "date-and-or-time" 63 | */ 64 | icaltype: "date-and-or-time", 65 | 66 | /** 67 | * The timezone. This can either be floating, UTC, or an instance of 68 | * ICAL.UtcOffset. 69 | * @type {ICAL.Timezone|ICAL.UtcOFfset} 70 | */ 71 | zone: null, 72 | 73 | /** 74 | * Returns a clone of the vcard date/time object. 75 | * 76 | * @return {ICAL.VCardTime} The cloned object 77 | */ 78 | clone: function() { 79 | return new ICAL.VCardTime(this._time, this.zone, this.icaltype); 80 | }, 81 | 82 | _normalize: function() { 83 | return this; 84 | }, 85 | 86 | /** 87 | * @inheritdoc 88 | */ 89 | utcOffset: function() { 90 | if (this.zone instanceof ICAL.UtcOffset) { 91 | return this.zone.toSeconds(); 92 | } else { 93 | return ICAL.Time.prototype.utcOffset.apply(this, arguments); 94 | } 95 | }, 96 | 97 | /** 98 | * Returns an RFC 6350 compliant representation of this object. 99 | * 100 | * @return {String} vcard date/time string 101 | */ 102 | toICALString: function() { 103 | return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString()); 104 | }, 105 | 106 | /** 107 | * The string representation of this date/time, in jCard form 108 | * (including : and - separators). 109 | * @return {String} 110 | */ 111 | toString: function toString() { 112 | var p2 = ICAL.helpers.pad2; 113 | var y = this.year, m = this.month, d = this.day; 114 | var h = this.hour, mm = this.minute, s = this.second; 115 | 116 | var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null; 117 | var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null; 118 | 119 | var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) + 120 | (hasMonth ? p2(m) : '') + 121 | (hasDay ? '-' + p2(d) : ''); 122 | var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') + 123 | (hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') + 124 | (hasMinute && hasSecond ? ':' : '') + 125 | (hasSecond ? p2(s) : ''); 126 | 127 | var zone; 128 | if (this.zone === ICAL.Timezone.utcTimezone) { 129 | zone = 'Z'; 130 | } else if (this.zone instanceof ICAL.UtcOffset) { 131 | zone = this.zone.toString(); 132 | } else if (this.zone === ICAL.Timezone.localTimezone) { 133 | zone = ''; 134 | } else if (this.zone instanceof ICAL.Timezone) { 135 | var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this)); 136 | zone = offset.toString(); 137 | } else { 138 | zone = ''; 139 | } 140 | 141 | switch (this.icaltype) { 142 | case "time": 143 | return timepart + zone; 144 | case "date-and-or-time": 145 | case "date-time": 146 | return datepart + (timepart == '--' ? '' : 'T' + timepart + zone); 147 | case "date": 148 | return datepart; 149 | } 150 | return null; 151 | } 152 | }); 153 | 154 | /** 155 | * Returns a new ICAL.VCardTime instance from a date and/or time string. 156 | * 157 | * @param {String} aValue The string to create from 158 | * @param {String} aIcalType The type for this instance, e.g. date-and-or-time 159 | * @return {ICAL.VCardTime} The date/time instance 160 | */ 161 | ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) { 162 | function part(v, s, e) { 163 | return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null; 164 | } 165 | var parts = aValue.split('T'); 166 | var dt = parts[0], tmz = parts[1]; 167 | var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : []; 168 | var zone = splitzone[0], tm = splitzone[1]; 169 | 170 | var stoi = ICAL.helpers.strictParseInt; 171 | var dtlen = dt ? dt.length : 0; 172 | var tmlen = tm ? tm.length : 0; 173 | 174 | var hasDashDate = dt && dt[0] == '-' && dt[1] == '-'; 175 | var hasDashTime = tm && tm[0] == '-'; 176 | 177 | var o = { 178 | year: hasDashDate ? null : part(dt, 0, 4), 179 | month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null, 180 | day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null, 181 | 182 | hour: hasDashTime ? null : part(tm, 0, 2), 183 | minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null, 184 | second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null 185 | }; 186 | 187 | if (zone == 'Z') { 188 | zone = ICAL.Timezone.utcTimezone; 189 | } else if (zone && zone[3] == ':') { 190 | zone = ICAL.UtcOffset.fromString(zone); 191 | } else { 192 | zone = null; 193 | } 194 | 195 | return new ICAL.VCardTime(o, zone, aIcalType); 196 | }; 197 | })(); 198 | -------------------------------------------------------------------------------- /lib/ical/period.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 5 | 6 | "use strict"; 7 | 8 | (function() { 9 | /** 10 | * @classdesc 11 | * This class represents the "period" value type, with various calculation 12 | * and manipulation methods. 13 | * 14 | * @description 15 | * The passed data object cannot contain both and end date and a duration. 16 | * 17 | * @class 18 | * @param {Object} aData An object with members of the period 19 | * @param {ICAL.Time=} aData.start The start of the period 20 | * @param {ICAL.Time=} aData.end The end of the period 21 | * @param {ICAL.Duration=} aData.duration The duration of the period 22 | */ 23 | ICAL.Period = function icalperiod(aData) { 24 | this.wrappedJSObject = this; 25 | 26 | if (aData && 'start' in aData) { 27 | if (aData.start && !(aData.start instanceof ICAL.Time)) { 28 | throw new TypeError('.start must be an instance of ICAL.Time'); 29 | } 30 | this.start = aData.start; 31 | } 32 | 33 | if (aData && aData.end && aData.duration) { 34 | throw new Error('cannot accept both end and duration'); 35 | } 36 | 37 | if (aData && 'end' in aData) { 38 | if (aData.end && !(aData.end instanceof ICAL.Time)) { 39 | throw new TypeError('.end must be an instance of ICAL.Time'); 40 | } 41 | this.end = aData.end; 42 | } 43 | 44 | if (aData && 'duration' in aData) { 45 | if (aData.duration && !(aData.duration instanceof ICAL.Duration)) { 46 | throw new TypeError('.duration must be an instance of ICAL.Duration'); 47 | } 48 | this.duration = aData.duration; 49 | } 50 | }; 51 | 52 | ICAL.Period.prototype = { 53 | 54 | /** 55 | * The start of the period 56 | * @type {ICAL.Time} 57 | */ 58 | start: null, 59 | 60 | /** 61 | * The end of the period 62 | * @type {ICAL.Time} 63 | */ 64 | end: null, 65 | 66 | /** 67 | * The duration of the period 68 | * @type {ICAL.Duration} 69 | */ 70 | duration: null, 71 | 72 | /** 73 | * The class identifier. 74 | * @constant 75 | * @type {String} 76 | * @default "icalperiod" 77 | */ 78 | icalclass: "icalperiod", 79 | 80 | /** 81 | * The type name, to be used in the jCal object. 82 | * @constant 83 | * @type {String} 84 | * @default "period" 85 | */ 86 | icaltype: "period", 87 | 88 | /** 89 | * Returns a clone of the duration object. 90 | * 91 | * @return {ICAL.Period} The cloned object 92 | */ 93 | clone: function() { 94 | return ICAL.Period.fromData({ 95 | start: this.start ? this.start.clone() : null, 96 | end: this.end ? this.end.clone() : null, 97 | duration: this.duration ? this.duration.clone() : null 98 | }); 99 | }, 100 | 101 | /** 102 | * Calculates the duration of the period, either directly or by subtracting 103 | * start from end date. 104 | * 105 | * @return {ICAL.Duration} The calculated duration 106 | */ 107 | getDuration: function duration() { 108 | if (this.duration) { 109 | return this.duration; 110 | } else { 111 | return this.end.subtractDate(this.start); 112 | } 113 | }, 114 | 115 | /** 116 | * Calculates the end date of the period, either directly or by adding 117 | * duration to start date. 118 | * 119 | * @return {ICAL.Time} The calculated end date 120 | */ 121 | getEnd: function() { 122 | if (this.end) { 123 | return this.end; 124 | } else { 125 | var end = this.start.clone(); 126 | end.addDuration(this.duration); 127 | return end; 128 | } 129 | }, 130 | 131 | /** 132 | * The string representation of this period. 133 | * @return {String} 134 | */ 135 | toString: function toString() { 136 | return this.start + "/" + (this.end || this.duration); 137 | }, 138 | 139 | /** 140 | * The jCal representation of this period type. 141 | * @return {Object} 142 | */ 143 | toJSON: function() { 144 | return [this.start.toString(), (this.end || this.duration).toString()]; 145 | }, 146 | 147 | /** 148 | * The iCalendar string representation of this period. 149 | * @return {String} 150 | */ 151 | toICALString: function() { 152 | return this.start.toICALString() + "/" + 153 | (this.end || this.duration).toICALString(); 154 | } 155 | }; 156 | 157 | /** 158 | * Creates a new {@link ICAL.Period} instance from the passed string. 159 | * 160 | * @param {String} str The string to parse 161 | * @param {ICAL.Property} prop The property this period will be on 162 | * @return {ICAL.Period} The created period instance 163 | */ 164 | ICAL.Period.fromString = function fromString(str, prop) { 165 | var parts = str.split('/'); 166 | 167 | if (parts.length !== 2) { 168 | throw new Error( 169 | 'Invalid string value: "' + str + '" must contain a "/" char.' 170 | ); 171 | } 172 | 173 | var options = { 174 | start: ICAL.Time.fromDateTimeString(parts[0], prop) 175 | }; 176 | 177 | var end = parts[1]; 178 | 179 | if (ICAL.Duration.isValueString(end)) { 180 | options.duration = ICAL.Duration.fromString(end); 181 | } else { 182 | options.end = ICAL.Time.fromDateTimeString(end, prop); 183 | } 184 | 185 | return new ICAL.Period(options); 186 | }; 187 | 188 | /** 189 | * Creates a new {@link ICAL.Period} instance from the given data object. 190 | * The passed data object cannot contain both and end date and a duration. 191 | * 192 | * @param {Object} aData An object with members of the period 193 | * @param {ICAL.Time=} aData.start The start of the period 194 | * @param {ICAL.Time=} aData.end The end of the period 195 | * @param {ICAL.Duration=} aData.duration The duration of the period 196 | * @return {ICAL.Period} The period instance 197 | */ 198 | ICAL.Period.fromData = function fromData(aData) { 199 | return new ICAL.Period(aData); 200 | }; 201 | 202 | /** 203 | * Returns a new period instance from the given jCal data array. The first 204 | * member is always the start date string, the second member is either a 205 | * duration or end date string. 206 | * 207 | * @param {Array} aData The jCal data array 208 | * @param {ICAL.Property} aProp The property this jCal data is on 209 | * @param {Boolean} aLenient If true, data value can be both date and date-time 210 | * @return {ICAL.Period} The period instance 211 | */ 212 | ICAL.Period.fromJSON = function(aData, aProp, aLenient) { 213 | function fromDateOrDateTimeString(aValue, aProp) { 214 | if (aLenient) { 215 | return ICAL.Time.fromString(aValue, aProp); 216 | } else { 217 | return ICAL.Time.fromDateTimeString(aValue, aProp); 218 | } 219 | } 220 | 221 | if (ICAL.Duration.isValueString(aData[1])) { 222 | return ICAL.Period.fromData({ 223 | start: fromDateOrDateTimeString(aData[0], aProp), 224 | duration: ICAL.Duration.fromString(aData[1]) 225 | }); 226 | } else { 227 | return ICAL.Period.fromData({ 228 | start: fromDateOrDateTimeString(aData[0], aProp), 229 | end: fromDateOrDateTimeString(aData[1], aProp) 230 | }); 231 | } 232 | }; 233 | })(); 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsical - Javascript parser for rfc5545 2 | 3 | This is a library to parse the iCalendar format defined in 4 | [rfc5545](http://tools.ietf.org/html/rfc5545), as well as similar formats like 5 | vCard. 6 | 7 | There are still some issues to be taken care of, but the library works for most 8 | cases. If you would like to help out and would like to discuss any API changes, 9 | please [contact me](mailto:mozilla@kewis.ch) or create an issue. 10 | 11 | The initial goal was to use it as a replacement for libical in the [Mozilla 12 | Calendar Project](http://www.mozilla.org/projects/calendar/), but the library 13 | has been written with the web in mind. This library is now called ICAL.js and 14 | enables you to do all sorts of cool experiments with calendar data and the web. 15 | I am also aiming for a caldav.js when this is done. Most algorithms here were 16 | taken from [libical](https://github.com/libical/libical). If you are bugfixing 17 | this library, please check if the fix can be upstreamed to libical. 18 | 19 | [![Build Status](https://secure.travis-ci.org/mozilla-comm/ical.js.png?branch=master)](http://travis-ci.org/mozilla-comm/ical.js) [![Coverage Status](https://coveralls.io/repos/mozilla-comm/ical.js/badge.svg)](https://coveralls.io/r/mozilla-comm/ical.js) [![npm version](https://badge.fury.io/js/ical.js.svg)](http://badge.fury.io/js/ical.js) [![CDNJS](https://img.shields.io/cdnjs/v/ical.js.svg)](https://cdnjs.com/libraries/ical.js) 20 | [![Greenkeeper badge](https://badges.greenkeeper.io/mozilla-comm/ical.js.svg)](https://greenkeeper.io/) [![Dependency Status](https://david-dm.org/mozilla-comm/ical.js.svg)](https://david-dm.org/mozilla-comm/ical.js) [![devDependency Status](https://david-dm.org/mozilla-comm/ical.js/dev-status.svg)](https://david-dm.org/mozilla-comm/ical.js?type=dev) 21 | 22 | ## Sandbox and Validator 23 | 24 | If you want to try out ICAL.js right now, there is a 25 | [jsfiddle](http://jsfiddle.net/kewisch/227efboL/) set up and ready to use. Read 26 | on for documentation and example links. 27 | 28 | There is also a validator that demonstrates how to use the library in a webpage 29 | in the [sandbox/](https://github.com/mozilla-comm/ical.js/tree/master/sandbox) 30 | subdirectory. 31 | 32 | [Try the validator online](http://mozilla-comm.github.com/ical.js/validator.html), it always uses the latest copy of ICAL.js. 33 | 34 | ## Installing 35 | 36 | You can install ICAL.js via [npm](https://www.npmjs.com/), if you would like to 37 | use it in Node.js: 38 | ``` 39 | npm install ical.js 40 | ``` 41 | 42 | Alternatively, it is also available via [bower](http://bower.io/) for front-end 43 | development: 44 | ``` 45 | bower install ical.js 46 | ``` 47 | 48 | ICAL.js has no dependencies and uses fairly basic JavaScript. Therefore, it 49 | should work in all versions of Node.js and modern browsers. It does use getters 50 | and setters, so the minimum version of Internet Explorer is 9. 51 | 52 | ## Documentation 53 | 54 | For a few guides with code samples, please check out 55 | [the wiki](https://github.com/mozilla-comm/ical.js/wiki). If you prefer, 56 | full API documentation [is available here](http://mozilla-comm.github.io/ical.js/api/). 57 | If you are missing anything, please don't hesitate to create an issue. 58 | 59 | ## Developing 60 | 61 | To contribute to ICAL.js you need to set up the development environment. This 62 | requires Node.js 8.x or later and grunt. Run the following steps to get 63 | started. 64 | 65 | Preferred way (to match building and packaging with official process): 66 | ``` 67 | yarn global add grunt-cli # Might need to run with sudo 68 | yarn --frozen-lockfile 69 | ``` 70 | 71 | Alternative way: 72 | ``` 73 | npm install -g grunt-cli # Might need to run with sudo 74 | npm install . 75 | ``` 76 | 77 | You can now dive into the code, run the tests and check coverage. 78 | 79 | ### Tests 80 | 81 | Tests can either be run via Node.js or in the browser, but setting up the testing 82 | infrastructure requires [node](https://github.com/nodejs/node). More 83 | information on how to set up and run tests can be found on 84 | [the wiki](https://github.com/mozilla-comm/ical.js/wiki/Running-Tests). 85 | 86 | #### in Node.js 87 | 88 | The quickest way to execute tests is using Node.js. Running the following command 89 | will run all test suites: performance, acceptance and unit tests. 90 | 91 | grunt test-node 92 | 93 | You can also select a single suite, or run a single test. 94 | 95 | grunt test-node:performance 96 | grunt test-node:acceptance 97 | grunt test-node:unit 98 | 99 | grunt test-node:single --test=test/parse_test.js 100 | 101 | Appending the `--debug` option to any of the above commands will run the 102 | test(s) with node-inspector. It will start the debugging server and open it in 103 | Chrome or Opera, depending on what you have installed. The tests will pause 104 | before execution starts so you can set breakpoints and debug the unit tests 105 | you are working on. 106 | 107 | If you run the performance tests comparison will be done between the current 108 | working version (latest), a previous build of ICAL.js (previous) and the 109 | unchanged copy of build/ical.js (from the master branch). See 110 | [the wiki](https://github.com/mozilla-comm/ical.js/wiki/Running-Tests) for more 111 | details. 112 | 113 | #### in the browser 114 | 115 | To run the browser tests, we are currently using [karma](http://karma-runner.github.io/). 116 | To run tests with karma, you can run the following targets: 117 | 118 | grunt test-browser # run all tests 119 | grunt karma:unit # run only the unit tests 120 | grunt karma:acceptance # run only the acceptance tests 121 | 122 | Now you can visit [http://localhost:9876](http://localhost:9876) in your 123 | browser. The test output will be shown in the console you started the grunt 124 | task from. You can also run a single test: 125 | 126 | grunt karma:single --test=test/parse_test.js 127 | 128 | The mentioned targets all run the tests from start to finish. If you would like 129 | to debug the tests instead, you can add the `--debug` flag. Once you open the 130 | browser there will be a "debug" button. Clicking on the button opens am empty 131 | page, but if you open your browser's developer tools you will see the test 132 | output. You can reload this page as often as you want until all tests are 133 | running. 134 | 135 | Last off, if you add the `--remote` option, karma will listen on all 136 | interfaces. This is useful if you are running the browser to test in a VM, for 137 | example when using [Internet Exporer VM images](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/). 138 | 139 | ### Code Coverage 140 | ICAL.js is set up to calculate code coverage. You can 141 | [view the coverage results](https://coveralls.io/r/mozilla-comm/ical.js) 142 | online, or run them locally to make sure new code is covered. Running `grunt 143 | coverage` will run the unit test suite measuring coverage. You can then open 144 | `coverage/lcov-report/index.html` to view the results in your browser. 145 | 146 | ### Linters 147 | To make sure all ICAL.js code uses a common style, please run the linters using 148 | `grunt linters`. Please make sure you fix any issues shown by this command 149 | before sending a pull request. 150 | 151 | ### Documentation 152 | You can generate the documentation locally, this is also helpful to ensure the 153 | jsdoc you have written is valid. To do so, run `grunt jsdoc`. You will find the 154 | output in the `api/` subdirectory. 155 | 156 | ### Packaging 157 | When you are done with your work, you can run `grunt package` to create the 158 | single-file build for use in the browser, including its minified counterpart 159 | and the source map. 160 | 161 | ## License 162 | ical.js is licensed under the 163 | [Mozilla Public License](https://www.mozilla.org/MPL/2.0/), version 2.0. 164 | -------------------------------------------------------------------------------- /test/parse_test.js: -------------------------------------------------------------------------------- 1 | suite('parserv2', function() { 2 | 3 | var subject; 4 | setup(function() { 5 | subject = ICAL.parse; 6 | }); 7 | 8 | /** 9 | * Full parser tests fetch two resources 10 | * (one to parse, one is expected 11 | */ 12 | suite('full parser tests', function() { 13 | var root = 'test/parser/'; 14 | var list = [ 15 | // icalendar tests 16 | 'rfc.ics', 17 | 'single_empty_vcalendar.ics', 18 | 'property_params.ics', 19 | 'newline_junk.ics', 20 | 'unfold_properties.ics', 21 | 'quoted_params.ics', 22 | 'multivalue.ics', 23 | 'values.ics', 24 | 'recur.ics', 25 | 'base64.ics', 26 | 'dates.ics', 27 | 'time.ics', 28 | 'boolean.ics', 29 | 'float.ics', 30 | 'integer.ics', 31 | 'period.ics', 32 | 'utc_offset.ics', 33 | 'component.ics', 34 | 'tzid_with_gmt.ics', 35 | 'multiple_root_components.ics', 36 | 37 | // vcard tests 38 | 'vcard.vcf', 39 | 'vcard_author.vcf', 40 | 'vcard3.vcf' 41 | ]; 42 | 43 | list.forEach(function(path) { 44 | suite(path.replace('_', ' '), function() { 45 | var input; 46 | var expected; 47 | 48 | // fetch ical 49 | setup(function(done) { 50 | testSupport.load(root + path, function(err, data) { 51 | if (err) { 52 | return done(new Error('failed to load ics')); 53 | } 54 | input = data; 55 | done(); 56 | }); 57 | }); 58 | 59 | // fetch json 60 | setup(function(done) { 61 | testSupport.load(root + path.replace(/vcf|ics$/, 'json'), function(err, data) { 62 | if (err) { 63 | return done(new Error('failed to load .json')); 64 | } 65 | try { 66 | expected = JSON.parse(data.trim()); 67 | } catch (e) { 68 | return done( 69 | new Error('expect json is invalid: \n\n' + data) 70 | ); 71 | } 72 | done(); 73 | }); 74 | }); 75 | 76 | function jsonEqual(actual, expected) { 77 | assert.deepEqual( 78 | actual, 79 | expected, 80 | 'hint use: ' + 81 | 'http://tlrobinson.net/projects/javascript-fun/jsondiff/\n\n' + 82 | '\nexpected:\n\n' + 83 | JSON.stringify(actual, null, 2) + 84 | '\n\n to equal:\n\n ' + 85 | JSON.stringify(expected, null, 2) + '\n\n' 86 | ); 87 | } 88 | 89 | test('round-trip', function() { 90 | var parsed = subject(input); 91 | var ical = ICAL.stringify(parsed); 92 | 93 | // NOTE: this is not an absolute test that serialization 94 | // works as our parser should be error tolerant and 95 | // its remotely possible that we consistently produce 96 | // ICAL that only we can parse. 97 | jsonEqual( 98 | subject(ical), 99 | expected 100 | ); 101 | }); 102 | 103 | test('compare', function() { 104 | var actual = subject(input); 105 | jsonEqual(actual, expected); 106 | }); 107 | }); 108 | }); 109 | }); 110 | 111 | suite('invalid ical', function() { 112 | 113 | test('invalid property', function() { 114 | var ical = 'BEGIN:VCALENDAR\n'; 115 | // no param or value token 116 | ical += 'DTSTART\n'; 117 | ical += 'DESCRIPTION:1\n'; 118 | ical += 'END:VCALENDAR'; 119 | 120 | assert.throws(function() { 121 | subject(ical); 122 | }, /invalid line/); 123 | }); 124 | 125 | test('invalid quoted params', function() { 126 | var ical = 'BEGIN:VCALENDAR\n'; 127 | ical += 'X-FOO;BAR="quoted\n'; 128 | // an invalid newline inside quoted parameter 129 | ical += 'params";FOO=baz:realvalue\n'; 130 | ical += 'END:VCALENDAR'; 131 | 132 | assert.throws(function() { 133 | subject(ical); 134 | }, /invalid line/); 135 | }); 136 | 137 | test('missing value with param delimiter', function() { 138 | var ical = 'BEGIN:VCALENDAR\n' + 139 | 'X-FOO;\n'; 140 | assert.throws(function() { 141 | subject(ical); 142 | }, "Invalid parameters in"); 143 | }); 144 | 145 | test('missing param name ', function() { 146 | var ical = 'BEGIN:VCALENDAR\n' + 147 | 'X-FOO;=\n'; 148 | assert.throws(function() { 149 | subject(ical); 150 | }, "Empty parameter name in"); 151 | }); 152 | 153 | test('missing param value', function() { 154 | var ical = 'BEGIN:VCALENDAR\n' + 155 | 'X-FOO;BAR=\n'; 156 | assert.throws(function() { 157 | subject(ical); 158 | }, "Missing parameter value in"); 159 | }); 160 | 161 | test('missing component end', function() { 162 | var ical = 'BEGIN:VCALENDAR\n'; 163 | ical += 'BEGIN:VEVENT\n'; 164 | ical += 'BEGIN:VALARM\n'; 165 | ical += 'DESCRIPTION: foo\n'; 166 | ical += 'END:VALARM'; 167 | // ended calendar before event 168 | ical += 'END:VCALENDAR'; 169 | 170 | assert.throws(function() { 171 | subject(ical); 172 | }, /invalid/); 173 | }); 174 | 175 | }); 176 | 177 | suite('#_parseParameters', function() { 178 | test('with processed text', function() { 179 | var input = ';FOO=x\\na'; 180 | var expected = { 181 | 'foo': 'x\na' 182 | }; 183 | 184 | assert.deepEqual( 185 | subject._parseParameters(input, 0, ICAL.design.defaultSet)[0], 186 | expected 187 | ); 188 | }); 189 | 190 | test('with multiple vCard TYPE parameters', function() { 191 | var input = ';TYPE=work;TYPE=voice'; 192 | var expected = { 193 | 'type': ['work', 'voice'] 194 | }; 195 | 196 | assert.deepEqual( 197 | subject._parseParameters(input, 0, ICAL.design.components.vcard)[0], 198 | expected 199 | ); 200 | }); 201 | 202 | test('with multiple iCalendar MEMBER parameters', function() { 203 | var input = ';MEMBER="urn:one","urn:two";MEMBER="urn:three"'; 204 | var expected = { 205 | 'member': ['urn:one', 'urn:two', 'urn:three'] 206 | }; 207 | 208 | assert.deepEqual( 209 | subject._parseParameters(input, 0, ICAL.design.components.vevent)[0], 210 | expected 211 | ); 212 | }); 213 | 214 | test('with comma in singleValue parameter', function() { 215 | var input = ';LABEL="A, B"'; 216 | var expected = { 217 | 'label': 'A, B' 218 | }; 219 | 220 | assert.deepEqual( 221 | subject._parseParameters(input, 0, ICAL.design.components.vcard)[0], 222 | expected 223 | ); 224 | }); 225 | 226 | test('with comma in singleValue parameter after multiValue parameter', function() { 227 | // TYPE allows multiple values, whereas LABEL doesn't. 228 | var input = ';TYPE=home;LABEL="A, B"'; 229 | var expected = { 230 | 'type': 'home', 231 | 'label': 'A, B' 232 | }; 233 | 234 | assert.deepEqual( 235 | subject._parseParameters(input, 0, ICAL.design.components.vcard)[0], 236 | expected 237 | ); 238 | }); 239 | }); 240 | 241 | test('#_parseMultiValue', function() { 242 | var values = 'woot\\, category,foo,bar,baz'; 243 | var result = []; 244 | assert.deepEqual( 245 | subject._parseMultiValue(values, ',', 'text', result, null, ICAL.design.defaultSet), 246 | ['woot, category', 'foo', 'bar', 'baz'] 247 | ); 248 | }); 249 | 250 | suite('#_parseValue', function() { 251 | test('text', function() { 252 | var value = 'start \\n next'; 253 | var expected = 'start \n next'; 254 | 255 | assert.equal( 256 | subject._parseValue(value, 'text', ICAL.design.defaultSet), 257 | expected 258 | ); 259 | }); 260 | }); 261 | 262 | suite('#_eachLine', function() { 263 | 264 | function unfold(input) { 265 | var result = []; 266 | 267 | subject._eachLine(input, function(err, line) { 268 | result.push(line); 269 | }); 270 | 271 | return result; 272 | } 273 | 274 | test('unfold single with \\r\\n', function() { 275 | var input = 'foo\r\n bar'; 276 | var expected = ['foobar']; 277 | 278 | assert.deepEqual(unfold(input), expected); 279 | }); 280 | 281 | test('with \\n', function() { 282 | var input = 'foo\nbar\n baz'; 283 | var expected = [ 284 | 'foo', 285 | 'bar baz' 286 | ]; 287 | 288 | assert.deepEqual(unfold(input), expected); 289 | }); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = function(grunt) { 6 | function loadOptionalTask(name) { 7 | var root = path.resolve('node_modules'); 8 | var tasksdir = path.join(root, name, 'tasks'); 9 | if (grunt.file.exists(tasksdir)) { 10 | grunt.loadNpmTasks(name); 11 | } 12 | } 13 | 14 | var pkg = grunt.file.readJSON('package.json'); 15 | grunt.initConfig({ 16 | pkg: pkg, 17 | libinfo: { 18 | cwd: 'lib/ical', 19 | doc: 'api', 20 | files: [ 21 | 'helpers.js', 'design.js', 'stringify.js', 'parse.js', 'component.js', 22 | 'property.js', 'utc_offset.js', 'binary.js', 'period.js', 'duration.js', 23 | 'timezone.js', 'timezone_service.js', 'time.js', 'vcard_time.js', 24 | 'recur.js', 'recur_iterator.js', 'recur_expansion.js', 'event.js', 25 | 'component_parser.js' 26 | ], 27 | test: { 28 | head: ['test/helper.js'], 29 | unit: ['test/*_test.js'], 30 | acceptance: ['test/acceptance/*_test.js'], 31 | performance: ['test/performance/*_test.js'] 32 | }, 33 | validator: { 34 | dev: 'https://rawgit.com/mozilla-comm/ical.js/master/build/ical.js', 35 | prod: 'https://cdn.rawgit.com/mozilla-comm/ical.js/<%= travis.commit %>/build/ical.js', 36 | dest: 'validator.html' 37 | } 38 | }, 39 | 40 | concat: { 41 | options: { 42 | separator: '' 43 | }, 44 | 45 | dist: { 46 | options: { 47 | process: function(src, filepath) { 48 | return src.replace('"use strict";', ''); 49 | } 50 | }, 51 | src: ['<%= libinfo.absfiles %>'], 52 | dest: 'build/ical.js' 53 | }, 54 | 55 | validator: { 56 | options: { 57 | process: function(src, filepath) { 58 | return src.replace(grunt.config('libinfo.validator.dev'), grunt.config('libinfo.validator.prod')); 59 | } 60 | }, 61 | src: ['sandbox/validator.html'], 62 | dest: '<%= libinfo.validator.dest %>' 63 | } 64 | }, 65 | 66 | mocha_istanbul: { 67 | coverage: { 68 | src: ['<%= libinfo.test.unit %>', '<%= libinfo.test.acceptance %>'], 69 | options: { 70 | root: './lib/ical/', 71 | require: ['<%= libinfo.test.head %>'], 72 | reporter: 'dot', 73 | ui: 'tdd' 74 | } 75 | } 76 | }, 77 | 78 | coveralls: { 79 | options: { 80 | force: true 81 | }, 82 | unit: { 83 | src: './coverage/lcov.info' 84 | } 85 | }, 86 | 87 | 'node-inspector': { 88 | test: { 89 | hidden: ['node_modules'] 90 | } 91 | }, 92 | 93 | concurrent: { 94 | all: ['mochacli:performance', 'mochacli:acceptance', 'mochacli:unit', 'node-inspector'], 95 | unit: ['mochacli:unit', 'node-inspector'], 96 | acceptance: ['mochacli:acceptance', 'node-inspector'], 97 | single: ['mochacli:single', 'node-inspector'] 98 | }, 99 | 100 | eslint: { 101 | src: ['<%= libinfo.absfiles %>'] 102 | }, 103 | 104 | mochacli: { 105 | options: { 106 | ui: 'tdd', 107 | require: ['<%= libinfo.test.head %>'], 108 | 'debug-brk': grunt.option('debug'), 109 | reporter: grunt.option('reporter') || 'spec' 110 | }, 111 | performance: { 112 | src: ['<%= libinfo.test.performance %>'] 113 | }, 114 | acceptance: { 115 | src: ['<%= libinfo.test.acceptance %>'] 116 | }, 117 | unit: { 118 | src: ['<%= libinfo.test.unit %>'] 119 | }, 120 | single: { 121 | src: [grunt.option('test')] 122 | } 123 | }, 124 | 125 | karma: { 126 | options: { 127 | singleRun: true, 128 | hostname: grunt.option('remote') ? '0.0.0.0' : 'localhost', 129 | port: 9876, 130 | colors: true, 131 | basePath: '', 132 | logLevel: grunt.option('verbose') ? 'DEBUG' : 'INFO', 133 | autoWatch: false, 134 | captureTimeout: 240000, 135 | browserNoActivityTimeout: 120000, 136 | frameworks: ['mocha', 'chai'], 137 | client: { 138 | mocha: { 139 | ui: 'tdd' 140 | } 141 | }, 142 | files: [ 143 | { pattern: 'samples/**/*.ics', included: false }, 144 | { pattern: 'test/parser/*', included: false }, 145 | '<%= libinfo.relfiles %>', 146 | '<%= libinfo.test.head %>' 147 | ] 148 | }, 149 | ci: { 150 | exitOnFailure: false, 151 | customLaunchers: pkg.saucelabs, 152 | browsers: Object.keys(pkg.saucelabs), 153 | reporters: ['saucelabs', 'spec'], 154 | sauceLabs: { 155 | testName: 'ICAL.js', 156 | startConnect: true 157 | }, 158 | 159 | files: { 160 | src: ['<%= libinfo.test.unit %>'] 161 | } 162 | }, 163 | single: { 164 | singleRun: !grunt.option('debug'), 165 | reporters: ['spec'], 166 | files: { 167 | src: [grunt.option('test')] 168 | } 169 | }, 170 | unit: { 171 | singleRun: !grunt.option('debug'), 172 | reporters: ['spec'], 173 | files: { 174 | src: ['<%= libinfo.test.unit %>'] 175 | } 176 | }, 177 | acceptance: { 178 | singleRun: !grunt.option('debug'), 179 | reporters: ['spec'], 180 | files: { 181 | src: ['<%= libinfo.test.acceptance %>'] 182 | } 183 | } 184 | }, 185 | 186 | uglify: { 187 | options: { 188 | sourceMap: true, 189 | compress: {}, 190 | mangle: { 191 | reserved: ['ICAL'] 192 | } 193 | }, 194 | dist: { 195 | files: { 196 | 'build/ical.min.js': ['build/ical.js'] 197 | } 198 | } 199 | }, 200 | release: { 201 | options: { 202 | tagName: 'v<%=version%>', 203 | tagMessage: 'v<%=version%>', 204 | additionalFiles: ['bower.json'], 205 | github: { 206 | repo: 'mozilla-comm/ical.js', 207 | accessTokenVar: 'GITHUB_TOKEN' 208 | } 209 | } 210 | }, 211 | jsdoc: { 212 | dist: { 213 | src: ['<%= libinfo.absfiles %>', 'README.md'], 214 | options: { 215 | destination: '<%= libinfo.doc %>', 216 | template: './node_modules/minami/', 217 | private: false 218 | } 219 | } 220 | }, 221 | 222 | 'gh-pages': { 223 | options: { 224 | clone: 'ghpages-stage', 225 | only: '<%= libinfo.doc %>', 226 | user: { 227 | name: 'Travis CI', 228 | email: 'builds@travis-ci.org' 229 | }, 230 | repo: 'git@github.com:mozilla-comm/ical.js.git', 231 | message: 'Update API documentation and validator for <%= travis.commit %>' 232 | }, 233 | src: ['<%= libinfo.doc %>/**', '<%= libinfo.validator.dest %>'] 234 | } 235 | }); 236 | 237 | grunt.config.set('libinfo.absfiles', grunt.config.get('libinfo.files').map(function(f) { 238 | return path.join(grunt.config.get('libinfo.cwd'), f); 239 | })); 240 | grunt.config.set('libinfo.relfiles', grunt.config.get('libinfo.files').map(function(f) { 241 | return path.join("lib", "ical", f); 242 | })); 243 | 244 | grunt.loadNpmTasks('grunt-concurrent'); 245 | grunt.loadNpmTasks('grunt-contrib-concat'); 246 | grunt.loadNpmTasks('grunt-contrib-uglify'); 247 | grunt.loadNpmTasks('grunt-coveralls'); 248 | grunt.loadNpmTasks('grunt-gh-pages'); 249 | grunt.loadNpmTasks('grunt-jsdoc'); 250 | grunt.loadNpmTasks('grunt-karma'); 251 | grunt.loadNpmTasks('grunt-mocha-cli'); 252 | grunt.loadNpmTasks('grunt-mocha-istanbul'); 253 | grunt.loadNpmTasks('grunt-release'); 254 | grunt.loadNpmTasks('gruntify-eslint'); 255 | 256 | loadOptionalTask('grunt-node-inspector'); 257 | 258 | grunt.loadTasks('tasks'); 259 | 260 | grunt.registerTask('default', ['package']); 261 | grunt.registerTask('package', ['concat:dist', 'uglify']); 262 | grunt.registerTask('coverage', 'mocha_istanbul'); 263 | grunt.registerTask('linters', ['eslint']); 264 | grunt.registerTask('test-browser', ['karma:unit', 'karma:acceptance']); 265 | grunt.registerTask('test', ['test-browser', 'test-node']); 266 | 267 | grunt.registerTask('ghpages-ci', ['jsdoc', 'concat:validator', 'run-on-master-leader:run-with-env:GITHUB_SSH_KEY:gh-pages']); 268 | grunt.registerTask('unit-ci', ['test-node:unit', 'test-node:acceptance', 'run-on-master-leader:karma:ci']); 269 | grunt.registerTask('coverage-ci', ['coverage', 'coveralls']); 270 | grunt.registerTask('test-ci', ['linters', 'unit-ci', 'coverage-ci', 'ghpages-ci']); 271 | 272 | // Additional tasks: 273 | // - tests.js: performance-update, test-node 274 | // - timezones.js: timezones 275 | }; 276 | -------------------------------------------------------------------------------- /test/recur_expansion_test.js: -------------------------------------------------------------------------------- 1 | suite('recur_expansion', function() { 2 | var component; 3 | var subject; 4 | var icsData = {}; 5 | var primary; 6 | 7 | function createSubject(file) { 8 | 9 | testSupport.defineSample(file, function(data) { 10 | icsData[file] = data; 11 | }); 12 | 13 | setup(function(done) { 14 | var exceptions = []; 15 | 16 | var parse = new ICAL.ComponentParser(); 17 | 18 | parse.onevent = function(event) { 19 | if (event.isRecurrenceException()) { 20 | exceptions.push(event); 21 | } else { 22 | primary = event; 23 | } 24 | }; 25 | 26 | parse.oncomplete = function() { 27 | exceptions.forEach(primary.relateException, primary); 28 | subject = new ICAL.RecurExpansion({ 29 | component: primary.component, 30 | dtstart: primary.startDate 31 | }); 32 | 33 | done(); 34 | } 35 | 36 | parse.process(icsData[file]); 37 | }); 38 | } 39 | 40 | createSubject('recur_instances.ics'); 41 | 42 | suite('initialization', function() { 43 | test('successful', function() { 44 | assert.deepEqual( 45 | new Date(2012, 9, 2, 10), 46 | subject.last.toJSDate() 47 | ); 48 | 49 | assert.instanceOf(subject.ruleIterators, Array); 50 | assert.ok(subject.exDates); 51 | }); 52 | 53 | test('invalid', function() { 54 | assert.throws(function() { 55 | new ICAL.RecurExpansion({}); 56 | }, ".dtstart (ICAL.Time) must be given"); 57 | assert.throws(function() { 58 | new ICAL.RecurExpansion({ 59 | dtstart: ICAL.Time.now() 60 | }); 61 | }, ".ruleIterators or .component must be given"); 62 | }); 63 | 64 | test('default', function() { 65 | var dtstart = ICAL.Time.fromData({ 66 | year: 2012, 67 | month: 2, 68 | day: 2 69 | }); 70 | var subject = new ICAL.RecurExpansion({ 71 | dtstart: dtstart, 72 | ruleIterators: [] 73 | }); 74 | 75 | assert.lengthOf(subject.ruleDates, 0); 76 | assert.lengthOf(subject.exDates, 0); 77 | assert.isFalse(subject.complete); 78 | 79 | assert.deepEqual(subject.toJSON(), { 80 | ruleIterators: [], 81 | ruleDates: [], 82 | exDates: [], 83 | ruleDateInc: undefined, 84 | exDateInc: undefined, 85 | dtstart: dtstart.toJSON(), 86 | last: dtstart.toJSON(), 87 | complete: false 88 | }); 89 | }); 90 | }); 91 | 92 | suite('#_ensureRules', function() { 93 | test('.ruleDates', function() { 94 | var expected = [ 95 | new Date(2012, 10, 5, 10), 96 | new Date(2012, 10, 10, 10), 97 | new Date(2012, 10, 30, 10) 98 | ]; 99 | 100 | 101 | var dates = subject.ruleDates.map(function(time) { 102 | return time.toJSDate(); 103 | }); 104 | 105 | assert.deepEqual(expected, dates); 106 | }); 107 | 108 | test('.exDates', function() { 109 | var expected = [ 110 | new Date(2012, 11, 4, 10), 111 | new Date(2013, 1, 5, 10), 112 | new Date(2013, 3, 2, 10) 113 | ]; 114 | 115 | var dates = subject.exDates.map(function(time) { 116 | return time.toJSDate(); 117 | }); 118 | 119 | assert.deepEqual(expected, dates); 120 | }); 121 | }); 122 | 123 | suite('#_nextRecurrenceIter', function() { 124 | var component; 125 | 126 | setup(function() { 127 | // setup a clean component with no rules 128 | component = primary.component.toJSON(); 129 | component = new ICAL.Component(component); 130 | 131 | // Simulate a more complicated event by using 132 | // the original as a base and adding more complex rrule's 133 | component.removeProperty('rrule'); 134 | }); 135 | 136 | test('when rule ends', function() { 137 | var start = { 138 | year: 2012, 139 | month: 1, 140 | day: 1 141 | }; 142 | 143 | component.removeAllProperties('rdate'); 144 | component.removeAllProperties('exdate'); 145 | component.addPropertyWithValue('rrule', { freq: "WEEKLY", count: 3, byday: ["SU"] }); 146 | 147 | var subject = new ICAL.RecurExpansion({ 148 | component: component, 149 | dtstart: start 150 | }); 151 | 152 | var expected = [ 153 | new Date(2012, 0, 1), 154 | new Date(2012, 0, 8), 155 | new Date(2012, 0, 15) 156 | ]; 157 | 158 | var max = 10; 159 | var i = 0; 160 | var next; 161 | var dates = []; 162 | 163 | while (i++ <= max && (next = subject.next())) { 164 | dates.push(next.toJSDate()); 165 | } 166 | 167 | assert.deepEqual(dates, expected); 168 | }); 169 | 170 | test('multiple rules', function() { 171 | component.addPropertyWithValue('rrule', { freq: "MONTHLY", bymonthday: [13] }); 172 | component.addPropertyWithValue('rrule', { freq: "WEEKLY", byday: ["TH"] }); 173 | 174 | var start = ICAL.Time.fromData({ 175 | year: 2012, 176 | month: 2, 177 | day: 2 178 | }); 179 | 180 | var subject = new ICAL.RecurExpansion({ 181 | component: component, 182 | dtstart: start 183 | }); 184 | 185 | var expected = [ 186 | new Date(2012, 1, 2), 187 | new Date(2012, 1, 9), 188 | new Date(2012, 1, 13), 189 | new Date(2012, 1, 16), 190 | new Date(2012, 1, 23) 191 | ]; 192 | 193 | var inc = 0; 194 | var max = expected.length; 195 | var next; 196 | var dates = []; 197 | 198 | while (inc++ < max) { 199 | next = subject._nextRecurrenceIter(); 200 | dates.push(next.last.toJSDate()); 201 | next.next(); 202 | } 203 | 204 | assert.deepEqual(dates, expected); 205 | }); 206 | 207 | }); 208 | 209 | suite('#next', function() { 210 | // I use JS dates widely because its much easier 211 | // to compare them via chai's deepEquals function 212 | var expected = [ 213 | new Date(2012, 9, 2, 10), 214 | new Date(2012, 10, 5, 10), 215 | new Date(2012, 10, 6, 10), 216 | new Date(2012, 10, 10, 10), 217 | new Date(2012, 10, 30, 10), 218 | new Date(2013, 0, 1, 10) 219 | ]; 220 | 221 | test('6 items', function() { 222 | var dates = []; 223 | var max = 6; 224 | var inc = 0; 225 | var next; 226 | 227 | while (inc++ < max && (next = subject.next())) { 228 | dates.push(next.toJSDate()); 229 | } 230 | 231 | assert.deepEqual( 232 | dates, 233 | expected 234 | ); 235 | }); 236 | }); 237 | 238 | suite('#next - finite', function() { 239 | createSubject('recur_instances_finite.ics'); 240 | 241 | test('until complete', function() { 242 | var max = 100; 243 | var inc = 0; 244 | var next; 245 | 246 | var dates = []; 247 | var expected = [ 248 | new Date(2012, 9, 2, 10), 249 | new Date(2012, 10, 5, 10), 250 | new Date(2012, 10, 6, 10), 251 | new Date(2012, 10, 10, 10), 252 | new Date(2012, 11, 4, 10) 253 | ]; 254 | 255 | while (inc++ < max && (next = subject.next())) { 256 | dates.push(next.toJSDate()); 257 | } 258 | 259 | // round trip 260 | subject = new ICAL.RecurExpansion(subject.toJSON()); 261 | 262 | while (inc++ < max && (next = subject.next())) { 263 | dates.push(next.toJSDate()); 264 | } 265 | 266 | assert.deepEqual(dates, expected); 267 | assert.isTrue(subject.complete, 'complete'); 268 | }); 269 | }); 270 | 271 | 272 | suite('#toJSON', function() { 273 | test('from start', function() { 274 | var json = subject.toJSON(); 275 | var newIter = new ICAL.RecurExpansion(json); 276 | var cur = 0; 277 | 278 | while (cur++ < 10) { 279 | assert.deepEqual( 280 | subject.next().toJSDate(), 281 | newIter.next().toJSDate(), 282 | 'failed compare at #' + cur 283 | ); 284 | } 285 | }); 286 | 287 | test('from two iterations', function() { 288 | subject.next(); 289 | subject.next(); 290 | 291 | var json = subject.toJSON(); 292 | var newIter = new ICAL.RecurExpansion(json); 293 | var cur = 0; 294 | 295 | while (cur++ < 10) { 296 | assert.deepEqual( 297 | subject.next().toJSDate(), 298 | newIter.next().toJSDate(), 299 | 'failed compare at #' + cur 300 | ); 301 | } 302 | }); 303 | 304 | }); 305 | 306 | suite('event without recurrences', function() { 307 | createSubject('minimal.ics'); 308 | 309 | test('iterate', function() { 310 | var dates = []; 311 | var next; 312 | 313 | var expected = primary.startDate.toJSDate(); 314 | 315 | while ((next = subject.next())) { 316 | dates.push(next.toJSDate()); 317 | } 318 | 319 | assert.deepEqual(dates[0], expected); 320 | assert.lengthOf(dates, 1); 321 | assert.isTrue(subject.complete); 322 | 323 | // json check 324 | subject = new ICAL.RecurExpansion( 325 | subject.toJSON() 326 | ); 327 | 328 | assert.isTrue(subject.complete, 'complete after json'); 329 | assert.ok(!subject.next(), 'next value'); 330 | }); 331 | 332 | }); 333 | 334 | }); 335 | -------------------------------------------------------------------------------- /test/timezone_test.js: -------------------------------------------------------------------------------- 1 | suite('timezone', function() { 2 | var icsData; 3 | var timezone; 4 | 5 | function timezoneTest(tzid, name, testCb) { 6 | if (typeof(name) === 'function') { 7 | testCb = name; 8 | name = 'parse'; 9 | } 10 | 11 | suite(tzid, function() { 12 | if (tzid == "UTC") { 13 | setup(function() { 14 | timezone = ICAL.Timezone.utcTimezone; 15 | }); 16 | } else if (tzid == "floating") { 17 | setup(function() { 18 | timezone = ICAL.Timezone.localTimezone; 19 | }); 20 | } else { 21 | testSupport.defineSample('timezones/' + tzid + '.ics', function(data) { 22 | icsData = data; 23 | }); 24 | 25 | setup(function() { 26 | var parsed = ICAL.parse(icsData); 27 | var vcalendar = new ICAL.Component(parsed); 28 | var comp = vcalendar.getFirstSubcomponent('vtimezone'); 29 | 30 | timezone = new ICAL.Timezone(comp); 31 | }); 32 | } 33 | 34 | test(name, testCb); 35 | }); 36 | } 37 | 38 | function utcHours(time) { 39 | var seconds = timezone.utcOffset( 40 | new ICAL.Time(time) 41 | ); 42 | 43 | // in hours 44 | return (seconds / 3600); 45 | } 46 | 47 | var sanityChecks = [ 48 | { 49 | // just before US DST 50 | time: { year: 2012, month: 3, day: 11, hour: 1, minute: 59 }, 51 | offsets: { 52 | 'America/Los_Angeles': -8, 53 | 'America/New_York': -5, 54 | 'America/Denver': -7, 55 | 'America/Atikokan': -5, // single tz 56 | 'UTC': 0, 57 | 'floating': 0 58 | } 59 | }, 60 | 61 | { 62 | // just after US DST 63 | time: { year: 2012, month: 3, day: 11, hour: 2 }, 64 | offsets: { 65 | 'America/Los_Angeles': -7, 66 | 'America/Denver': -6, 67 | 'America/New_York': -4, 68 | 'America/Atikokan': -5, 69 | 'UTC': 0, 70 | 'floating': 0 71 | } 72 | }, 73 | 74 | { 75 | time: { year: 2004, month: 10, day: 31, hour: 0, minute: 59, second: 59 }, 76 | offsets: { 77 | 'America/Denver': -6 78 | } 79 | }, 80 | 81 | { 82 | time: { year: 2004, month: 10, day: 31, hour: 1 }, 83 | offsets: { 84 | 'America/Denver': -7 85 | } 86 | }, 87 | 88 | 89 | // Edge case timezone that defines an RDATE with VALUE=DATE 90 | { 91 | // just before DST 92 | time: { year: 1980, month: 1, day: 1, hour: 0, minute: 59 }, 93 | offsets: { 94 | 'Makebelieve/RDATE_test': -4, 95 | 'Makebelieve/RDATE_utc_test': -5 96 | } 97 | }, 98 | 99 | { 100 | // just after DST 101 | time: { year: 1980, month: 1, day: 1, hour: 1 }, 102 | offsets: { 103 | 'Makebelieve/RDATE_test': -5, 104 | 'Makebelieve/RDATE_utc_test': -5 105 | } 106 | }, 107 | 108 | // Edge case where RDATE is defined in UTC 109 | { 110 | // just before DST 111 | time: { year: 1990, month: 1, day: 1, hour: 0, minute: 59 }, 112 | offsets: { 113 | 'Makebelieve/RDATE_test': -4, 114 | 'Makebelieve/RDATE_utc_test': -4 115 | } 116 | }, 117 | 118 | { 119 | // just after DST 120 | time: { year: 1990, month: 1, day: 1, hour: 2 }, 121 | offsets: { 122 | 'Makebelieve/RDATE_test': -5, 123 | 'Makebelieve/RDATE_utc_test': -5 124 | } 125 | }, 126 | 127 | // Edge case timezone where an RRULE with UNTIL in UTC is specified 128 | { 129 | // Just before DST 130 | time: { year: 1975, month: 1, day: 1, hour: 1, minute: 0, second: 0 }, 131 | offsets: { 132 | 'Makebelieve/RRULE_UNTIL_test': -5 133 | } 134 | }, 135 | { 136 | // Just after DST 137 | time: { year: 1975, month: 1, day: 1, hour: 3, minute: 0, second: 0 }, 138 | offsets: { 139 | 'Makebelieve/RRULE_UNTIL_test': -4 140 | } 141 | }, 142 | { 143 | // After the RRULE ends 144 | time: { year: 1985, month: 1, day: 1, hour: 3, minute: 0, second: 0 }, 145 | offsets: { 146 | 'Makebelieve/RRULE_UNTIL_test': -4 147 | } 148 | } 149 | ]; 150 | 151 | // simple format checks 152 | sanityChecks.forEach(function(item) { 153 | var title = 'time: ' + JSON.stringify(item.time); 154 | 155 | suite(title, function() { 156 | for (var tzid in item.offsets) { 157 | timezoneTest(tzid, tzid + " offset " + item.offsets[tzid], function(tzid) { 158 | assert.equal( 159 | utcHours(item.time), 160 | item.offsets[tzid] 161 | ); 162 | }.bind(this, tzid)); 163 | } 164 | }); 165 | }); 166 | 167 | timezoneTest('America/Los_Angeles', '#expandedUntilYear', function() { 168 | 169 | function calcYear(yr) { 170 | return Math.max(ICAL.Timezone._minimumExpansionYear, yr) + 171 | ICAL.Timezone.EXTRA_COVERAGE; 172 | } 173 | 174 | var time = new ICAL.Time({ 175 | year: 2012, 176 | zone: timezone 177 | }); 178 | var expectedCoverage = calcYear(time.year); 179 | 180 | time.utcOffset(); 181 | assert.equal(timezone.expandedUntilYear, expectedCoverage); 182 | 183 | time = new ICAL.Time({ 184 | year: 2014, 185 | zone: timezone 186 | }); 187 | 188 | time.utcOffset(); 189 | assert.equal(timezone.expandedUntilYear, expectedCoverage); 190 | 191 | time = new ICAL.Time({ 192 | year: 1997, 193 | zone: timezone 194 | }); 195 | time.utcOffset(); 196 | assert.equal(timezone.expandedUntilYear, expectedCoverage); 197 | 198 | time = new ICAL.Time({ 199 | year: expectedCoverage + 3, 200 | zone: timezone 201 | }); 202 | expectedCoverage = calcYear(time.year); 203 | time.utcOffset(); 204 | assert.equal(timezone.expandedUntilYear, expectedCoverage); 205 | 206 | time = new ICAL.Time({ 207 | year: ICAL.Timezone.MAX_YEAR + 1, 208 | zone: timezone 209 | }); 210 | time.utcOffset(); 211 | assert.equal(timezone.expandedUntilYear, ICAL.Timezone.MAX_YEAR); 212 | }); 213 | 214 | suite('#convertTime', function() { 215 | timezoneTest('America/Los_Angeles', 'convert date-time from utc', function() { 216 | var subject = new ICAL.Time.fromString('2012-03-11T01:59:00Z'); 217 | var subject2 = subject.convertToZone(timezone); 218 | assert.equal(subject2.zone.tzid, timezone.tzid); 219 | assert.equal(subject2.toString(), '2012-03-10T17:59:00'); 220 | }); 221 | 222 | timezoneTest('America/Los_Angeles', 'convert date from utc', function() { 223 | var subject = new ICAL.Time.fromString('2012-03-11'); 224 | var subject2 = subject.convertToZone(timezone); 225 | assert.equal(subject2.zone.tzid, timezone.tzid); 226 | assert.equal(subject2.toString(), '2012-03-11'); 227 | }); 228 | timezoneTest('America/Los_Angeles', 'convert local time to zone', function() { 229 | var subject = new ICAL.Time.fromString('2012-03-11T01:59:00'); 230 | subject.zone = ICAL.Timezone.localTimezone; 231 | assert.equal(subject.toString(), '2012-03-11T01:59:00'); 232 | 233 | var subject2 = subject.convertToZone(timezone); 234 | assert.equal(subject2.toString(), '2012-03-11T01:59:00'); 235 | 236 | var subject3 = subject2.convertToZone(ICAL.Timezone.localTimezone); 237 | assert.equal(subject3.toString(), '2012-03-11T01:59:00'); 238 | }); 239 | }); 240 | 241 | suite('#fromData', function() { 242 | timezoneTest('America/Los_Angeles', 'string component', function() { 243 | var subject = new ICAL.Timezone({ 244 | component: timezone.component.toString(), 245 | tzid: 'Makebelieve/Different' 246 | }); 247 | 248 | assert.equal(subject.expandedUntilYear, 0); 249 | assert.equal(subject.tzid, 'Makebelieve/Different'); 250 | assert.equal(subject.component.getFirstPropertyValue('tzid'), 'America/Los_Angeles'); 251 | }); 252 | 253 | timezoneTest('America/Los_Angeles', 'component in data', function() { 254 | var subject = new ICAL.Timezone({ 255 | component: timezone.component, 256 | }); 257 | 258 | assert.equal(subject.tzid, 'America/Los_Angeles'); 259 | assert.deepEqual(subject.component, timezone.component); 260 | }); 261 | 262 | timezoneTest('America/Los_Angeles', 'with strange component', function() { 263 | var subject = new ICAL.Timezone({ 264 | component: 123 265 | }); 266 | 267 | assert.isNull(subject.component); 268 | }); 269 | }); 270 | 271 | suite('#utcOffset', function() { 272 | test('empty vtimezone', function() { 273 | var subject = new ICAL.Timezone({ 274 | component: new ICAL.Component('vtimezone') 275 | }); 276 | 277 | assert.equal(subject.utcOffset(ICAL.Time.fromString('2012-01-01')), 0); 278 | }); 279 | 280 | test('empty STANDARD/DAYLIGHT', function() { 281 | var subject = new ICAL.Timezone({ 282 | component: new ICAL.Component(['vtimezone', [], [ 283 | ['standard', [], []], 284 | ['daylight', [], []] 285 | ]]) 286 | }); 287 | 288 | assert.equal(subject.utcOffset(ICAL.Time.fromString('2012-01-01')), 0); 289 | }); 290 | }); 291 | 292 | suite('#toString', function() { 293 | timezoneTest('America/Los_Angeles', 'toString', function() { 294 | assert.equal(timezone.toString(), "America/Los_Angeles"); 295 | assert.equal(timezone.tzid, "America/Los_Angeles"); 296 | assert.equal(timezone.tznames, ""); 297 | 298 | timezone.tznames = "test"; 299 | assert.equal(timezone.toString(), "test"); 300 | assert.equal(timezone.tzid, "America/Los_Angeles"); 301 | assert.equal(timezone.tznames, "test"); 302 | }); 303 | }); 304 | 305 | test('#_compare_change_fn', function() { 306 | var subject = ICAL.Timezone._compare_change_fn; 307 | 308 | var a = new ICAL.Time({ 309 | year: 2015, 310 | month: 6, 311 | day: 15, 312 | hour: 12, 313 | minute: 30, 314 | second: 30 315 | }); 316 | 317 | function vary(prop) { 318 | var b = a.clone(); 319 | assert.equal(subject(a, b), 0); 320 | b[prop] += 1; 321 | assert.equal(subject(a, b), -1); 322 | b[prop] -= 2; 323 | assert.equal(subject(a, b), 1); 324 | } 325 | 326 | ['year', 'month', 'day', 'hour', 'minute', 'second'].forEach(vary); 327 | }); 328 | }); 329 | --------------------------------------------------------------------------------