Skip to content

Commit 02a4770

Browse files
committed
Add vCard 2.1 support
This commit adds full support for parsing vCard 2.1 format, based on the work from PR kewisch#389. Key features implemented: - New vcard21 design set with proper text escaping rules - Text type that escapes backslash, semicolon, and comma but does not treat commas as multi-value separators (vCard 2.1 uses single values) - Support for vCard 2.1 specific parameters (encoding, charset) - Version detection logic to automatically switch to vcard21 parser when VERSION:2.1 is encountered - Comprehensive test coverage with vcard21.vcf and vcard21.json fixtures - All existing tests continue to pass The implementation correctly handles the key differences between vCard 2.1 and later versions: - Escape sequences: only backslash, semicolon, and comma need escaping - No comma-separated multi-value properties - Semicolons still used for structured values - Support for BASE64 encoding parameter (instead of 'b')
1 parent ffde044 commit 02a4770

5 files changed

Lines changed: 186 additions & 4 deletions

File tree

lib/ical/design.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,107 @@ let vcard3Properties = extend(commonProperties, {
902902
key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
903903
});
904904

905+
// vCard 2.1 text type - escapes backslash, semicolon, and comma
906+
// \n is kept as literal "\n" (two characters), not converted to newline
907+
// Note: commas are still escaped but not used as multi-value separators
908+
const FROM_VCARD21_NEWLINE = /\\\\|\\;|\\,/g;
909+
const TO_VCARD21_NEWLINE = /\\|;|,/g;
910+
911+
let vcard21Values = extend(commonValues, {
912+
binary: icalValues.binary,
913+
date: vcardValues.date,
914+
"date-time": vcardValues["date-time"],
915+
"phone-number": vcardValues["phone-number"],
916+
uri: icalValues.uri,
917+
text: createTextType(FROM_VCARD21_NEWLINE, TO_VCARD21_NEWLINE),
918+
time: icalValues.time,
919+
vcard: icalValues.text,
920+
"utc-offset": {
921+
toICAL: function(aValue) {
922+
return aValue.slice(0, 7);
923+
},
924+
925+
fromICAL: function(aValue) {
926+
return aValue.slice(0, 7);
927+
},
928+
929+
decorate: function(aValue) {
930+
return UtcOffset.fromString(aValue);
931+
},
932+
933+
undecorate: function(aValue) {
934+
return aValue.toString();
935+
}
936+
}
937+
});
938+
939+
let vcard21Params = {
940+
"type": {
941+
valueType: "text",
942+
multiValue: ","
943+
},
944+
"value": {
945+
// since the value here is a 'type' lowercase is used.
946+
values: ["text", "uri", "date", "date-time", "phone-number", "time",
947+
"boolean", "integer", "float", "utc-offset", "vcard", "binary"],
948+
allowXName: true,
949+
allowIanaToken: true
950+
},
951+
"encoding": {
952+
values: ["quoted-printable", "base64", "8bit", "7bit"],
953+
allowXName: false,
954+
allowIanaToken: false
955+
},
956+
"charset": {
957+
valueType: "text"
958+
}
959+
};
960+
961+
let vcard21Properties = extend(commonProperties, {
962+
fn: DEFAULT_TYPE_TEXT,
963+
n: { defaultType: "text", structuredValue: ";" },
964+
nickname: DEFAULT_TYPE_TEXT,
965+
photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
966+
bday: {
967+
defaultType: "date-time",
968+
allowedTypes: ["date-time", "date"],
969+
detectType: function(string) {
970+
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
971+
}
972+
},
973+
974+
adr: { defaultType: "text", structuredValue: ";" },
975+
label: DEFAULT_TYPE_TEXT,
976+
977+
tel: { defaultType: "phone-number" },
978+
email: DEFAULT_TYPE_TEXT,
979+
mailer: DEFAULT_TYPE_TEXT,
980+
981+
tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] },
982+
geo: { defaultType: "float", structuredValue: ";" },
983+
984+
title: DEFAULT_TYPE_TEXT,
985+
role: DEFAULT_TYPE_TEXT,
986+
logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
987+
agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] },
988+
org: DEFAULT_TYPE_TEXT_STRUCTURED,
989+
990+
note: DEFAULT_TYPE_TEXT,
991+
prodid: DEFAULT_TYPE_TEXT,
992+
rev: {
993+
defaultType: "date-time",
994+
allowedTypes: ["date-time", "date"],
995+
detectType: function(string) {
996+
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
997+
}
998+
},
999+
"sort-string": DEFAULT_TYPE_TEXT,
1000+
sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
1001+
1002+
class: DEFAULT_TYPE_TEXT,
1003+
key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
1004+
});
1005+
9051006
/**
9061007
* iCalendar design set
9071008
* @type {designSet}
@@ -938,6 +1039,18 @@ let vcard3Set = {
9381039
propertyGroups: true
9391040
};
9401041

1042+
/**
1043+
* vCard 2.1 design set
1044+
* @type {designSet}
1045+
*/
1046+
let vcard21Set = {
1047+
name: "vcard21",
1048+
value: vcard21Values,
1049+
param: vcard21Params,
1050+
property: vcard21Properties,
1051+
propertyGroups: true
1052+
};
1053+
9411054
/**
9421055
* The design data, used by the parser to determine types for properties and
9431056
* other metadata needed to produce correct jCard/jCal data.
@@ -987,6 +1100,7 @@ const design = {
9871100
components: {
9881101
vcard: vcardSet,
9891102
vcard3: vcard3Set,
1103+
vcard21: vcard21Set,
9901104
vevent: icalSet,
9911105
vtodo: icalSet,
9921106
vjournal: icalSet,
@@ -1015,6 +1129,12 @@ const design = {
10151129
*/
10161130
vcard3: vcard3Set,
10171131

1132+
/**
1133+
* The design set for vCard 2.1 (rfc1521/rfc1522/rfc1523) components.
1134+
* @type {designSet}
1135+
*/
1136+
vcard21: vcard21Set,
1137+
10181138
/**
10191139
* Gets the design set for the given component name.
10201140
*

lib/ical/parse.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,13 @@ parse._handleContentLine = function(line, state) {
289289
result = [ungroupedName, params, valueType, value];
290290
}
291291
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
292-
// component with as value 4.0, note that 3.0 does not have this requirement.
293-
if (state.component[0] === 'vcard' && state.component[1].length === 0 &&
294-
!(name === 'version' && value === '4.0')) {
295-
state.designSet = design.getDesignSet("vcard3");
292+
// component with as value 4.0, note that 3.0 and 2.1 do not have this requirement.
293+
if (state.component[0] === 'vcard' && state.component[1].length === 0) {
294+
if (name === 'version' && value === '2.1') {
295+
state.designSet = design.getDesignSet("vcard21");
296+
} else if (!(name === 'version' && value === '4.0')) {
297+
state.designSet = design.getDesignSet("vcard3");
298+
}
296299
}
297300
state.component[1].push(result);
298301
};

test/parse_test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ suite('parserv2', function() {
3939
'vcard.vcf',
4040
'vcard_author.vcf',
4141
'vcard3.vcf',
42+
'vcard21.vcf',
4243
'vcard_grouped.vcf',
4344
'escape_semicolon.vcf'
4445
];

test/parser/vcard21.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
"vcard",
3+
[
4+
[ "version", {}, "text", "2.1" ],
5+
[ "fn", {}, "text", "Mr. John Q. Public, Esq." ],
6+
[ "n", {}, "text", [ "Public", "John", "Quinlan", "Mr.", "Esq." ] ],
7+
[ "nickname", {}, "text", "Robbie" ],
8+
[ "photo", { "encoding": "BASE64", "type": "JPEG" }, "binary", "SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBpbWFnZSBqdXN0IGEgdGVzdC4=" ],
9+
[ "bday", {}, "date", "1996-04-15" ],
10+
[ "adr", { "type": "HOME" }, "text", ["", "", "123 Main Street", "Any Town", "CA", "91921-1234" ] ],
11+
[ "label", { "type": "HOME" }, "text", "Mr.John Q. Public, Esq.\\nMail Drop: TNE QB\\n123 Main Street\\nAny Town, CA 91921-1234\\nU.S.A." ],
12+
[ "tel", { "type": [ "WORK", "VOICE" ] }, "phone-number", "+1-213-555-1234" ],
13+
[ "email", { "type": "INTERNET" }, "text", "jqpublic@xyz.dom1.com" ],
14+
[ "mailer", {}, "text", "PigeonMail 2.1" ],
15+
[ "tz", {}, "utc-offset", "-05:00" ],
16+
[ "geo", {}, "float", [37.386013, -122.082932 ] ],
17+
[ "title", {}, "text", "Director, Research and Development" ],
18+
[ "role", {}, "text", "Programmer" ],
19+
[ "logo", { "encoding": "BASE64", "type": "JPEG" }, "binary", "SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBpbWFnZSBqdXN0IGEgdGVzdC4=" ],
20+
[ "org", {}, "text", ["ABC, Inc.", "North American Division", "Marketing" ] ],
21+
[ "note", {}, "text", "This fax number is operational 0800 to 1715 EST, Mon-Fri." ],
22+
[ "prodid", {}, "text", "-//ONLINE DIRECTORY//NONSGML Version 1//EN" ],
23+
[ "rev", {}, "date-time", "1995-10-31T22:27:10Z" ],
24+
[ "sort-string", {}, "text", "Harten" ],
25+
[ "sound", { "encoding": "BASE64", "type": "BASIC" }, "binary", "VGhlcmUgaXMgbm8gc291bmQgaW4gc3BhY2U=" ],
26+
[ "class", {}, "text", "PUBLIC" ],
27+
[ "key", { "encoding": "BASE64" }, "binary", "Tm90IHRoZSBrZXkgeW91IGFyZSBsb29raW5nIGZvcg==" ]
28+
],
29+
[]
30+
]

test/parser/vcard21.vcf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BEGIN:VCARD
2+
VERSION:2.1
3+
FN:Mr. John Q. Public\, Esq.
4+
N:Public;John;Quinlan;Mr.;Esq.
5+
NICKNAME:Robbie
6+
PHOTO;ENCODING=BASE64;TYPE=JPEG:SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBp
7+
bWFnZSBqdXN0IGEgdGVzdC4=
8+
BDAY:1996-04-15
9+
ADR;TYPE=HOME:;;123 Main Street;Any Town;CA;91921-1234
10+
LABEL;TYPE=HOME:Mr.John Q. Public\, Esq.\nMail Drop: TNE QB\n123 Main Street\nAny Town\, CA 91921-1234\nU.S.A.
11+
TEL;TYPE=WORK,VOICE:+1-213-555-1234
12+
EMAIL;TYPE=INTERNET:jqpublic@xyz.dom1.com
13+
MAILER:PigeonMail 2.1
14+
TZ:-05:00
15+
GEO:37.386013;-122.082932
16+
TITLE:Director\, Research and Development
17+
ROLE:Programmer
18+
LOGO;ENCODING=BASE64;TYPE=JPEG:SGVsbG8sIHRoaXMgaXMgbm90IGEgcmVhbCBp
19+
bWFnZSBqdXN0IGEgdGVzdC4=
20+
ORG:ABC\, Inc.;North American Division;Marketing
21+
NOTE:This fax number is operational 0800 to 1715 EST\, Mon-Fri.
22+
PRODID:-//ONLINE DIRECTORY//NONSGML Version 1//EN
23+
REV:1995-10-31T22:27:10Z
24+
SORT-STRING:Harten
25+
SOUND;ENCODING=BASE64;TYPE=BASIC:VGhlcmUgaXMgbm8gc291bmQgaW4gc3BhY2U=
26+
CLASS:PUBLIC
27+
KEY;ENCODING=BASE64:Tm90IHRoZSBrZXkgeW91IGFyZSBsb29raW5nIGZvcg==
28+
END:VCARD

0 commit comments

Comments
 (0)