├── VERSION
├── dist
├── latest.js
├── vcard-0.1.min.js
├── vcard-0.2dev.min.js
├── vcardjs-0.2.min.js
├── vcardjs-0.3.min.js
├── vcardjs-0.2.js
└── vcardjs-0.3.js
├── .gitignore
├── config.ru
├── package.json
├── Makefile
├── test
├── runner.html
├── runner.js
└── vcf.js
├── examples
└── load.html
├── LICENSE
├── README.org
└── src
├── Math.uuid.js
├── vcard.js
└── vcf.js
/VERSION:
--------------------------------------------------------------------------------
1 | 0.3
2 |
--------------------------------------------------------------------------------
/dist/latest.js:
--------------------------------------------------------------------------------
1 | vcardjs-0.3.min.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | \#*
3 | .#*
4 | *.swp
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # -*- mode:ruby -*-
2 |
3 | run Rack::File.new('.')
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vcardjs",
3 | "desc": "vCardJS - a vCard 4.0 implementation in JavaScript.",
4 | "version": "0.3.0",
5 | "private": false,
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "git://github.com/nilclass/vcardjs.git"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | COMPRESS=shrinksafe
2 | NAME=vcardjs-`cat VERSION`
3 |
4 | .PHONY: build
5 | build: src/*.js
6 | cat build/head src/*.js build/tail > dist/$(NAME).js
7 | $(COMPRESS) dist/$(NAME).js > dist/$(NAME).min.js
8 |
9 | release: build
10 | rm -f dist/latest.js
11 | ln -s $(NAME).min.js dist/latest.js
12 |
--------------------------------------------------------------------------------
/test/runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Available tests:
17 |
18 |
19 |
20 |
21 |
22 | test markup, will be hidden
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/load.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
24 |
25 |
26 | Load vCard
27 | IN:
28 |
29 | OUT:
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (C) 2012 Niklas Cathor (http://github.com/nilclass)
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is furnished
9 | to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
--------------------------------------------------------------------------------
/test/runner.js:
--------------------------------------------------------------------------------
1 |
2 | (function($) {
3 |
4 |
5 | // SOURCE: http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
6 | var params = {};
7 | var e,
8 | a = /\+/g, // Regex for replacing addition symbol with a space
9 | r = /([^&=]+)=?([^&]*)/g,
10 | d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
11 | q = window.location.search.substring(1);
12 |
13 | while (e = r.exec(q))
14 | params[d(e[1])] = d(e[2]);
15 | // -------------------------------------------------------------------------
16 |
17 | var tests = {
18 | vcf: "VCF"
19 | };
20 |
21 | function setTitle(title) {
22 | $('#qunit-header').html(
23 | $('#qunit-header').html().replace('{{TITLE}}', title)
24 | );
25 | }
26 |
27 | function renderNav() {
28 | var nav = $('#nav');
29 | for(var key in tests) {
30 | var label = tests[key];
31 | var item = $('');
32 | item.attr('href', '?test=' + key);
33 | item.text(label);
34 | nav.append(item);
35 | }
36 | }
37 |
38 | $(document).ready(function() {
39 | renderNav();
40 |
41 | if(params.test) {
42 | setTitle(tests[params.test]);
43 | $('head').append($('
40 |
41 |
42 |
43 |
44 |
45 |
59 |
60 |
61 | Load vCard
62 | IN:
63 |
64 | OUT:
65 |
66 |
67 |
68 | #+END_SRC
69 |
70 | ** References
71 | - [[http://datatracker.ietf.org/doc/rfc6350/?include_text%3D1][RFC 6350 - vCard Format Specification (Proposed Standard)]]
72 | - [[http://microformats.org/wiki/jCard][jCard]]
73 | - [[http://microformats.org/wiki/json][ufJSON - JSON representation of microformats]]
74 | - [[http://docs.jquery.com/QUnit][QUnit]]
75 |
--------------------------------------------------------------------------------
/src/Math.uuid.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Math.uuid.js (v1.4)
3 | http://www.broofa.com
4 | mailto:robert@broofa.com
5 |
6 | Copyright (c) 2010 Robert Kieffer
7 | Dual licensed under the MIT and GPL licenses.
8 | */
9 |
10 | /*
11 | * Generate a random uuid.
12 | *
13 | * USAGE: Math.uuid(length, radix)
14 | * length - the desired number of characters
15 | * radix - the number of allowable values for each character.
16 | *
17 | * EXAMPLES:
18 | * // No arguments - returns RFC4122, version 4 ID
19 | * >>> Math.uuid()
20 | * "92329D39-6F5C-4520-ABFC-AAB64544E172"
21 | *
22 | * // One argument - returns ID of the specified length
23 | * >>> Math.uuid(15) // 15 character ID (default base=62)
24 | * "VcydxgltxrVZSTV"
25 | *
26 | * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
27 | * >>> Math.uuid(8, 2) // 8 character ID (base=2)
28 | * "01001010"
29 | * >>> Math.uuid(8, 10) // 8 character ID (base=10)
30 | * "47473046"
31 | * >>> Math.uuid(8, 16) // 8 character ID (base=16)
32 | * "098F4D35"
33 | */
34 | (function() {
35 | // Private array of chars to use
36 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
37 |
38 | Math.uuid = function (len, radix) {
39 | var chars = CHARS, uuid = [], i;
40 | radix = radix || chars.length;
41 |
42 | if (len) {
43 | // Compact form
44 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
45 | } else {
46 | // rfc4122, version 4 form
47 | var r;
48 |
49 | // rfc4122 requires these characters
50 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
51 | uuid[14] = '4';
52 |
53 | // Fill in random data. At i==19 set the high bits of clock sequence as
54 | // per rfc4122, sec. 4.1.5
55 | for (i = 0; i < 36; i++) {
56 | if (!uuid[i]) {
57 | r = 0 | Math.random()*16;
58 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
59 | }
60 | }
61 | }
62 |
63 | return uuid.join('');
64 | };
65 |
66 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance
67 | // by minimizing calls to random()
68 | Math.uuidFast = function() {
69 | var chars = CHARS, uuid = new Array(36), rnd=0, r;
70 | for (var i = 0; i < 36; i++) {
71 | if (i==8 || i==13 || i==18 || i==23) {
72 | uuid[i] = '-';
73 | } else if (i==14) {
74 | uuid[i] = '4';
75 | } else {
76 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0;
77 | r = rnd & 0xf;
78 | rnd = rnd >> 4;
79 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
80 | }
81 | }
82 | return uuid.join('');
83 | };
84 |
85 | // A more compact, but less performant, RFC4122v4 solution:
86 | Math.uuidCompact = function() {
87 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
88 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
89 | return v.toString(16);
90 | });
91 | };
92 | })();
93 |
--------------------------------------------------------------------------------
/dist/vcard-0.1.min.js:
--------------------------------------------------------------------------------
1 | var VCard;
2 | (function(){
3 | VCard=function(){
4 | };
5 | VCard.prototype={setAttribute:function(_1,_2){
6 | console.log("set attribute",_1,_2);
7 | this[_1]=_2;
8 | }};
9 | })();
10 | var VCF;
11 | (function(){
12 | VCF={simpleKeys:["VERSION","FN","PHOTO"],csvKeys:["NICKNAME"],dateAndOrTimeKeys:["BDAY","ANNIVERSARY"],parse:function(_1,_2,_3){
13 | var _4=null;
14 | if(!_3){
15 | _3=this;
16 | }
17 | this.lex(_1,function(_5,_6,_7){
18 | function _8(_9){
19 | if(_4){
20 | _4.setAttribute(_5.toLowerCase(),_9);
21 | }
22 | };
23 | if(_5=="BEGIN"){
24 | _4=new VCard();
25 | }else{
26 | if(_5=="END"){
27 | if(_4){
28 | _2.apply(_3,[_4]);
29 | _4=null;
30 | }
31 | }else{
32 | if(this.simpleKeys.indexOf(_5)!=-1){
33 | _8(_6);
34 | }else{
35 | if(this.csvKeys.indexOf(_5)!=-1){
36 | _8(_6.split(","));
37 | }else{
38 | if(this.dateAndOrTimeKeys.indexOf(_5)!=-1){
39 | if(_7.VALUE=="text"){
40 | _8(_6);
41 | }else{
42 | if(_7.CALSCALE&&_7.CALSCALE!="gregorian"){
43 | }else{
44 | _8(this.parseDateAndOrTime(_6));
45 | }
46 | }
47 | }else{
48 | if(_5=="N"){
49 | _8(this.parseName(_6));
50 | }else{
51 | if(_5=="GENDER"){
52 | _8(this.parseGender(_6));
53 | }else{
54 | console.log("WARNING: unhandled key: ",_5);
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 | });
63 | },nameParts:["family-name","given-name","additional-name","honorific-prefix","honorific-suffix"],parseName:function(_a){
64 | var _b=_a.split(";");
65 | var n={};
66 | for(var i in _b){
67 | if(_b[i]){
68 | n[this.nameParts[i]]=_b[i].split(",");
69 | }
70 | }
71 | return n;
72 | },parseGender:function(_c){
73 | var _d={};
74 | var _e=_c.split(";");
75 | switch(_e[0]){
76 | case "M":
77 | _d.sex="male";
78 | break;
79 | case "F":
80 | _d.sex="female";
81 | break;
82 | case "O":
83 | _d.sex="other";
84 | }
85 | if(_e[1]){
86 | _d.identity=_e[1];
87 | }
88 | return _d;
89 | },dateRE:/^(\d{4})(\d{2})(\d{2})$/,dateReducedARE:/^(\d{4})\-(\d{2})$/,dateReducedBRE:/^(\d{4})$/,dateTruncatedMDRE:/^\-{2}(\d{2})(\d{2})$/,dateTruncatedDRE:/^\-{3}(\d{2})$/,timeRE:/^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedARE:/^(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedBRE:/^(\d{2})([+\-]\d+|Z|)$/,timeTruncatedMSRE:/^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeTruncatedSRE:/^\-{3}(\d{2})([+\-]\d+|Z|)$/,parseDate:function(_f){
90 | var md;
91 | var y,m,d;
92 | if((md=_f.match(this.dateRE))){
93 | y=md[1];
94 | m=md[2];
95 | d=md[3];
96 | }else{
97 | if((md=_f.match(this.dateReducedARE))){
98 | y=md[1];
99 | m=md[2];
100 | }else{
101 | if((md=_f.match(this.dateReducedBRE))){
102 | y=md[1];
103 | }else{
104 | if((md=_f.match(this.dateTruncatedMDRE))){
105 | m=md[1];
106 | d=md[2];
107 | }else{
108 | if((md=_f.match(this.dateTruncatedDRE))){
109 | d=md[1];
110 | }else{
111 | console.error("WARNING: failed to parse date: ",_f);
112 | return null;
113 | }
114 | }
115 | }
116 | }
117 | }
118 | var dt=new Date(0);
119 | if(typeof (y)!="undefined"){
120 | dt.setUTCFullYear(y);
121 | }
122 | if(typeof (m)!="undefined"){
123 | dt.setUTCMonth(m-1);
124 | }
125 | if(typeof (d)!="undefined"){
126 | dt.setUTCDate(d);
127 | }
128 | return dt;
129 | },parseTime:function(_10){
130 | var md;
131 | var h,m,s,tz;
132 | if((md=_10.match(this.timeRE))){
133 | h=md[1];
134 | m=md[2];
135 | s=md[3];
136 | tz=md[4];
137 | }else{
138 | if((md=_10.match(this.timeReducedARE))){
139 | h=md[1];
140 | m=md[2];
141 | tz=md[3];
142 | }else{
143 | if((md=_10.match(this.timeReducedBRE))){
144 | h=md[1];
145 | tz=md[2];
146 | }else{
147 | if((md=_10.match(this.timeTruncatedMSRE))){
148 | m=md[1];
149 | s=md[2];
150 | tz=md[3];
151 | }else{
152 | if((md=_10.match(this.timeTruncatedSRE))){
153 | s=md[1];
154 | tz=md[2];
155 | }else{
156 | console.error("WARNING: failed to parse time: ",_10);
157 | return null;
158 | }
159 | }
160 | }
161 | }
162 | }
163 | var dt=new Date(0);
164 | if(typeof (h)!="undefined"){
165 | dt.setUTCHours(h);
166 | }
167 | if(typeof (m)!="undefined"){
168 | dt.setUTCMinutes(m);
169 | }
170 | if(typeof (s)!="undefined"){
171 | dt.setUTCSeconds(s);
172 | }
173 | if(tz){
174 | dt=this.applyTimezone(dt,tz);
175 | }
176 | return dt;
177 | },addDates:function(_11,_12,_13){
178 | if(typeof (_13)=="undefined"){
179 | _13=true;
180 | }
181 | if(!_11){
182 | return _12;
183 | }
184 | if(!_12){
185 | return _11;
186 | }
187 | var a=Number(_11);
188 | var b=Number(_12);
189 | var c=_13?a+b:a-b;
190 | return new Date(c);
191 | },applyTimezone:function(_14,tz){
192 | var md;
193 | if((md=tz.match(/^([+\-])(\d{2})(\d{2})?/))){
194 | var _15=new Date(0);
195 | _15.setUTCHours(md[2]);
196 | _15.setUTCMinutes(md[3]||0);
197 | return this.addDates(_14,_15,md[1]=="+");
198 | }else{
199 | return _14;
200 | }
201 | },parseDateTime:function(_16){
202 | var _17=_16.split("T");
203 | var t=this.parseDate(_17[0]);
204 | var d=this.parseTime(_17[1]);
205 | return this.addDates(t,d);
206 | },parseDateAndOrTime:function(_18){
207 | switch(_18.indexOf("T")){
208 | case 0:
209 | return this.parseTime(_18.slice(1));
210 | case -1:
211 | return this.parseDate(_18);
212 | default:
213 | return this.parseDateTime(_18);
214 | }
215 | },lineRE:/^([^\s].*)(?:\r?\n|$)/,foldedLineRE:/^\s(.+)(?:\r?\n|$)/,lex:function(_19,_1a){
216 | var md,_1b=null,_1c=0;
217 | for(;;){
218 | if((md=_19.match(this.lineRE))){
219 | if(_1b){
220 | this.lexLine(_1b,_1a);
221 | }
222 | _1b=md[1];
223 | _1c=md[0].length;
224 | }else{
225 | if((md=_19.match(this.foldedLineRE))){
226 | if(_1b){
227 | _1b+=md[1];
228 | _1c=md[0].length;
229 | }else{
230 | }
231 | }else{
232 | console.error("Unmatched line: "+_1b);
233 | }
234 | }
235 | _19=_19.slice(_1c);
236 | if(!_19){
237 | break;
238 | }
239 | }
240 | if(_1b){
241 | this.lexLine(_1b,_1a);
242 | }
243 | _1b=null;
244 | },lexLine:function(_1d,_1e){
245 | var tmp="";
246 | var key=null,_1f={},_20=null,_21=null;
247 | function _22(){
248 | if(key){
249 | if(_21){
250 | _1f[_21]=tmp;
251 | }else{
252 | console.error("Invalid attribute: ",tmp,"Line dropped.");
253 | return;
254 | }
255 | }else{
256 | key=tmp;
257 | }
258 | };
259 | for(var i in _1d){
260 | var c=_1d[i];
261 | switch(c){
262 | case ":":
263 | _22();
264 | _20=_1d.slice(Number(i)+1);
265 | _1e.apply(this,[key,_20,_1f]);
266 | return;
267 | case ";":
268 | _22();
269 | tmp="";
270 | break;
271 | case "=":
272 | _21=tmp;
273 | tmp="";
274 | break;
275 | default:
276 | tmp+=c;
277 | }
278 | }
279 | }};
280 | })();
281 |
282 |
--------------------------------------------------------------------------------
/dist/vcard-0.2dev.min.js:
--------------------------------------------------------------------------------
1 | var VCard;
2 | (function(){
3 | VCard=function(){
4 | };
5 | VCard.prototype={setAttribute:function(_1,_2){
6 | this[_1]=_2;
7 | },addAttribute:function(_3,_4){
8 | console.log("add attribute",_3,_4);
9 | if(VCard.multivaluedKeys[_3]){
10 | if(this[_3]){
11 | this[_3].push(_4);
12 | }else{
13 | this.setAttribute(_3,[_4]);
14 | }
15 | }else{
16 | this.setAttribute(_3,_4);
17 | }
18 | },toJCard:function(){
19 | var _5={};
20 | for(var k in VCard.allKeys){
21 | var _6=VCard.allKeys[k];
22 | if(this[_6]){
23 | _5[_6]=this[_6];
24 | }
25 | }
26 | return _5;
27 | }};
28 | VCard.allKeys=["fn","n","nickname","photo","bday","anniversary","gender","tel","email"];
29 | VCard.multivaluedKeys={email:true,tel:true};
30 | })();
31 | var VCF;
32 | (function(){
33 | VCF={simpleKeys:["VERSION","FN","PHOTO"],csvKeys:["NICKNAME"],dateAndOrTimeKeys:["BDAY","ANNIVERSARY"],parse:function(_1,_2,_3){
34 | var _4=null;
35 | if(!_3){
36 | _3=this;
37 | }
38 | this.lex(_1,function(_5,_6,_7){
39 | function _8(_9){
40 | if(_4){
41 | _4.addAttribute(_5.toLowerCase(),_9);
42 | }
43 | };
44 | if(_5=="BEGIN"){
45 | _4=new VCard();
46 | }else{
47 | if(_5=="END"){
48 | if(_4){
49 | _2.apply(_3,[_4]);
50 | _4=null;
51 | }
52 | }else{
53 | if(this.simpleKeys.indexOf(_5)!=-1){
54 | _8(_6);
55 | }else{
56 | if(this.csvKeys.indexOf(_5)!=-1){
57 | _8(_6.split(","));
58 | }else{
59 | if(this.dateAndOrTimeKeys.indexOf(_5)!=-1){
60 | if(_7.VALUE=="text"){
61 | _8(_6);
62 | }else{
63 | if(_7.CALSCALE&&_7.CALSCALE!="gregorian"){
64 | }else{
65 | _8(this.parseDateAndOrTime(_6));
66 | }
67 | }
68 | }else{
69 | if(_5=="N"){
70 | _8(this.parseName(_6));
71 | }else{
72 | if(_5=="GENDER"){
73 | _8(this.parseGender(_6));
74 | }else{
75 | if(_5=="TEL"){
76 | _8({type:(_7.TYPE||"voice"),value:_6});
77 | }else{
78 | if(_5=="EMAIL"){
79 | _8({type:_7.TYPE,value:_6});
80 | }else{
81 | if(_5=="IMPP"){
82 | _8({value:_6});
83 | }else{
84 | console.log("WARNING: unhandled key: ",_5);
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 | });
96 | },nameParts:["family-name","given-name","additional-name","honorific-prefix","honorific-suffix"],parseName:function(_a){
97 | var _b=_a.split(";");
98 | var n={};
99 | for(var i in _b){
100 | if(_b[i]){
101 | n[this.nameParts[i]]=_b[i].split(",");
102 | }
103 | }
104 | return n;
105 | },parseGender:function(_c){
106 | var _d={};
107 | var _e=_c.split(";");
108 | switch(_e[0]){
109 | case "M":
110 | _d.sex="male";
111 | break;
112 | case "F":
113 | _d.sex="female";
114 | break;
115 | case "O":
116 | _d.sex="other";
117 | }
118 | if(_e[1]){
119 | _d.identity=_e[1];
120 | }
121 | return _d;
122 | },dateRE:/^(\d{4})(\d{2})(\d{2})$/,dateReducedARE:/^(\d{4})\-(\d{2})$/,dateReducedBRE:/^(\d{4})$/,dateTruncatedMDRE:/^\-{2}(\d{2})(\d{2})$/,dateTruncatedDRE:/^\-{3}(\d{2})$/,timeRE:/^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedARE:/^(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedBRE:/^(\d{2})([+\-]\d+|Z|)$/,timeTruncatedMSRE:/^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeTruncatedSRE:/^\-{3}(\d{2})([+\-]\d+|Z|)$/,parseDate:function(_f){
123 | var md;
124 | var y,m,d;
125 | if((md=_f.match(this.dateRE))){
126 | y=md[1];
127 | m=md[2];
128 | d=md[3];
129 | }else{
130 | if((md=_f.match(this.dateReducedARE))){
131 | y=md[1];
132 | m=md[2];
133 | }else{
134 | if((md=_f.match(this.dateReducedBRE))){
135 | y=md[1];
136 | }else{
137 | if((md=_f.match(this.dateTruncatedMDRE))){
138 | m=md[1];
139 | d=md[2];
140 | }else{
141 | if((md=_f.match(this.dateTruncatedDRE))){
142 | d=md[1];
143 | }else{
144 | console.error("WARNING: failed to parse date: ",_f);
145 | return null;
146 | }
147 | }
148 | }
149 | }
150 | }
151 | var dt=new Date(0);
152 | if(typeof (y)!="undefined"){
153 | dt.setUTCFullYear(y);
154 | }
155 | if(typeof (m)!="undefined"){
156 | dt.setUTCMonth(m-1);
157 | }
158 | if(typeof (d)!="undefined"){
159 | dt.setUTCDate(d);
160 | }
161 | return dt;
162 | },parseTime:function(_10){
163 | var md;
164 | var h,m,s,tz;
165 | if((md=_10.match(this.timeRE))){
166 | h=md[1];
167 | m=md[2];
168 | s=md[3];
169 | tz=md[4];
170 | }else{
171 | if((md=_10.match(this.timeReducedARE))){
172 | h=md[1];
173 | m=md[2];
174 | tz=md[3];
175 | }else{
176 | if((md=_10.match(this.timeReducedBRE))){
177 | h=md[1];
178 | tz=md[2];
179 | }else{
180 | if((md=_10.match(this.timeTruncatedMSRE))){
181 | m=md[1];
182 | s=md[2];
183 | tz=md[3];
184 | }else{
185 | if((md=_10.match(this.timeTruncatedSRE))){
186 | s=md[1];
187 | tz=md[2];
188 | }else{
189 | console.error("WARNING: failed to parse time: ",_10);
190 | return null;
191 | }
192 | }
193 | }
194 | }
195 | }
196 | var dt=new Date(0);
197 | if(typeof (h)!="undefined"){
198 | dt.setUTCHours(h);
199 | }
200 | if(typeof (m)!="undefined"){
201 | dt.setUTCMinutes(m);
202 | }
203 | if(typeof (s)!="undefined"){
204 | dt.setUTCSeconds(s);
205 | }
206 | if(tz){
207 | dt=this.applyTimezone(dt,tz);
208 | }
209 | return dt;
210 | },addDates:function(_11,_12,_13){
211 | if(typeof (_13)=="undefined"){
212 | _13=true;
213 | }
214 | if(!_11){
215 | return _12;
216 | }
217 | if(!_12){
218 | return _11;
219 | }
220 | var a=Number(_11);
221 | var b=Number(_12);
222 | var c=_13?a+b:a-b;
223 | return new Date(c);
224 | },applyTimezone:function(_14,tz){
225 | var md;
226 | if((md=tz.match(/^([+\-])(\d{2})(\d{2})?/))){
227 | var _15=new Date(0);
228 | _15.setUTCHours(md[2]);
229 | _15.setUTCMinutes(md[3]||0);
230 | return this.addDates(_14,_15,md[1]=="+");
231 | }else{
232 | return _14;
233 | }
234 | },parseDateTime:function(_16){
235 | var _17=_16.split("T");
236 | var t=this.parseDate(_17[0]);
237 | var d=this.parseTime(_17[1]);
238 | return this.addDates(t,d);
239 | },parseDateAndOrTime:function(_18){
240 | switch(_18.indexOf("T")){
241 | case 0:
242 | return this.parseTime(_18.slice(1));
243 | case -1:
244 | return this.parseDate(_18);
245 | default:
246 | return this.parseDateTime(_18);
247 | }
248 | },lineRE:/^([^\s].*)(?:\r?\n|$)/,foldedLineRE:/^\s(.+)(?:\r?\n|$)/,lex:function(_19,_1a){
249 | var md,_1b=null,_1c=0;
250 | for(;;){
251 | if((md=_19.match(this.lineRE))){
252 | if(_1b){
253 | this.lexLine(_1b,_1a);
254 | }
255 | _1b=md[1];
256 | _1c=md[0].length;
257 | }else{
258 | if((md=_19.match(this.foldedLineRE))){
259 | if(_1b){
260 | _1b+=md[1];
261 | _1c=md[0].length;
262 | }else{
263 | }
264 | }else{
265 | console.error("Unmatched line: "+_1b);
266 | }
267 | }
268 | _19=_19.slice(_1c);
269 | if(!_19){
270 | break;
271 | }
272 | }
273 | if(_1b){
274 | this.lexLine(_1b,_1a);
275 | }
276 | _1b=null;
277 | },lexLine:function(_1d,_1e){
278 | var tmp="";
279 | var key=null,_1f={},_20=null,_21=null;
280 | function _22(){
281 | if(key){
282 | if(_21){
283 | _1f[_21]=tmp;
284 | }else{
285 | console.error("Invalid attribute: ",tmp,"Line dropped.");
286 | return;
287 | }
288 | }else{
289 | key=tmp;
290 | }
291 | };
292 | for(var i in _1d){
293 | var c=_1d[i];
294 | switch(c){
295 | case ":":
296 | _22();
297 | _20=_1d.slice(Number(i)+1);
298 | _1e.apply(this,[key,_20,_1f]);
299 | return;
300 | case ";":
301 | _22();
302 | tmp="";
303 | break;
304 | case "=":
305 | _21=tmp;
306 | tmp="";
307 | break;
308 | default:
309 | tmp+=c;
310 | }
311 | }
312 | }};
313 | })();
314 |
315 |
--------------------------------------------------------------------------------
/src/vcard.js:
--------------------------------------------------------------------------------
1 |
2 | // exported globals
3 | var VCard;
4 |
5 | (function() {
6 |
7 | VCard = function(attributes) {
8 | this.changed = false;
9 | if(typeof(attributes) === 'object') {
10 | for(var key in attributes) {
11 | this[key] = attributes[key];
12 | this.changed = true;
13 | }
14 | }
15 | };
16 |
17 | VCard.prototype = {
18 |
19 | // Check validity of this VCard instance. Properties that can be generated,
20 | // will be generated. If any error is found, false is returned and vcard.errors
21 | // set to an Array of [attribute, errorType] arrays.
22 | // Otherwise true is returned.
23 | //
24 | // In case of multivalued properties, the "attribute" part of the error is
25 | // the attribute name, plus it's index (starting at 0). Example: email0, tel7, ...
26 | //
27 | // It is recommended to call this method even if this VCard object was imported,
28 | // as some software (e.g. Gmail) doesn't generate UIDs.
29 | validate: function() {
30 | var errors = [];
31 |
32 | function addError(attribute, type) {
33 | errors.push([attribute, type]);
34 | }
35 |
36 | if(! this.fn) { // FN is a required attribute
37 | addError("fn", "required");
38 | }
39 |
40 | // make sure multivalued properties are *always* in array form
41 | for(var key in VCard.multivaluedKeys) {
42 | if(this[key] && ! (this[key] instanceof Array)) {
43 | this[key] = [this[key]];
44 | }
45 | }
46 |
47 | // make sure compound fields have their type & value set
48 | // (to prevent mistakes such as vcard.addAttribute('email', 'foo@bar.baz')
49 | function validateCompoundWithType(attribute, values) {
50 | for(var i in values) {
51 | var value = values[i];
52 | if(typeof(value) !== 'object') {
53 | errors.push([attribute + '-' + i, "not-an-object"]);
54 | } else if(! value.type) {
55 | errors.push([attribute + '-' + i, "missing-type"]);
56 | } else if(! value.value) { // empty values are not allowed.
57 | errors.push([attribute + '-' + i, "missing-value"]);
58 | }
59 | }
60 | }
61 |
62 | if(this.email) {
63 | validateCompoundWithType('email', this.email);
64 | }
65 |
66 | if(this.tel) {
67 | validateCompoundWithType('email', this.tel);
68 | }
69 |
70 | if(! this.uid) {
71 | this.addAttribute('uid', this.generateUID());
72 | }
73 |
74 | if(! this.rev) {
75 | this.addAttribute('rev', this.generateRev());
76 | }
77 |
78 | this.errors = errors;
79 |
80 | return ! (errors.length > 0);
81 | },
82 |
83 | // generate a UID. This generates a UUID with uuid: URN namespace, as suggested
84 | // by RFC 6350, 6.7.6
85 | generateUID: function() {
86 | return 'uuid:' + Math.uuid();
87 | },
88 |
89 | // generate revision timestamp (a full ISO 8601 date/time string in basic format)
90 | generateRev: function() {
91 | return (new Date()).toISOString().replace(/[\.\:\-]/g, '');
92 | },
93 |
94 | // Set the given attribute to the given value.
95 | // This sets vcard.changed to true, so you can check later whether anything
96 | // was updated by your code.
97 | setAttribute: function(key, value) {
98 | this[key] = value;
99 | this.changed = true;
100 | },
101 |
102 | // Set the given attribute to the given value.
103 | // If the given attribute's key has cardinality > 1, instead of overwriting
104 | // the current value, an additional value is appended.
105 | addAttribute: function(key, value) {
106 | console.log('add attribute', key, value);
107 | if(! value) {
108 | return;
109 | }
110 | if(VCard.multivaluedKeys[key]) {
111 | if(this[key]) {
112 | console.log('multivalued push');
113 | this[key].push(value)
114 | } else {
115 | console.log('multivalued set');
116 | this.setAttribute(key, [value]);
117 | }
118 | } else {
119 | this.setAttribute(key, value);
120 | }
121 | },
122 |
123 | // convenience method to get a JSON serialized jCard.
124 | toJSON: function() {
125 | return JSON.stringify(this.toJCard());
126 | },
127 |
128 | // Copies all properties (i.e. all specified in VCard.allKeys) to a new object
129 | // and returns it.
130 | // Useful to serialize to JSON afterwards.
131 | toJCard: function() {
132 | var jcard = {};
133 | for(var k in VCard.allKeys) {
134 | var key = VCard.allKeys[k];
135 | if(this[key]) {
136 | jcard[key] = this[key];
137 | }
138 | }
139 | return jcard;
140 | },
141 |
142 | // synchronizes two vcards, using the mechanisms described in
143 | // RFC 6350, Section 7.
144 | // Returns a new VCard object.
145 | // If a property is present in both source vcards, and that property's
146 | // maximum cardinality is 1, then the value from the second (given) vcard
147 | // precedes.
148 | //
149 | // TODO: implement PID matching as described in 7.3.1
150 | merge: function(other) {
151 | if(typeof(other.uid) !== 'undefined' &&
152 | typeof(this.uid) !== 'undefined' &&
153 | other.uid !== this.uid) {
154 | // 7.1.1
155 | throw "Won't merge vcards without matching UIDs.";
156 | }
157 |
158 | var result = new VCard();
159 |
160 | function mergeProperty(key) {
161 | if(other[key]) {
162 | if(other[key] == this[key]) {
163 | result.setAttribute(this[key]);
164 | } else {
165 | result.addAttribute(this[key]);
166 | result.addAttribute(other[key]);
167 | }
168 | } else {
169 | result[key] = this[key];
170 | }
171 | }
172 |
173 | for(key in this) { // all properties of this
174 | mergeProperty(key);
175 | }
176 | for(key in other) { // all properties of other *not* in this
177 | if(! result[key]) {
178 | mergeProperty(key);
179 | }
180 | }
181 | }
182 | };
183 |
184 | VCard.enums = {
185 | telType: ["text", "voice", "fax", "cell", "video", "pager", "textphone"],
186 | relatedType: ["contact", "acquaintance", "friend", "met", "co-worker",
187 | "colleague", "co-resident", "neighbor", "child", "parent",
188 | "sibling", "spouse", "kin", "muse", "crush", "date",
189 | "sweetheart", "me", "agent", "emergency"],
190 | // FIXME: these aren't actually defined anywhere. just very commmon.
191 | // maybe there should be more?
192 | emailType: ["work", "home", "internet"],
193 | langType: ["work", "home"],
194 |
195 | };
196 |
197 | VCard.allKeys = [
198 | 'fn', 'n', 'nickname', 'photo', 'bday', 'anniversary', 'gender',
199 | 'tel', 'email', 'impp', 'lang', 'tz', 'geo', 'title', 'role', 'logo',
200 | 'org', 'member', 'related', 'categories', 'note', 'prodid', 'rev',
201 | 'sound', 'uid'
202 | ];
203 |
204 | VCard.multivaluedKeys = {
205 | email: true,
206 | tel: true,
207 | geo: true,
208 | title: true,
209 | role: true,
210 | logo: true,
211 | org: true,
212 | member: true,
213 | related: true,
214 | categories: true,
215 | note: true
216 | };
217 |
218 | })();
219 |
--------------------------------------------------------------------------------
/test/vcf.js:
--------------------------------------------------------------------------------
1 |
2 | /***
3 | ** DATE / TIME PARSER TESTS
4 | **/
5 |
6 | module('VCF.parseDate');
7 |
8 | test("YYYYMMDD", function() {
9 | var date = VCF.parseDate('19870417');
10 | equal(date.getUTCFullYear(), 1987, 'year');
11 | equal(date.getUTCMonth(), 3, 'month');
12 | equal(date.getUTCDate(), 17, 'day');
13 | });
14 |
15 | test("YYYY-MM", function() {
16 | var date = VCF.parseDate('1987-04');
17 | equal(date.getUTCFullYear(), 1987, 'year');
18 | equal(date.getUTCMonth(), 3, 'month');
19 | });
20 |
21 | test("YYYY", function() {
22 | var date = VCF.parseDate('1987');
23 | equal(date.getUTCFullYear(), 1987, 'year');
24 | });
25 |
26 | test("--MMDD", function() {
27 | var date = VCF.parseDate('--0417');
28 | equal(date.getUTCMonth(), 3, 'month');
29 | equal(date.getUTCDate(), 17, 'day')
30 | });
31 |
32 | test("---DD", function() {
33 | var date = VCF.parseDate('---17');
34 | equal(date.getUTCDate(), 17, 'day')
35 | });
36 |
37 | module('VCF.parseTime');
38 |
39 | test("HHmmss", function() {
40 | var time = VCF.parseTime('235930');
41 | equal(time.getUTCHours(), 23, 'hours');
42 | equal(time.getUTCMinutes(), 59, 'minutes');
43 | equal(time.getUTCSeconds(), 30, 'seconds');
44 | });
45 |
46 | test("HHmmssZ", function() {
47 | var time = VCF.parseTime('235930Z');
48 | equal(time.getUTCHours(), 23, 'hours');
49 | equal(time.getUTCMinutes(), 59, 'minutes');
50 | equal(time.getUTCSeconds(), 30, 'seconds');
51 | });
52 |
53 | test("HHmmss+NN", function() {
54 | var time = VCF.parseTime('215930+02');
55 | equal(time.getUTCHours(), 23, 'hours');
56 | equal(time.getUTCMinutes(), 59, 'minutes');
57 | equal(time.getUTCSeconds(), 30, 'seconds');
58 | });
59 |
60 | test("HHmmss+NNNN", function() {
61 | var time = VCF.parseTime('162930+0730');
62 | equal(time.getUTCHours(), 23, 'hours');
63 | equal(time.getUTCMinutes(), 59, 'minutes');
64 | equal(time.getUTCSeconds(), 30, 'seconds');
65 | });
66 |
67 | test("HHmmss-NN", function() {
68 | var time = VCF.parseTime('015930-02');
69 | equal(time.getUTCHours(), 23, 'hours');
70 | equal(time.getUTCMinutes(), 59, 'minutes');
71 | equal(time.getUTCSeconds(), 30, 'seconds');
72 | });
73 |
74 | test("HHmmss-NNNN", function() {
75 | var time = VCF.parseTime('012930-0130');
76 | equal(time.getUTCHours(), 23, 'hours');
77 | equal(time.getUTCMinutes(), 59, 'minutes');
78 | equal(time.getUTCSeconds(), 30, 'seconds');
79 | });
80 |
81 | test("HHmm", function() {
82 | var time = VCF.parseTime('2359');
83 | equal(time.getUTCHours(), 23, 'hours');
84 | equal(time.getUTCMinutes(), 59, 'minutes');
85 | });
86 |
87 | test("HH", function() {
88 | var time = VCF.parseTime('23');
89 | equal(time.getUTCHours(), 23, 'hours');
90 | });
91 |
92 | test("--mmss", function() {
93 | var time = VCF.parseTime('--5930');
94 | equal(time.getUTCMinutes(), 59, 'minutes');
95 | equal(time.getUTCSeconds(), 30, 'seconds');
96 | });
97 |
98 | test("---ss", function() {
99 | var time = VCF.parseTime('---30');
100 | equal(time.getUTCSeconds(), 30, 'seconds');
101 | });
102 |
103 | module("VCF.parseDateTime");
104 |
105 | test("YYYYMMDDTHHmmss", function() {
106 | var datetime = VCF.parseDateTime('19870417T172345');
107 | equal(datetime.getUTCFullYear(), 1987, 'year');
108 | equal(datetime.getUTCMonth(), 3, 'month');
109 | equal(datetime.getUTCDate(), 17, 'day');
110 | equal(datetime.getUTCHours(), 17, 'hours');
111 | equal(datetime.getUTCMinutes(), 23, 'minutes');
112 | equal(datetime.getUTCSeconds(), 45, 'seconds');
113 | });
114 |
115 | test("--MMDDTHHmm", function() {
116 | var datetime = VCF.parseDateTime('--0417T1723');
117 | equal(datetime.getUTCMonth(), 3, 'month');
118 | equal(datetime.getUTCDate(), 17, 'day');
119 | equal(datetime.getUTCHours(), 17, 'hours');
120 | equal(datetime.getUTCMinutes(), 23, 'minutes');
121 | });
122 |
123 | test("---DDTHH", function() {
124 | var datetime = VCF.parseDateTime('---17T14');
125 | equal(datetime.getUTCDate(), 17, 'day');
126 | equal(datetime.getUTCHours(), 14, 'hours');
127 | });
128 |
129 | module("VCF.parseDateAndOrTime");
130 |
131 | test("THHmmss", function() {
132 | var time = VCF.parseDateAndOrTime('T235930');
133 | equal(time.getUTCHours(), 23, 'hours');
134 | equal(time.getUTCMinutes(), 59, 'minutes');
135 | equal(time.getUTCSeconds(), 30, 'seconds');
136 | });
137 |
138 |
139 | test("YYYYMMDDTHHmmss", function() {
140 | var datetime = VCF.parseDateAndOrTime('19870417T172345');
141 | equal(datetime.getUTCFullYear(), 1987, 'year');
142 | equal(datetime.getUTCMonth(), 3, 'month');
143 | equal(datetime.getUTCDate(), 17, 'day');
144 | equal(datetime.getUTCHours(), 17, 'hours');
145 | equal(datetime.getUTCMinutes(), 23, 'minutes');
146 | equal(datetime.getUTCSeconds(), 45, 'seconds');
147 | });
148 |
149 |
150 | test("YYYY", function() {
151 | var date = VCF.parseDateAndOrTime('1987');
152 | equal(date.getUTCFullYear(), 1987, 'year');
153 | });
154 |
155 |
156 | /***
157 | ** LEXER TESTS
158 | **/
159 |
160 | module('VCF.lex');
161 |
162 | function testTokens(actual, expected) {
163 | var exp = expected.shift();
164 | if(! exp) {
165 | return;
166 | }
167 | equal(actual[0], exp[0], 'key = ' + exp[0]);
168 | equal(actual[1], exp[1], 'value = ' + exp[1]);
169 | deepEqual(actual[2], exp[2], 'attrs = ' + JSON.stringify(exp[2]));
170 | start();
171 | }
172 |
173 | var sample1 =
174 | "BEGIN:VCARD\r\n" +
175 | "VERSION:4.0\r\n" +
176 | "END:VCARD";
177 |
178 | asyncTest("BEGIN, END and VERSION are recognized", function() {
179 | var expected = [
180 | ['BEGIN', 'VCARD', {}],
181 | ['VERSION', '4.0', {}],
182 | ['END', 'VCARD', {}]
183 | ]
184 | VCF.lex(sample1, function() {
185 | testTokens(arguments, expected);
186 | });
187 | });
188 |
189 | var sample2 =
190 | "BEGIN:VCARD\r\n" +
191 | "VERSION:4.0\r\n" +
192 | "FN:I am a full name\r\n" +
193 | "END:VCARD";
194 |
195 | asyncTest("Simple FN statement is recognized", function() {
196 | var expected = [
197 | null, null,
198 | ['FN', "I am a full name", {}],
199 | null
200 | ]
201 | VCF.lex(sample2, function() {
202 | testTokens(arguments, expected);
203 | });
204 | });
205 |
206 | var sample3 =
207 | "BEGIN:VCARD\r\n" +
208 | "VERSION:4.0\r\n" +
209 | "FN:I am a long name, that is broken in\r\n" +
210 | " to more than just a single line\r\n" +
211 | "\t to be exact three.\r\n" +
212 | "END:VCARD";
213 |
214 | asyncTest("Folded FN statement is recognized", function() {
215 | var expected = [
216 | null, null,
217 | ['FN', "I am a long name, that is broken into more than just a single line to be exact three.", {}],
218 | null
219 | ]
220 | VCF.lex(sample3, function() {
221 | testTokens(arguments, expected);
222 | });
223 | });
224 |
225 | /***
226 | ** PARSER TESTS
227 | **/
228 |
229 | module('VCF.parse');
230 |
231 | var sample4 =
232 | "BEGIN:VCARD\r\n" +
233 | "END:VCARD\r\n";
234 |
235 | asyncTest("A single vCard is recognized", function() {
236 | VCF.parse(sample4, function(vc) {
237 | ok(vc instanceof VCard, "this is a VCard object");
238 | start();
239 | });
240 | });
241 |
242 | var sample5 =
243 | "BEGIN:VCARD\r\n" +
244 | "END:VCARD\r\n" +
245 | "BEGIN:VCARD\r\n" +
246 | "END:VCARD\r\n";
247 |
248 |
249 | asyncTest("Two vCards are recognized", function() {
250 | var i = 0;
251 | VCF.parse(sample5, function(vc) {
252 | ok(vc instanceof VCard, "this is a VCard object");
253 | i += 1;
254 | start();
255 | });
256 | equal(i, 2, "two vcards have been yielded");
257 | });
258 |
259 | var sample6 =
260 | "BEGIN:VCARD\r\n" +
261 | "VERSION:4.0\r\n" +
262 | "END:VCARD\r\n";
263 |
264 | asyncTest("The version attribute is set correctly", function() {
265 | VCF.parse(sample6, function(vc) {
266 | equal(vc.version, '4.0', "vcard.version is 4.0");
267 | start();
268 | });
269 | });
270 |
271 | var sample7 =
272 | "BEGIN:VCARD\r\n" +
273 | "VERSION:4.0\r\n" +
274 | "FN:My formatted name\r\n" +
275 | "END:VCARD\r\n";
276 |
277 | asyncTest("The fn attribute is set correctly", function() {
278 |
279 | VCF.parse(sample7, function(vc) {
280 | equal(vc.fn, 'My formatted name', "vcard.fn is correct");
281 | start();
282 | });
283 |
284 | });
285 |
286 | var sample8 =
287 | "BEGIN:VCARD\r\n" +
288 | "VERSION:4.0\r\n" +
289 | "N:Lessing;Gotthold;Ephraim;;\r\n" +
290 | "END:VCARD\r\n";
291 |
292 | asyncTest("A three-part name is set correctly", function() {
293 | VCF.parse(sample8, function(vc) {
294 | ok(vc.n, "Name is set");
295 | deepEqual(vc.n, {
296 | 'given-name': ['Gotthold'],
297 | 'family-name': ['Lessing'],
298 | 'additional-name': ['Ephraim']
299 | });
300 |
301 | start();
302 | });
303 | });
304 |
305 |
306 | var sample9 =
307 | "BEGIN:VCARD\r\n" +
308 | "VERSION:4.0\r\n" +
309 | "N:Lessing;Gotthold;Ephraim,Soundso;Dr.,Prof.;von und zu hier und da\r\n" +
310 | "END:VCARD\r\n";
311 |
312 | asyncTest("A complex name with all parts and multiple values per part is set correctly", function() {
313 | VCF.parse(sample9, function(vc) {
314 | deepEqual(vc.n, {
315 | 'given-name': ['Gotthold'],
316 | 'family-name': ['Lessing'],
317 | 'additional-name': ['Ephraim', 'Soundso'],
318 | 'honorific-prefix': ['Dr.', 'Prof.'],
319 | 'honorific-suffix': ['von und zu hier und da']
320 | });
321 | start();
322 | });
323 | });
324 |
325 | var sample10 =
326 | "BEGIN:VCARD\r\n" +
327 | "VERSION:4.0\r\n" +
328 | "NICKNAME:foo,bar,baz\r\n" +
329 | "END:VCARD\r\n";
330 |
331 | asyncTest("nicknames are recognized", function(vc) {
332 | VCF.parse(sample10, function(vc) {
333 | deepEqual(vc.nickname, ['foo', 'bar', 'baz']);
334 | start();
335 | });
336 | });
337 |
338 | var sample11 =
339 | "BEGIN:VCARD\r\n" +
340 | "VERSION:4.0\r\n" +
341 | "BDAY:--0417\r\n" +
342 | "END:VCARD";
343 |
344 | asyncTest('birthday is recognized', function(vc) {
345 | VCF.parse(sample11, function(vc) {
346 | ok(vc.bday, "Birthday is set");
347 | equal(vc.bday.getUTCDate(), 17, "Day is correct");
348 | equal(vc.bday.getUTCMonth(), 3, "Month is correct");
349 | start();
350 | });
351 | });
352 |
353 |
354 | var sample12 =
355 | "BEGIN:VCARD\r\n" +
356 | "VERSION:4.0\r\n" +
357 | "GENDER:{gender}\r\n" +
358 | "END:VCARD";
359 |
360 | function testGender(text, value, expectedResult) {
361 | asyncTest("Gender: " + text, function() {
362 | VCF.parse(sample12.replace('{gender}', value), function(vc) {
363 | ok(vc.gender, "Gender is set");
364 | deepEqual(vc.gender, expectedResult);
365 | start();
366 | });
367 | });
368 | }
369 |
370 | testGender("male without identity", "M", {sex:'male'});
371 | testGender("female without identity", "F", {sex:'female'});
372 | testGender("female with identity", "F;boy", {sex:'female',identity:'boy'});
373 | testGender("other without identity", "O", {sex:'other'});
374 | testGender("none without identity", "N", {});
375 |
--------------------------------------------------------------------------------
/dist/vcardjs-0.2.min.js:
--------------------------------------------------------------------------------
1 | define("vcardjs",function(){
2 | (function(){
3 | var _1="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
4 | Math.uuid=function(_2,_3){
5 | var _4=_1,_5=[],i;
6 | _3=_3||_4.length;
7 | if(_2){
8 | for(i=0;i<_2;i++){
9 | _5[i]=_4[0|Math.random()*_3];
10 | }
11 | }else{
12 | var r;
13 | _5[8]=_5[13]=_5[18]=_5[23]="-";
14 | _5[14]="4";
15 | for(i=0;i<36;i++){
16 | if(!_5[i]){
17 | r=0|Math.random()*16;
18 | _5[i]=_4[(i==19)?(r&3)|8:r];
19 | }
20 | }
21 | }
22 | return _5.join("");
23 | };
24 | Math.uuidFast=function(){
25 | var _6=_1,_7=new Array(36),_8=0,r;
26 | for(var i=0;i<36;i++){
27 | if(i==8||i==13||i==18||i==23){
28 | _7[i]="-";
29 | }else{
30 | if(i==14){
31 | _7[i]="4";
32 | }else{
33 | if(_8<=2){
34 | _8=33554432+(Math.random()*16777216)|0;
35 | }
36 | r=_8&15;
37 | _8=_8>>4;
38 | _7[i]=_6[(i==19)?(r&3)|8:r];
39 | }
40 | }
41 | }
42 | return _7.join("");
43 | };
44 | Math.uuidCompact=function(){
45 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){
46 | var r=Math.random()*16|0,v=c=="x"?r:(r&3|8);
47 | return v.toString(16);
48 | });
49 | };
50 | })();
51 | var _9;
52 | (function(){
53 | _9=function(_a){
54 | this.changed=false;
55 | if(typeof (_a)==="object"){
56 | for(var _b in _a){
57 | this[_b]=_a[_b];
58 | this.changed=true;
59 | }
60 | }
61 | };
62 | _9.prototype={validate:function(){
63 | var _c=[];
64 | function _d(_e,_f){
65 | _c.push([_e,_f]);
66 | };
67 | if(!this.fn){
68 | _d("fn","required");
69 | }
70 | for(var key in _9.multivaluedKeys){
71 | if(this[key]&&!(this[key] instanceof Array)){
72 | this[key]=[this[key]];
73 | }
74 | }
75 | function _10(_11,_12){
76 | for(var i in _12){
77 | var _13=_12[i];
78 | if(typeof (_13)!=="object"){
79 | _c.push([_11+"-"+i,"not-an-object"]);
80 | }else{
81 | if(!_13.type){
82 | _c.push([_11+"-"+i,"missing-type"]);
83 | }else{
84 | if(!_13.value){
85 | _c.push([_11+"-"+i,"missing-value"]);
86 | }
87 | }
88 | }
89 | }
90 | };
91 | if(this.email){
92 | _10("email",this.email);
93 | }
94 | if(this.tel){
95 | _10("email",this.tel);
96 | }
97 | if(!this.uid){
98 | this.addAttribute("uid",this.generateUID());
99 | }
100 | if(!this.rev){
101 | this.addAttribute("rev",this.generateRev());
102 | }
103 | this.errors=_c;
104 | return !(_c.length>0);
105 | },generateUID:function(){
106 | return "uuid:"+Math.uuid();
107 | },generateRev:function(){
108 | return (new Date()).toISOString().replace(/[\.\:\-]/g,"");
109 | },setAttribute:function(key,_14){
110 | this[key]=_14;
111 | this.changed=true;
112 | },addAttribute:function(key,_15){
113 | console.log("add attribute",key,_15);
114 | if(!_15){
115 | return;
116 | }
117 | if(_9.multivaluedKeys[key]){
118 | if(this[key]){
119 | console.log("multivalued push");
120 | this[key].push(_15);
121 | }else{
122 | console.log("multivalued set");
123 | this.setAttribute(key,[_15]);
124 | }
125 | }else{
126 | this.setAttribute(key,_15);
127 | }
128 | },toJSON:function(){
129 | return JSON.stringify(this.toJCard());
130 | },toJCard:function(){
131 | var _16={};
132 | for(var k in _9.allKeys){
133 | var key=_9.allKeys[k];
134 | if(this[key]){
135 | _16[key]=this[key];
136 | }
137 | }
138 | return _16;
139 | },merge:function(_17){
140 | if(typeof (_17.uid)!=="undefined"&&typeof (this.uid)!=="undefined"&&_17.uid!==this.uid){
141 | throw "Won't merge vcards without matching UIDs.";
142 | }
143 | var _18=new _9();
144 | function _19(key){
145 | if(_17[key]){
146 | if(_17[key]==this[key]){
147 | _18.setAttribute(this[key]);
148 | }else{
149 | _18.addAttribute(this[key]);
150 | _18.addAttribute(_17[key]);
151 | }
152 | }else{
153 | _18[key]=this[key];
154 | }
155 | };
156 | for(key in this){
157 | _19(key);
158 | }
159 | for(key in _17){
160 | if(!_18[key]){
161 | _19(key);
162 | }
163 | }
164 | }};
165 | _9.enums={telType:["text","voice","fax","cell","video","pager","textphone"],relatedType:["contact","acquaintance","friend","met","co-worker","colleague","co-resident","neighbor","child","parent","sibling","spouse","kin","muse","crush","date","sweetheart","me","agent","emergency"],emailType:["work","home","internet"],langType:["work","home"]};
166 | _9.allKeys=["fn","n","nickname","photo","bday","anniversary","gender","tel","email","impp","lang","tz","geo","title","role","logo","org","member","related","categories","note","prodid","rev","sound","uid"];
167 | _9.multivaluedKeys={email:true,tel:true,geo:true,title:true,role:true,logo:true,org:true,member:true,related:true,categories:true,note:true};
168 | })();
169 | var VCF;
170 | (function(){
171 | VCF={simpleKeys:["VERSION","FN","PHOTO","GEO","TITLE","ROLE","LOGO","MEMBER","NOTE","PRODID","SOUND","UID"],csvKeys:["NICKNAME","CATEGORIES"],dateAndOrTimeKeys:["BDAY","ANNIVERSARY","REV"],parse:function(_1a,_1b,_1c){
172 | var _1d=null;
173 | if(!_1c){
174 | _1c=this;
175 | }
176 | this.lex(_1a,function(key,_1e,_1f){
177 | function _20(val){
178 | if(_1d){
179 | _1d.addAttribute(key.toLowerCase(),val);
180 | }
181 | };
182 | if(key=="BEGIN"){
183 | _1d=new _9();
184 | }else{
185 | if(key=="END"){
186 | if(_1d){
187 | _1b.apply(_1c,[_1d]);
188 | _1d=null;
189 | }
190 | }else{
191 | if(this.simpleKeys.indexOf(key)!=-1){
192 | _20(_1e);
193 | }else{
194 | if(this.csvKeys.indexOf(key)!=-1){
195 | _20(_1e.split(","));
196 | }else{
197 | if(this.dateAndOrTimeKeys.indexOf(key)!=-1){
198 | if(_1f.VALUE=="text"){
199 | _20(_1e);
200 | }else{
201 | if(_1f.CALSCALE&&_1f.CALSCALE!="gregorian"){
202 | }else{
203 | _20(this.parseDateAndOrTime(_1e));
204 | }
205 | }
206 | }else{
207 | if(key=="N"){
208 | _20(this.parseName(_1e));
209 | }else{
210 | if(key=="GENDER"){
211 | _20(this.parseGender(_1e));
212 | }else{
213 | if(key=="TEL"){
214 | _20({type:(_1f.TYPE||"voice"),pref:_1f.PREF,value:_1e});
215 | }else{
216 | if(key=="EMAIL"){
217 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1e});
218 | }else{
219 | if(key=="IMPP"){
220 | _20({value:_1e});
221 | }else{
222 | if(key=="LANG"){
223 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1e});
224 | }else{
225 | if(key=="TZ"){
226 | if(_1f.VALUE=="utc-offset"){
227 | _20({"utc-offset":this.parseTimezone(_1e)});
228 | }else{
229 | _20({name:_1e});
230 | }
231 | }else{
232 | if(key=="ORG"){
233 | var _21=_1e.split(";");
234 | _20({"organization-name":_21[0],"organization-unit":_21[1]});
235 | }else{
236 | if(key=="RELATED"){
237 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1f.VALUE});
238 | }else{
239 | console.log("WARNING: unhandled key: ",key);
240 | }
241 | }
242 | }
243 | }
244 | }
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 | }
252 | }
253 | }
254 | });
255 | },nameParts:["family-name","given-name","additional-name","honorific-prefix","honorific-suffix"],parseName:function(_22){
256 | var _23=_22.split(";");
257 | var n={};
258 | for(var i in _23){
259 | if(_23[i]){
260 | n[this.nameParts[i]]=_23[i].split(",");
261 | }
262 | }
263 | return n;
264 | },parseGender:function(_24){
265 | var _25={};
266 | var _26=_24.split(";");
267 | switch(_26[0]){
268 | case "M":
269 | _25.sex="male";
270 | break;
271 | case "F":
272 | _25.sex="female";
273 | break;
274 | case "O":
275 | _25.sex="other";
276 | }
277 | if(_26[1]){
278 | _25.identity=_26[1];
279 | }
280 | return _25;
281 | },dateRE:/^(\d{4})(\d{2})(\d{2})$/,dateReducedARE:/^(\d{4})\-(\d{2})$/,dateReducedBRE:/^(\d{4})$/,dateTruncatedMDRE:/^\-{2}(\d{2})(\d{2})$/,dateTruncatedDRE:/^\-{3}(\d{2})$/,timeRE:/^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedARE:/^(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedBRE:/^(\d{2})([+\-]\d+|Z|)$/,timeTruncatedMSRE:/^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeTruncatedSRE:/^\-{3}(\d{2})([+\-]\d+|Z|)$/,parseDate:function(_27){
282 | var md;
283 | var y,m,d;
284 | if((md=_27.match(this.dateRE))){
285 | y=md[1];
286 | m=md[2];
287 | d=md[3];
288 | }else{
289 | if((md=_27.match(this.dateReducedARE))){
290 | y=md[1];
291 | m=md[2];
292 | }else{
293 | if((md=_27.match(this.dateReducedBRE))){
294 | y=md[1];
295 | }else{
296 | if((md=_27.match(this.dateTruncatedMDRE))){
297 | m=md[1];
298 | d=md[2];
299 | }else{
300 | if((md=_27.match(this.dateTruncatedDRE))){
301 | d=md[1];
302 | }else{
303 | console.error("WARNING: failed to parse date: ",_27);
304 | return null;
305 | }
306 | }
307 | }
308 | }
309 | }
310 | var dt=new Date(0);
311 | if(typeof (y)!="undefined"){
312 | dt.setUTCFullYear(y);
313 | }
314 | if(typeof (m)!="undefined"){
315 | dt.setUTCMonth(m-1);
316 | }
317 | if(typeof (d)!="undefined"){
318 | dt.setUTCDate(d);
319 | }
320 | return dt;
321 | },parseTime:function(_28){
322 | var md;
323 | var h,m,s,tz;
324 | if((md=_28.match(this.timeRE))){
325 | h=md[1];
326 | m=md[2];
327 | s=md[3];
328 | tz=md[4];
329 | }else{
330 | if((md=_28.match(this.timeReducedARE))){
331 | h=md[1];
332 | m=md[2];
333 | tz=md[3];
334 | }else{
335 | if((md=_28.match(this.timeReducedBRE))){
336 | h=md[1];
337 | tz=md[2];
338 | }else{
339 | if((md=_28.match(this.timeTruncatedMSRE))){
340 | m=md[1];
341 | s=md[2];
342 | tz=md[3];
343 | }else{
344 | if((md=_28.match(this.timeTruncatedSRE))){
345 | s=md[1];
346 | tz=md[2];
347 | }else{
348 | console.error("WARNING: failed to parse time: ",_28);
349 | return null;
350 | }
351 | }
352 | }
353 | }
354 | }
355 | var dt=new Date(0);
356 | if(typeof (h)!="undefined"){
357 | dt.setUTCHours(h);
358 | }
359 | if(typeof (m)!="undefined"){
360 | dt.setUTCMinutes(m);
361 | }
362 | if(typeof (s)!="undefined"){
363 | dt.setUTCSeconds(s);
364 | }
365 | if(tz){
366 | dt=this.applyTimezone(dt,tz);
367 | }
368 | return dt;
369 | },addDates:function(_29,_2a,_2b){
370 | if(typeof (_2b)=="undefined"){
371 | _2b=true;
372 | }
373 | if(!_29){
374 | return _2a;
375 | }
376 | if(!_2a){
377 | return _29;
378 | }
379 | var a=Number(_29);
380 | var b=Number(_2a);
381 | var c=_2b?a+b:a-b;
382 | return new Date(c);
383 | },parseTimezone:function(tz){
384 | var md;
385 | if((md=tz.match(/^([+\-])(\d{2})(\d{2})?/))){
386 | var _2c=new Date(0);
387 | _2c.setUTCHours(md[2]);
388 | _2c.setUTCMinutes(md[3]||0);
389 | return Number(_2c)*(md[1]=="+"?+1:-1);
390 | }else{
391 | return null;
392 | }
393 | },applyTimezone:function(_2d,tz){
394 | var _2e=this.parseTimezone(tz);
395 | if(_2e){
396 | return new Date(Number(_2d)+_2e);
397 | }else{
398 | return _2d;
399 | }
400 | },parseDateTime:function(_2f){
401 | var _30=_2f.split("T");
402 | var t=this.parseDate(_30[0]);
403 | var d=this.parseTime(_30[1]);
404 | return this.addDates(t,d);
405 | },parseDateAndOrTime:function(_31){
406 | switch(_31.indexOf("T")){
407 | case 0:
408 | return this.parseTime(_31.slice(1));
409 | case -1:
410 | return this.parseDate(_31);
411 | default:
412 | return this.parseDateTime(_31);
413 | }
414 | },lineRE:/^([^\s].*)(?:\r?\n|$)/,foldedLineRE:/^\s(.+)(?:\r?\n|$)/,lex:function(_32,_33){
415 | var md,_34=null,_35=0;
416 | for(;;){
417 | if((md=_32.match(this.lineRE))){
418 | if(_34){
419 | this.lexLine(_34,_33);
420 | }
421 | _34=md[1];
422 | _35=md[0].length;
423 | }else{
424 | if((md=_32.match(this.foldedLineRE))){
425 | if(_34){
426 | _34+=md[1];
427 | _35=md[0].length;
428 | }else{
429 | }
430 | }else{
431 | console.error("Unmatched line: "+_34);
432 | }
433 | }
434 | _32=_32.slice(_35);
435 | if(!_32){
436 | break;
437 | }
438 | }
439 | if(_34){
440 | this.lexLine(_34,_33);
441 | }
442 | _34=null;
443 | },lexLine:function(_36,_37){
444 | var tmp="";
445 | var key=null,_38={},_39=null,_3a=null;
446 | function _3b(){
447 | if(key){
448 | if(_3a){
449 | _38[_3a]=tmp;
450 | }else{
451 | console.error("Invalid attribute: ",tmp,"Line dropped.");
452 | return;
453 | }
454 | }else{
455 | key=tmp;
456 | }
457 | };
458 | for(var i in _36){
459 | var c=_36[i];
460 | switch(c){
461 | case ":":
462 | _3b();
463 | _39=_36.slice(Number(i)+1);
464 | _37.apply(this,[key,_39,_38]);
465 | return;
466 | case ";":
467 | _3b();
468 | tmp="";
469 | break;
470 | case "=":
471 | _3a=tmp;
472 | tmp="";
473 | break;
474 | default:
475 | tmp+=c;
476 | }
477 | }
478 | }};
479 | })();
480 | return {VCF:VCF,VCard:_9};
481 | });
482 |
483 |
--------------------------------------------------------------------------------
/dist/vcardjs-0.3.min.js:
--------------------------------------------------------------------------------
1 | define("vcardjs",function(){
2 | (function(){
3 | var _1="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
4 | Math.uuid=function(_2,_3){
5 | var _4=_1,_5=[],i;
6 | _3=_3||_4.length;
7 | if(_2){
8 | for(i=0;i<_2;i++){
9 | _5[i]=_4[0|Math.random()*_3];
10 | }
11 | }else{
12 | var r;
13 | _5[8]=_5[13]=_5[18]=_5[23]="-";
14 | _5[14]="4";
15 | for(i=0;i<36;i++){
16 | if(!_5[i]){
17 | r=0|Math.random()*16;
18 | _5[i]=_4[(i==19)?(r&3)|8:r];
19 | }
20 | }
21 | }
22 | return _5.join("");
23 | };
24 | Math.uuidFast=function(){
25 | var _6=_1,_7=new Array(36),_8=0,r;
26 | for(var i=0;i<36;i++){
27 | if(i==8||i==13||i==18||i==23){
28 | _7[i]="-";
29 | }else{
30 | if(i==14){
31 | _7[i]="4";
32 | }else{
33 | if(_8<=2){
34 | _8=33554432+(Math.random()*16777216)|0;
35 | }
36 | r=_8&15;
37 | _8=_8>>4;
38 | _7[i]=_6[(i==19)?(r&3)|8:r];
39 | }
40 | }
41 | }
42 | return _7.join("");
43 | };
44 | Math.uuidCompact=function(){
45 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){
46 | var r=Math.random()*16|0,v=c=="x"?r:(r&3|8);
47 | return v.toString(16);
48 | });
49 | };
50 | })();
51 | var _9;
52 | (function(){
53 | _9=function(_a){
54 | this.changed=false;
55 | if(typeof (_a)==="object"){
56 | for(var _b in _a){
57 | this[_b]=_a[_b];
58 | this.changed=true;
59 | }
60 | }
61 | };
62 | _9.prototype={validate:function(){
63 | var _c=[];
64 | function _d(_e,_f){
65 | _c.push([_e,_f]);
66 | };
67 | if(!this.fn){
68 | _d("fn","required");
69 | }
70 | for(var key in _9.multivaluedKeys){
71 | if(this[key]&&!(this[key] instanceof Array)){
72 | this[key]=[this[key]];
73 | }
74 | }
75 | function _10(_11,_12){
76 | for(var i in _12){
77 | var _13=_12[i];
78 | if(typeof (_13)!=="object"){
79 | _c.push([_11+"-"+i,"not-an-object"]);
80 | }else{
81 | if(!_13.type){
82 | _c.push([_11+"-"+i,"missing-type"]);
83 | }else{
84 | if(!_13.value){
85 | _c.push([_11+"-"+i,"missing-value"]);
86 | }
87 | }
88 | }
89 | }
90 | };
91 | if(this.email){
92 | _10("email",this.email);
93 | }
94 | if(this.tel){
95 | _10("email",this.tel);
96 | }
97 | if(!this.uid){
98 | this.addAttribute("uid",this.generateUID());
99 | }
100 | if(!this.rev){
101 | this.addAttribute("rev",this.generateRev());
102 | }
103 | this.errors=_c;
104 | return !(_c.length>0);
105 | },generateUID:function(){
106 | return "uuid:"+Math.uuid();
107 | },generateRev:function(){
108 | return (new Date()).toISOString().replace(/[\.\:\-]/g,"");
109 | },setAttribute:function(key,_14){
110 | this[key]=_14;
111 | this.changed=true;
112 | },addAttribute:function(key,_15){
113 | console.log("add attribute",key,_15);
114 | if(!_15){
115 | return;
116 | }
117 | if(_9.multivaluedKeys[key]){
118 | if(this[key]){
119 | console.log("multivalued push");
120 | this[key].push(_15);
121 | }else{
122 | console.log("multivalued set");
123 | this.setAttribute(key,[_15]);
124 | }
125 | }else{
126 | this.setAttribute(key,_15);
127 | }
128 | },toJSON:function(){
129 | return JSON.stringify(this.toJCard());
130 | },toJCard:function(){
131 | var _16={};
132 | for(var k in _9.allKeys){
133 | var key=_9.allKeys[k];
134 | if(this[key]){
135 | _16[key]=this[key];
136 | }
137 | }
138 | return _16;
139 | },merge:function(_17){
140 | if(typeof (_17.uid)!=="undefined"&&typeof (this.uid)!=="undefined"&&_17.uid!==this.uid){
141 | throw "Won't merge vcards without matching UIDs.";
142 | }
143 | var _18=new _9();
144 | function _19(key){
145 | if(_17[key]){
146 | if(_17[key]==this[key]){
147 | _18.setAttribute(this[key]);
148 | }else{
149 | _18.addAttribute(this[key]);
150 | _18.addAttribute(_17[key]);
151 | }
152 | }else{
153 | _18[key]=this[key];
154 | }
155 | };
156 | for(key in this){
157 | _19(key);
158 | }
159 | for(key in _17){
160 | if(!_18[key]){
161 | _19(key);
162 | }
163 | }
164 | }};
165 | _9.enums={telType:["text","voice","fax","cell","video","pager","textphone"],relatedType:["contact","acquaintance","friend","met","co-worker","colleague","co-resident","neighbor","child","parent","sibling","spouse","kin","muse","crush","date","sweetheart","me","agent","emergency"],emailType:["work","home","internet"],langType:["work","home"]};
166 | _9.allKeys=["fn","n","nickname","photo","bday","anniversary","gender","tel","email","impp","lang","tz","geo","title","role","logo","org","member","related","categories","note","prodid","rev","sound","uid"];
167 | _9.multivaluedKeys={email:true,tel:true,geo:true,title:true,role:true,logo:true,org:true,member:true,related:true,categories:true,note:true};
168 | })();
169 | var VCF;
170 | (function(){
171 | VCF={simpleKeys:["VERSION","FN","PHOTO","GEO","TITLE","ROLE","LOGO","MEMBER","NOTE","PRODID","SOUND","UID"],csvKeys:["NICKNAME","CATEGORIES"],dateAndOrTimeKeys:["BDAY","ANNIVERSARY","REV"],parse:function(_1a,_1b,_1c){
172 | var _1d=null;
173 | if(!_1c){
174 | _1c=this;
175 | }
176 | this.lex(_1a,function(key,_1e,_1f){
177 | function _20(val){
178 | if(_1d){
179 | _1d.addAttribute(key.toLowerCase(),val);
180 | }
181 | };
182 | if(key=="BEGIN"){
183 | _1d=new _9();
184 | }else{
185 | if(key=="END"){
186 | if(_1d){
187 | _1b.apply(_1c,[_1d]);
188 | _1d=null;
189 | }
190 | }else{
191 | if(this.simpleKeys.indexOf(key)!=-1){
192 | _20(_1e);
193 | }else{
194 | if(this.csvKeys.indexOf(key)!=-1){
195 | _20(_1e.split(","));
196 | }else{
197 | if(this.dateAndOrTimeKeys.indexOf(key)!=-1){
198 | if(_1f.VALUE=="text"){
199 | _20(_1e);
200 | }else{
201 | if(_1f.CALSCALE&&_1f.CALSCALE!="gregorian"){
202 | }else{
203 | _20(this.parseDateAndOrTime(_1e));
204 | }
205 | }
206 | }else{
207 | if(key=="N"){
208 | _20(this.parseName(_1e));
209 | }else{
210 | if(key=="GENDER"){
211 | _20(this.parseGender(_1e));
212 | }else{
213 | if(key=="TEL"){
214 | _20({type:(_1f.TYPE||"voice"),pref:_1f.PREF,value:_1e});
215 | }else{
216 | if(key=="EMAIL"){
217 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1e});
218 | }else{
219 | if(key=="IMPP"){
220 | _20({value:_1e});
221 | }else{
222 | if(key=="LANG"){
223 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1e});
224 | }else{
225 | if(key=="TZ"){
226 | if(_1f.VALUE=="utc-offset"){
227 | _20({"utc-offset":this.parseTimezone(_1e)});
228 | }else{
229 | _20({name:_1e});
230 | }
231 | }else{
232 | if(key=="ORG"){
233 | var _21=_1e.split(";");
234 | _20({"organization-name":_21[0],"organization-unit":_21[1]});
235 | }else{
236 | if(key=="RELATED"){
237 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1f.VALUE});
238 | }else{
239 | if(key=="ADR"){
240 | _20({type:_1f.TYPE,pref:_1f.PREF,value:_1e});
241 | }else{
242 | console.log("WARNING: unhandled key: ",key);
243 | }
244 | }
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 | }
252 | }
253 | }
254 | }
255 | }
256 | }
257 | }
258 | });
259 | },nameParts:["family-name","given-name","additional-name","honorific-prefix","honorific-suffix"],parseName:function(_22){
260 | var _23=_22.split(";");
261 | var n={};
262 | for(var i in _23){
263 | if(_23[i]){
264 | n[this.nameParts[i]]=_23[i].split(",");
265 | }
266 | }
267 | return n;
268 | },parseGender:function(_24){
269 | var _25={};
270 | var _26=_24.split(";");
271 | switch(_26[0]){
272 | case "M":
273 | _25.sex="male";
274 | break;
275 | case "F":
276 | _25.sex="female";
277 | break;
278 | case "O":
279 | _25.sex="other";
280 | }
281 | if(_26[1]){
282 | _25.identity=_26[1];
283 | }
284 | return _25;
285 | },dateRE:/^(\d{4})(\d{2})(\d{2})$/,dateReducedARE:/^(\d{4})\-(\d{2})$/,dateReducedBRE:/^(\d{4})$/,dateTruncatedMDRE:/^\-{2}(\d{2})(\d{2})$/,dateTruncatedDRE:/^\-{3}(\d{2})$/,timeRE:/^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedARE:/^(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeReducedBRE:/^(\d{2})([+\-]\d+|Z|)$/,timeTruncatedMSRE:/^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/,timeTruncatedSRE:/^\-{3}(\d{2})([+\-]\d+|Z|)$/,parseDate:function(_27){
286 | var md;
287 | var y,m,d;
288 | if((md=_27.match(this.dateRE))){
289 | y=md[1];
290 | m=md[2];
291 | d=md[3];
292 | }else{
293 | if((md=_27.match(this.dateReducedARE))){
294 | y=md[1];
295 | m=md[2];
296 | }else{
297 | if((md=_27.match(this.dateReducedBRE))){
298 | y=md[1];
299 | }else{
300 | if((md=_27.match(this.dateTruncatedMDRE))){
301 | m=md[1];
302 | d=md[2];
303 | }else{
304 | if((md=_27.match(this.dateTruncatedDRE))){
305 | d=md[1];
306 | }else{
307 | console.error("WARNING: failed to parse date: ",_27);
308 | return null;
309 | }
310 | }
311 | }
312 | }
313 | }
314 | var dt=new Date(0);
315 | if(typeof (y)!="undefined"){
316 | dt.setUTCFullYear(y);
317 | }
318 | if(typeof (m)!="undefined"){
319 | dt.setUTCMonth(m-1);
320 | }
321 | if(typeof (d)!="undefined"){
322 | dt.setUTCDate(d);
323 | }
324 | return dt;
325 | },parseTime:function(_28){
326 | var md;
327 | var h,m,s,tz;
328 | if((md=_28.match(this.timeRE))){
329 | h=md[1];
330 | m=md[2];
331 | s=md[3];
332 | tz=md[4];
333 | }else{
334 | if((md=_28.match(this.timeReducedARE))){
335 | h=md[1];
336 | m=md[2];
337 | tz=md[3];
338 | }else{
339 | if((md=_28.match(this.timeReducedBRE))){
340 | h=md[1];
341 | tz=md[2];
342 | }else{
343 | if((md=_28.match(this.timeTruncatedMSRE))){
344 | m=md[1];
345 | s=md[2];
346 | tz=md[3];
347 | }else{
348 | if((md=_28.match(this.timeTruncatedSRE))){
349 | s=md[1];
350 | tz=md[2];
351 | }else{
352 | console.error("WARNING: failed to parse time: ",_28);
353 | return null;
354 | }
355 | }
356 | }
357 | }
358 | }
359 | var dt=new Date(0);
360 | if(typeof (h)!="undefined"){
361 | dt.setUTCHours(h);
362 | }
363 | if(typeof (m)!="undefined"){
364 | dt.setUTCMinutes(m);
365 | }
366 | if(typeof (s)!="undefined"){
367 | dt.setUTCSeconds(s);
368 | }
369 | if(tz){
370 | dt=this.applyTimezone(dt,tz);
371 | }
372 | return dt;
373 | },addDates:function(_29,_2a,_2b){
374 | if(typeof (_2b)=="undefined"){
375 | _2b=true;
376 | }
377 | if(!_29){
378 | return _2a;
379 | }
380 | if(!_2a){
381 | return _29;
382 | }
383 | var a=Number(_29);
384 | var b=Number(_2a);
385 | var c=_2b?a+b:a-b;
386 | return new Date(c);
387 | },parseTimezone:function(tz){
388 | var md;
389 | if((md=tz.match(/^([+\-])(\d{2})(\d{2})?/))){
390 | var _2c=new Date(0);
391 | _2c.setUTCHours(md[2]);
392 | _2c.setUTCMinutes(md[3]||0);
393 | return Number(_2c)*(md[1]=="+"?+1:-1);
394 | }else{
395 | return null;
396 | }
397 | },applyTimezone:function(_2d,tz){
398 | var _2e=this.parseTimezone(tz);
399 | if(_2e){
400 | return new Date(Number(_2d)+_2e);
401 | }else{
402 | return _2d;
403 | }
404 | },parseDateTime:function(_2f){
405 | var _30=_2f.split("T");
406 | var t=this.parseDate(_30[0]);
407 | var d=this.parseTime(_30[1]);
408 | return this.addDates(t,d);
409 | },parseDateAndOrTime:function(_31){
410 | switch(_31.indexOf("T")){
411 | case 0:
412 | return this.parseTime(_31.slice(1));
413 | case -1:
414 | return this.parseDate(_31);
415 | default:
416 | return this.parseDateTime(_31);
417 | }
418 | },lineRE:/^([^\s].*)(?:\r?\n|$)/,foldedLineRE:/^\s(.+)(?:\r?\n|$)/,lex:function(_32,_33){
419 | var md,_34=null,_35=0;
420 | for(;;){
421 | if((md=_32.match(this.lineRE))){
422 | if(_34&&_34.indexOf("QUOTED-PRINTABLE")!=-1&&_34.slice(-1)=="="){
423 | _34=_34.slice(0,-1)+md[1];
424 | _35=md[0].length;
425 | }else{
426 | if(_34){
427 | this.lexLine(_34,_33);
428 | }
429 | _34=md[1];
430 | _35=md[0].length;
431 | }
432 | }else{
433 | if((md=_32.match(this.foldedLineRE))){
434 | if(_34){
435 | _34+=md[1];
436 | _35=md[0].length;
437 | }else{
438 | }
439 | }else{
440 | console.error("Unmatched line: "+_34);
441 | }
442 | }
443 | _32=_32.slice(_35);
444 | if(!_32){
445 | break;
446 | }
447 | }
448 | if(_34){
449 | this.lexLine(_34,_33);
450 | }
451 | _34=null;
452 | },lexLine:function(_36,_37){
453 | var tmp="";
454 | var key=null,_38={},_39=null,_3a=null;
455 | var qp=_36.indexOf("ENCODING=QUOTED-PRINTABLE");
456 | if(qp!=-1){
457 | _36=_36.substr(0,qp)+this.decodeQP(_36.substr(qp+25));
458 | }
459 | function _3b(){
460 | if(key){
461 | if(_3a){
462 | _38[_3a]=tmp.split(",");
463 | }else{
464 | if(tmp=="PREF"){
465 | _38.PREF=1;
466 | }else{
467 | if(_38.TYPE){
468 | _38.TYPE.push(tmp);
469 | }else{
470 | _38.TYPE=[tmp];
471 | }
472 | }
473 | }
474 | }else{
475 | key=tmp;
476 | }
477 | };
478 | for(var i in _36){
479 | var c=_36[i];
480 | switch(c){
481 | case ":":
482 | _3b();
483 | _39=_36.slice(Number(i)+1);
484 | _37.apply(this,[key,_39,_38]);
485 | return;
486 | case ";":
487 | _3b();
488 | tmp="";
489 | break;
490 | case "=":
491 | _3a=tmp;
492 | tmp="";
493 | break;
494 | default:
495 | tmp+=c;
496 | }
497 | }
498 | },decodeQP:function(str){
499 | str=(str||"").toString();
500 | str=str.replace(/\=(?:\r?\n|$)/g,"");
501 | var _3c="";
502 | for(var i=0,len=str.length;i {"sex":"male"}
183 | * "GENDER:M;man" -> {"sex":"male","identity":"man"}
184 | * "GENDER:F;girl" -> {"sex":"female","identity":"girl"}
185 | * "GENDER:M;girl" -> {"sex":"male","identity":"girl"}
186 | * "GENDER:F;boy" -> {"sex":"female","identity":"boy"}
187 | * "GENDER:N;woman" -> {"identity":"woman"}
188 | * "GENDER:O;potted plant" -> {"sex":"other","identity":"potted plant"}
189 | */
190 | parseGender: function(value) { // 6.2.7
191 | var gender = {};
192 | var parts = value.split(';');
193 | switch(parts[0]) {
194 | case 'M':
195 | gender.sex = 'male';
196 | break;
197 | case 'F':
198 | gender.sex = 'female';
199 | break;
200 | case 'O':
201 | gender.sex = 'other';
202 | }
203 | if(parts[1]) {
204 | gender.identity = parts[1];
205 | }
206 | return gender;
207 | },
208 |
209 | /** Date/Time parser.
210 | *
211 | * This implements only the parts of ISO 8601, that are
212 | * allowed by RFC 6350.
213 | * Paranthesized examples all represent (parts of):
214 | * 31st of January 1970, 23 Hours, 59 Minutes, 30 Seconds
215 | **/
216 |
217 | /** DATE **/
218 |
219 | // [ISO.8601.2004], 4.1.2.2, basic format:
220 | dateRE: /^(\d{4})(\d{2})(\d{2})$/, // (19700131)
221 |
222 | // [ISO.8601.2004], 4.1.2.3 a), basic format:
223 | dateReducedARE: /^(\d{4})\-(\d{2})$/, // (1970-01)
224 |
225 | // [ISO.8601.2004], 4.1.2.3 b), basic format:
226 | dateReducedBRE: /^(\d{4})$/, // (1970)
227 |
228 | // truncated representation from [ISO.8601.2000], 5.3.1.4.
229 | // I don't have access to that document, so relying on examples
230 | // from RFC 6350:
231 | dateTruncatedMDRE: /^\-{2}(\d{2})(\d{2})$/, // (--0131)
232 | dateTruncatedDRE: /^\-{3}(\d{2})$/, // (---31)
233 |
234 | /** TIME **/
235 |
236 | // (Note: it is unclear to me which of these are supposed to support
237 | // timezones. Allowing them for all. If timezones are ommitted,
238 | // defaulting to UTC)
239 |
240 | // [ISO.8601.2004, 4.2.2.2, basic format:
241 | timeRE: /^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (235930)
242 | // [ISO.8601.2004, 4.2.2.3 a), basic format:
243 | timeReducedARE: /^(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (2359)
244 | // [ISO.8601.2004, 4.2.2.3 b), basic format:
245 | timeReducedBRE: /^(\d{2})([+\-]\d+|Z|)$/, // (23)
246 | // truncated representation from [ISO.8601.2000], see above.
247 | timeTruncatedMSRE: /^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (--5930)
248 | timeTruncatedSRE: /^\-{3}(\d{2})([+\-]\d+|Z|)$/, // (---30)
249 |
250 | parseDate: function(data) {
251 | var md;
252 | var y, m, d;
253 | if((md = data.match(this.dateRE))) {
254 | y = md[1]; m = md[2]; d = md[3];
255 | } else if((md = data.match(this.dateReducedARE))) {
256 | y = md[1]; m = md[2];
257 | } else if((md = data.match(this.dateReducedBRE))) {
258 | y = md[1];
259 | } else if((md = data.match(this.dateTruncatedMDRE))) {
260 | m = md[1]; d = md[2];
261 | } else if((md = data.match(this.dateTruncatedDRE))) {
262 | d = md[1];
263 | } else {
264 | console.error("WARNING: failed to parse date: ", data);
265 | return null;
266 | }
267 | var dt = new Date(0);
268 | if(typeof(y) != 'undefined') { dt.setUTCFullYear(y); }
269 | if(typeof(m) != 'undefined') { dt.setUTCMonth(m - 1); }
270 | if(typeof(d) != 'undefined') { dt.setUTCDate(d); }
271 | return dt;
272 | },
273 |
274 | parseTime: function(data) {
275 | var md;
276 | var h, m, s, tz;
277 | if((md = data.match(this.timeRE))) {
278 | h = md[1]; m = md[2]; s = md[3];
279 | tz = md[4];
280 | } else if((md = data.match(this.timeReducedARE))) {
281 | h = md[1]; m = md[2];
282 | tz = md[3];
283 | } else if((md = data.match(this.timeReducedBRE))) {
284 | h = md[1];
285 | tz = md[2];
286 | } else if((md = data.match(this.timeTruncatedMSRE))) {
287 | m = md[1]; s = md[2];
288 | tz = md[3];
289 | } else if((md = data.match(this.timeTruncatedSRE))) {
290 | s = md[1];
291 | tz = md[2];
292 | } else {
293 | console.error("WARNING: failed to parse time: ", data);
294 | return null;
295 | }
296 |
297 | var dt = new Date(0);
298 | if(typeof(h) != 'undefined') { dt.setUTCHours(h); }
299 | if(typeof(m) != 'undefined') { dt.setUTCMinutes(m); }
300 | if(typeof(s) != 'undefined') { dt.setUTCSeconds(s); }
301 |
302 | if(tz) {
303 | dt = this.applyTimezone(dt, tz);
304 | }
305 |
306 | return dt;
307 | },
308 |
309 | // add two dates. if addSub is false, substract instead of add.
310 | addDates: function(aDate, bDate, addSub) {
311 | if(typeof(addSub) == 'undefined') { addSub = true };
312 | if(! aDate) { return bDate; }
313 | if(! bDate) { return aDate; }
314 | var a = Number(aDate);
315 | var b = Number(bDate);
316 | var c = addSub ? a + b : a - b;
317 | return new Date(c);
318 | },
319 |
320 | parseTimezone: function(tz) {
321 | var md;
322 | if((md = tz.match(/^([+\-])(\d{2})(\d{2})?/))) {
323 | var offset = new Date(0);
324 | offset.setUTCHours(md[2]);
325 | offset.setUTCMinutes(md[3] || 0);
326 | return Number(offset) * (md[1] == '+' ? +1 : -1);
327 | } else {
328 | return null;
329 | }
330 | },
331 |
332 | applyTimezone: function(date, tz) {
333 | var offset = this.parseTimezone(tz);
334 | if(offset) {
335 | return new Date(Number(date) + offset);
336 | } else {
337 | return date;
338 | }
339 | },
340 |
341 | parseDateTime: function(data) {
342 | var parts = data.split('T');
343 | var t = this.parseDate(parts[0]);
344 | var d = this.parseTime(parts[1]);
345 | return this.addDates(t, d);
346 | },
347 |
348 | parseDateAndOrTime: function(data) {
349 | switch(data.indexOf('T')) {
350 | case 0:
351 | return this.parseTime(data.slice(1));
352 | case -1:
353 | return this.parseDate(data);
354 | default:
355 | return this.parseDateTime(data);
356 | }
357 | },
358 |
359 | lineRE: /^([^\s].*)(?:\r?\n|$)/, // spec wants CRLF, but we're on the internet. reality is chaos.
360 | foldedLineRE:/^\s(.+)(?:\r?\n|$)/,
361 |
362 | // lex the given input, calling the callback for each line, with
363 | // the following arguments:
364 | // * key - key of the statement, such as 'BEGIN', 'FN', 'N', ...
365 | // * value - value of the statement, i.e. everything after the first ':'
366 | // * attrs - object containing attributes, such as {"TYPE":"work"}
367 | lex: function(input, callback) {
368 |
369 | var md, line = null, length = 0;
370 |
371 | for(;;) {
372 | if((md = input.match(this.lineRE))) {
373 | // Unfold quoted-printables (vCard 2.1) into a single line before parsing.
374 | // "Soft" linebreaks are indicated by a '=' at the end of the line, and do
375 | // not affect the underlying data.
376 | if(line && line.indexOf('QUOTED-PRINTABLE') != -1 && line.slice(-1) == '=') {
377 | line = line.slice(0,-1) + md[1];
378 | length = md[0].length;
379 | } else {
380 | if(line) {
381 | this.lexLine(line, callback);
382 | }
383 | line = md[1];
384 | length = md[0].length;
385 | }
386 | } else if((md = input.match(this.foldedLineRE))) {
387 | if(line) {
388 | line += md[1];
389 | length = md[0].length;
390 | } else {
391 | // ignore folded junk.
392 | }
393 | } else {
394 | console.error("Unmatched line: " + line);
395 | }
396 |
397 | input = input.slice(length);
398 |
399 | if(! input) {
400 | break;
401 | }
402 | }
403 |
404 | if(line) {
405 | // last line.
406 | this.lexLine(line, callback);
407 | }
408 |
409 | line = null;
410 | },
411 |
412 | lexLine: function(line, callback) {
413 | var tmp = '';
414 | var key = null, attrs = {}, value = null, attrKey = null;
415 |
416 | //If our value is a quoted-printable (vCard 2.1), decode it and discard the encoding attribute
417 | var qp = line.indexOf('ENCODING=QUOTED-PRINTABLE');
418 | if(qp != -1){
419 | line = line.substr(0,qp) + this.decodeQP(line.substr(qp+25));
420 | }
421 |
422 | function finalizeKeyOrAttr() {
423 | if(key) {
424 | if(attrKey) {
425 | attrs[attrKey] = tmp.split(',');
426 | } else {
427 | //"Floating" attributes are probably vCard 2.1 TYPE or PREF values.
428 | if(tmp == "PREF"){
429 | attrs.PREF = 1;
430 | } else {
431 | if (attrs.TYPE) attrs.TYPE.push(tmp);
432 | else attrs.TYPE = [tmp];
433 | }
434 | }
435 | } else {
436 | key = tmp;
437 | }
438 | }
439 |
440 | for(var i in line) {
441 | var c = line[i];
442 |
443 | switch(c) {
444 | case ':':
445 | finalizeKeyOrAttr();
446 | value = line.slice(Number(i) + 1);
447 | callback.apply(
448 | this,
449 | [key, value, attrs]
450 | );
451 | return;
452 | case ';':
453 | finalizeKeyOrAttr();
454 | tmp = '';
455 | break;
456 | case '=':
457 | attrKey = tmp;
458 | tmp = '';
459 | break;
460 | default:
461 | tmp += c;
462 | }
463 | }
464 | },
465 | /** Quoted Printable Parser
466 | *
467 | * Parses quoted-printable strings, which sometimes appear in
468 | * vCard 2.1 files (usually the address field)
469 | *
470 | * Code adapted from:
471 | * https://github.com/andris9/mimelib
472 | *
473 | **/
474 | decodeQP: function(str){
475 | str = (str || "").toString();
476 | str = str.replace(/\=(?:\r?\n|$)/g, "");
477 | var str2 = "";
478 | for(var i=0, len = str.length; i>> Math.uuid()
29 | * "92329D39-6F5C-4520-ABFC-AAB64544E172"
30 | *
31 | * // One argument - returns ID of the specified length
32 | * >>> Math.uuid(15) // 15 character ID (default base=62)
33 | * "VcydxgltxrVZSTV"
34 | *
35 | * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
36 | * >>> Math.uuid(8, 2) // 8 character ID (base=2)
37 | * "01001010"
38 | * >>> Math.uuid(8, 10) // 8 character ID (base=10)
39 | * "47473046"
40 | * >>> Math.uuid(8, 16) // 8 character ID (base=16)
41 | * "098F4D35"
42 | */
43 | (function() {
44 | // Private array of chars to use
45 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
46 |
47 | Math.uuid = function (len, radix) {
48 | var chars = CHARS, uuid = [], i;
49 | radix = radix || chars.length;
50 |
51 | if (len) {
52 | // Compact form
53 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
54 | } else {
55 | // rfc4122, version 4 form
56 | var r;
57 |
58 | // rfc4122 requires these characters
59 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
60 | uuid[14] = '4';
61 |
62 | // Fill in random data. At i==19 set the high bits of clock sequence as
63 | // per rfc4122, sec. 4.1.5
64 | for (i = 0; i < 36; i++) {
65 | if (!uuid[i]) {
66 | r = 0 | Math.random()*16;
67 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
68 | }
69 | }
70 | }
71 |
72 | return uuid.join('');
73 | };
74 |
75 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance
76 | // by minimizing calls to random()
77 | Math.uuidFast = function() {
78 | var chars = CHARS, uuid = new Array(36), rnd=0, r;
79 | for (var i = 0; i < 36; i++) {
80 | if (i==8 || i==13 || i==18 || i==23) {
81 | uuid[i] = '-';
82 | } else if (i==14) {
83 | uuid[i] = '4';
84 | } else {
85 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0;
86 | r = rnd & 0xf;
87 | rnd = rnd >> 4;
88 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
89 | }
90 | }
91 | return uuid.join('');
92 | };
93 |
94 | // A more compact, but less performant, RFC4122v4 solution:
95 | Math.uuidCompact = function() {
96 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
97 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
98 | return v.toString(16);
99 | });
100 | };
101 | })();
102 |
103 | // exported globals
104 | var VCard;
105 |
106 | (function() {
107 |
108 | VCard = function(attributes) {
109 | this.changed = false;
110 | if(typeof(attributes) === 'object') {
111 | for(var key in attributes) {
112 | this[key] = attributes[key];
113 | this.changed = true;
114 | }
115 | }
116 | };
117 |
118 | VCard.prototype = {
119 |
120 | // Check validity of this VCard instance. Properties that can be generated,
121 | // will be generated. If any error is found, false is returned and vcard.errors
122 | // set to an Array of [attribute, errorType] arrays.
123 | // Otherwise true is returned.
124 | //
125 | // In case of multivalued properties, the "attribute" part of the error is
126 | // the attribute name, plus it's index (starting at 0). Example: email0, tel7, ...
127 | //
128 | // It is recommended to call this method even if this VCard object was imported,
129 | // as some software (e.g. Gmail) doesn't generate UIDs.
130 | validate: function() {
131 | var errors = [];
132 |
133 | function addError(attribute, type) {
134 | errors.push([attribute, type]);
135 | }
136 |
137 | if(! this.fn) { // FN is a required attribute
138 | addError("fn", "required");
139 | }
140 |
141 | // make sure multivalued properties are *always* in array form
142 | for(var key in VCard.multivaluedKeys) {
143 | if(this[key] && ! (this[key] instanceof Array)) {
144 | this[key] = [this[key]];
145 | }
146 | }
147 |
148 | // make sure compound fields have their type & value set
149 | // (to prevent mistakes such as vcard.addAttribute('email', 'foo@bar.baz')
150 | function validateCompoundWithType(attribute, values) {
151 | for(var i in values) {
152 | var value = values[i];
153 | if(typeof(value) !== 'object') {
154 | errors.push([attribute + '-' + i, "not-an-object"]);
155 | } else if(! value.type) {
156 | errors.push([attribute + '-' + i, "missing-type"]);
157 | } else if(! value.value) { // empty values are not allowed.
158 | errors.push([attribute + '-' + i, "missing-value"]);
159 | }
160 | }
161 | }
162 |
163 | if(this.email) {
164 | validateCompoundWithType('email', this.email);
165 | }
166 |
167 | if(this.tel) {
168 | validateCompoundWithType('email', this.tel);
169 | }
170 |
171 | if(! this.uid) {
172 | this.addAttribute('uid', this.generateUID());
173 | }
174 |
175 | if(! this.rev) {
176 | this.addAttribute('rev', this.generateRev());
177 | }
178 |
179 | this.errors = errors;
180 |
181 | return ! (errors.length > 0);
182 | },
183 |
184 | // generate a UID. This generates a UUID with uuid: URN namespace, as suggested
185 | // by RFC 6350, 6.7.6
186 | generateUID: function() {
187 | return 'uuid:' + Math.uuid();
188 | },
189 |
190 | // generate revision timestamp (a full ISO 8601 date/time string in basic format)
191 | generateRev: function() {
192 | return (new Date()).toISOString().replace(/[\.\:\-]/g, '');
193 | },
194 |
195 | // Set the given attribute to the given value.
196 | // This sets vcard.changed to true, so you can check later whether anything
197 | // was updated by your code.
198 | setAttribute: function(key, value) {
199 | this[key] = value;
200 | this.changed = true;
201 | },
202 |
203 | // Set the given attribute to the given value.
204 | // If the given attribute's key has cardinality > 1, instead of overwriting
205 | // the current value, an additional value is appended.
206 | addAttribute: function(key, value) {
207 | console.log('add attribute', key, value);
208 | if(! value) {
209 | return;
210 | }
211 | if(VCard.multivaluedKeys[key]) {
212 | if(this[key]) {
213 | console.log('multivalued push');
214 | this[key].push(value)
215 | } else {
216 | console.log('multivalued set');
217 | this.setAttribute(key, [value]);
218 | }
219 | } else {
220 | this.setAttribute(key, value);
221 | }
222 | },
223 |
224 | // convenience method to get a JSON serialized jCard.
225 | toJSON: function() {
226 | return JSON.stringify(this.toJCard());
227 | },
228 |
229 | // Copies all properties (i.e. all specified in VCard.allKeys) to a new object
230 | // and returns it.
231 | // Useful to serialize to JSON afterwards.
232 | toJCard: function() {
233 | var jcard = {};
234 | for(var k in VCard.allKeys) {
235 | var key = VCard.allKeys[k];
236 | if(this[key]) {
237 | jcard[key] = this[key];
238 | }
239 | }
240 | return jcard;
241 | },
242 |
243 | // synchronizes two vcards, using the mechanisms described in
244 | // RFC 6350, Section 7.
245 | // Returns a new VCard object.
246 | // If a property is present in both source vcards, and that property's
247 | // maximum cardinality is 1, then the value from the second (given) vcard
248 | // precedes.
249 | //
250 | // TODO: implement PID matching as described in 7.3.1
251 | merge: function(other) {
252 | if(typeof(other.uid) !== 'undefined' &&
253 | typeof(this.uid) !== 'undefined' &&
254 | other.uid !== this.uid) {
255 | // 7.1.1
256 | throw "Won't merge vcards without matching UIDs.";
257 | }
258 |
259 | var result = new VCard();
260 |
261 | function mergeProperty(key) {
262 | if(other[key]) {
263 | if(other[key] == this[key]) {
264 | result.setAttribute(this[key]);
265 | } else {
266 | result.addAttribute(this[key]);
267 | result.addAttribute(other[key]);
268 | }
269 | } else {
270 | result[key] = this[key];
271 | }
272 | }
273 |
274 | for(key in this) { // all properties of this
275 | mergeProperty(key);
276 | }
277 | for(key in other) { // all properties of other *not* in this
278 | if(! result[key]) {
279 | mergeProperty(key);
280 | }
281 | }
282 | }
283 | };
284 |
285 | VCard.enums = {
286 | telType: ["text", "voice", "fax", "cell", "video", "pager", "textphone"],
287 | relatedType: ["contact", "acquaintance", "friend", "met", "co-worker",
288 | "colleague", "co-resident", "neighbor", "child", "parent",
289 | "sibling", "spouse", "kin", "muse", "crush", "date",
290 | "sweetheart", "me", "agent", "emergency"],
291 | // FIXME: these aren't actually defined anywhere. just very commmon.
292 | // maybe there should be more?
293 | emailType: ["work", "home", "internet"],
294 | langType: ["work", "home"],
295 |
296 | };
297 |
298 | VCard.allKeys = [
299 | 'fn', 'n', 'nickname', 'photo', 'bday', 'anniversary', 'gender',
300 | 'tel', 'email', 'impp', 'lang', 'tz', 'geo', 'title', 'role', 'logo',
301 | 'org', 'member', 'related', 'categories', 'note', 'prodid', 'rev',
302 | 'sound', 'uid'
303 | ];
304 |
305 | VCard.multivaluedKeys = {
306 | email: true,
307 | tel: true,
308 | geo: true,
309 | title: true,
310 | role: true,
311 | logo: true,
312 | org: true,
313 | member: true,
314 | related: true,
315 | categories: true,
316 | note: true
317 | };
318 |
319 | })();
320 | /**
321 | ** VCF - Parser for the vcard format.
322 | **
323 | ** This is purely a vCard 4.0 implementation, as described in RFC 6350.
324 | **
325 | ** The generated VCard object roughly corresponds to the JSON representation
326 | ** of a hCard, as described here: http://microformats.org/wiki/jcard
327 | ** (Retrieved May 17, 2012)
328 | **
329 | **/
330 |
331 | var VCF;
332 |
333 | (function() {
334 | VCF = {
335 |
336 | simpleKeys: [
337 | 'VERSION',
338 | 'FN', // 6.2.1
339 | 'PHOTO', // 6.2.4 (we don't care about URIs [yet])
340 | 'GEO', // 6.5.2 (SHOULD also b a URI)
341 | 'TITLE', // 6.6.1
342 | 'ROLE', // 6.6.2
343 | 'LOGO', // 6.6.3 (also [possibly data:] URI)
344 | 'MEMBER', // 6.6.5
345 | 'NOTE', // 6.7.2
346 | 'PRODID', // 6.7.3
347 | 'SOUND', // 6.7.5
348 | 'UID', // 6.7.6
349 | ],
350 | csvKeys: [
351 | 'NICKNAME', // 6.2.3
352 | 'CATEGORIES', // 6.7.1
353 | ],
354 | dateAndOrTimeKeys: [
355 | 'BDAY', // 6.2.5
356 | 'ANNIVERSARY', // 6.2.6
357 | 'REV', // 6.7.4
358 | ],
359 |
360 | // parses the given input, constructing VCard objects.
361 | // if the input contains multiple (properly seperated) vcards,
362 | // the callback may be called multiple times, with one vcard given
363 | // each time.
364 | // The third argument specifies the context in which to evaluate
365 | // the given callback.
366 | parse: function(input, callback, context) {
367 | var vcard = null;
368 |
369 | if(! context) {
370 | context = this;
371 | }
372 |
373 | this.lex(input, function(key, value, attrs) {
374 | function setAttr(val) {
375 | if(vcard) {
376 | vcard.addAttribute(key.toLowerCase(), val);
377 | }
378 | }
379 | if(key == 'BEGIN') {
380 | vcard = new VCard();
381 | } else if(key == 'END') {
382 | if(vcard) {
383 | callback.apply(context, [vcard]);
384 | vcard = null;
385 | }
386 |
387 | } else if(this.simpleKeys.indexOf(key) != -1) {
388 | setAttr(value);
389 |
390 | } else if(this.csvKeys.indexOf(key) != -1) {
391 | setAttr(value.split(','));
392 |
393 | } else if(this.dateAndOrTimeKeys.indexOf(key) != -1) {
394 | if(attrs.VALUE == 'text') {
395 | // times can be expressed as "text" as well,
396 | // e.g. "ca 1800", "next week", ...
397 | setAttr(value);
398 | } else if(attrs.CALSCALE && attrs.CALSCALE != 'gregorian') {
399 | // gregorian calendar is the only calscale mentioned
400 | // in RFC 6350. I do not intend to support anything else
401 | // (yet).
402 | } else {
403 | // FIXME: handle TZ attribute.
404 | setAttr(this.parseDateAndOrTime(value));
405 | }
406 |
407 | } else if(key == 'N') { // 6.2.2
408 | setAttr(this.parseName(value));
409 |
410 | } else if(key == 'GENDER') { // 6.2.7
411 | setAttr(this.parseGender(value));
412 |
413 | } else if(key == 'TEL') { // 6.4.1
414 | setAttr({
415 | type: (attrs.TYPE || 'voice'),
416 | pref: attrs.PREF,
417 | value: value
418 | });
419 |
420 | } else if(key == 'EMAIL') { // 6.4.2
421 | setAttr({
422 | type: attrs.TYPE,
423 | pref: attrs.PREF,
424 | value: value
425 | });
426 |
427 | } else if(key == 'IMPP') { // 6.4.3
428 | // RFC 6350 doesn't define TYPEs for IMPP addresses.
429 | // It just seems odd to me to have multiple email addresses and phone numbers,
430 | // but not multiple IMPP addresses.
431 | setAttr({ value: value });
432 |
433 | } else if(key == 'LANG') { // 6.4.4
434 | setAttr({
435 | type: attrs.TYPE,
436 | pref: attrs.PREF,
437 | value: value
438 | });
439 |
440 | } else if(key == 'TZ') { // 6.5.1
441 | // neither hCard nor jCard mention anything about the TZ
442 | // property, except that it's singular (which it is *not* in
443 | // RFC 6350).
444 | // using compound representation.
445 | if(attrs.VALUE == 'utc-offset') {
446 | setAttr({ 'utc-offset': this.parseTimezone(value) });
447 | } else {
448 | setAttr({ name: value });
449 | }
450 |
451 | } else if(key == 'ORG') { // 6.6.4
452 | var parts = value.split(';');
453 | setAttr({
454 | 'organization-name': parts[0],
455 | 'organization-unit': parts[1]
456 | });
457 |
458 | } else if(key == 'RELATED') { // 6.6.6
459 | setAttr({
460 | type: attrs.TYPE,
461 | pref: attrs.PREF,
462 | value: attrs.VALUE
463 | });
464 |
465 | } else {
466 | console.log('WARNING: unhandled key: ', key);
467 | }
468 | });
469 | },
470 |
471 | nameParts: [
472 | 'family-name', 'given-name', 'additional-name',
473 | 'honorific-prefix', 'honorific-suffix'
474 | ],
475 |
476 | parseName: function(name) { // 6.2.2
477 | var parts = name.split(';');
478 | var n = {};
479 | for(var i in parts) {
480 | if(parts[i]) {
481 | n[this.nameParts[i]] = parts[i].split(',');
482 | }
483 | }
484 | return n;
485 | },
486 |
487 | /**
488 | * The representation of gender for hCards (and hence their JSON
489 | * representation) is undefined, as hCard is based on RFC 2436, which
490 | * doesn't define the GENDER attribute.
491 | * This method uses a compound representation.
492 | *
493 | * Examples:
494 | * "GENDER:M" -> {"sex":"male"}
495 | * "GENDER:M;man" -> {"sex":"male","identity":"man"}
496 | * "GENDER:F;girl" -> {"sex":"female","identity":"girl"}
497 | * "GENDER:M;girl" -> {"sex":"male","identity":"girl"}
498 | * "GENDER:F;boy" -> {"sex":"female","identity":"boy"}
499 | * "GENDER:N;woman" -> {"identity":"woman"}
500 | * "GENDER:O;potted plant" -> {"sex":"other","identity":"potted plant"}
501 | */
502 | parseGender: function(value) { // 6.2.7
503 | var gender = {};
504 | var parts = value.split(';');
505 | switch(parts[0]) {
506 | case 'M':
507 | gender.sex = 'male';
508 | break;
509 | case 'F':
510 | gender.sex = 'female';
511 | break;
512 | case 'O':
513 | gender.sex = 'other';
514 | }
515 | if(parts[1]) {
516 | gender.identity = parts[1];
517 | }
518 | return gender;
519 | },
520 |
521 | /** Date/Time parser.
522 | *
523 | * This implements only the parts of ISO 8601, that are
524 | * allowed by RFC 6350.
525 | * Paranthesized examples all represent (parts of):
526 | * 31st of January 1970, 23 Hours, 59 Minutes, 30 Seconds
527 | **/
528 |
529 | /** DATE **/
530 |
531 | // [ISO.8601.2004], 4.1.2.2, basic format:
532 | dateRE: /^(\d{4})(\d{2})(\d{2})$/, // (19700131)
533 |
534 | // [ISO.8601.2004], 4.1.2.3 a), basic format:
535 | dateReducedARE: /^(\d{4})\-(\d{2})$/, // (1970-01)
536 |
537 | // [ISO.8601.2004], 4.1.2.3 b), basic format:
538 | dateReducedBRE: /^(\d{4})$/, // (1970)
539 |
540 | // truncated representation from [ISO.8601.2000], 5.3.1.4.
541 | // I don't have access to that document, so relying on examples
542 | // from RFC 6350:
543 | dateTruncatedMDRE: /^\-{2}(\d{2})(\d{2})$/, // (--0131)
544 | dateTruncatedDRE: /^\-{3}(\d{2})$/, // (---31)
545 |
546 | /** TIME **/
547 |
548 | // (Note: it is unclear to me which of these are supposed to support
549 | // timezones. Allowing them for all. If timezones are ommitted,
550 | // defaulting to UTC)
551 |
552 | // [ISO.8601.2004, 4.2.2.2, basic format:
553 | timeRE: /^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (235930)
554 | // [ISO.8601.2004, 4.2.2.3 a), basic format:
555 | timeReducedARE: /^(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (2359)
556 | // [ISO.8601.2004, 4.2.2.3 b), basic format:
557 | timeReducedBRE: /^(\d{2})([+\-]\d+|Z|)$/, // (23)
558 | // truncated representation from [ISO.8601.2000], see above.
559 | timeTruncatedMSRE: /^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (--5930)
560 | timeTruncatedSRE: /^\-{3}(\d{2})([+\-]\d+|Z|)$/, // (---30)
561 |
562 | parseDate: function(data) {
563 | var md;
564 | var y, m, d;
565 | if((md = data.match(this.dateRE))) {
566 | y = md[1]; m = md[2]; d = md[3];
567 | } else if((md = data.match(this.dateReducedARE))) {
568 | y = md[1]; m = md[2];
569 | } else if((md = data.match(this.dateReducedBRE))) {
570 | y = md[1];
571 | } else if((md = data.match(this.dateTruncatedMDRE))) {
572 | m = md[1]; d = md[2];
573 | } else if((md = data.match(this.dateTruncatedDRE))) {
574 | d = md[1];
575 | } else {
576 | console.error("WARNING: failed to parse date: ", data);
577 | return null;
578 | }
579 | var dt = new Date(0);
580 | if(typeof(y) != 'undefined') { dt.setUTCFullYear(y); }
581 | if(typeof(m) != 'undefined') { dt.setUTCMonth(m - 1); }
582 | if(typeof(d) != 'undefined') { dt.setUTCDate(d); }
583 | return dt;
584 | },
585 |
586 | parseTime: function(data) {
587 | var md;
588 | var h, m, s, tz;
589 | if((md = data.match(this.timeRE))) {
590 | h = md[1]; m = md[2]; s = md[3];
591 | tz = md[4];
592 | } else if((md = data.match(this.timeReducedARE))) {
593 | h = md[1]; m = md[2];
594 | tz = md[3];
595 | } else if((md = data.match(this.timeReducedBRE))) {
596 | h = md[1];
597 | tz = md[2];
598 | } else if((md = data.match(this.timeTruncatedMSRE))) {
599 | m = md[1]; s = md[2];
600 | tz = md[3];
601 | } else if((md = data.match(this.timeTruncatedSRE))) {
602 | s = md[1];
603 | tz = md[2];
604 | } else {
605 | console.error("WARNING: failed to parse time: ", data);
606 | return null;
607 | }
608 |
609 | var dt = new Date(0);
610 | if(typeof(h) != 'undefined') { dt.setUTCHours(h); }
611 | if(typeof(m) != 'undefined') { dt.setUTCMinutes(m); }
612 | if(typeof(s) != 'undefined') { dt.setUTCSeconds(s); }
613 |
614 | if(tz) {
615 | dt = this.applyTimezone(dt, tz);
616 | }
617 |
618 | return dt;
619 | },
620 |
621 | // add two dates. if addSub is false, substract instead of add.
622 | addDates: function(aDate, bDate, addSub) {
623 | if(typeof(addSub) == 'undefined') { addSub = true };
624 | if(! aDate) { return bDate; }
625 | if(! bDate) { return aDate; }
626 | var a = Number(aDate);
627 | var b = Number(bDate);
628 | var c = addSub ? a + b : a - b;
629 | return new Date(c);
630 | },
631 |
632 | parseTimezone: function(tz) {
633 | var md;
634 | if((md = tz.match(/^([+\-])(\d{2})(\d{2})?/))) {
635 | var offset = new Date(0);
636 | offset.setUTCHours(md[2]);
637 | offset.setUTCMinutes(md[3] || 0);
638 | return Number(offset) * (md[1] == '+' ? +1 : -1);
639 | } else {
640 | return null;
641 | }
642 | },
643 |
644 | applyTimezone: function(date, tz) {
645 | var offset = this.parseTimezone(tz);
646 | if(offset) {
647 | return new Date(Number(date) + offset);
648 | } else {
649 | return date;
650 | }
651 | },
652 |
653 | parseDateTime: function(data) {
654 | var parts = data.split('T');
655 | var t = this.parseDate(parts[0]);
656 | var d = this.parseTime(parts[1]);
657 | return this.addDates(t, d);
658 | },
659 |
660 | parseDateAndOrTime: function(data) {
661 | switch(data.indexOf('T')) {
662 | case 0:
663 | return this.parseTime(data.slice(1));
664 | case -1:
665 | return this.parseDate(data);
666 | default:
667 | return this.parseDateTime(data);
668 | }
669 | },
670 |
671 | lineRE: /^([^\s].*)(?:\r?\n|$)/, // spec wants CRLF, but we're on the internet. reality is chaos.
672 | foldedLineRE:/^\s(.+)(?:\r?\n|$)/,
673 |
674 | // lex the given input, calling the callback for each line, with
675 | // the following arguments:
676 | // * key - key of the statement, such as 'BEGIN', 'FN', 'N', ...
677 | // * value - value of the statement, i.e. everything after the first ':'
678 | // * attrs - object containing attributes, such as {"TYPE":"work"}
679 | lex: function(input, callback) {
680 |
681 | var md, line = null, length = 0;
682 |
683 | for(;;) {
684 | if((md = input.match(this.lineRE))) {
685 | if(line) {
686 | this.lexLine(line, callback);
687 | }
688 | line = md[1];
689 | length = md[0].length;
690 | } else if((md = input.match(this.foldedLineRE))) {
691 | if(line) {
692 | line += md[1];
693 | length = md[0].length;
694 | } else {
695 | // ignore folded junk.
696 | }
697 | } else {
698 | console.error("Unmatched line: " + line);
699 | }
700 |
701 | input = input.slice(length);
702 |
703 | if(! input) {
704 | break;
705 | }
706 | }
707 |
708 | if(line) {
709 | // last line.
710 | this.lexLine(line, callback);
711 | }
712 |
713 | line = null;
714 | },
715 |
716 | lexLine: function(line, callback) {
717 | var tmp = '';
718 | var key = null, attrs = {}, value = null, attrKey = null;
719 |
720 | function finalizeKeyOrAttr() {
721 | if(key) {
722 | if(attrKey) {
723 | attrs[attrKey] = tmp;
724 | } else {
725 | console.error("Invalid attribute: ", tmp, 'Line dropped.');
726 | return;
727 | }
728 | } else {
729 | key = tmp;
730 | }
731 | }
732 |
733 | for(var i in line) {
734 | var c = line[i];
735 |
736 | switch(c) {
737 | case ':':
738 | finalizeKeyOrAttr();
739 | value = line.slice(Number(i) + 1);
740 | callback.apply(
741 | this,
742 | [key, value, attrs]
743 | );
744 | return;
745 | case ';':
746 | finalizeKeyOrAttr();
747 | tmp = '';
748 | break;
749 | case '=':
750 | attrKey = tmp;
751 | tmp = '';
752 | break;
753 | default:
754 | tmp += c;
755 | }
756 | }
757 | }
758 |
759 | };
760 |
761 | })();
762 | return {
763 | VCF: VCF,
764 | VCard: VCard
765 | };
766 | });
--------------------------------------------------------------------------------
/dist/vcardjs-0.3.js:
--------------------------------------------------------------------------------
1 | /**
2 | * vCardJS - a vCard 4.0 implementation in JavaScript
3 | *
4 | * (c) 2012 - Niklas Cathor
5 | *
6 | * Latest source: https://github.com/nilclass/vcardjs
7 | **/
8 |
9 | define('vcardjs', function() {
10 | /*!
11 | Math.uuid.js (v1.4)
12 | http://www.broofa.com
13 | mailto:robert@broofa.com
14 |
15 | Copyright (c) 2010 Robert Kieffer
16 | Dual licensed under the MIT and GPL licenses.
17 | */
18 |
19 | /*
20 | * Generate a random uuid.
21 | *
22 | * USAGE: Math.uuid(length, radix)
23 | * length - the desired number of characters
24 | * radix - the number of allowable values for each character.
25 | *
26 | * EXAMPLES:
27 | * // No arguments - returns RFC4122, version 4 ID
28 | * >>> Math.uuid()
29 | * "92329D39-6F5C-4520-ABFC-AAB64544E172"
30 | *
31 | * // One argument - returns ID of the specified length
32 | * >>> Math.uuid(15) // 15 character ID (default base=62)
33 | * "VcydxgltxrVZSTV"
34 | *
35 | * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
36 | * >>> Math.uuid(8, 2) // 8 character ID (base=2)
37 | * "01001010"
38 | * >>> Math.uuid(8, 10) // 8 character ID (base=10)
39 | * "47473046"
40 | * >>> Math.uuid(8, 16) // 8 character ID (base=16)
41 | * "098F4D35"
42 | */
43 | (function() {
44 | // Private array of chars to use
45 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
46 |
47 | Math.uuid = function (len, radix) {
48 | var chars = CHARS, uuid = [], i;
49 | radix = radix || chars.length;
50 |
51 | if (len) {
52 | // Compact form
53 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
54 | } else {
55 | // rfc4122, version 4 form
56 | var r;
57 |
58 | // rfc4122 requires these characters
59 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
60 | uuid[14] = '4';
61 |
62 | // Fill in random data. At i==19 set the high bits of clock sequence as
63 | // per rfc4122, sec. 4.1.5
64 | for (i = 0; i < 36; i++) {
65 | if (!uuid[i]) {
66 | r = 0 | Math.random()*16;
67 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
68 | }
69 | }
70 | }
71 |
72 | return uuid.join('');
73 | };
74 |
75 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance
76 | // by minimizing calls to random()
77 | Math.uuidFast = function() {
78 | var chars = CHARS, uuid = new Array(36), rnd=0, r;
79 | for (var i = 0; i < 36; i++) {
80 | if (i==8 || i==13 || i==18 || i==23) {
81 | uuid[i] = '-';
82 | } else if (i==14) {
83 | uuid[i] = '4';
84 | } else {
85 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0;
86 | r = rnd & 0xf;
87 | rnd = rnd >> 4;
88 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
89 | }
90 | }
91 | return uuid.join('');
92 | };
93 |
94 | // A more compact, but less performant, RFC4122v4 solution:
95 | Math.uuidCompact = function() {
96 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
97 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
98 | return v.toString(16);
99 | });
100 | };
101 | })();
102 |
103 | // exported globals
104 | var VCard;
105 |
106 | (function() {
107 |
108 | VCard = function(attributes) {
109 | this.changed = false;
110 | if(typeof(attributes) === 'object') {
111 | for(var key in attributes) {
112 | this[key] = attributes[key];
113 | this.changed = true;
114 | }
115 | }
116 | };
117 |
118 | VCard.prototype = {
119 |
120 | // Check validity of this VCard instance. Properties that can be generated,
121 | // will be generated. If any error is found, false is returned and vcard.errors
122 | // set to an Array of [attribute, errorType] arrays.
123 | // Otherwise true is returned.
124 | //
125 | // In case of multivalued properties, the "attribute" part of the error is
126 | // the attribute name, plus it's index (starting at 0). Example: email0, tel7, ...
127 | //
128 | // It is recommended to call this method even if this VCard object was imported,
129 | // as some software (e.g. Gmail) doesn't generate UIDs.
130 | validate: function() {
131 | var errors = [];
132 |
133 | function addError(attribute, type) {
134 | errors.push([attribute, type]);
135 | }
136 |
137 | if(! this.fn) { // FN is a required attribute
138 | addError("fn", "required");
139 | }
140 |
141 | // make sure multivalued properties are *always* in array form
142 | for(var key in VCard.multivaluedKeys) {
143 | if(this[key] && ! (this[key] instanceof Array)) {
144 | this[key] = [this[key]];
145 | }
146 | }
147 |
148 | // make sure compound fields have their type & value set
149 | // (to prevent mistakes such as vcard.addAttribute('email', 'foo@bar.baz')
150 | function validateCompoundWithType(attribute, values) {
151 | for(var i in values) {
152 | var value = values[i];
153 | if(typeof(value) !== 'object') {
154 | errors.push([attribute + '-' + i, "not-an-object"]);
155 | } else if(! value.type) {
156 | errors.push([attribute + '-' + i, "missing-type"]);
157 | } else if(! value.value) { // empty values are not allowed.
158 | errors.push([attribute + '-' + i, "missing-value"]);
159 | }
160 | }
161 | }
162 |
163 | if(this.email) {
164 | validateCompoundWithType('email', this.email);
165 | }
166 |
167 | if(this.tel) {
168 | validateCompoundWithType('email', this.tel);
169 | }
170 |
171 | if(! this.uid) {
172 | this.addAttribute('uid', this.generateUID());
173 | }
174 |
175 | if(! this.rev) {
176 | this.addAttribute('rev', this.generateRev());
177 | }
178 |
179 | this.errors = errors;
180 |
181 | return ! (errors.length > 0);
182 | },
183 |
184 | // generate a UID. This generates a UUID with uuid: URN namespace, as suggested
185 | // by RFC 6350, 6.7.6
186 | generateUID: function() {
187 | return 'uuid:' + Math.uuid();
188 | },
189 |
190 | // generate revision timestamp (a full ISO 8601 date/time string in basic format)
191 | generateRev: function() {
192 | return (new Date()).toISOString().replace(/[\.\:\-]/g, '');
193 | },
194 |
195 | // Set the given attribute to the given value.
196 | // This sets vcard.changed to true, so you can check later whether anything
197 | // was updated by your code.
198 | setAttribute: function(key, value) {
199 | this[key] = value;
200 | this.changed = true;
201 | },
202 |
203 | // Set the given attribute to the given value.
204 | // If the given attribute's key has cardinality > 1, instead of overwriting
205 | // the current value, an additional value is appended.
206 | addAttribute: function(key, value) {
207 | console.log('add attribute', key, value);
208 | if(! value) {
209 | return;
210 | }
211 | if(VCard.multivaluedKeys[key]) {
212 | if(this[key]) {
213 | console.log('multivalued push');
214 | this[key].push(value)
215 | } else {
216 | console.log('multivalued set');
217 | this.setAttribute(key, [value]);
218 | }
219 | } else {
220 | this.setAttribute(key, value);
221 | }
222 | },
223 |
224 | // convenience method to get a JSON serialized jCard.
225 | toJSON: function() {
226 | return JSON.stringify(this.toJCard());
227 | },
228 |
229 | // Copies all properties (i.e. all specified in VCard.allKeys) to a new object
230 | // and returns it.
231 | // Useful to serialize to JSON afterwards.
232 | toJCard: function() {
233 | var jcard = {};
234 | for(var k in VCard.allKeys) {
235 | var key = VCard.allKeys[k];
236 | if(this[key]) {
237 | jcard[key] = this[key];
238 | }
239 | }
240 | return jcard;
241 | },
242 |
243 | // synchronizes two vcards, using the mechanisms described in
244 | // RFC 6350, Section 7.
245 | // Returns a new VCard object.
246 | // If a property is present in both source vcards, and that property's
247 | // maximum cardinality is 1, then the value from the second (given) vcard
248 | // precedes.
249 | //
250 | // TODO: implement PID matching as described in 7.3.1
251 | merge: function(other) {
252 | if(typeof(other.uid) !== 'undefined' &&
253 | typeof(this.uid) !== 'undefined' &&
254 | other.uid !== this.uid) {
255 | // 7.1.1
256 | throw "Won't merge vcards without matching UIDs.";
257 | }
258 |
259 | var result = new VCard();
260 |
261 | function mergeProperty(key) {
262 | if(other[key]) {
263 | if(other[key] == this[key]) {
264 | result.setAttribute(this[key]);
265 | } else {
266 | result.addAttribute(this[key]);
267 | result.addAttribute(other[key]);
268 | }
269 | } else {
270 | result[key] = this[key];
271 | }
272 | }
273 |
274 | for(key in this) { // all properties of this
275 | mergeProperty(key);
276 | }
277 | for(key in other) { // all properties of other *not* in this
278 | if(! result[key]) {
279 | mergeProperty(key);
280 | }
281 | }
282 | }
283 | };
284 |
285 | VCard.enums = {
286 | telType: ["text", "voice", "fax", "cell", "video", "pager", "textphone"],
287 | relatedType: ["contact", "acquaintance", "friend", "met", "co-worker",
288 | "colleague", "co-resident", "neighbor", "child", "parent",
289 | "sibling", "spouse", "kin", "muse", "crush", "date",
290 | "sweetheart", "me", "agent", "emergency"],
291 | // FIXME: these aren't actually defined anywhere. just very commmon.
292 | // maybe there should be more?
293 | emailType: ["work", "home", "internet"],
294 | langType: ["work", "home"],
295 |
296 | };
297 |
298 | VCard.allKeys = [
299 | 'fn', 'n', 'nickname', 'photo', 'bday', 'anniversary', 'gender',
300 | 'tel', 'email', 'impp', 'lang', 'tz', 'geo', 'title', 'role', 'logo',
301 | 'org', 'member', 'related', 'categories', 'note', 'prodid', 'rev',
302 | 'sound', 'uid'
303 | ];
304 |
305 | VCard.multivaluedKeys = {
306 | email: true,
307 | tel: true,
308 | geo: true,
309 | title: true,
310 | role: true,
311 | logo: true,
312 | org: true,
313 | member: true,
314 | related: true,
315 | categories: true,
316 | note: true
317 | };
318 |
319 | })();
320 | /**
321 | ** VCF - Parser for the vcard format.
322 | **
323 | ** This is purely a vCard 4.0 implementation, as described in RFC 6350.
324 | **
325 | ** The generated VCard object roughly corresponds to the JSON representation
326 | ** of a hCard, as described here: http://microformats.org/wiki/jcard
327 | ** (Retrieved May 17, 2012)
328 | **
329 | **/
330 |
331 | var VCF;
332 |
333 | (function() {
334 | VCF = {
335 |
336 | simpleKeys: [
337 | 'VERSION',
338 | 'FN', // 6.2.1
339 | 'PHOTO', // 6.2.4 (we don't care about URIs [yet])
340 | 'GEO', // 6.5.2 (SHOULD also b a URI)
341 | 'TITLE', // 6.6.1
342 | 'ROLE', // 6.6.2
343 | 'LOGO', // 6.6.3 (also [possibly data:] URI)
344 | 'MEMBER', // 6.6.5
345 | 'NOTE', // 6.7.2
346 | 'PRODID', // 6.7.3
347 | 'SOUND', // 6.7.5
348 | 'UID', // 6.7.6
349 | ],
350 | csvKeys: [
351 | 'NICKNAME', // 6.2.3
352 | 'CATEGORIES', // 6.7.1
353 | ],
354 | dateAndOrTimeKeys: [
355 | 'BDAY', // 6.2.5
356 | 'ANNIVERSARY', // 6.2.6
357 | 'REV', // 6.7.4
358 | ],
359 |
360 | // parses the given input, constructing VCard objects.
361 | // if the input contains multiple (properly seperated) vcards,
362 | // the callback may be called multiple times, with one vcard given
363 | // each time.
364 | // The third argument specifies the context in which to evaluate
365 | // the given callback.
366 | parse: function(input, callback, context) {
367 | var vcard = null;
368 |
369 | if(! context) {
370 | context = this;
371 | }
372 |
373 | this.lex(input, function(key, value, attrs) {
374 | function setAttr(val) {
375 | if(vcard) {
376 | vcard.addAttribute(key.toLowerCase(), val);
377 | }
378 | }
379 | if(key == 'BEGIN') {
380 | vcard = new VCard();
381 | } else if(key == 'END') {
382 | if(vcard) {
383 | callback.apply(context, [vcard]);
384 | vcard = null;
385 | }
386 |
387 | } else if(this.simpleKeys.indexOf(key) != -1) {
388 | setAttr(value);
389 |
390 | } else if(this.csvKeys.indexOf(key) != -1) {
391 | setAttr(value.split(','));
392 |
393 | } else if(this.dateAndOrTimeKeys.indexOf(key) != -1) {
394 | if(attrs.VALUE == 'text') {
395 | // times can be expressed as "text" as well,
396 | // e.g. "ca 1800", "next week", ...
397 | setAttr(value);
398 | } else if(attrs.CALSCALE && attrs.CALSCALE != 'gregorian') {
399 | // gregorian calendar is the only calscale mentioned
400 | // in RFC 6350. I do not intend to support anything else
401 | // (yet).
402 | } else {
403 | // FIXME: handle TZ attribute.
404 | setAttr(this.parseDateAndOrTime(value));
405 | }
406 |
407 | } else if(key == 'N') { // 6.2.2
408 | setAttr(this.parseName(value));
409 |
410 | } else if(key == 'GENDER') { // 6.2.7
411 | setAttr(this.parseGender(value));
412 |
413 | } else if(key == 'TEL') { // 6.4.1
414 | setAttr({
415 | type: (attrs.TYPE || 'voice'),
416 | pref: attrs.PREF,
417 | value: value
418 | });
419 |
420 | } else if(key == 'EMAIL') { // 6.4.2
421 | setAttr({
422 | type: attrs.TYPE,
423 | pref: attrs.PREF,
424 | value: value
425 | });
426 |
427 | } else if(key == 'IMPP') { // 6.4.3
428 | // RFC 6350 doesn't define TYPEs for IMPP addresses.
429 | // It just seems odd to me to have multiple email addresses and phone numbers,
430 | // but not multiple IMPP addresses.
431 | setAttr({ value: value });
432 |
433 | } else if(key == 'LANG') { // 6.4.4
434 | setAttr({
435 | type: attrs.TYPE,
436 | pref: attrs.PREF,
437 | value: value
438 | });
439 |
440 | } else if(key == 'TZ') { // 6.5.1
441 | // neither hCard nor jCard mention anything about the TZ
442 | // property, except that it's singular (which it is *not* in
443 | // RFC 6350).
444 | // using compound representation.
445 | if(attrs.VALUE == 'utc-offset') {
446 | setAttr({ 'utc-offset': this.parseTimezone(value) });
447 | } else {
448 | setAttr({ name: value });
449 | }
450 |
451 | } else if(key == 'ORG') { // 6.6.4
452 | var parts = value.split(';');
453 | setAttr({
454 | 'organization-name': parts[0],
455 | 'organization-unit': parts[1]
456 | });
457 |
458 | } else if(key == 'RELATED') { // 6.6.6
459 | setAttr({
460 | type: attrs.TYPE,
461 | pref: attrs.PREF,
462 | value: attrs.VALUE
463 | });
464 |
465 | } else if(key =='ADR'){
466 | setAttr({
467 | type: attrs.TYPE,
468 | pref: attrs.PREF,
469 | value: value
470 | });
471 | //TODO: Handle 'LABEL' field.
472 | } else {
473 | console.log('WARNING: unhandled key: ', key);
474 | }
475 | });
476 | },
477 |
478 | nameParts: [
479 | 'family-name', 'given-name', 'additional-name',
480 | 'honorific-prefix', 'honorific-suffix'
481 | ],
482 |
483 | parseName: function(name) { // 6.2.2
484 | var parts = name.split(';');
485 | var n = {};
486 | for(var i in parts) {
487 | if(parts[i]) {
488 | n[this.nameParts[i]] = parts[i].split(',');
489 | }
490 | }
491 | return n;
492 | },
493 |
494 | /**
495 | * The representation of gender for hCards (and hence their JSON
496 | * representation) is undefined, as hCard is based on RFC 2436, which
497 | * doesn't define the GENDER attribute.
498 | * This method uses a compound representation.
499 | *
500 | * Examples:
501 | * "GENDER:M" -> {"sex":"male"}
502 | * "GENDER:M;man" -> {"sex":"male","identity":"man"}
503 | * "GENDER:F;girl" -> {"sex":"female","identity":"girl"}
504 | * "GENDER:M;girl" -> {"sex":"male","identity":"girl"}
505 | * "GENDER:F;boy" -> {"sex":"female","identity":"boy"}
506 | * "GENDER:N;woman" -> {"identity":"woman"}
507 | * "GENDER:O;potted plant" -> {"sex":"other","identity":"potted plant"}
508 | */
509 | parseGender: function(value) { // 6.2.7
510 | var gender = {};
511 | var parts = value.split(';');
512 | switch(parts[0]) {
513 | case 'M':
514 | gender.sex = 'male';
515 | break;
516 | case 'F':
517 | gender.sex = 'female';
518 | break;
519 | case 'O':
520 | gender.sex = 'other';
521 | }
522 | if(parts[1]) {
523 | gender.identity = parts[1];
524 | }
525 | return gender;
526 | },
527 |
528 | /** Date/Time parser.
529 | *
530 | * This implements only the parts of ISO 8601, that are
531 | * allowed by RFC 6350.
532 | * Paranthesized examples all represent (parts of):
533 | * 31st of January 1970, 23 Hours, 59 Minutes, 30 Seconds
534 | **/
535 |
536 | /** DATE **/
537 |
538 | // [ISO.8601.2004], 4.1.2.2, basic format:
539 | dateRE: /^(\d{4})(\d{2})(\d{2})$/, // (19700131)
540 |
541 | // [ISO.8601.2004], 4.1.2.3 a), basic format:
542 | dateReducedARE: /^(\d{4})\-(\d{2})$/, // (1970-01)
543 |
544 | // [ISO.8601.2004], 4.1.2.3 b), basic format:
545 | dateReducedBRE: /^(\d{4})$/, // (1970)
546 |
547 | // truncated representation from [ISO.8601.2000], 5.3.1.4.
548 | // I don't have access to that document, so relying on examples
549 | // from RFC 6350:
550 | dateTruncatedMDRE: /^\-{2}(\d{2})(\d{2})$/, // (--0131)
551 | dateTruncatedDRE: /^\-{3}(\d{2})$/, // (---31)
552 |
553 | /** TIME **/
554 |
555 | // (Note: it is unclear to me which of these are supposed to support
556 | // timezones. Allowing them for all. If timezones are ommitted,
557 | // defaulting to UTC)
558 |
559 | // [ISO.8601.2004, 4.2.2.2, basic format:
560 | timeRE: /^(\d{2})(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (235930)
561 | // [ISO.8601.2004, 4.2.2.3 a), basic format:
562 | timeReducedARE: /^(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (2359)
563 | // [ISO.8601.2004, 4.2.2.3 b), basic format:
564 | timeReducedBRE: /^(\d{2})([+\-]\d+|Z|)$/, // (23)
565 | // truncated representation from [ISO.8601.2000], see above.
566 | timeTruncatedMSRE: /^\-{2}(\d{2})(\d{2})([+\-]\d+|Z|)$/, // (--5930)
567 | timeTruncatedSRE: /^\-{3}(\d{2})([+\-]\d+|Z|)$/, // (---30)
568 |
569 | parseDate: function(data) {
570 | var md;
571 | var y, m, d;
572 | if((md = data.match(this.dateRE))) {
573 | y = md[1]; m = md[2]; d = md[3];
574 | } else if((md = data.match(this.dateReducedARE))) {
575 | y = md[1]; m = md[2];
576 | } else if((md = data.match(this.dateReducedBRE))) {
577 | y = md[1];
578 | } else if((md = data.match(this.dateTruncatedMDRE))) {
579 | m = md[1]; d = md[2];
580 | } else if((md = data.match(this.dateTruncatedDRE))) {
581 | d = md[1];
582 | } else {
583 | console.error("WARNING: failed to parse date: ", data);
584 | return null;
585 | }
586 | var dt = new Date(0);
587 | if(typeof(y) != 'undefined') { dt.setUTCFullYear(y); }
588 | if(typeof(m) != 'undefined') { dt.setUTCMonth(m - 1); }
589 | if(typeof(d) != 'undefined') { dt.setUTCDate(d); }
590 | return dt;
591 | },
592 |
593 | parseTime: function(data) {
594 | var md;
595 | var h, m, s, tz;
596 | if((md = data.match(this.timeRE))) {
597 | h = md[1]; m = md[2]; s = md[3];
598 | tz = md[4];
599 | } else if((md = data.match(this.timeReducedARE))) {
600 | h = md[1]; m = md[2];
601 | tz = md[3];
602 | } else if((md = data.match(this.timeReducedBRE))) {
603 | h = md[1];
604 | tz = md[2];
605 | } else if((md = data.match(this.timeTruncatedMSRE))) {
606 | m = md[1]; s = md[2];
607 | tz = md[3];
608 | } else if((md = data.match(this.timeTruncatedSRE))) {
609 | s = md[1];
610 | tz = md[2];
611 | } else {
612 | console.error("WARNING: failed to parse time: ", data);
613 | return null;
614 | }
615 |
616 | var dt = new Date(0);
617 | if(typeof(h) != 'undefined') { dt.setUTCHours(h); }
618 | if(typeof(m) != 'undefined') { dt.setUTCMinutes(m); }
619 | if(typeof(s) != 'undefined') { dt.setUTCSeconds(s); }
620 |
621 | if(tz) {
622 | dt = this.applyTimezone(dt, tz);
623 | }
624 |
625 | return dt;
626 | },
627 |
628 | // add two dates. if addSub is false, substract instead of add.
629 | addDates: function(aDate, bDate, addSub) {
630 | if(typeof(addSub) == 'undefined') { addSub = true };
631 | if(! aDate) { return bDate; }
632 | if(! bDate) { return aDate; }
633 | var a = Number(aDate);
634 | var b = Number(bDate);
635 | var c = addSub ? a + b : a - b;
636 | return new Date(c);
637 | },
638 |
639 | parseTimezone: function(tz) {
640 | var md;
641 | if((md = tz.match(/^([+\-])(\d{2})(\d{2})?/))) {
642 | var offset = new Date(0);
643 | offset.setUTCHours(md[2]);
644 | offset.setUTCMinutes(md[3] || 0);
645 | return Number(offset) * (md[1] == '+' ? +1 : -1);
646 | } else {
647 | return null;
648 | }
649 | },
650 |
651 | applyTimezone: function(date, tz) {
652 | var offset = this.parseTimezone(tz);
653 | if(offset) {
654 | return new Date(Number(date) + offset);
655 | } else {
656 | return date;
657 | }
658 | },
659 |
660 | parseDateTime: function(data) {
661 | var parts = data.split('T');
662 | var t = this.parseDate(parts[0]);
663 | var d = this.parseTime(parts[1]);
664 | return this.addDates(t, d);
665 | },
666 |
667 | parseDateAndOrTime: function(data) {
668 | switch(data.indexOf('T')) {
669 | case 0:
670 | return this.parseTime(data.slice(1));
671 | case -1:
672 | return this.parseDate(data);
673 | default:
674 | return this.parseDateTime(data);
675 | }
676 | },
677 |
678 | lineRE: /^([^\s].*)(?:\r?\n|$)/, // spec wants CRLF, but we're on the internet. reality is chaos.
679 | foldedLineRE:/^\s(.+)(?:\r?\n|$)/,
680 |
681 | // lex the given input, calling the callback for each line, with
682 | // the following arguments:
683 | // * key - key of the statement, such as 'BEGIN', 'FN', 'N', ...
684 | // * value - value of the statement, i.e. everything after the first ':'
685 | // * attrs - object containing attributes, such as {"TYPE":"work"}
686 | lex: function(input, callback) {
687 |
688 | var md, line = null, length = 0;
689 |
690 | for(;;) {
691 | if((md = input.match(this.lineRE))) {
692 | // Unfold quoted-printables (vCard 2.1) into a single line before parsing.
693 | // "Soft" linebreaks are indicated by a '=' at the end of the line, and do
694 | // not affect the underlying data.
695 | if(line && line.indexOf('QUOTED-PRINTABLE') != -1 && line.slice(-1) == '=') {
696 | line = line.slice(0,-1) + md[1];
697 | length = md[0].length;
698 | } else {
699 | if(line) {
700 | this.lexLine(line, callback);
701 | }
702 | line = md[1];
703 | length = md[0].length;
704 | }
705 | } else if((md = input.match(this.foldedLineRE))) {
706 | if(line) {
707 | line += md[1];
708 | length = md[0].length;
709 | } else {
710 | // ignore folded junk.
711 | }
712 | } else {
713 | console.error("Unmatched line: " + line);
714 | }
715 |
716 | input = input.slice(length);
717 |
718 | if(! input) {
719 | break;
720 | }
721 | }
722 |
723 | if(line) {
724 | // last line.
725 | this.lexLine(line, callback);
726 | }
727 |
728 | line = null;
729 | },
730 |
731 | lexLine: function(line, callback) {
732 | var tmp = '';
733 | var key = null, attrs = {}, value = null, attrKey = null;
734 |
735 | //If our value is a quoted-printable (vCard 2.1), decode it and discard the encoding attribute
736 | var qp = line.indexOf('ENCODING=QUOTED-PRINTABLE');
737 | if(qp != -1){
738 | line = line.substr(0,qp) + this.decodeQP(line.substr(qp+25));
739 | }
740 |
741 | function finalizeKeyOrAttr() {
742 | if(key) {
743 | if(attrKey) {
744 | attrs[attrKey] = tmp.split(',');
745 | } else {
746 | //"Floating" attributes are probably vCard 2.1 TYPE or PREF values.
747 | if(tmp == "PREF"){
748 | attrs.PREF = 1;
749 | } else {
750 | if (attrs.TYPE) attrs.TYPE.push(tmp);
751 | else attrs.TYPE = [tmp];
752 | }
753 | }
754 | } else {
755 | key = tmp;
756 | }
757 | }
758 |
759 | for(var i in line) {
760 | var c = line[i];
761 |
762 | switch(c) {
763 | case ':':
764 | finalizeKeyOrAttr();
765 | value = line.slice(Number(i) + 1);
766 | callback.apply(
767 | this,
768 | [key, value, attrs]
769 | );
770 | return;
771 | case ';':
772 | finalizeKeyOrAttr();
773 | tmp = '';
774 | break;
775 | case '=':
776 | attrKey = tmp;
777 | tmp = '';
778 | break;
779 | default:
780 | tmp += c;
781 | }
782 | }
783 | },
784 | /** Quoted Printable Parser
785 | *
786 | * Parses quoted-printable strings, which sometimes appear in
787 | * vCard 2.1 files (usually the address field)
788 | *
789 | * Code adapted from:
790 | * https://github.com/andris9/mimelib
791 | *
792 | **/
793 | decodeQP: function(str){
794 | str = (str || "").toString();
795 | str = str.replace(/\=(?:\r?\n|$)/g, "");
796 | var str2 = "";
797 | for(var i=0, len = str.length; i