From d6d8e1d6548fe23c79ae6b64da6ff0ff05eb29fa Mon Sep 17 00:00:00 2001
From: Jonathan Bernard <jonathan@jdbernard.com>
Date: Sun, 26 Mar 2023 20:45:52 -0500
Subject: [PATCH] WIP vcard 3.0 implementation.

---
 doc/abnf.txt                     |  117 +++
 doc/rfc1766.txt                  |  507 ++++++++++++
 doc/rfc2426-errata-to-submit.txt |   47 ++
 src/vcard.nim                    |    7 -
 src/vcard/private/util.nim       |   20 +
 src/vcard3.nim                   | 1248 ++++++++++++++++++++++++++++++
 tests/test1.nim                  |    4 +-
 vcard.nimble                     |    2 +-
 8 files changed, 1942 insertions(+), 10 deletions(-)
 create mode 100644 doc/abnf.txt
 create mode 100644 doc/rfc1766.txt
 create mode 100644 doc/rfc2426-errata-to-submit.txt
 delete mode 100644 src/vcard.nim
 create mode 100644 src/vcard/private/util.nim
 create mode 100644 src/vcard3.nim

diff --git a/doc/abnf.txt b/doc/abnf.txt
new file mode 100644
index 0000000..d140f61
--- /dev/null
+++ b/doc/abnf.txt
@@ -0,0 +1,117 @@
+ABNF for 3.0:
+=============
+
+   vcard_entity = 1*(vcard)
+
+   vcard        = [group "."] "BEGIN" ":" "VCARD" 1*CRLF
+                  1*(contentline)
+        ;A vCard object MUST include the VERSION, FN and N types.
+                  [group "."] "END" ":" "VCARD" 1*CRLF
+
+   contentline  = [group "."] name *(";" param) ":" value CRLF
+      ; When parsing a content line, folded lines MUST first
+      ; be unfolded according to the unfolding procedure
+      ; described above.
+      ; When generating a content line, lines longer than 75
+      ; characters SHOULD be folded according to the folding
+      ; procedure described above.
+
+   group        = 1*(ALPHA / DIGIT / "-")
+
+   name         = x-name / iana-token
+
+   iana-token   = 1*(ALPHA / DIGIT / "-")
+      ; identifier registered with IANA
+
+   x-name       = "x-" 1*(ALPHA / DIGIT / "-")
+      ; Names that begin with "x-" or "X-" are
+      ; reserved for experimental use, not intended for released
+      ; products, or for use in bilateral agreements.
+
+   param        = param-name "=" param-value *("," param-value)
+
+   param-name   = x-name / iana-token
+
+   param-value  = ptext / quoted-string
+
+   ptext  = *SAFE-CHAR
+
+   value = *VALUE-CHAR
+         / valuespec      ; valuespec defined in section 5.8.4
+
+   quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
+
+   NON-ASCII    = %x80-FF
+      ; use restricted by charset parameter
+      ; on outer MIME object (UTF-8 preferred)
+
+   QSAFE-CHAR   = WSP / %x21 / %x23-7E / NON-ASCII
+      ; Any character except CTLs, DQUOTE
+
+   SAFE-CHAR    = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-ASCII
+      ; Any character except CTLs, DQUOTE, ";", ":", ","
+
+   VALUE-CHAR   = WSP / VCHAR / NON-ASCII
+      ; any textual character
+
+
+ABNF for 4.0:
+=============
+
+   vcard-entity = 1*vcard
+
+   vcard = "BEGIN:VCARD" CRLF
+           "VERSION:4.0" CRLF
+           1*contentline
+           "END:VCARD" CRLF
+     ; A vCard object MUST include the VERSION and FN properties.
+     ; VERSION MUST come immediately after BEGIN:VCARD.
+
+   contentline = [group "."] name *(";" param) ":" value CRLF
+     ; When parsing a content line, folded lines must first
+     ; be unfolded according to the unfolding procedure
+     ; described in Section 3.2.
+     ; When generating a content line, lines longer than 75
+     ; characters SHOULD be folded according to the folding
+     ; procedure described in Section 3.2.
+
+   group = 1*(ALPHA / DIGIT / "-")
+   name  = "SOURCE" / "KIND" / "FN" / "N" / "NICKNAME"
+         / "PHOTO" / "BDAY" / "ANNIVERSARY" / "GENDER" / "ADR" / "TEL"
+         / "EMAIL" / "IMPP" / "LANG" / "TZ" / "GEO" / "TITLE" / "ROLE"
+         / "LOGO" / "ORG" / "MEMBER" / "RELATED" / "CATEGORIES"
+         / "NOTE" / "PRODID" / "REV" / "SOUND" / "UID" / "CLIENTPIDMAP"
+         / "URL" / "KEY" / "FBURL" / "CALADRURI" / "CALURI" / "XML"
+         / iana-token / x-name
+     ; Parsing of the param and value is based on the "name" as
+     ; defined in ABNF sections below.
+     ; Group and name are case-insensitive.
+
+   iana-token = 1*(ALPHA / DIGIT / "-")
+     ; identifier registered with IANA
+
+   x-name = "x-" 1*(ALPHA / DIGIT / "-")
+     ; Names that begin with "x-" or "X-" are
+     ; reserved for experimental use, not intended for released
+     ; products, or for use in bilateral agreements.
+
+   param = language-param / value-param / pref-param / pid-param
+         / type-param / geo-parameter / tz-parameter / sort-as-param
+         / calscale-param / any-param
+     ; Allowed parameters depend on property name.
+
+   param-value = *SAFE-CHAR / DQUOTE *QSAFE-CHAR DQUOTE
+
+   any-param  = (iana-token / x-name) "=" param-value *("," param-value)
+
+   NON-ASCII = UTF8-2 / UTF8-3 / UTF8-4
+     ; UTF8-{2,3,4} are defined in [RFC3629]
+
+   QSAFE-CHAR = WSP / "!" / %x23-7E / NON-ASCII
+     ; Any character except CTLs, DQUOTE
+
+   SAFE-CHAR = WSP / "!" / %x23-39 / %x3C-7E / NON-ASCII
+     ; Any character except CTLs, DQUOTE, ";", ":"
+
+   VALUE-CHAR = WSP / VCHAR / NON-ASCII
+     ; Any textual character
diff --git a/doc/rfc1766.txt b/doc/rfc1766.txt
new file mode 100644
index 0000000..901c50e
--- /dev/null
+++ b/doc/rfc1766.txt
@@ -0,0 +1,507 @@
+
+
+
+
+
+
+Network Working Group                                      H. Alvestrand
+Request for Comments: 1766                                       UNINETT
+Category: Standards Track                                     March 1995
+
+
+                Tags for the Identification of Languages
+
+Status of this Memo
+
+   This document specifies an Internet standards track protocol for the
+   Internet community, and requests discussion and suggestions for
+   improvements.  Please refer to the current edition of the "Internet
+   Official Protocol Standards" (STD 1) for the standardization state
+   and status of this protocol.  Distribution of this memo is unlimited.
+
+Abstract
+
+   This document describes a language tag for use in cases where it is
+   desired to indicate the language used in an information object.
+
+   It also defines a Content-language: header, for use in the case where
+   one desires to indicate the language of something that has RFC-822-
+   like headers, like MIME body parts or Web documents, and a new
+   parameter to the Multipart/Alternative type, to aid in the usage of
+   the Content-Language: header.
+
+1.  Introduction
+
+   There are a number of languages spoken by human beings in this world.
+
+   A great number of these people would prefer to have information
+   presented in a language that they understand.
+
+   In some contexts, it is possible to have information in more than one
+   language, or it might be possible to provide tools for assisting in
+   the understanding of a language (like dictionaries).
+
+   A prerequisite for any such function is a means of labelling the
+   information content with an identifier for the language in which is
+   is written.
+
+   In the tradition of solving only problems that we think we
+   understand, this document specifies an identifier mechanism, and one
+   possible use for it.
+
+
+
+
+
+
+
+Alvestrand                                                      [Page 1]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+2.  The Language tag
+
+   The language tag is composed of 1 or more parts: A primary language
+   tag and a (possibly empty) series of subtags.
+
+   The syntax of this tag in RFC-822 EBNF is:
+
+    Language-Tag = Primary-tag *( "-" Subtag )
+    Primary-tag = 1*8ALPHA
+    Subtag = 1*8ALPHA
+
+   Whitespace is not allowed within the tag.
+
+   All tags are to be treated as case insensitive; there exist
+   conventions for capitalization of some of them, but these should not
+   be taken to carry meaning.
+
+   The namespace of language tags is administered by the IANA according
+   to the rules in section 5 of this document.
+
+   The following registrations are predefined:
+
+   In the primary language tag:
+
+    -    All 2-letter tags are interpreted according to ISO standard
+         639, "Code for the representation of names of languages" [ISO
+         639].
+
+    -    The value "i" is reserved for IANA-defined registrations
+
+    -    The value "x" is reserved for private use. Subtags of "x"
+         will not be registered by the IANA.
+
+    -    Other values cannot be assigned except by updating this
+         standard.
+
+   The reason for reserving all other tags is to be open towards new
+   revisions of ISO 639; the use of "i" and "x" is the minimum we can do
+   here to be able to extend the mechanism to meet our requirements.
+
+   In the first subtag:
+
+    -    All 2-letter codes are interpreted as ISO 3166 alpha-2
+         country codes denoting the area in which the language is
+         used.
+
+    -    Codes of 3 to 8 letters may be registered with the IANA by
+         anyone who feels a need for it, according to the rules in
+
+
+
+Alvestrand                                                      [Page 2]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+         chapter 5 of this document.
+
+   The information in the subtag may for instance be:
+
+    -    Country identification, such as en-US (this usage is
+         described in ISO 639)
+
+    -    Dialect or variant information, such as no-nynorsk or en-
+         cockney
+
+    -    Languages not listed in ISO 639 that are not variants of
+         any listed language, which can be registered with the i-
+         prefix, such as i-cherokee
+
+    -    Script variations, such as az-arabic and az-cyrillic
+
+   In the second and subsequent subtag, any value can be registered.
+
+   NOTE: The ISO 639/ISO 3166 convention is that language names are
+   written in lower case, while country codes are written in upper case.
+   This convention is recommended, but not enforced; the tags are case
+   insensitive.
+
+   NOTE: ISO 639 defines a registration authority for additions to and
+   changes in the list of languages in ISO 639. This authority is:
+
+         International Information Centre for Terminology (Infoterm)
+         P.O. Box 130
+         A-1021 Wien
+         Austria
+         Phone: +43 1  26 75 35 Ext. 312
+         Fax:   +43 1 216 32 72
+
+   The following codes have been added in 1989 (nothing later): ug
+   (Uigur), iu (Inuktitut, also called Eskimo), za (Zhuang), he (Hebrew,
+   replacing iw), yi (Yiddish, replacing ji), and id (Indonesian,
+   replacing in).
+
+   NOTE: The registration agency for ISO 3166 (country codes) is:
+
+         ISO 3166 Maintenance Agency Secretariat
+         c/o DIN Deutches Institut fuer Normung
+         Burggrafenstrasse 6
+         Postfach 1107
+         D-10787 Berlin
+         Germany
+         Phone: +49 30 26 01 320
+         Fax:   +49 30 26 01 231
+
+
+
+Alvestrand                                                      [Page 3]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+   The country codes AA, QM-QZ, XA-XZ and ZZ are reserved by ISO 3166 as
+   user-assigned codes.
+
+2.1.  Meaning of the language tag
+
+   The language tag always defines a language as spoken (or written) by
+   human beings for communication of information to other human beings.
+   Computer languages are explicitly excluded.
+
+   There is no guaranteed relationship between languages whose tags
+   start out with the same series of subtags; especially, they are NOT
+   guraranteed to be mutually comprehensible, although this will
+   sometimes be the case.
+
+   Applications should always treat language tags as a single token; the
+   division into main tag and subtags is an administrative mechanism,
+   not a navigation aid.
+
+   The relationship between the tag and the information it relates to is
+   defined by the standard describing the context in which it appears.
+   So, this section can only give possible examples of its usage.
+
+    -    For a single information object, it should be taken as the
+         set of languages that is required for a complete
+         comprehension of the complete object. Example: Simple text.
+
+    -    For an aggregation of information objects, it should be taken
+         as the set of languages used inside components of that
+         aggregation.  Examples: Document stores and libraries.
+
+    -    For information objects whose purpose in life is providing
+         alternatives, it should be regarded as a hint that the
+         material inside is provided in several languages, and that
+         one has to inspect each of the alternatives in order to find
+         its language or languages.  In this case, multiple languages
+         need not mean that one needs to be multilingual to get
+         complete understanding of the document. Example: MIME
+         multipart/alternative.
+
+    -    It would be possible to define (for instance) an SGML DTD
+         that defines a <LANG xx> tag for indicating that following or
+         contained text is written in this language, such that one
+         could write "<LANG FR>C'est la vie</LANG>"; the Norwegian-
+         speaking user could then access a French-Norwegian dictionary
+         to find out what the quote meant.
+
+
+
+
+
+
+Alvestrand                                                      [Page 4]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+3.  The Content-language header
+
+   The Language header is intended for use in the case where one desires
+   to indicate the language(s) of something that has RFC-822-like
+   headers, like MIME body parts or Web documents.
+
+   The RFC-822 EBNF of the Language header is:
+
+    Language-Header = "Content-Language" ":" 1#Language-tag
+
+   Note that the Language-Header is allowed to list several languages in
+   a comma-separated list.
+
+   Whitespace is allowed, which means also that one can place
+   parenthesized comments anywhere in the language sequence.
+
+3.1.  Examples of Content-language values
+
+   NOTE: NONE of the subtags shown in this document have actually been
+   assigned; they are used for illustration purposes only.
+
+   Norwegian official document, with parallel text in both official
+   versions of Norwegian. (Both versions are readable by all
+   Norwegians).
+
+      Content-Type: multipart/alternative;
+             differences=content-language
+      Content-Language: no-nynorsk, no-bokmaal
+
+   Voice recording from the London docks
+
+      Content-type: audio/basic
+      Content-Language: en-cockney
+
+   Document in Sami, which does not have an ISO 639 code, and is spoken
+   in several countries, but with about half the speakers in Norway,
+   with six different, mutually incomprehensible dialects:
+
+      Content-type: text/plain; charset=iso-8859-10
+      Content-Language: i-sami-no (North Sami)
+
+   An English-French dictionary
+
+      Content-type: application/dictionary
+      Content-Language: en, fr (This is a dictionary)
+
+   An official EC document (in a few of its official languages)
+
+
+
+
+Alvestrand                                                      [Page 5]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+      Content-type: multipart/alternative
+      Content-Language: en, fr, de, da, el, it
+
+   An excerpt from Star Trek
+
+      Content-type: video/mpeg
+      Content-Language: x-klingon
+
+4.  Use of Content-Language with Multipart/Alternative
+
+   When using the Multipart/Alternative body part of MIME, it is
+   possible to have the body parts giving the same information content
+   in different languages. In this case, one should put a Content-
+   Language header on each of the body parts, and a summary Content-
+   Language header onto the Multipart/Alternative itself.
+
+4.1.  The differences parameter to multipart/alternative
+
+   As defined in RFC 1541, Multipart/Alternative only has one parameter:
+   boundary.
+
+   The common usage of Multipart/Alternative is to have more than one
+   format of the same message (f.ex. PostScript and ASCII).
+
+   The use of language tags to differentiate between different
+   alternatives will certainly not lead all MIME UAs to present the most
+   sensible body part as default.
+
+   Therefore, a new parameter is defined, to allow the configuration of
+   MIME readers to handle language differences in a sensible manner.
+
+    Name: Differences
+    Value: One or more of
+         Content-Type
+         Content-Language
+
+   Further values can be registered with IANA; it must be the name of a
+   header for which a definition exists in a published RFC.  If not
+   present, Differences=Content-Type is assumed.
+
+   The intent is that the MIME reader can look at these headers of the
+   message component to do an intelligent choice of what to present to
+   the user, based on knowledge about the user preferences and
+   capabilities.
+
+   (The intent of having registration with IANA of the fields used in
+   this context is to maintain a list of usages that a mail UA may
+   expect to see, not to reject usages.)
+
+
+
+Alvestrand                                                      [Page 6]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+   (NOTE: The MIME specification [RFC 1521], section 7.2, states that
+   headers not beginning with "Content-" are generally to be ignored in
+   body parts. People defining a header for use with "differences="
+   should take note of this.)
+
+   The mechanism for deciding which body part to present is outside the
+   scope of this document.
+
+    MIME EXAMPLE:
+
+    Content-Type: multipart/alternative; differences=Content-Language;
+              boundary="limit"
+    Content-Language: en, fr, de
+
+    --limit
+    Content-Language: fr
+
+    Le renard brun et agile saute par dessus le chien paresseux
+    --limit
+    Content-Language: de
+    Content-Type: text/plain; charset=iso-8859-1
+    Content-Transfer-encoding: quoted-printable
+
+    Der schnelle braune Fuchs h=FCpft =FCber den faulen Hund
+    --limit
+    Content-Language: en
+
+    The quick brown fox jumps over the lazy dog
+    --limit--
+
+   When composing a message, the choice of sequence may be somewhat
+   arbitrary. However, non-MIME mail readers will show the first body
+   part first, meaning that this should most likely be the language
+   understood by most of the recipients.
+
+5.  IANA registration procedure for language tags
+
+   Any language tag must start with an existing tag, and extend it.
+
+   This registration form should be used by anyone who wants to use a
+   language tag not defined by ISO or IANA.
+
+
+
+
+
+
+
+
+
+
+Alvestrand                                                      [Page 7]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+----------------------------------------------------------------------
+LANGUAGE TAG REGISTRATION FORM
+
+Name of requester          :
+E-mail address of requester:
+Tag to be registered       :
+
+English name of language   :
+
+Native name of language (transcribed into ASCII):
+
+Reference to published description of the language (book or article):
+----------------------------------------------------------------------
+
+   The language form must be sent to <ietf-types@uninett.no> for a 2-
+   week review period before submitting it to IANA.  (This is an open
+   list. Requests to be added should be sent to <ietf-types-
+   request@uninett.no>.)
+
+   When the two week period has passed, the language tag reviewer, who
+   is appointed by the IETF Applications Area Director, either forwards
+   the request to IANA@ISI.EDU, or rejects it because of significant
+   objections raised on the list.
+
+   Decisions made by the reviewer may be appealed to the IESG.
+
+   All registered forms are available online in the directory
+   ftp://ftp.isi.edu/in-notes/iana/assignments/languages/
+
+6.  Security Considerations
+
+   Security issues are not discussed in this memo.
+
+7.  Character set considerations
+
+   Codes may always be expressed using the US-ASCII character repertoire
+   (a-z), which is present in most character sets.
+
+   The issue of deciding upon the rendering of a character set based on
+   the language tag is not addressed in this memo; however, it is
+   thought impossible to make such a decision correctly for all cases
+   unless means of switching language in the middle of a text are
+   defined (for example, a rendering engine that decides font based on
+   Japanese or Chinese language will fail to work when a mixed
+   Japanese-Chinese text is encountered)
+
+
+
+
+
+
+Alvestrand                                                      [Page 8]
+
+RFC 1766                      Language Tag                    March 1995
+
+
+8.  Acknowledgements
+
+   This document has benefited from innumberable rounds of review and
+   comments in various fora of the IETF and the Internet working groups.
+   As so, any list of contributors is bound to be incomplete; please
+   regard the following as only a selection from the group of people who
+   have contributed to make this document what it is today.
+
+   In alphabetical order:
+
+   Tim Berners-Lee, Nathaniel Borenstein, Jim Conklin, Dave Crocker,
+   Ned Freed, Tim Goodwin, Olle Jarnefors, John Klensin, Keith Moore,
+   Masataka Ohta, Keld Jorn Simonsen, Rhys Weatherley, and many, many
+   others.
+
+9.  Author's Address
+
+   Harald Tveit Alvestrand
+   UNINETT
+   Pb. 6883 Elgeseter
+   N-7002 TRONDHEIM
+   NORWAY
+
+   EMail: Harald.T.Alvestrand@uninett.no
+   Phone: +47 73 59 70 94
+
+10.  References
+
+    [ISO 639]
+         ISO 639:1988 (E/F) - Code for the representation of names of
+         languages - The International Organization for
+         Standardization, 1st edition, 1988 17 pages Prepared by
+         ISO/TC 37 - Terminology (principles and coordination).
+
+    [ISO 3166]
+         ISO 3166:1988 (E/F) - Codes for the representation of names
+         of countries - The International Organization for
+         Standardization, 3rd edition, 1988-08-15.
+
+    [RFC 1521]
+         Borenstein, N., and N. Freed, "MIME Part One: Mechanisms for
+         Specifying and Describing the Format of Internet Message
+         Bodies", RFC 1521, Bellcore, Innosoft, September 1993.
+
+    [RFC 1327]
+         Kille, S., "Mapping between X.400(1988) / ISO 10021 and RFC
+         822", RFC 1327, University College London, May 1992.
+
+
+
+
+Alvestrand                                                      [Page 9]
+
diff --git a/doc/rfc2426-errata-to-submit.txt b/doc/rfc2426-errata-to-submit.txt
new file mode 100644
index 0000000..ba6e9b7
--- /dev/null
+++ b/doc/rfc2426-errata-to-submit.txt
@@ -0,0 +1,47 @@
+Lines 1863-1868:
+
+>   ;For name="REV"
+>   param        = ["VALUE" =" "date-time"]
+>        ; Only value parameters allowed. Values are case insensitive.
+>
+>   param        =/ "VALUE" =" "date"
+>        ; Only value parameters allowed. Values are case insensitive.
+
+"VALUE" =" should be "VALUE" "="
+
+----
+Lines
+
+According to section 3.4.1, the TZ type uses the utc-offset-value by default,
+but can be reset to use the text type (see example on lines 885-886).
+
+The ABNF in section 4 disallows this (lines 1766-1771):
+
+>   ;For name="TZ"
+>   param        = ""
+>        ; No parameters allowed
+>
+>   value        = utc-offset-value
+
+If the description and example in section 3.4.1 is intended behavior, this
+should probably read
+
+>   ;For name="TZ"
+>   param        = tz-utc-offset-param
+>
+>   param        =/ tz-text-param
+>
+>   value        = tz-utc-offset-value
+>       ; Value and parameter MUST match
+>
+>   value        =/ tz-text-value
+>       ; Value and parameter MUST match
+>
+>   tz-utc-offset-param = ""
+>       ; No parameters allowed
+>
+>   tz-text-param = "VALUE" "=" "text"
+>
+>   tz-utc-offset-value = utc-offset-value
+>
+>   tz-text-value = text-value
diff --git a/src/vcard.nim b/src/vcard.nim
deleted file mode 100644
index 4b2a270..0000000
--- a/src/vcard.nim
+++ /dev/null
@@ -1,7 +0,0 @@
-# This is just an example to get you started. A typical library package
-# exports the main API in this file. Note that you cannot rename this file
-# but you can remove it if you wish.
-
-proc add*(x, y: int): int =
-  ## Adds two files together.
-  return x + y
diff --git a/src/vcard/private/util.nim b/src/vcard/private/util.nim
new file mode 100644
index 0000000..363cb08
--- /dev/null
+++ b/src/vcard/private/util.nim
@@ -0,0 +1,20 @@
+import strutils
+
+func foldContentLine*(s: string): string =
+  result = ""
+  var rem = s
+  while rem.len > 75: # TODO: unicode multi-byte safe?
+    result &= rem[0..<75] & "\r\n "
+    rem = rem[75..^1]
+  result &= rem
+
+func unfoldContentLine*(s: string): string =
+  return s.multiReplace([("\r\n ", "")])
+
+template indexOfIt*(s, pred: untyped): int =
+  var result = -1
+  for idx, it {.inject.} in pairs(s):
+    if pred:
+      result = idx
+      break
+  result
diff --git a/src/vcard3.nim b/src/vcard3.nim
new file mode 100644
index 0000000..c28b918
--- /dev/null
+++ b/src/vcard3.nim
@@ -0,0 +1,1248 @@
+# vCard 3.0 and 4.0 Nm implementation
+# © 2022 Jonathan Bernard
+
+## The `vcard` module implements a high-performance vCard parser for both
+## versions 3.0 (defined by RFCs [2425][rfc2425] and  [2426][rfc2426]) and 4.0
+## (defined by RFC [6350][rfc6350])
+##
+## [rfc2425]: https://tools.ietf.org/html/rfc2425
+## [rfc2426]: https://tools.ietf.org/html/rfc2426
+## [rfc6350]: https://tools.ietf.org/html/rfc6350
+
+import std/base64, std/lexbase, std/macros, std/options, std/sequtils,
+  std/streams, std/strutils, std/times
+
+import vcard/private/util
+
+type
+#[
+  TokKind = enum
+    tkInvalid,
+    tkEof,
+    tkQuotedString,
+    tkValue,
+    tkPtext,
+    tkColon,
+    tkComma,
+    tk
+]#
+
+#[
+  VC3_Content*[T] = tuple[
+    name: string,
+    group: Option[string],
+    value: T]
+]#
+
+  VC3_ValueTypes = enum
+    vtUri = "uri",
+    vtText = "text",
+    vtDate = "date",
+    vtTime = "time",
+    vtDateTime = "date-time",
+    vtInteger = "integer",
+    vtBoolean = "boolean",
+    vtFloat = "float",
+    vtBinary = "binary",
+    vtVCard = "vcard"
+    vtPhoneNumber = "phone-number"
+    vtUtcOffset = "utc-offset"
+
+  VC3_XParam* = tuple[name, value: string]
+
+  VC3_Content* = ref object of RootObj
+    contentId: int
+    group*: Option[string]
+    name*: string
+
+  VC3_ContentList* = openarray[VC3_Content]
+
+  VC3_SimpleTextContent* = ref object of VC3_Content
+    value*: string
+    isPText*: bool # true if VALUE=ptext, false by default
+    language*: Option[string]
+    xParams: seq[VC3_XParam]
+
+  VC3_BinaryContent* = ref object of VC3_Content
+    valueType*: Option[string] # binary / uri. Stored separately from ENCODING
+                               # (captured in the isInline field) because the
+                               # VALUE parameter is not set by default, but is
+                               # allowed to be set.
+    value*: string  # either a URI or bit sequence, both stored as string
+    binaryType*: Option[string]
+    isInline*: bool # true if ENCODING=b, false by default
+
+  VC3_Name* = ref object of VC3_Content
+    value*: string
+
+  VC3_Profile* = ref object of VC3_Content
+
+  VC3_Source* = ref object of VC3_Content
+    valueType*: Option[string]  # uri
+    value*: string # URI
+    context*: Option[string]
+    xParams*: seq[VC3_XParam]
+
+  VC3_Fn* = ref object of VC3_SimpleTextContent
+
+  VC3_N* = ref object of VC3_Content
+    family*: seq[string]
+    given*: seq[string]
+    additional*: seq[string]
+    prefixes*: seq[string]
+    suffixes*: seq[string]
+    language*: Option[string]
+    isPText*: bool # true if VALUE=ptext, false by default
+    xParams*: seq[VC3_XParam]
+
+  VC3_Nickname* = ref object of VC3_SimpleTextContent
+
+  VC3_Photo* = ref object of VC3_BinaryContent
+
+  VC3_Bday* = ref object of VC3_Content
+    valueType*: Option[string] # date / date-time
+    value*: DateTime
+
+  VC3_AdrTypes* = enum
+    # Standard types defined in RFC2426
+    atDom = "dom"
+    atIntl = "intl"
+    atPostal = "postal"
+    atParcel = "parcel"
+    atHome = "home"
+    atWork = "work"
+    atPref = "pref"
+
+  VC3_Adr* = ref object of VC3_Content
+    adrType*: seq[string]
+    poBox*: string
+    extendedAdr*: string
+    streetAdr*: string
+    locality*: string
+    region*: string
+    postalCode*: string
+    country*: string
+    isPText*: bool # true if VALUE=ptext, false by default
+    language*: Option[string]
+    xParams*: seq[VC3_XParam]
+
+  VC3_Label* = ref object of VC3_SimpleTextContent
+    adrType*: seq[string]
+
+  VC3_TelTypes* = enum
+    ttHome = "home",
+    ttWork = "work",
+    ttPref = "pref",
+    ttVoice = "voice",
+    ttFax = "fax",
+    ttMsg = "msg",
+    ttCell = "cell",
+    ttPager = "pager",
+    ttBbs = "bbs",
+    ttModem = "modem",
+    ttCar = "car",
+    ttIsdn = "isdn",
+    ttVideo = "video",
+    ttPcs = "pcs"
+
+  VC3_Tel* = ref object of VC3_Content
+    telType*: seq[string]
+    value*: string
+
+  VC3_EmailType* = enum
+    etInternet = "internet",
+    etX400 = "x400"
+
+  VC3_Email* = ref object of VC3_Content
+    emailType*: seq[string]
+    value*: string
+
+  VC3_Mailer* = ref object of VC3_SimpleTextContent
+
+  VC3_TZ* = ref object of VC3_Content
+    value*: string
+    isText*: bool # true if VALUE=text, false by default
+
+  VC3_Geo* = ref object of VC3_Content
+    lat*, long*: float
+
+  VC3_Title* = ref object of VC3_SimpleTextContent
+
+  VC3_Role* = ref object of VC3_SimpleTextContent
+
+  VC3_Logo* = ref object of VC3_BinaryContent
+
+  VC3_Agent* = ref object of VC3_Content
+    value*: string  # either an escaped vCard object, or a URI
+    isInline*: bool # false if VALUE=uri, true by default
+
+  VC3_Org* = ref object of VC3_Content
+    value*: seq[string]
+    isPText*: bool # true if VALUE=ptext, false by default
+    language*: Option[string]
+    xParams*: seq[VC3_XParam]
+
+  VC3_Categories* = ref object of VC3_Content
+    value*: seq[string]
+    isPText*: bool # true if VALUE=ptext, false by default
+    language*: Option[string]
+    xParams*: seq[VC3_XParam]
+
+  VC3_Note* = ref object of VC3_SimpleTextContent
+
+  VC3_Prodid* = ref object of VC3_SimpleTextContent
+
+  VC3_Rev* = ref object of VC3_Content
+    valueType*: Option[string] # date / date-time
+    value*: DateTime
+
+  VC3_SortString* = ref object of VC3_SimpleTextContent
+
+  VC3_Sound* = ref object of VC3_BinaryContent
+
+  VC3_UID* = ref object of VC3_Content
+    value*: string
+
+  VC3_URL* = ref object of VC3_Content
+    value*: string
+
+  VC3_Version* = ref object of VC3_Content
+    value*: string # 3.0
+
+  VC3_Class* = ref object of VC3_Content
+    value*: string
+
+  VC3_Key* = ref object of VC3_BinaryContent
+    keyType*: Option[string] # x509 / pgp
+
+  VC3_XType* = ref object of VC3_SimpleTextContent
+
+  VCard3* = object
+    nextContentId: int
+    content*: seq[VC3_Content]
+
+const DATE_FMT = "yyyy-MM-dd"
+const DATETIME_FMT = "yyyy-MM-dd'T'HH:mm:sszz"
+
+# Internal Utility/Implementation
+# =============================================================================
+
+template findAll[T](c: VC3_ContentList): seq[T] =
+  c.filterIt(it of typeof(T)).mapIt(cast[T](it))
+
+template findFirst[T](c: VC3_ContentList): Option[T] =
+  let found = c.filterIt(it of typeof(T)).mapIt(cast[T](it))
+  if found.len > 0: some(found[0])
+  else: none[T]()
+
+template takeContentId(vc3: var VCard3): int =
+  vc3.nextContentId += 1
+  vc3.nextContentId - 1
+
+macro assignFields(assign: untyped, fields: varargs[untyped]): untyped =
+  result = assign
+
+  for f in fields:
+    let exp = newNimNode(nnkExprColonExpr)
+    exp.add(f)
+    exp.add(f)
+    result.add(exp)
+
+
+# Initializers
+# =============================================================================
+
+func clone(vc3: VCard3): VCard3 =
+  result = VCard3(
+    nextContentId: vc3.nextContentId,
+    content: vc3.content)
+
+func newVC3_Name*(value: string, group = none[string]()): VC3_Name =
+  return VC3_Name(name: "NAME", value: value, group: group)
+
+func newVC3_Source*(
+  value: string,
+  context = none[string](),
+  inclValue = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Source =
+
+  return assignFields(
+    VC3_Source(
+      name: "SOURCE",
+      valueType: if inclValue: some("uri")
+                 else: none[string]()),
+    value, context, group, xParams)
+
+func newVC3_Fn*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Fn =
+
+  return assignFields(
+    VC3_Fn(name: "FN"),
+    value, language, isPText, group, xParams)
+
+func newVC3_N*(
+  family: seq[string] = @[],
+  given: seq[string] = @[],
+  additional: seq[string] = @[],
+  prefixes: seq[string] = @[],
+  suffixes: seq[string] = @[],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_N =
+
+  return assignFields(
+    VC3_N(name: "N"),
+    family, given, additional, prefixes, suffixes, language, xParams)
+
+func newVC3_Nickname*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Nickname =
+
+  return assignFields(
+    VC3_Nickname(name: "NICKNAME"),
+    value, language, isPText, group, xParams)
+
+func newVC3_Photo*(
+  value: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VC3_Photo =
+
+  return assignFields(
+    VC3_Photo(name: "PHOTO"),
+    value, valueType, binaryType, isInline, group)
+
+func newVC3_Bday*(
+  value: DateTime,
+  valueType = none[string](),
+  group = none[string]()): VC3_Bday =
+
+    return assignFields(VC3_Bday(name: "BDAY"), value, valueType, group)
+
+func newVC3_Adr*(
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  poBox = "",
+  extendedAdr = "",
+  streetAdr = "",
+  locality = "",
+  region = "",
+  postalCode = "",
+  country = "",
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[]): VC3_Adr =
+
+  return assignFields(
+    VC3_Adr(name: "ADR"),
+    adrType, poBox, extendedAdr, streetAdr, locality, region, postalCode,
+    country, isPText, language, xParams)
+
+func newVC3_Label*(
+  value: string,
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Label =
+
+  return assignFields(
+    VC3_Label(name: "LABEL"),
+    value, adrType, language, isPText, group, xParams)
+
+func newVC3_Tel*(
+  value: string,
+  telType = @[$ttVoice],
+  group = none[string]()): VC3_Tel =
+
+  return VC3_Tel(name: "TEL", telType: telType, group: group)
+
+func newVC3_Email*(
+  value: string,
+  emailType = @[$etInternet],
+  group = none[string]()): VC3_Email =
+
+  return VC3_Email(name: "EMAIL", emailType: emailType, group: group)
+
+func newVC3_Mailer*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Mailer =
+
+  return assignFields(
+    VC3_Mailer(name: "MAILER"),
+    value, language, isPText, xParams, group)
+
+func newVC3_TZ*(value: string, isText = false, group = none[string]()): VC3_TZ =
+  return assignFields(VC3_TZ(name: "TZ"), value, isText, group)
+
+func newVC3_Geo*(lat, long: float, group = none[string]()): VC3_Geo =
+  return assignFields(VC3_Geo(name: "GEO"), lat, long, group)
+
+func newVC3_Title*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Title =
+
+  return assignFields(
+    VC3_Title(name: "TITLE"),
+    value, language, isPText, xParams, group)
+
+func newVC3_Role*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Role =
+
+  return assignFields(
+    VC3_Role(name: "ROLE"),
+    value, language, isPText, xParams, group)
+
+func newVC3_Logo*(
+  value: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VC3_Logo =
+
+  return assignFields(
+    VC3_Logo(name: "LOGO"),
+    value, valueType, binaryType, isInline, group)
+
+func newVC3_Agent*(
+  value: string,
+  isInline = true,
+  group = none[string]()): VC3_Agent =
+
+  return VC3_Agent(name: "AGENT", isInline: isInline, group: group)
+
+func newVC3_Org*(
+  value: seq[string],
+  isPText = false,
+  language = none[string](),
+  xParams: seq[VC3_XParam] = @[]): VC3_Org =
+
+  return assignFields(
+    VC3_Org(name: "ORG"),
+    value, isPText, language, xParams)
+
+func newVC3_Categories*(
+  value: seq[string],
+  isPText = false,
+  language = none[string](),
+  xParams: seq[VC3_XParam] = @[]): VC3_Categories =
+
+  return assignFields(
+    VC3_Categories(name: "CATEGORIES"),
+    value, isPText, language, xParams)
+
+func newVC3_Note*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Note =
+
+  return assignFields(
+    VC3_Note(name: "NOTE"),
+    value, language, isPText, xParams, group)
+
+func newVC3_Prodid*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_Prodid =
+
+  return assignFields(
+    VC3_Prodid(name: "PRODID"),
+    value, language, isPText, xParams, group)
+
+func newVC3_Rev*(
+  value: DateTime,
+  valueType = none[string](),
+  group = none[string]()): VC3_Rev =
+
+  return assignFields(VC3_Rev(name: "REV"), value, valueType, group)
+
+func newVC3_SortString*(
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_SortString =
+
+  return assignFields(
+    VC3_SortString(name: "SORTSTRING"),
+    value, language, isPText, xParams, group)
+
+func newVC3_Sound*(
+  value: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VC3_Sound =
+
+  return assignFields(
+    VC3_Sound(name: "SOUND"),
+    value, valueType, binaryType, isInline, group)
+
+func newVC3_UID*(value: string, group = none[string]()): VC3_UID =
+  return VC3_UID(name: "UID", value: value, group: group)
+
+func newVC3_URL*(value: string, group = none[string]()): VC3_Url =
+  return VC3_Url(name: "URL", value: value, group: group)
+
+func newVC3_Version*(group = none[string]()): VC3_Version =
+  return VC3_Version(name: "VERSION", value: "3.0", group: group)
+
+func newVC3_Class*(value: string, group = none[string]()): VC3_Class =
+  return VC3_Class(name: "CLASS", value: value, group: group)
+
+func newVC3_Key*(
+  value: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VC3_Key =
+
+  return assignFields(
+    VC3_Key(name: "KEY"),
+    value, valueType, binaryType, isInline, group)
+
+func newVC3_XType*(
+  name: string,
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VC3_XType =
+
+  if not name.toLower.startsWith("x-"):
+    raise newException(ValueError, "Extended types must begin with 'x-'.")
+
+  return assignFields(
+    VC3_XType(name: name),
+    value, language, isPText, xParams, group)
+
+# Accessors
+# =============================================================================
+
+func forGroup*(vc: VC3_ContentList, group: string): seq[VC3_Content] =
+  return vc.filterIt(it.group.isSome and it.group.get == group)
+
+func groups*(vc: VC3_ContentList): seq[string] =
+  result = @[]
+  for c in vc:
+    if c.group.isSome:
+      let grp = c.group.get
+      if not result.contains(grp): result.add(grp)
+
+func name*(c: VC3_ContentList): Option[VC3_Name] = findFirst[VC3_Name](c)
+func name*(vc3: VCard3): Option[VC3_Name] = vc3.content.name
+
+func profile*(c: VC3_ContentList): Option[VC3_Profile] =
+  findFirst[VC3_Profile](c)
+func profile*(vc3: VCard3): Option[VC3_Profile] = vc3.content.profile
+
+func source*(c: VC3_ContentList): seq[VC3_Source] = findAll[VC3_Source](c)
+func source*(vc3: VCard3): seq[VC3_Source] = vc3.content.source
+
+func fn*(c: VC3_ContentList): seq[VC3_Fn] = findAll[VC3_Fn](c)
+func fn*(vc3: VCard3): seq[VC3_Fn] = vc3.content.fn
+
+func n*(c: VC3_ContentList): seq[VC3_N] = findAll[VC3_N](c)
+func n*(vc3: VCard3): seq[VC3_N] = vc3.content.n
+
+func nickname*(c: VC3_ContentList): seq[VC3_Nickname] = findAll[VC3_Nickname](c)
+func nickname*(vc3: VCard3): seq[VC3_Nickname] = vc3.content.nickname
+
+func photo*(c: VC3_ContentList): seq[VC3_Photo] = findAll[VC3_Photo](c)
+func photo*(vc3: VCard3): seq[VC3_Photo] = vc3.content.photo
+
+func bday*(c: VC3_ContentList): Option[VC3_Bday] = findFirst[VC3_Bday](c)
+func bday*(vc3: VCard3): Option[VC3_Bday] = vc3.content.bday
+
+func adr*(c: VC3_ContentList): seq[VC3_Adr] = findAll[VC3_Adr](c)
+func adr*(vc3: VCard3): seq[VC3_Adr] = vc3.content.adr
+
+func label*(c: VC3_ContentList): seq[VC3_Label] = findAll[VC3_Label](c)
+func label*(vc3: VCard3): seq[VC3_Label] = vc3.content.label
+
+func tel*(c: VC3_ContentList): seq[VC3_Tel] = findAll[VC3_Tel](c)
+func tel*(vc3: VCard3): seq[VC3_Tel] = vc3.content.tel
+
+func email*(c: VC3_ContentList): seq[VC3_Email] = findAll[VC3_Email](c)
+func email*(vc3: VCard3): seq[VC3_Email] = vc3.content.email
+
+func mailer*(c: VC3_ContentList): Option[VC3_Mailer] = findFirst[VC3_Mailer](c)
+func mailer*(vc3: VCard3): Option[VC3_Mailer] = vc3.content.mailer
+
+func tz*(c: VC3_ContentList): Option[VC3_Tz] = findFirst[VC3_Tz](c)
+func tz*(vc3: VCard3): Option[VC3_Tz] = vc3.content.tz
+
+func geo*(c: VC3_ContentList): Option[VC3_Geo] = findFirst[VC3_Geo](c)
+func geo*(vc3: VCard3): Option[VC3_Geo] = vc3.content.geo
+
+func title*(c: VC3_ContentList): seq[VC3_Title] = findAll[VC3_Title](c)
+func title*(vc3: VCard3): seq[VC3_Title] = vc3.content.title
+
+func role*(c: VC3_ContentList): seq[VC3_Role] = findAll[VC3_Role](c)
+func role*(vc3: VCard3): seq[VC3_Role] = vc3.content.role
+
+func logo*(c: VC3_ContentList): seq[VC3_Logo] = findAll[VC3_Logo](c)
+func logo*(vc3: VCard3): seq[VC3_Logo] = vc3.content.logo
+
+func agent*(c: VC3_ContentList): Option[VC3_Agent] = findFirst[VC3_Agent](c)
+func agent*(vc3: VCard3): Option[VC3_Agent] = vc3.content.agent
+
+func org*(c: VC3_ContentList): Option[VC3_Org] = findFirst[VC3_Org](c)
+func org*(vc3: VCard3): Option[VC3_Org] = vc3.content.org
+
+func categories*(c: VC3_ContentList): Option[VC3_Categories] =
+  findFirst[VC3_Categories](c)
+func categories*(vc3: VCard3): Option[VC3_Categories] = vc3.content.categories
+
+func note*(c: VC3_ContentList): Option[VC3_Note] = findFirst[VC3_Note](c)
+func note*(vc3: VCard3): Option[VC3_Note] = vc3.content.note
+
+func prodid*(c: VC3_ContentList): Option[VC3_Prodid] = findFirst[VC3_Prodid](c)
+func prodid*(vc3: VCard3): Option[VC3_Prodid] = vc3.content.prodid
+
+func rev*(c: VC3_ContentList): Option[VC3_Rev] = findFirst[VC3_Rev](c)
+func rev*(vc3: VCard3): Option[VC3_Rev] = vc3.content.rev
+
+func sortstring*(c: VC3_ContentList): Option[VC3_SortString] =
+  findFirst[VC3_SortString](c)
+func sortstring*(vc3: VCard3): Option[VC3_SortString] = vc3.content.sortstring
+
+func sound*(c: VC3_ContentList): seq[VC3_Sound] = findAll[VC3_Sound](c)
+func sound*(vc3: VCard3): seq[VC3_Sound] = vc3.content.sound
+
+func uid*(c: VC3_ContentList): Option[VC3_Uid] = findFirst[VC3_Uid](c)
+func uid*(vc3: VCard3): Option[VC3_Uid] = vc3.content.uid
+
+func url*(c: VC3_ContentList): Option[VC3_Url] = findFirst[VC3_Url](c)
+func url*(vc3: VCard3): Option[VC3_Url] = vc3.content.url
+
+func version*(c: VC3_ContentList): VC3_Version =
+  let found = findFirst[VC3_Version](c)
+  if found.isSome: return found.get
+  else: return VC3_Version(
+    contentId: c.len + 1,
+    group: none[string](),
+    name: "VERSION",
+    value: "3.0")
+func version*(vc3: VCard3): VC3_Version = vc3.content.version
+
+func class*(c: VC3_ContentList): Option[VC3_Class] = findFirst[VC3_Class](c)
+func class*(vc3: VCard3): Option[VC3_Class] = vc3.content.class
+
+func key*(c: VC3_ContentList): seq[VC3_Key] = findAll[VC3_Key](c)
+func key*(vc3: VCard3): seq[VC3_Key] = vc3.content.key
+
+func xTypes*(c: VC3_ContentList): seq[VC3_XType] = findAll[VC3_XType](c)
+func xTypes*(vc3: VCard3): seq[VC3_XType] = vc3.content.xTypes
+
+# Setters
+# =============================================================================
+
+func setContent[T](vc3: var VCard3, newContent: var T): void =
+  let existingIdx = vc3.content.indexOfIt(it of T)
+  if existingIdx < 0:
+    newContent.contentId = vc3.takeContentId
+    vc3.content.add(newContent)
+  else:
+    newContent.contentId = vc3.content[existingIdx].contentId
+    vc3.content[existingIdx] = newContent
+
+func setContent[T](vc3: VCard3, newContent: var T): VCard3 =
+  result = vc3
+  result.setContent(newContent)
+
+func add[T](vc3: var VCard3, newContent: var T): void =
+  newContent.contentId = vc3.takeContentId
+  vc3.content.add(newContent)
+
+func add[T](vc3: VCard3, newContent: var T): VCard3 =
+  result = vc3
+  result.add(newContent)
+
+func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
+  for c in content:
+    let existingIdx = vc3.content.indexOfIt(it.contentId == c.contentId)
+    if existingIdx < 0: vc3.content.add(c)
+    else: c.content[existingIdx] = c
+
+func setName*(vc3: var VCard3, name: string, group = none[string]()): void =
+  var name = newVC3_Name(name, group)
+  vc3.setContent(name)
+
+func setName*(vc3: VCard3, name: string, group = none[string]()): VCard3 =
+  result = vc3
+  result.setName(name, group)
+
+func addSource*(
+  vc3: var VCard3,
+  source: string,
+  context = none[string](),
+  setValue = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Source(source, context, setValue, xParams, group)
+  vc3.add(c)
+
+func addSource*(
+  vc3: VCard3,
+  source: string,
+  context = none[string](),
+  setValue = false,
+  xParams: seq[VC3_XParam] = @[]): VCard3 =
+
+  result = vc3
+  result.addSource(source, context, setValue, xParams)
+
+func setFn*(
+  vc3: var VCard3,
+  fn: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_FN(fn, language, isPText, xParams, group)
+  vc3.setContent(c)
+
+func setFn*(
+  vc3: VCard3,
+  fn: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setFn(fn, language, isPText, xParams, group)
+
+
+func addFn*(
+  vc3: var VCard3,
+  fn: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_FN(fn, language, isPText, xParams, group)
+  vc3.add(c)
+
+func addFn*(
+  vc3: VCard3,
+  fn: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addFn(fn, language, isPText, xParams, group)
+
+func setN*(
+  vc3: var VCard3,
+  family: seq[string] = @[],
+  given: seq[string] = @[],
+  additional: seq[string] = @[],
+  prefixes: seq[string] = @[],
+  suffixes: seq[string] = @[],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_N(family, given, additional, prefixes, suffixes, language,
+    isPText, xParams, group)
+  vc3.setContent(c)
+
+func setN*(
+  vc3: VCard3,
+  family: seq[string] = @[],
+  given: seq[string] = @[],
+  additional: seq[string] = @[],
+  prefixes: seq[string] = @[],
+  suffixes: seq[string] = @[],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setN(family, given, additional, prefixes, suffixes, language, isPText,
+    xParams, group)
+
+func addNickname*(
+  vc3: var VCard3,
+  nickname: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Nickname(nickname, language, isPText, xParams, group)
+  vc3.add(c)
+
+func addNickname*(
+  vc3: VCard3,
+  nickname: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addNickname(nickname, language, isPText, xParams, group)
+
+func addPhoto*(
+  vc3: var VCard3,
+  photo: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): void =
+
+  var c = newVC3_Photo(photo, valueType, binaryType, isInline, group)
+  vc3.add(c)
+
+func addPhoto*(
+  vc3: VCard3,
+  photo: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addPhoto(photo, valueType, binaryType, isInline, group)
+
+func setBday*(
+  vc3: var VCard3,
+  bday: DateTime,
+  valueType = none[string](),
+  group = none[string]()): void =
+
+  var c = newVC3_Bday(bday, valueType, group)
+  vc3.setContent(c)
+
+func setBday*(
+  vc3: VCard3,
+  bday: DateTime,
+  valueType = none[string](),
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setBday(bday, valueType, group)
+
+func addAdr*(
+  vc3: var VCard3,
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  poBox = "",
+  extendedAdr = "",
+  streetAdr = "",
+  locality = "",
+  region = "",
+  postalCode = "",
+  country = "",
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[]): void =
+
+  var c = newVC3_Adr(adrType, poBox, extendedAdr, streetAdr, locality, region,
+    postalCode, country, language, isPText, xParams)
+  vc3.add(c)
+
+func addAdr*(
+  vc3: VCard3,
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  poBox = "",
+  extendedAdr = "",
+  streetAdr = "",
+  locality = "",
+  region = "",
+  postalCode = "",
+  country = "",
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[]): VCard3 =
+
+  result = vc3
+  result.addAdr(adrType, poBox, extendedAdr, streetAdr, locality, region,
+    postalCode, country, language, isPText, xParams)
+
+func addLabel*(
+  vc3: var VCard3,
+  label: string,
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Label(label, adrType, language, isPText, xParams, group)
+  vc3.add(c)
+
+func addLabel*(
+  vc3: VCard3,
+  label: string,
+  adrType = @[$atIntl,$atPostal,$atParcel,$atWork],
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addLabel(label, adrType, language, isPText, xParams, group)
+
+func addTel*(
+  vc3: var VCard3,
+  tel: string,
+  telType = @[$ttVoice],
+  group = none[string]()): void =
+
+  var c = newVC3_Tel(tel, telType, group)
+  vc3.add(c)
+
+func addTel*(
+  vc3: VCard3,
+  tel: string,
+  telType = @[$ttVoice],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addTel(tel, telType, group)
+
+func addEmail*(
+  vc3: var VCard3,
+  email: string,
+  emailType = @[$etInternet],
+  group = none[string]()): void =
+
+  var c = newVC3_Email(email, emailType, group)
+  vc3.add(c)
+
+func addEmail*(
+  vc3: VCard3,
+  email: string,
+  emailType = @[$etInternet],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addEmail(email, emailType, group)
+
+func setMailer*(
+  vc3: var VCard3,
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Mailer(value, language, isPText, xParams, group)
+  vc3.setContent(c)
+
+func setMailer*(
+  vc3: VCard3,
+  value: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setMailer(value, language, isPText, xParams, group)
+
+func setTZ*(
+  vc3: var VCard3,
+  value: string,
+  isText = false,
+  group = none[string]()): void =
+
+  var c = newVC3_TZ(value, isText, group)
+  vc3.setContent(c)
+
+func setTZ*(
+  vc3: VCard3,
+  value: string,
+  isText = false,
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setTZ(value, isText, group)
+
+func setGeo*(
+  vc3: var VCard3,
+  lat, long: float,
+  group = none[string]()): void =
+
+  var c = newVC3_Geo(lat, long, group)
+  vc3.setContent(c)
+
+func setGeo*(
+  vc3: VCard3,
+  lat, long: float,
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.setGeo(lat, long, group)
+
+func addTitle*(
+  vc3: var VCard3,
+  title: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Title(title, language, isPText, xParams, group)
+  vc3.add(c)
+
+func addTitle*(
+  vc3: VCard3,
+  title: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addTitle(title, language, isPText, xParams, group)
+
+func addRole*(
+  vc3: var VCard3,
+  role: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): void =
+
+  var c = newVC3_Role(role, language, isPText, xParams, group)
+  vc3.add(c)
+
+func addRole*(
+  vc3: VCard3,
+  role: string,
+  language = none[string](),
+  isPText = false,
+  xParams: seq[VC3_XParam] = @[],
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addRole(role, language, isPText, xParams, group)
+
+func addLogo*(
+  vc3: var VCard3,
+  logo: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): void =
+
+  var c = newVC3_Logo(logo, valueType, binaryType, isInline, group)
+  vc3.add(c)
+
+func addLogo*(
+  vc3: VCard3,
+  logo: string,
+  valueType = some("uri"),
+  binaryType = none[string](),
+  isInline = false,
+  group = none[string]()): VCard3 =
+
+  result = vc3
+  result.addLogo(logo, valueType, binaryType, isInline, group)
+
+func setAgent
+#[
+# TODO
+agent
+org
+categories
+note
+prodid
+rev
+sortstring
+sound
+uid
+url
+version
+class
+key
+]#
+
+# Output
+# =============================================================================
+
+func nameWithGroup(s: VC3_Content): string =
+  if s.group.isSome: s.group.get & "." & s.name
+  else: s.name
+
+func serialize(s: seq[VC3_XParam]): string =
+  result = ""
+  for x in s: result &= ";" & x.name & "=" & x.value
+
+func serialize(s: VC3_Source): string =
+  result = s.nameWithGroup
+  if s.valueType.isSome: result &= ";VALUE=" & s.valueType.get
+  if s.context.isSome: result &= ";CONTEXT=" & s.context.get
+  result &= serialize(s.xParams)
+  result &= ":" & s.value
+
+func serialize(n: VC3_N): string =
+  result = n.nameWithGroup
+  if n.isPText: result &= ";VALUE=ptext"
+  if n.language.isSome: result &= ";LANGUAGE=" & n.language.get
+  result &= serialize(n.xParams)
+  result &= ":" &
+    n.family.join(",") & ";" &
+    n.given.join(",") & ";" &
+    n.additional.join(",") & ";" &
+    n.prefixes.join(",") & ";" &
+    n.suffixes.join(",")
+
+func serialize(b: VC3_Bday): string =
+  result = b.nameWithGroup
+  if b.valueType.isSome and b.valueType.get == "date-time":
+    result &= ";VALUE=date-time:" & b.value.format(DATETIME_FMT)
+  else:
+    result &= ";VALUE=date:" & b.value.format(DATE_FMT)
+
+func serialize(a: VC3_Adr): string =
+  result = a.nameWithGroup
+  if a.adrType.len > 0: result &= ";TYPE=" & a.adrType.join(",")
+  if a.isPText: result &= ";VALUE=ptext"
+  if a.language.isSome: result &= ";LANGUAGE=" & a.language.get
+  result &= serialize(a.xParams)
+  result &= ":" &
+    a.poBox & ";" &
+    a.extendedAdr & ";" &
+    a.streetAdr & ";" &
+    a.locality & ";" &
+    a.region & ";" &
+    a.postalCode & ";" &
+    a.country
+
+proc serialize(t: VC3_Tel): string =
+  result = t.nameWithGroup
+  if t.telType.len > 0: result &= ";TYPE=" & t.telType.join(",")
+  result &= ":" & t.value
+
+proc serialize(t: VC3_Email): string =
+  result = t.nameWithGroup
+  if t.emailType.len > 0: result &= ";TYPE=" & t.emailType.join(",")
+  result &= ":" & t.value
+
+func serialize(s: VC3_SimpleTextContent): string =
+  result = s.nameWithGroup
+  if s.isPText: result &= ";VALUE=ptext"
+  if s.language.isSome: result &= ";LANGUAGE=" & s.language.get
+  result &= serialize(s.xParams)
+  result &= ":" & s.value
+
+proc serialize(b: VC3_BinaryContent): string =
+  result = b.nameWithGroup
+  if b.valueType.isSome: result &= ";VALUE=" & b.valueType.get
+  if b.isInline: result &= ";ENCODING=b"
+  if b.binaryType.isSome: result &= ";TYPE=" & b.binaryType.get
+  result &= ":"
+  if b.isInline: result &= base64.encode(b.value)
+  else: result &= b.value
+
+proc serialize(z: VC3_TZ): string =
+  result = z.nameWithGroup
+  if z.isText: result &= ";VALUE=text"
+  result &= ":" & z.value
+
+proc serialize(g: VC3_Geo): string =
+  result = g.nameWithGroup & ":" & $g.lat & ";" & $g.long
+
+proc serialize(a: VC3_Agent): string =
+  result = a.nameWithGroup
+  if not a.isInline: result &= ";VALUE=uri"
+  result &= ":" & a.value
+
+proc serialize(o: VC3_Org): string =
+  result = o.nameWithGroup
+  if o.isPText: result &= ";VALUE=ptext"
+  if o.language.isSome: result &= ";LANGUAGE=" & o.language.get
+  result &= serialize(o.xParams)
+  result &= ":" & o.value.join(",")
+
+proc serialize(c: VC3_Categories): string =
+  result = c.nameWithGroup
+  if c.isPText: result &= ";VALUE=ptext"
+  if c.language.isSome: result &= ";LANGUAGE=" & c.language.get
+  result &= serialize(c.xParams)
+  result &= ":" & c.value.join(",")
+
+proc serialize(r: VC3_Rev): string =
+  result = r.nameWithGroup
+  if r.valueType.isSome and r.valueType.get == "date-time":
+    result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT)
+  else:
+    result &= ";VALUE=date:" & r.value.format(DATE_FMT)
+
+proc serialize(u: VC3_UID): string =
+  result = u.nameWithGroup & ":" & u.value
+
+proc serialize(u: VC3_URL): string =
+  result = u.nameWithGroup & ":" & u.value
+
+proc serialize(u: VC3_Version): string =
+  result = u.nameWithGroup & ":" & u.value
+
+proc serialize(u: VC3_Class): string =
+  result = u.nameWithGroup & ":" & u.value
+
+proc serialize(c: VC3_Content): string =
+  if c of VC3_Name: return c.nameWithGroup & ":" & cast[VC3_Name](c).value
+  elif c of VC3_Profile: return c.nameWithGroup & ":VCARD"
+  elif c of VC3_Source: return serialize(cast[VC3_Source](c))
+  elif c of VC3_N: return serialize(cast[VC3_N](c))
+  elif c of VC3_Bday: return serialize(cast[VC3_Bday](c))
+  elif c of VC3_Adr: return serialize(cast[VC3_Adr](c))
+  elif c of VC3_Tel: return serialize(cast[VC3_Tel](c))
+  elif c of VC3_Email: return serialize(cast[VC3_Email](c))
+  elif c of VC3_TZ: return serialize(cast[VC3_TZ](c))
+  elif c of VC3_Geo: return serialize(cast[VC3_Geo](c))
+  elif c of VC3_Agent: return serialize(cast[VC3_Agent](c))
+  elif c of VC3_Org: return serialize(cast[VC3_Org](c))
+  elif c of VC3_Categories: return serialize(cast[VC3_Categories](c))
+  elif c of VC3_Rev: return serialize(cast[VC3_Rev](c))
+  elif c of VC3_UID: return serialize(cast[VC3_UID](c))
+  elif c of VC3_URL: return serialize(cast[VC3_URL](c))
+  elif c of VC3_Version: return serialize(cast[VC3_Version](c))
+  elif c of VC3_Class: return serialize(cast[VC3_Class](c))
+  elif c of VC3_SimpleTextContent:
+    return serialize(cast[VC3_SimpleTextContent](c))
+  elif c of VC3_BinaryContent:
+    return serialize(cast[VC3_BinaryContent](c))
+
+proc `$`*(vc3: VCard3): string =
+  result = "BEGIN:vCard\r\n"
+  result &= "VERSION:3.0\r\n"
+  for c in vc3.content.filterIt(not (it of VC3_Version)):
+    result &= foldContentLine(serialize(c)) & "\r\n"
+  result &= "END:vCard\r\n"
diff --git a/tests/test1.nim b/tests/test1.nim
index 822cc14..d200c62 100644
--- a/tests/test1.nim
+++ b/tests/test1.nim
@@ -7,6 +7,6 @@
 
 import unittest
 
-import nim_vcard
+import vcard3
 test "can add":
-  check add(5, 5) == 10
+  check 5 + 5 == 10
diff --git a/vcard.nimble b/vcard.nimble
index 1b3856f..79eea98 100644
--- a/vcard.nimble
+++ b/vcard.nimble
@@ -2,7 +2,7 @@
 
 version       = "0.1.0"
 author        = "Jonathan Bernard"
-description   = "Nim parser for the vCard format (versions 3.0 and 4.0)."
+description   = "Nim parser for the vCard format version 3.0 (4.0 planned)."
 license       = "MIT"
 srcDir        = "src"