Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
47c62cce6d | |||
a1dac6eaaf | |||
2c625349bf | |||
0f353e97ca | |||
c4717b4b00 | |||
419d794c68 | |||
a0cd676521 | |||
2a48974f3a |
53
README.md
53
README.md
@ -1,3 +1,56 @@
|
||||
# VCard
|
||||
|
||||
`nim-vcard` is a pure nim implementation of the VCard format defined in RFCs
|
||||
2425, 2426, and 6350. It allows you to parse and serialize VCards, as well as
|
||||
create VCards programmatically. It aims to be a complete implememtation,
|
||||
supporting all of the features of the VCard3 standard. Because the standard
|
||||
provides many features that may be rarely used, this library also provides a
|
||||
simplified API for more typical use-cases.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```vcard
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
UID: 5db6f100-e2d6-4e8d-951f-d920586bc069
|
||||
N:Foster;Jack;Allen;;
|
||||
FN:Allen Foster
|
||||
REV:20230408T122102Z
|
||||
EMAIL;TYPE=home;TYPE=pref:allen@fosters.test
|
||||
EMAIL;TYPE=work:jack.foster@company.test
|
||||
TEL;TYPE=CELL:+1 (555) 123-4567
|
||||
END:VCARD
|
||||
```
|
||||
|
||||
```nim
|
||||
import vcard
|
||||
|
||||
# Reading in an existing vcard
|
||||
let vcards = parseVCard3File("jack.vcf")
|
||||
assert vcards.len == 1
|
||||
let vcAllen = vcards[0]
|
||||
|
||||
assert vcAllen.email.len == 2
|
||||
assert vcAllen.email[0].value == "allen@fosters.test"
|
||||
assert vcAllen.n.first == "Jack"
|
||||
|
||||
|
||||
# Creating a new VCard
|
||||
var vcSusan: VCard3
|
||||
vcSusan.add(
|
||||
newVC3_N(given = "Susan", family = "Foster"),
|
||||
newVC3_Email(value = "susan@fosters.test", emailType = @["PREF", $etInternet),
|
||||
newVC3_Tel(
|
||||
value = "+1 (555) 444-3889",
|
||||
telType = @[$ttHome, $ttCell, $ttVoice, $ttMsg])
|
||||
)
|
||||
writeFile("susan.vcf", $vcSusan)
|
||||
```
|
||||
|
||||
## Future Goals
|
||||
|
||||
* VCard 4.0 support
|
||||
|
||||
## Debugging
|
||||
|
||||
*Need to clean up and organize*
|
||||
|
2691
doc/rfc6352.txt
Normal file
2691
doc/rfc6352.txt
Normal file
File diff suppressed because it is too large
Load Diff
3
src/vcard.nim
Normal file
3
src/vcard.nim
Normal file
@ -0,0 +1,3 @@
|
||||
import vcard/vcard3
|
||||
|
||||
export vcard3
|
@ -12,16 +12,16 @@ const DATE_TIME_FMTS = [
|
||||
"yyyy-MM-dd'T'HH:mm:ss'.'fffzzz",
|
||||
]
|
||||
|
||||
const ALL_FMTS = DATE_FMTS.toSeq & DATE_TIME_FMTS.toSeq
|
||||
const ALL_FMTS = DATE_TIME_FMTS.toSeq & DATE_FMTS.toSeq
|
||||
|
||||
proc parseDateTimeStr(
|
||||
dateStr: string,
|
||||
dateFmts: openarray[string]
|
||||
): DateTime {.inline.} =
|
||||
): DateTime {.inline, raises:[ValueError].} =
|
||||
|
||||
for fmt in dateFmts:
|
||||
try: result = parse(dateStr, fmt)
|
||||
except: discard
|
||||
except ValueError: discard
|
||||
|
||||
if not result.isInitialized:
|
||||
raise newException(ValueError, "cannot parse date: " & dateStr )
|
||||
|
@ -388,7 +388,7 @@ func newVC3_Email*(
|
||||
emailType = @[$etInternet],
|
||||
group = none[string]()): VC3_Email =
|
||||
|
||||
return VC3_Email(name: "EMAIL", emailType: emailType, group: group)
|
||||
return assignFields(VC3_Email(name: "EMAIL"), value, emailType, group)
|
||||
|
||||
func newVC3_Mailer*(
|
||||
value: string,
|
||||
@ -523,8 +523,8 @@ func newVC3_Sound*(
|
||||
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_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)
|
||||
@ -551,7 +551,7 @@ func newVC3_XType*(
|
||||
xParams: seq[VC3_XParam] = @[],
|
||||
group = none[string]()): VC3_XType =
|
||||
|
||||
if not name.startsWith("x-"):
|
||||
if not name.startsWith("X-"):
|
||||
raise newException(ValueError, "Extended types must begin with 'x-'.")
|
||||
|
||||
return assignFields(
|
||||
@ -652,11 +652,11 @@ 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 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 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)
|
||||
@ -680,26 +680,30 @@ func xTypes*(vc3: VCard3): seq[VC3_XType] = vc3.content.xTypes
|
||||
# Setters
|
||||
# =============================================================================
|
||||
|
||||
func set*[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 set*[T](vc3: var VCard3, content: varargs[T]): void =
|
||||
for c in content:
|
||||
var nc = c
|
||||
let existingIdx = vc3.content.indexOfIt(it of T)
|
||||
if existingIdx < 0:
|
||||
nc.contentId = vc3.takeContentId
|
||||
vc3.content.add(nc)
|
||||
else:
|
||||
nc.contentId = vc3.content[existingIdx].contentId
|
||||
vc3.content[existingIdx] = nc
|
||||
|
||||
func set*[T](vc3: VCard3, newContent: var T): VCard3 =
|
||||
func set*[T](vc3: VCard3, content: varargs[T]): VCard3 =
|
||||
result = vc3
|
||||
result.set(newContent)
|
||||
result.set(content)
|
||||
|
||||
func add*[T](vc3: var VCard3, newContent: T): void =
|
||||
newContent.contentId = vc3.takeContentId
|
||||
vc3.content.add(newContent)
|
||||
func add*[T](vc3: var VCard3, content: varargs[T]): void =
|
||||
for c in content:
|
||||
var nc = c
|
||||
nc.contentId = vc3.takeContentId
|
||||
vc3.content.add(nc)
|
||||
|
||||
func add*[T](vc3: VCard3, newContent: T): VCard3 =
|
||||
func add*[T](vc3: VCard3, content: varargs[T]): VCard3 =
|
||||
result = vc3
|
||||
result.add(newContent)
|
||||
result.add(content)
|
||||
|
||||
func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
|
||||
for c in content:
|
||||
@ -707,6 +711,13 @@ func updateOrAdd*[T](vc3: var VCard3, content: seq[T]): VCard3 =
|
||||
if existingIdx < 0: vc3.content.add(c)
|
||||
else: c.content[existingIdx] = c
|
||||
|
||||
func updateOrAdd*[T](vc3: VCard3, content: seq[T]): VCard3 =
|
||||
result = vc3
|
||||
for c in content:
|
||||
let existingIdx = result.content.indexOfIt(it.contentId == c.contentId)
|
||||
if existingIdx < 0: result.content.add(c)
|
||||
else: c.content[existingIdx] = c
|
||||
|
||||
# TODO: simplify with macros?
|
||||
# macro generateImmutableVersion()...
|
||||
# generateImmutableVersion("set", "add", "setName", "addSource")
|
||||
@ -1294,8 +1305,10 @@ 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)
|
||||
elif r.valueType.isSome and r.valueType.get == "date":
|
||||
result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT)
|
||||
else:
|
||||
result &= ";VALUE=date:" & r.value.format(DATE_FMT)
|
||||
result &= r.value.format(DATETIME_FMT)
|
||||
|
||||
proc serialize(u: VC3_UID | VC3_URL | VC3_VERSION | VC3_Class): string =
|
||||
result = u.nameWithGroup & ":" & u.value
|
||||
@ -1779,10 +1792,10 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
|
||||
isInline = params.existsWithValue("ENCODING", "B")))
|
||||
|
||||
of $cnUid:
|
||||
result.add(newVC3_Uid(group = group, value = p.readValue))
|
||||
result.add(newVC3_UID(group = group, value = p.readValue))
|
||||
|
||||
of $cnUrl:
|
||||
result.add(newVC3_Url(group = group, value = p.readValue))
|
||||
result.add(newVC3_URL(group = group, value = p.readValue))
|
||||
|
||||
of $cnVersion:
|
||||
p.expect("3.0")
|
||||
@ -1801,7 +1814,7 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
|
||||
isInline = params.existsWithValue("ENCODING", "B")))
|
||||
|
||||
else:
|
||||
if not name.startsWith("x-"):
|
||||
if not name.startsWith("X-"):
|
||||
p.error("unrecognized content type: '$1'" % [name])
|
||||
|
||||
result.add(newVC3_XType(
|
26
tests/jdb.vcf
Executable file
26
tests/jdb.vcf
Executable file
@ -0,0 +1,26 @@
|
||||
BEGIN:VCARD
|
||||
PRODID:-//CyrusIMAP.org//Cyrus 3.7.0-alpha0-927-gf4c98c8499-fm-202208..//EN
|
||||
VERSION:3.0
|
||||
UID:cdaf67dc-b702-41ac-9c26-bb61df3032d2
|
||||
N:Bernard;Jonathan;;;
|
||||
FN:Jonathan Bernard
|
||||
ORG:;
|
||||
TITLE:
|
||||
NICKNAME:
|
||||
NOTE:
|
||||
REV:20220908T122102Z
|
||||
EMAIL;TYPE=home;TYPE=pref:jonathan@jdbernard.com
|
||||
EMAIL;TYPE=work:jdb@jdb-software.com
|
||||
email2.X-ABLabel:Obsolete
|
||||
email2.EMAIL:jonathan.bernard@sailpoint.com
|
||||
email3.X-ABLabel:Obsolete
|
||||
email3.EMAIL:jonathan.bernard@accenture.com
|
||||
email4.X-ABLabel:Obsolete
|
||||
email4.EMAIL:jbernard@fairwaytech.com
|
||||
email5.X-ABLabel:Obsolete
|
||||
email5.EMAIL:jobernar@linkedin.com
|
||||
EMAIL;TYPE=work:jbernard@vectra.ai
|
||||
TEL;TYPE=CELL:(512) 777-1602
|
||||
tel1.X-ABLabel:Mobile (alernate)
|
||||
tel1.TEL:(512) 784-2388
|
||||
END:VCARD
|
@ -53,3 +53,12 @@ suite "vcard/vcard3":
|
||||
vcards[0].fn.value == "Frank Dawson"
|
||||
vcards[0].email.len == 2
|
||||
(vcards[0].email --> find(it.emailType.contains("PREF"))).isSome
|
||||
|
||||
test "Jonathan Bernard VCard":
|
||||
#const jdbVcard = readFile("tests/jdb.vcf")
|
||||
let jdb = parseVCard3File("tests/jdb.vcf")[0]
|
||||
check:
|
||||
jdb.email.len == 7
|
||||
jdb.email[0].value == "jonathan@jdbernard.com"
|
||||
jdb.email[0].emailType.contains("pref")
|
||||
jdb.fn.value == "Jonathan Bernard"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Nim parser for the vCard format version 3.0 (4.0 planned)."
|
||||
license = "MIT"
|
||||
|
Reference in New Issue
Block a user