# kabc Library API Documentation

Overview | Class Hierarchy | Classes | Classes (annotated) | Members | Namespaces | Source Files

# vcard21parser.cpp

    00001 /*
    00002     This file is part of libkabc.
    00003     Copyright (c) 2001 Mark Westcott <mark@houseoffish.org>
    00004
    00005     This library is free software; you can redistribute it and/or
    00006     modify it under the terms of the GNU Library General Public
    00007     License as published by the Free Software Foundation; either
    00008     version 2 of the License, or (at your option) any later version.
    00009
    00010     This library is distributed in the hope that it will be useful,
    00011     but WITHOUT ANY WARRANTY; without even the implied warranty of
    00012     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    00013     Library General Public License for more details.
    00014
    00015     You should have received a copy of the GNU Library General Public License
    00016     along with this library; see the file COPYING.LIB.  If not, write to
    00017     the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
    00018     Boston, MA 02111-1307, USA.
    00019 */
    00020
    00021 #include <qmap.h>
    00022 #include <qregexp.h>
    00023 #include <kmdcodec.h>
    00024
    00025 #include "vcard21parser.h"
    00026 #include "vcardconverter.h"
    00027
    00028 using namespace KABC;
    00029
    00030 bool VCardLineX::isValid() const
    00031 {
    00032   // Invalid: if it is "begin:vcard" or "end:vcard"
    00033   if ( name == VCARD_BEGIN_N || name == VCARD_END_N )
    00034     return false;
    00035
    00036   if ( name[0] == 'x' && name[1] == '-' ) // A custom x- line
    00037     return true;
    00038
    00039   // This is long but it makes it a bit faster (and saves me from using
    00040   // a tree which is probably the ideal situation, but a bit memory heavy)
    00041   switch( name[0] ) {
    00042     case 'a':
    00043       if ( name == VCARD_ADR && qualified &&
    00044                    (qualifiers.contains(VCARD_ADR_DOM)    ||
    00045                     qualifiers.contains(VCARD_ADR_INTL)   ||
    00046                     qualifiers.contains(VCARD_ADR_POSTAL) ||
    00047                     qualifiers.contains(VCARD_ADR_HOME)   ||
    00048                     qualifiers.contains(VCARD_ADR_WORK)   ||
    00049                     qualifiers.contains(VCARD_ADR_PREF)
    00050          ) )
    00051         return true;
    00052
    00053       if ( name == VCARD_AGENT )
    00054         return true;
    00055       break;
    00056
    00057     case 'b':
    00058       if ( name == VCARD_BDAY )
    00059         return true;
    00060       break;
    00061
    00062     case 'c':
    00063       if ( name == VCARD_CATEGORIES )
    00064         return true;
    00065       if ( name == VCARD_CLASS && qualified &&
    00066                    (qualifiers.contains(VCARD_CLASS_PUBLIC)      ||
    00067                     qualifiers.contains(VCARD_CLASS_PRIVATE)     ||
    00068                     qualifiers.contains(VCARD_CLASS_CONFIDENTIAL)
    00069          ) )
    00070         return true;
    00071       break;
    00072
    00073     case 'e':
    00074       if ( name == VCARD_EMAIL && qualified &&
    00075                   (qualifiers.contains(VCARD_EMAIL_INTERNET) ||
    00076                    qualifiers.contains(VCARD_EMAIL_PREF)     ||
    00077                    qualifiers.contains(VCARD_EMAIL_X400)
    00078          ) )
    00079         return true;
    00080       break;
    00081
    00082     case 'f':
    00083       if ( name == VCARD_FN )
    00084         return true;
    00085       break;
    00086
    00087     case 'g':
    00088       if ( name == VCARD_GEO )
    00089         return true;
    00090       break;
    00091
    00092     case 'k':
    00093       if ( name == VCARD_KEY && qualified &&
    00094                    (qualifiers.contains(VCARD_KEY_X509) ||
    00095                     qualifiers.contains(VCARD_KEY_PGP)
    00096          ) )
    00097         return true;
    00098       break;
    00099
    00100     case 'l':
    00101       if ( name == VCARD_LABEL )
    00102         return true;
    00103       if ( name == VCARD_LOGO )
    00104         return true;
    00105       break;
    00106
    00107     case 'm':
    00108       if ( name == VCARD_MAILER )
    00109         return true;
    00110       break;
    00111
    00112     case 'n':
    00113       if ( name == VCARD_N )
    00114         return true;
    00115       if ( name == VCARD_NAME )
    00116         return true;
    00117       if ( name == VCARD_NICKNAME )
    00118         return true;
    00119       if ( name == VCARD_NOTE )
    00120         return true;
    00121       break;
    00122
    00123     case 'o':
    00124       if ( name == VCARD_ORG )
    00125         return true;
    00126       break;
    00127
    00128     case 'p':
    00129       if ( name == VCARD_PHOTO )
    00130         return true;
    00131       if ( name == VCARD_PROFILE )
    00132         return true;
    00133       if ( name == VCARD_PRODID )
    00134         return true;
    00135       break;
    00136
    00137     case 'r':
    00138       if ( name == VCARD_ROLE )
    00139         return true;
    00140       if ( name == VCARD_REV )
    00141         return true;
    00142       break;
    00143
    00144     case 's':
    00145       if ( name == VCARD_SOURCE )
    00146         return true;
    00147       if ( name == VCARD_SOUND )
    00148         return true;
    00149       break;
    00150
    00151     case 't':
    00152       if ( name == VCARD_TEL && qualified &&
    00153                    (qualifiers.contains(VCARD_TEL_HOME)  ||
    00154                     qualifiers.contains(VCARD_TEL_WORK)  ||
    00155                     qualifiers.contains(VCARD_TEL_PREF)  ||
    00156                     qualifiers.contains(VCARD_TEL_VOICE) ||
    00157                     qualifiers.contains(VCARD_TEL_FAX)   ||
    00158                     qualifiers.contains(VCARD_TEL_MSG)   ||
    00159                     qualifiers.contains(VCARD_TEL_CELL)  ||
    00160                     qualifiers.contains(VCARD_TEL_PAGER) ||
    00161                     qualifiers.contains(VCARD_TEL_BBS)   ||
    00162                     qualifiers.contains(VCARD_TEL_MODEM) ||
    00163                     qualifiers.contains(VCARD_TEL_CAR)   ||
    00164                     qualifiers.contains(VCARD_TEL_ISDN)  ||
    00165                     qualifiers.contains(VCARD_TEL_VIDEO) ||
    00166                     qualifiers.contains(VCARD_TEL_PCS)
    00167          ) )
    00168         return true;
    00169       if ( name == VCARD_TZ )
    00170         return true;
    00171       if ( name == VCARD_TITLE )
    00172         return true;
    00173       break;
    00174
    00175     case 'u':
    00176       if ( name == VCARD_URL )
    00177         return true;
    00178       if ( name == VCARD_UID )
    00179         return true;
    00180       break;
    00181
    00182     case 'v':
    00183       if ( name == VCARD_VERSION )
    00184         return true;
    00185       break;
    00186     default:
    00187       break;
    00188   }
    00189
    00190   return false;
    00191 }
    00192
    00193
    00194 VCard21Parser::VCard21Parser()
    00195 {
    00196 }
    00197
    00198 VCard21Parser::~VCard21Parser()
    00199 {
    00200 }
    00201
    00202 void VCard21Parser::readFromString(KABC::AddressBook *addressbook, const QString &data)
    00203 {
    00204   KABC::Addressee mAddressee = readFromString(data);
    00205   addressbook->insertAddressee(mAddressee);
    00206 }
    00207
    00208 KABC::Addressee VCard21Parser::readFromString( const QString &data)
    00209 {
    00210   KABC::Addressee addressee;
    00211   VCard21ParserImpl *mVCard = VCard21ParserImpl::parseVCard(data);
    00212   QString tmpStr;
    00213
    00214   // Check if parsing failed
    00215   if (mVCard == 0)
    00216   {
    00217      kdDebug() << "Parsing failed" << endl;
    00218      return addressee;
    00219   }
    00220   //set the addressees name and formated name
    00221   QStringList tmpList = mVCard->getValues(VCARD_N);
    00222   QString formattedName = "";
    00223   if (tmpList.count() > 0)
    00224     addressee.setFamilyName(tmpList[0]);
    00225   if (tmpList.count() > 1)
    00226     addressee.setGivenName(tmpList[1]);
    00227   if (tmpList.count() > 2)
    00228     addressee.setAdditionalName(tmpList[2]);
    00229   if (tmpList.count() > 3)
    00230     addressee.setPrefix(tmpList[3]);
    00231   if (tmpList.count() > 4)
    00232     addressee.setSuffix(tmpList[4]);
    00233
    00234   tmpStr = (mVCard->getValue(VCARD_FN));
    00235   if (!tmpStr.isEmpty())
    00236     addressee.setFormattedName(tmpStr);
    00237
    00238   //set the addressee's nick name
    00239   tmpStr = mVCard->getValue(VCARD_NICKNAME);
    00240   addressee.setNickName(tmpStr);
    00241   //set the addressee's organization
    00242   tmpStr = mVCard->getValue(VCARD_ORG);
    00243   addressee.setOrganization(tmpStr);
    00244   //set the addressee's title
    00245   tmpStr = mVCard->getValue(VCARD_TITLE);
    00246   addressee.setTitle(tmpStr);
    00247   //set the addressee's email - we can only deal with two.  The preferenced one and one other.
    00248   tmpStr = mVCard->getValue(VCARD_EMAIL, VCARD_EMAIL_INTERNET);
    00249   addressee.insertEmail(tmpStr, false);
    00250   tmpStr = mVCard->getValue(VCARD_EMAIL,VCARD_EMAIL_PREF);
    00251   addressee.insertEmail(tmpStr, true);
    00252   //set the addressee's url
    00253   tmpStr = mVCard->getValue(VCARD_URL);
    00254   if (tmpStr.isEmpty()) tmpStr = mVCard->getValue(VCARD_URL, VCARD_ADR_WORK);
    00255   if (tmpStr.isEmpty()) tmpStr = mVCard->getValue(VCARD_URL, VCARD_ADR_HOME);
    00256   if (!tmpStr.isEmpty()) {
    00257     addressee.setUrl(KURL(tmpStr));
    00258   }
    00259
    00260   //set the addressee's birthday
    00261   tmpStr = mVCard->getValue(VCARD_BDAY);
    00262   addressee.setBirthday(VCardStringToDate(tmpStr));
    00263
    00264   //set the addressee's phone numbers
    00265   for ( QValueListIterator<VCardLineX> i = mVCard->_vcdata->begin();i != mVCard->_vcdata->end(); ++i ) {
    00266     if ( (*i).name == VCARD_TEL ) {
    00267       int type = 0;
    00268       if ( (*i).qualified ) {
    00269         if ( (*i).qualifiers.contains( VCARD_TEL_HOME ) )
    00270           type |= PhoneNumber::Home;
    00271         if ( (*i).qualifiers.contains( VCARD_TEL_WORK ) )
    00272           type |= PhoneNumber::Work;
    00273         if ( (*i).qualifiers.contains( VCARD_TEL_PREF ) )
    00274           type |= PhoneNumber::Pref;
    00275         // if ( (*i).qualifiers.contains( VCARD_TEL_VOICE ) )
    00276         //  type |= PhoneNumber::Voice;
    00277         if ( (*i).qualifiers.contains( VCARD_TEL_FAX ) )
    00278           type |= PhoneNumber::Fax;
    00279         if ( (*i).qualifiers.contains( VCARD_TEL_MSG ) )
    00280           type |= PhoneNumber::Msg;
    00281         if ( (*i).qualifiers.contains( VCARD_TEL_CELL ) )
    00282           type |= PhoneNumber::Cell;
    00283         if ( (*i).qualifiers.contains( VCARD_TEL_PAGER ) )
    00284           type |= PhoneNumber::Pager;
    00285         if ( (*i).qualifiers.contains( VCARD_TEL_BBS ) )
    00286           type |= PhoneNumber::Bbs;
    00287         if ( (*i).qualifiers.contains( VCARD_TEL_MODEM ) )
    00288           type |= PhoneNumber::Modem;
    00289         if ( (*i).qualifiers.contains( VCARD_TEL_CAR ) )
    00290           type |= PhoneNumber::Car;
    00291         if ( (*i).qualifiers.contains( VCARD_TEL_ISDN ) )
    00292           type |= PhoneNumber::Isdn;
    00293         if ( (*i).qualifiers.contains( VCARD_TEL_VIDEO ) )
    00294           type |= PhoneNumber::Video;
    00295         if ( (*i).qualifiers.contains( VCARD_TEL_PCS ) )
    00296           type |= PhoneNumber::Pcs;
    00297       }
    00298       addressee.insertPhoneNumber( PhoneNumber( (*i).parameters[ 0 ], type ) );
    00299     }
    00300   }
    00301
    00302   //set the addressee's addresses
    00303   for ( QValueListIterator<VCardLineX> i = mVCard->_vcdata->begin();i != mVCard->_vcdata->end(); ++i ) {
    00304     if ( (*i).name == VCARD_ADR ) {
    00305       int type = 0;
    00306       if ( (*i).qualified ) {
    00307         if ( (*i).qualifiers.contains( VCARD_ADR_DOM ) )
    00308           type |= Address::Dom;
    00309         if ( (*i).qualifiers.contains( VCARD_ADR_INTL ) )
    00310           type |= Address::Intl;
    00311         if ( (*i).qualifiers.contains( VCARD_ADR_POSTAL ) )
    00312           type |= Address::Postal;
    00313         if ( (*i).qualifiers.contains( VCARD_ADR_PARCEL ) )
    00314           type |= Address::Parcel;
    00315         if ( (*i).qualifiers.contains( VCARD_ADR_HOME ) )
    00316           type |= Address::Home;
    00317         if ( (*i).qualifiers.contains( VCARD_ADR_WORK ) )
    00318           type |= Address::Work;
    00319         if ( (*i).qualifiers.contains( VCARD_ADR_PREF ) )
    00320           type |= Address::Pref;
    00321       }
    00322       addressee.insertAddress( readAddressFromQStringList( (*i).parameters, type ) );
    00323     }
    00324   }
    00325
    00326   //set the addressee's delivery label
    00327   tmpStr = mVCard->getValue(VCARD_LABEL);
    00328   if (!tmpStr.isEmpty()) {
    00329     tmpStr.replace("\r\n","\n");
    00330     Address tmpAddress;
    00331     tmpAddress.setLabel(tmpStr);
    00332     addressee.insertAddress(tmpAddress);
    00333   }
    00334
    00335   //set the addressee's notes
    00336   tmpStr = mVCard->getValue(VCARD_NOTE);
    00337   tmpStr.replace("\r\n","\n");
    00338   addressee.setNote(tmpStr);
    00339
    00340   //set the addressee's timezone
    00341   tmpStr = mVCard->getValue(VCARD_TZ);
    00342   TimeZone tmpZone(tmpStr.toInt());
    00343   addressee.setTimeZone(tmpZone);
    00344
    00345   //set the addressee's geographical position
    00346   tmpList = mVCard->getValues(VCARD_GEO);
    00347   if (tmpList.count()==2)
    00348   {
    00349     tmpStr = tmpList[0];
    00350     float glat = tmpStr.toFloat();
    00351     tmpStr = tmpList[1];
    00352     float glong = tmpStr.toFloat();
    00353     Geo tmpGeo(glat,glong);
    00354     addressee.setGeo(tmpGeo);
    00355   }
    00356
    00357   //set the last revision date
    00358   tmpStr = mVCard->getValue(VCARD_REV);
    00359   addressee.setRevision(VCardStringToDate(tmpStr));
    00360
    00361   //set the role of the addressee
    00362   tmpStr = mVCard->getValue(VCARD_ROLE);
    00363   addressee.setRole(tmpStr);
    00364
    00365   return addressee;
    00366 }
    00367
    00368
    00369
    00370 KABC::Address VCard21Parser::readAddressFromQStringList ( const QStringList &data, const int type )
    00371 {
    00372   KABC::Address mAddress;
    00373   mAddress.setType( type );
    00374
    00375   if ( data.count() > 0 )
    00376     mAddress.setPostOfficeBox( data[0] );
    00377   if ( data.count() > 1 )
    00378     mAddress.setExtended( data[1] );
    00379   if ( data.count() > 2 )
    00380     mAddress.setStreet( data[2] );
    00381   if ( data.count() > 3 )
    00382     mAddress.setLocality( data[3] );
    00383   if ( data.count() > 4 )
    00384     mAddress.setRegion( data[4] );
    00385   if ( data.count() > 5 )
    00386     mAddress.setPostalCode( data[5] );
    00387   if ( data.count() > 6 )
    00388     mAddress.setCountry( data[6] );
    00389
    00390   return mAddress;
    00391 }
    00392
    00393
    00394 VCard21ParserImpl *VCard21ParserImpl::parseVCard( const QString& vc, int *err )
    00395 {
    00396   int _err = 0;
    00397   int _state = VC_STATE_BEGIN;
    00398
    00399   QValueList<VCardLineX> *_vcdata;
    00400   QValueList<QString> lines;
    00401
    00402   _vcdata = new QValueList<VCardLineX>;
    00403
    00404   lines = QStringList::split( QRegExp( "[\x0d\x0a]" ), vc );
    00405
    00406   // for each line in the vCard
    00407   for ( QStringList::Iterator j = lines.begin(); j != lines.end(); ++j ) {
    00408     VCardLineX _vcl;
    00409
    00410     // take spaces off the end - ugly but necessary hack
    00411     for ( int g = (*j).length()-1; g > 0 && (*j)[g].isSpace(); --g )
    00412       (*j)[g] = 0;
    00413
    00414     // first token:
    00415     //   verify state, update if necessary
    00416     if ( _state & VC_STATE_BEGIN) {
    00417       if ( !qstricmp( (*j).latin1(), VCARD_BEGIN ) ) {
    00418         _state = VC_STATE_BODY;
    00419         continue;
    00420       } else {
    00421         _err = VC_ERR_NO_BEGIN;
    00422         break;
    00423       }
    00424     } else if ( _state & VC_STATE_BODY ) {
    00425       if ( !qstricmp( (*j).latin1(), VCARD_END ) ) {
    00426         _state |= VC_STATE_END;
    00427         break;
    00428       }
    00429
    00430       // split into two tokens
    00431       int colon = (*j).find( ':' );
    00432       if ( colon < 0 ) {
    00433         _err = VC_ERR_INVALID_LINE;
    00434         break;
    00435       }
    00436
    00437       QString key = (*j).left( colon );
    00438       QString value = (*j).mid( colon + 1 );
    00439
    00440       // check for qualifiers and
    00441       // set name, qualified, qualifier(s)
    00442       QStringList keyTokens = QStringList::split( ';', key );
    00443       bool qp = false, first_pass = true;
    00444       bool b64 = false;
    00445
    00446       if ( keyTokens.count() > 0 ) {
    00447         _vcl.qualified = false;
    00448         _vcl.name = keyTokens[ 0 ].lower();
    00449
    00450         for ( QStringList::Iterator z = keyTokens.begin(); z != keyTokens.end(); ++z ) {
    00451           QString zz = (*z).lower();
    00452           if ( zz == VCARD_QUOTED_PRINTABLE || zz == VCARD_ENCODING_QUOTED_PRINTABLE ) {
    00453             qp = true;
    00454           } else if ( zz == VCARD_BASE64 ) {
    00455             b64 = true;
    00456           } else if ( !first_pass ) {
    00457             _vcl.qualified = true;
    00458             _vcl.qualifiers.append( zz );
    00459           }
    00460           first_pass = false;
    00461         }
    00462       } else {
    00463         _err = VC_ERR_INVALID_LINE;
    00464       }
    00465
    00466       if ( _err != 0 )
    00467         break;
    00468
    00469       if ( _vcl.name == VCARD_VERSION )
    00470         _state |= VC_STATE_HAVE_VERSION;
    00471
    00472       if ( _vcl.name == VCARD_N || _vcl.name == VCARD_FN )
    00473         _state |= VC_STATE_HAVE_N;
    00474
    00475       // second token:
    00476       //    split into tokens by ;
    00477       //    add to parameters vector
    00478       if ( b64 ) {
    00479         if ( value[ value.length() - 1 ] != '=' )
    00480           do {
    00481             value += *( ++j );
    00482           } while ( (*j)[ (*j).length() - 1 ] != '=' );
    00483       } else {
    00484         if ( qp ) { // join any split lines
    00485           while ( value[ value.length() - 1 ] == '=' ) {
    00486             value.remove( value.length() - 1, 1 );
    00487             value.append(*( ++j ));
    00488           }
    00489         }
    00490         _vcl.parameters = QStringList::split( ';', value, true );
    00491         if ( qp ) { // decode the quoted printable
    00492           for ( QStringList::Iterator z = _vcl.parameters.begin(); z != _vcl.parameters.end(); ++z )
    00493             *z = KCodecs::quotedPrintableDecode( (*z).latin1() );
    00494         }
    00495       }
    00496     } else {
    00497       _err = VC_ERR_INTERNAL;
    00498       break;
    00499     }
    00500
    00501     // validate VCardLineX
    00502     if ( !_vcl.isValid() ) {
    00503       _err = VC_ERR_INVALID_LINE;
    00504       break;
    00505     }
    00506
    00507     // add to vector
    00508     _vcdata->append( _vcl );
    00509   }
    00510
    00511   // errors to check at the last minute (exit state related)
    00512   if ( _err == 0 ) {
    00513     if ( !( _state & VC_STATE_END ) ) // we have to have an end!!
    00514       _err = VC_ERR_NO_END;
    00515
    00516     if ( !( _state & VC_STATE_HAVE_N ) || // we have to have the mandatories!
    00517          !( _state & VC_STATE_HAVE_VERSION ) )
    00518       _err = VC_ERR_MISSING_MANDATORY;
    00519   }
    00520
    00521   // set the error message if we can, and only return an object
    00522   // if the vCard was valid.
    00523   if ( err )
    00524     *err = _err;
    00525
    00526   if ( _err != 0 ) {
    00527     delete _vcdata;
    00528     return 0;
    00529   }
    00530
    00531   return new VCard21ParserImpl( _vcdata );
    00532 }
    00533
    00534 VCard21ParserImpl::VCard21ParserImpl(QValueList<VCardLineX> *_vcd) : _vcdata(_vcd)
    00535 {
    00536 }
    00537
    00538
    00539 QString VCard21ParserImpl::getValue(const QString& name, const QString& qualifier)
    00540 {
    00541   QString failed;
    00542   const QString lowname = name.lower();
    00543   const QString lowqualifier = qualifier.lower();
    00544
    00545   for (QValueListIterator<VCardLineX> i = _vcdata->begin();i != _vcdata->end();++i) {
    00546    if ((*i).name == lowname && (*i).qualified && (*i).qualifiers.contains(lowqualifier)) {
    00547     if ((*i).parameters.count() > 0)
    00548      return (*i).parameters[0];
    00549     else return failed;
    00550     }
    00551   }
    00552   return failed;
    00553 }
    00554
    00555
    00556 QString VCard21ParserImpl::getValue(const QString& name)
    00557 {
    00558   QString failed;
    00559   const QString lowname = name.lower();
    00560
    00561   for (QValueListIterator<VCardLineX> i = _vcdata->begin();i != _vcdata->end();++i) {
    00562     if ((*i).name == lowname && !(*i).qualified) {
    00563       if ((*i).parameters.count() > 0)
    00564         return (*i).parameters[0];
    00565       else return failed;
    00566     }
    00567    }
    00568   return failed;
    00569 }
    00570
    00571
    00572 QStringList VCard21ParserImpl::getValues(const QString& name)
    00573 {
    00574   const QString lowname = name.lower();
    00575   for (QValueListIterator<VCardLineX> i = _vcdata->begin();i != _vcdata->end();++i) {
    00576     if ((*i).name == lowname && !(*i).qualified)
    00577       return (*i).parameters;
    00578   }
    00579   // failed.
    00580   return QStringList();
    00581 }
    00582
    00583 QStringList VCard21ParserImpl::getValues(const QString& name, const QString& qualifier)
    00584 {
    00585   const QString lowname = name.lower();
    00586   const QString lowqualifier = qualifier.lower();
    00587   for (QValueListIterator<VCardLineX> i = _vcdata->begin();i != _vcdata->end();++i) {
    00588     if ((*i).name == lowname && (*i).qualified && (*i).qualifiers.contains(lowqualifier))
    00589        return (*i).parameters;
    00590   }
    00591   // failed.
    00592   return QStringList();
    00593 }
    00594
    00595

This file is part of the documentation for kabc Library Version 3.4.1.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Sat Jun 11 20:31:32 2005 by doxygen 1.4.1 written by Dimitri van Heesch, © 1997-2003