--- /dev/null
+#!/usr/bin/lua
+
+local Vcard = {}
+
+-- constructor, synonymous to load()
+function Vcard:new(path)
+ return self:load(path)
+end
+
+-- constructor
+-- load and parse vcard file, return new vcard object
+function Vcard:load(path)
+ local file, data
+
+ file = io.open(path)
+ if not file then return nil; end
+ data = file:read("a")
+ file:close()
+
+ return self:parse(data)
+end
+
+-- constructor
+-- parse text block of a vcard, return new vcard object
+function Vcard:parse(data)
+ local line, key, attr, value, i, v
+ local r = {}
+
+ -- unwrap line continuations, remove carriage return at EOL
+ data = data:gsub("\r?\n[ \t]", "")
+ data = data:gsub("\r\n", "\n")
+
+ -- parse lines into fields
+ for line in data:gmatch("[^\n]+") do
+ key, attr, value = self:_parse_line(line)
+ if not r[key] then r[key] = { attr = {} } end
+ for i,v in ipairs(self:_split_line(value)) do
+ table.insert(r[key], v)
+ r[key].attr[#r[key]] = attr
+ end
+ end
+
+ setmetatable(r, { __index = self })
+ return r
+end
+
+-- internal
+-- split vcard line into key, value, and an array of attributes
+function Vcard:_parse_line(line)
+ local key, value
+ local attr = {}
+
+ key = line:match('^([^:;]+)[;:].*$')
+ if not key then return nil end
+
+ line = line:sub(#key + 1 )
+ a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$')
+ while a do
+ table.insert(attr, a)
+ line = line:sub(#a + 2)
+ a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$')
+ end
+
+ value = line:match("^:(.*)$")
+ if not value then return nil end
+
+ key = key:upper()
+ return key, attr, value
+end
+
+-- internal
+-- split value fields aggregated by comma ignoring escaped commas
+function Vcard:_split_line(line)
+ local f
+ local r = { "" }
+
+ while line ~= "" and line ~= "\\" do
+ if line:match("^,") then
+ table.insert(r, "")
+ line = line:sub(2)
+ end
+ f = line:match("^(\\.)") or line:match("^([^\\,]+)")
+ r[#r] = r[#r] .. f
+ line = line:sub(#f + 1)
+ end
+ return r
+end
+
+-- internal
+-- development tests
+function Vcard:_test()
+ local vcf = Vcard:parse([=[
+BEGIN:VCARD
+VERSION:1.0
+UID:1
+N:Lastname;Firstname;Middle Names; Title; Suffix
+PHONE;TYPE=Home:123456789,666999
+PHONE;TYPE=Work;TYPE=Cell:987654321
+END:VCARD
+]=])
+
+ assert( vcf["PHONE"][1] == "123456789", "Phone/1 wrong number" )
+ assert( vcf["PHONE"][2] == "666999", "Phone/2 wrong number" )
+ assert( vcf["PHONE"].attr[2][1] == vcf["PHONE"].attr[1][1] )
+ assert( vcf["PHONE"][3] == "987654321", "Phone/3 wrong number" )
+ assert( vcf["PHONE"].attr[3][2] == "TYPE=Cell", "Phone/3 attr type=cell" )
+end
+
+return Vcard