+#!/usr/bin/lua
+
+local Vcard = {
+ _component_fields = { N = true; GENDER = true; ADR = true; ORG = true; }
+}
+
+-- 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, n
+ local r = {}
+ setmetatable(r, { __index = self })
+
+ -- 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
+ if Vcard._component_fields[key] then
+ table.insert(r[key], self:components(value));
+ r[key].attr[#r[key]] = attr
+ r[key][#r[key]][0] = value;
+ else for i,v in ipairs(self:_split_by(value, ",")) do
+ table.insert(r[key], v)
+ r[key].attr[#r[key]] = attr
+ end end
+ end
+
+ -- try to ensure existence of FN field
+ if not r["FN"] and r["N"] then
+ n = r["N"][1]
+ r["FN"] = { (n[4] or " ") .. " " .. (n[2] or " ") .. " " ..
+ (n[3] or " ") .. " " .. (n[1] or " ") .. " " ..
+ (n[5] or " ") }
+ r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
+ end
+ if not r["FN"] and r["NICKNAME"] then
+ r["FN"] = { r["NICKNAME"][1] }
+ r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
+ end
+
+ return r
+end
+
+-- split field components (i.e. the N field)
+function Vcard:components(line)
+ return self:_split_by(line, ";")
+end
+
+function Vcard:unescape(line)
+ local f local out = ""
+
+ while line ~= "" and line ~= "\\" do
+ if (function() f = line:match("^\\n([^\\]*)") return f end)() then
+ out = out .. "\n" .. f
+ line = line:sub(#f + 3)
+ elseif (function() f = line:match("^\\(.[^\\]*)") return f end)() then
+ out = out .. f
+ line = line:sub(#f + 2)
+ elseif (function() f = line:match("^([^\\]+)") return f end)() then
+ out = out .. f
+ line = line:sub(#f + 1)
+ end
+ end
+
+ return out
+end
+
+function Vcard:escape(line)
+ return line:gsub("\\", "\\\\"):gsub(",", "\\,"):gsub(";", "\\;"):gsub("\n", "\\n")
+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 string by specified character, unless the character is ecapped by \
+function Vcard:_split_by(line, split)
+ local f local r = { "" }
+
+ while line ~= "" and line ~= "\\" do
+ if line:match("^" .. split) then
+ table.insert(r, "")
+ line = line:sub(2)
+ end
+ f = line:match("^(\\.)") or line:match("^([^\\" .. split .. "]+)")
+ r[#r] = r[#r] .. f
+ line = line:sub(#f + 1)
+ end
+ return r
+end
+
+-- internal
+-- development tests
+function Vcard:_test()
+ local vcf = self:parse([=[
+BEGIN:VCARD
+VERSION:1.0
+UID:1
+N:Lastname;Firstname;Middle Names; Title; Suffix
+NICKNAME: Bone Crusher, Cookie Monster
+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" )
+ assert( vcf["FN"][1] == "Title Firstname Middle Names Lastname Suffix" )
+ assert( vcf["N"][1][1] == "Lastname" )
+ assert( vcf["N"][1][4] == " Title" )
+
+ vcf = self:parse([=[
+BEGIN:VCARD
+VERSION:1.0
+UID:1
+NICKNAME: Bone Crusher, Cookie Monster
+PHONE;TYPE=Home:123456789,666999
+PHONE;TYPE=Work;TYPE=Cell:987654321
+END:VCARD
+]=])
+ assert( vcf["FN"][1] == "Bone Crusher" )
+
+ print("Vcard OK")
+end
+
+return Vcard