Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deserialization for ICS #7

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
BSD 3-Clause License

Copyright (c) 2020, Jonas Franz
Copyright (c) 2020, Lukas Himsel
All rights reserved.

Expand Down
4 changes: 2 additions & 2 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: example
environment:
sdk: '>=2.1.0 <3.0.0'
dependencies:
intl: ^0.15.7
nanoid: ^0.0.6
intl: any
nanoid: any
ical:
path: ..

1 change: 1 addition & 0 deletions lib/serializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'src/abstract.dart';
export 'src/subcomponents.dart';
export 'src/calendar.dart';
export 'src/event.dart';
export 'src/parser.dart';
//export 'src/journal.dart';
//export 'src/timezone.dart';
//export 'src/todo.dart';
25 changes: 23 additions & 2 deletions lib/src/abstract.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:ical/serializer.dart';
import 'package:ical/src/structure.dart';
import 'package:ical/src/utils.dart';

import 'utils.dart' as utils;
Expand All @@ -9,6 +10,10 @@ abstract class AbstractSerializer {
String serialize();
}

abstract class AbstractDeserializer {
void deserialize(ICalStructure structure);
}

class IClass {
final String _label;
@override
Expand Down Expand Up @@ -105,7 +110,8 @@ class IOrganizer {
}
}

abstract class ICalendarElement extends AbstractSerializer {
abstract class ICalendarElement
implements AbstractSerializer, AbstractDeserializer {
IOrganizer organizer;
String uid;
String summary;
Expand Down Expand Up @@ -143,12 +149,27 @@ abstract class ICalendarElement extends AbstractSerializer {
if (summary != null) out.writeln('SUMMARY:${escapeValue(summary)}');
if (url != null) out.writeln('URL:${url}');
if (classification != null) out.writeln('CLASS:$classification');
if (description != null)
if (description != null) {
out.writeln('DESCRIPTION:${escapeValue(description)}');
}
if (rrule != null) out.write(rrule.serialize());

return out.toString();
}

@override
void deserialize(ICalStructure structure) {
uid = structure["UID"]?.value;
categories = structure["CATEGORIES"]?.value?.split(",");
comment = structure["COMMENT"]?.value;
summary = structure["SUMMARY"]?.value;
url = structure["URL"]?.value;
final classificationString = structure["CLASSIFICATION"]?.value;
if (classificationString != null)
classification = IClass._(classificationString);
// TODO support rrule
}

// TODO ATTENDEE
// TODO CONTACT
}
Expand Down
17 changes: 15 additions & 2 deletions lib/src/calendar.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'abstract.dart';
import 'package:ical/serializer.dart';
import 'package:ical/src/structure.dart';

import 'utils.dart' as utils;

class ICalendar extends AbstractSerializer {
class ICalendar implements AbstractSerializer, AbstractDeserializer {
List<ICalendarElement> _elements = <ICalendarElement>[];
String company;
String product;
Expand All @@ -18,6 +20,8 @@ class ICalendar extends AbstractSerializer {
addAll(List<ICalendarElement> elements) => _elements.addAll(elements);
addElement(ICalendarElement element) => _elements.add(element);

List<IEvent> get events => _elements.whereType<IEvent>().toList();

@override
String serialize() {
var out = StringBuffer()
Expand All @@ -37,4 +41,13 @@ class ICalendar extends AbstractSerializer {
out.writeln('END:VCALENDAR');
return out.toString();
}

@override
void deserialize(ICalStructure structure) {
_elements = structure.children
.where((child) => child.type == "VEVENT")
.map((event) => IEvent()..deserialize(event))
.cast<ICalendarElement>()
.toList();
}
}
23 changes: 23 additions & 0 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:ical/src/structure.dart';

import 'abstract.dart';
import 'subcomponents.dart';
import 'utils.dart' as utils;
Expand Down Expand Up @@ -79,6 +81,27 @@ class IEvent extends ICalendarElement with EventToDo {
..writeln('END:VEVENT');
return out.toString();
}

@override
void deserialize(ICalStructure structure) {
super.deserialize(structure);
start = _parseDate(structure["DTSTART"]);
end = _parseDate(structure["DTEND"]);
// TODO support duration
transparency = structure["TRANSP"] != null
? ITimeTransparency._(structure["TRANSP"].value)
: null;
status = structure["STATUS"] != null
? IEventStatus._(structure["STATUS"].value)
: null;
// TODO support missing event to do;
}

DateTime _parseDate(ICalRow row) {
if(row == null || row.value == null) return null;
// TODO: add TZID support
return DateTime.parse(row.value);
}
}

class IEventStatus {
Expand Down
42 changes: 42 additions & 0 deletions lib/src/parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:convert';

import 'package:ical/serializer.dart';
import 'package:ical/src/structure.dart';
import 'package:ical/src/utils.dart';

const _rowRegex = r"""^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$""";
const _middleRegex = r"""(?<key>[^=;]+)=(?<value>[^;]+)""";

class ICalParser {
List<ICalRow> parseText(String text) {
// unfold strings
text = text.replaceAll("\r\n", "\n").replaceAll("\n ", "");
return LineSplitter()
.convert(text)
.map(_parseLine)
.where((element) => element != null)
.toList();
}

ICalRow _parseLine(String line) {
final regex = RegExp(_rowRegex);
final match = regex.firstMatch(line);
if(match == null) return null;
final String key = match.group(1);
final String middle = match.group(2);
final String value = unescapeValue(match.group(3));
return ICalRow(key, value, properties: _parseMiddlePart(middle));
}

Map<String, String> _parseMiddlePart(String middle) {
final matches = RegExp(_middleRegex).allMatches(middle);
final entries = matches
.map((match) => MapEntry(match.namedGroup("key"), unescapeValue(match.namedGroup("value"))));
return Map.fromEntries(entries);
}

ICalendar parseCalender(String text) {
final rows = parseText(text);
return ICalendar()..deserialize(ICalStructure.fromRows(rows));
}
}
42 changes: 42 additions & 0 deletions lib/src/structure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class ICalStructure {
final String type;
final List<ICalRow> rows;
final List<ICalStructure> children;

ICalRow operator [](String key) {
return rows.firstWhere((row) => row.key == key, orElse: () => null);
}

const ICalStructure(this.type, this.rows, this.children);

factory ICalStructure.fromRows(List<ICalRow> rows) {
final beginRow = rows.removeAt(0);
if(beginRow == null) throw Exception("No begin row found");
final type = beginRow.value;
final List<ICalStructure> children = [];

var beginIndex = rows.indexWhere((element) => element.key == "BEGIN");
while(beginIndex != -1) {
final subType = rows[beginIndex].value;
final endIndex = rows.indexWhere((element) => element.key == "END" && element.value == subType) + 1;
if(endIndex == 0) throw Exception("No END row found for type $subType");
final subStructure = rows.getRange(beginIndex, endIndex);
children.add(ICalStructure.fromRows(subStructure.toList()));
rows.removeRange(beginIndex, endIndex);
beginIndex = rows.indexWhere((element) => element.key == "BEGIN");
}
// remove END row
// TODO check if end row is valid
final endRow = rows.removeLast();
if(endRow.key != "END" || endRow.value != type) throw Exception("Invalid END row");
return ICalStructure(type, rows, children);
}
}

class ICalRow {
final String key;
final String value;
final Map<String, String> properties;

const ICalRow(this.key, this.value, {this.properties = const {}});
}
7 changes: 7 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ String escapeValue(String val) => val
.replaceAll('\t', '\\t')
.replaceAll(',', '\\,')
.replaceAll(';', '\\;');

String unescapeValue(String val) => val
.replaceAll('\\\\', '\\')
.replaceAll('\\n', '\n')
.replaceAll('\\t', '\t')
.replaceAll('\\,', ',')
.replaceAll('\\;', ';');
5 changes: 2 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ environment:
sdk: ">=2.0.0 <3.0.0"

dependencies:
intl: ^0.16.0
nanoid: ^0.1.0
intl: ^0.17.0
nanoid: ^1.0.0

dev_dependencies:
test: ^1.5.1+1
pedantic: ^1.8.0

83 changes: 83 additions & 0 deletions test/parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:ical/src/parser.dart';
import 'package:test/test.dart';

main() {
group('Parser', () {
ICalParser parser;
setUp(() {
parser = ICalParser();
});
test('parse single row', () {
const withMiddle = "DTSTART;VALUE=DATE:20200908";
const withoutMiddle = "CREATED:20200827T080438Z";

var result = parser.parseText(withMiddle);
var row = result.firstWhere((element) => element.key == "DTSTART");
expect(row?.value, "20200908");
expect(row?.properties["VALUE"], "DATE");

result = parser.parseText(withoutMiddle);
row = result.firstWhere((element) => element.key == "CREATED");
expect(row?.value, "20200827T080438Z");
expect(row?.properties?.length, 0);
});

test('parse multiple rows', () {
const testIcs = """CREATED:20200827T080438Z
DTSTAMP:20200827T080438Z
LAST-MODIFIED:20200827T080438Z
DTSTART;VALUE=DATE:20200907
DTEND;VALUE=DATE:20200908""";
final result = parser.parseText(testIcs);
expect(result.length, 5);
expect(result.map((e) => e.key),
["CREATED", "DTSTAMP", "LAST-MODIFIED", "DTSTART", "DTEND"]);
});

test('parse calendar', () {
const foldedRow = """TEST:testtesttesttesttesttest
testtest""";
final row = parser
.parseText(foldedRow)
.firstWhere((element) => element.key == "TEST");
expect(row?.value, "testtesttesttesttesttesttesttest");
});

test('parse example ICalStructure', () {
const testIcs = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
SUMMARY:Access-A-Ride Pickup
DTSTART;TZID=America/New_York:20130802T103400
DTEND;TZID=America/New_York:20130802T110400
LOCATION:1000 Broadway Ave.\, Brooklyn
DESCRIPTION: Access-A-Ride trip to 900 Jay St.\, Brooklyn
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:Pickup Reminder
ACTION:DISPLAY
END:VALARM
END:VEVENT
BEGIN:VEVENT
SUMMARY:Access-A-Ride Pickup
DTSTART;TZID=America/New_York:20130802T200000
DTEND;TZID=America/New_York:20130802T203000
LOCATION:900 Jay St.\, Brooklyn
DESCRIPTION: Access-A-Ride trip to 1000 Broadway Ave.\, Brooklyn
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:Pickup Reminder
ACTION:DISPLAY
END:VALARM
END:VEVENT
END:VCALENDAR""";
final result = parser.parseCalender(testIcs);
expect(result.events.length, 2);
});
});
}
Loading