parse method
Implementation
void parse(String content, Contact contact) {
var lines = encode(content).split('\n').map((String x) => x.trim());
for (final line in lines) {
final parts = line.split(':');
if (parts.length >= 2 && !buildingPhoto) {
// This is the normal case. Just go on normally.
} else if (parts.length < 2 && !buildingPhoto) {
// Indicates invalid line, we'll just ignore it.
continue;
} else if (parts.length < 2 && buildingPhoto) {
// Continue building photo.
photo += line;
continue;
} else if (parts.length >= 2 && buildingPhoto) {
// Stop building photo, and go on normally.
buildingPhoto = false;
contact.photo = base64.decode(decode(photo));
}
final firstPart = parts[0];
final opParts = firstPart.split(';');
final opComplete = opParts[0].toUpperCase();
// iOS exports repeated fields with custom labels like this:
// TEL;type=HOME;type=VOICE;type=pref:(333) 111-2222
// item2.TEL:(242) 164-5875
// item2.X-ABLabel:custom_label
// For simplicity we'll just drop the label in such cases
final op = opComplete.split('.').last;
var params = <Param>[];
for (var part in opParts.sublist(1)) {
final paramsParts = part.split('=');
if (paramsParts.length >= 2) {
params.add(Param(paramsParts[0].toUpperCase(),
paramsParts.sublist(1).join('=').toUpperCase()));
}
}
final val = parts.sublist(1).join(':');
if (op == 'BEGIN') {
// nothing to do
} else if (op == 'END') {
// nothing to do
} else if (op == 'VERSION') {
if (val == '2.1') {
version = Version.V2_1;
} else if (val == '3' || val == '3.' || val == '3.0') {
version = Version.V3;
} else if (val == '4' || val == '4.' || val == '4.0') {
version = Version.V4;
} else {
// invalid version, falling back on version 3
}
} else if (op == 'PRODID') {
// ignored
} else if (op == 'N') {
// format is N:<last>;<first>;<middle>;<prefix>;<suffix>
final parts = val.split(';');
if (parts.length != 5) continue;
contact.name.last = decode(parts[0]);
contact.name.first = decode(parts[1]);
contact.name.middle = decode(parts[2]);
contact.name.prefix = decode(parts[3]);
contact.name.suffix = decode(parts[4]);
} else if (op == 'FN') {
// formatted name
contact.displayName = decode(val);
} else if (op == 'NICKNAME') {
// format is N:<nickname 1>[,<nickname 2>[,...]]
final parts = val.split(',');
contact.name.nickname = decode(parts[0]);
} else if (op == 'TEL') {
// TEL;VALUE=uri;PREF=1;TYPE="voice,home":tel:+1-555-555-5555;ext=5555
// TEL;type=HOME;type=VOICE;type=pref:1 (234) 567-893
// TEL;type=WORK;type=VOICE:(987) 654-321
// TEL;TYPE=cell:(123) 555-583
if (val.isEmpty) continue;
Phone phone;
if (val.startsWith('tel:')) {
phone = Phone(decode(val.substring(4).split(';')[0]));
} else {
phone = Phone(decode(val));
}
for (var param in params) {
if (param.key == 'TYPE') {
if (param.value == 'HOME') {
phone.label = PhoneLabel.home;
} else if (param.value == 'MOBILE' || param.value == 'CELL') {
phone.label = PhoneLabel.mobile;
} else if (param.value == 'WORK') {
phone.label = PhoneLabel.work;
} else if (param.value == 'PREF') {
phone.isPrimary = true;
}
}
}
contact.phones.add(phone);
} else if (op == 'EMAIL') {
// EMAIL;TYPE=work:jqpublic@xyz.example.com
// EMAIL;PREF=1:jane_doe@example.com
// EMAIL;type=INTERNET;type=HOME;type=pref:e@a.com
// EMAIL;type=INTERNET;type=WORK:e@b.com
if (val.isEmpty) continue;
var email = Email(decode(val));
for (var param in params) {
if (param.key == 'TYPE') {
if (param.value == 'HOME') {
email.label = EmailLabel.home;
} else if (param.value == 'MOBILE' || param.value == 'CELL') {
email.label = EmailLabel.mobile;
} else if (param.value == 'WORK') {
email.label = EmailLabel.work;
} else if (param.value == 'PREF') {
email.isPrimary = true;
}
} else if (param.key == 'PREF') {
email.isPrimary = true;
}
}
contact.emails.add(email);
} else if (op == 'ADR') {
// ADR;TYPE=home:;;123 Main St.;Springfield;IL;12345;USA
// item1.ADR;type=HOME;type=pref:;;52 rue des Fleurs;Libourne;;33500;France
// item2.ADR;type=WORK:;;22 Foo St;San Francisco;CA;94100;United States
// Format is ADR:<pobox>:<extended address>:<street>:<locality (city)>:
// <region (state/province)>:<postal code>:country
// The spec says first two should be empty to avoid problems, so we'll
// just ignore them.
var addressParts = val.split(';');
if (addressParts.length != 7) continue;
final street = addressParts[2];
final locality = addressParts[3];
final region = addressParts[4];
final postal = addressParts[5];
final country = addressParts[6];
var components = <String>[];
if (street.isNotEmpty) {
components.add(street.trim());
}
if (locality.isNotEmpty || region.isNotEmpty || postal.isNotEmpty) {
// e.g. San Francisco CA 94100
// e.g. Libourne 33500
// not perfect but good enough
components
.add([locality, region, postal].map((x) => x.trim()).join(' '));
}
if (country.isNotEmpty) components.add(country);
if (components.isEmpty) continue;
var address = Address(decode(components.join('\n')));
for (var param in params) {
if (param.key == 'TYPE') {
if (param.value == 'HOME') {
address.label = AddressLabel.home;
} else if (param.value == 'WORK') {
address.label = AddressLabel.work;
}
}
}
contact.addresses.add(address);
} else if (op == 'ORG') {
// Format is ORG:<company>[;<division>[:<subdivision>...]]
var org = decode(val.split(';')[0]);
if (org.isEmpty) continue;
if (contact.organizations.isEmpty) {
contact.organizations = [Organization()];
}
contact.organizations.first.company = decode(org);
} else if (op == 'TITLE') {
// Format is TITLE:<title>
if (val.isEmpty) continue;
if (contact.organizations.isEmpty) {
contact.organizations = [Organization()];
}
contact.organizations.first.title = decode(val);
} else if (op == 'URL') {
// URLs have types, but we ignore them for now
if (val.isEmpty) continue;
contact.websites.add(Website(decode(val)));
} else if (op == 'IMPP') {
// IMPP;PREF=1:xmpp:alice@example.com
// IMPP:aim:johndoe@aol.com
// item7.IMPP;X-SERVICE-TYPE=Skype;type=pref:skype:skypehandle
// item7.X-ABLabel:Skype
// item8.IMPP;X-SERVICE-TYPE=QQ:x-apple:qqhandle
// item8.X-ABLabel:QQ
var imParts = val.split(':');
if (imParts.length != 2) continue;
final name = imParts[1];
final type = imParts[0].toLowerCase();
if (name.isEmpty) continue;
var label = SocialMediaLabel.custom;
var customLabel = '';
switch (type) {
case 'skype':
label = SocialMediaLabel.skype;
break;
case 'snapchat':
label = SocialMediaLabel.snapchat;
break;
case 'facebook':
label = SocialMediaLabel.facebook;
break;
case 'twitter':
label = SocialMediaLabel.twitter;
break;
case 'wechat':
label = SocialMediaLabel.wechat;
break;
case 'qq':
case 'qqchat':
label = SocialMediaLabel.qqchat;
break;
case 'telegram':
label = SocialMediaLabel.telegram;
break;
case 'discord':
label = SocialMediaLabel.discord;
break;
case 'jabber':
label = SocialMediaLabel.jabber;
break;
case 'yahoo':
label = SocialMediaLabel.yahoo;
break;
case 'x-apple':
customLabel = imParts[0];
break;
default:
customLabel = type;
}
contact.socialMedias.add(
SocialMedia(decode(name), label: label, customLabel: customLabel));
break;
} else if (op == 'X-SOCIALPROFILE') {
// X-SOCIALPROFILE;type=twitter:http://twitter.com/twit
// X-SOCIALPROFILE;type=facebook:http://www.facebook.com/fb
if (val.isEmpty) continue;
var label = SocialMediaLabel.custom;
var customLabel = '';
for (var param in params) {
if (param.key == 'TYPE') {
var type = param.value.toLowerCase();
switch (type) {
case 'skype':
label = SocialMediaLabel.skype;
break;
case 'snapchat':
label = SocialMediaLabel.snapchat;
break;
case 'facebook':
label = SocialMediaLabel.facebook;
break;
case 'twitter':
label = SocialMediaLabel.twitter;
break;
case 'wechat':
label = SocialMediaLabel.wechat;
break;
case 'qqchat':
label = SocialMediaLabel.qqchat;
break;
case 'telegram':
label = SocialMediaLabel.telegram;
break;
case 'discord':
label = SocialMediaLabel.discord;
break;
case 'jabber':
label = SocialMediaLabel.jabber;
break;
case 'yahoo':
label = SocialMediaLabel.yahoo;
break;
default:
customLabel = param.value;
}
}
}
if (label == null) continue;
contact.socialMedias.add(
SocialMedia(decode(val), label: label, customLabel: customLabel));
} else if (op == 'PHOTO') {
// 2.1: PHOTO;JPEG:http://example.com/photo.jpg
// 2.1: PHOTO;JPEG;ENCODING=BASE64:[base64-data]
// 3.0: PHOTO;TYPE=JPEG;VALUE=URI:http://example.com/photo.jpg
// 3.0: PHOTO;TYPE=JPEG;ENCODING=b:[base64-data]
// 4.0: PHOTO;MEDIATYPE=image/jpeg:http://example.com/photo.jpg
// 4.0: PHOTO:data:image/jpeg;base64,[base64-data]
// 3.0: PHOTO;ENCODING=b;TYPE=JPEG:[bytes]
var encoding = '';
switch (version) {
case Version.V2_1:
case Version.V3:
// Not perfect, but will do for now.
if (val.startsWith('http') || val.startsWith('ftp')) {
// not yet implemented
} else {
encoding = val;
}
break;
case Version.V4:
if (val.startsWith('http') || val.startsWith('ftp')) {
// not yet implemented
} else {
encoding = val.split(',').last;
}
}
if (encoding.isEmpty) continue;
photo = encoding;
buildingPhoto = true;
} else if (op == 'BDAY') {
// not yet implemented
} else if (op == 'NOTES') {
// not yet implemented
} else if (op == 'X-ABDATE') {
// not yet implemented
} else if (op == 'X-ABRELATEDNAMES') {
// not yet implemented
} else if (op == 'X-ALTBDAY') {
// not yet implemented
} else if (op == 'NOTES') {
// not yet implemented
} else if (op == 'X-ABLABEL') {
// currently ignored
} else if (op == 'X-ABADR') {
// currently ignored
}
}
}