Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
55 changes: 51 additions & 4 deletions calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down
Loading