diff --git a/calendar.go b/calendar.go index 5ff1988..3a0f936 100644 --- a/calendar.go +++ b/calendar.go @@ -679,7 +679,42 @@ func parseCalendarFromHttpRequest(client HttpClientLike, request *http.Request) return cal, err } +// ParseOption provides functional options for ParseCalendar +type ParseOption func(*parseConfig) error + +type parseConfig struct { + allowPropertiesAfterComponents bool +} + +// WithRelaxedParsing allows properties to appear after components have started, +// which is technically non-compliant with RFC 5545 but occurs in real-world files +func WithRelaxedParsing() ParseOption { + return func(cfg *parseConfig) error { + cfg.allowPropertiesAfterComponents = true + return nil + } +} + func ParseCalendar(r io.Reader) (*Calendar, error) { + // Default behavior maintains backward compatibility (strict mode) + return ParseCalendarWithOptions(r) +} + +func ParseCalendarWithOptions(r io.Reader, options ...ParseOption) (*Calendar, error) { + cfg := &parseConfig{ + allowPropertiesAfterComponents: false, // default to strict RFC compliance + } + + for _, opt := range options { + if err := opt(cfg); err != nil { + return nil, fmt.Errorf("invalid parse option: %w", err) + } + } + + return parseCalendarInternal(r, cfg) +} + +func parseCalendarInternal(r io.Reader, cfg *parseConfig) (*Calendar, error) { state := "begin" c := &Calendar{} cs := NewCalendarStream(r) @@ -753,7 +788,13 @@ func ParseCalendar(r io.Reader) (*Calendar, error) { c.Components = append(c.Components, co) } default: - return nil, errors.New("malformed calendar; expected begin or end") + if cfg.allowPropertiesAfterComponents { + // Allow properties between components (non-standard but occurs in real-world ICS files) + // These properties are added to the calendar properties list + c.CalendarProperties = append(c.CalendarProperties, CalendarProperty{*line}) + } else { + return nil, errors.New("malformed calendar; expected begin or end") + } } case "end": return nil, errors.New("malformed calendar; unexpected end") diff --git a/calendar_test.go b/calendar_test.go index 8f19229..486c02e 100644 --- a/calendar_test.go +++ b/calendar_test.go @@ -284,9 +284,10 @@ END:VCALENDAR func TestParseCalendar(t *testing.T) { testCases := []struct { - name string - input string - output string + name string + input string + output string + parseOptions []ParseOption // Add options field }{ { name: "test custom fields in calendar", @@ -379,13 +380,59 @@ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=SU CLASS:PUBLIC END:VEVENT END:VCALENDAR +`, + }, + { + name: "test properties after components", + input: `BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/London +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +DTSTART:19701025T020000 +END:STANDARD +END:VTIMEZONE +TIMEZONE-ID:VT +X-WR-TIMEZONE:VT +BEGIN:VEVENT +DTSTART:20230101T120000Z +SUMMARY:Test Event +END:VEVENT +END:VCALENDAR +`, + parseOptions: []ParseOption{WithRelaxedParsing()}, // Use relaxed parsing for this test + output: `BEGIN:VCALENDAR +VERSION:2.0 +TIMEZONE-ID:VT +X-WR-TIMEZONE:VT +BEGIN:VTIMEZONE +TZID:Europe/London +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +DTSTART:19701025T020000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART:20230101T120000Z +SUMMARY:Test Event +END:VEVENT +END:VCALENDAR `, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - c, err := ParseCalendar(strings.NewReader(tc.input)) + var c *Calendar + var err error + if len(tc.parseOptions) > 0 { + c, err = ParseCalendarWithOptions(strings.NewReader(tc.input), tc.parseOptions...) + } else { + c, err = ParseCalendar(strings.NewReader(tc.input)) + } if !assert.NoError(t, err) { return }