├── 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 | 17 |

{{TITLE}}

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 | 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 |     
    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(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