-- constructor
-- parse text block of a vcard, return new vcard object
function Vcard:parse(data)
- local line, key, attr, value, i, v
+ local line, key, attr, value, i, v, n
local r = {}
-- unwrap line continuations, remove carriage return at EOL
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
+ for i,v in ipairs(self:_split_by(value, ",")) do
table.insert(r[key], v)
r[key].attr[#r[key]] = attr
end
end
+ -- try to ensure existence of FN field
+ if not r["FN"] and r["N"] then
+ n = self:components(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
+
setmetatable(r, { __index = self })
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 = {}
+ local key, value local attr = {}
key = line:match('^([^:;]+)[;:].*$')
if not key then return nil end
end
-- internal
--- split value fields aggregated by comma ignoring escaped commas
-function Vcard:_split_line(line)
- local f
- local r = { "" }
+-- 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("^,") then
+ if line:match("^" .. split) then
table.insert(r, "")
line = line:sub(2)
end
- f = line:match("^(\\.)") or line:match("^([^\\,]+)")
+ f = line:match("^(\\.)") or line:match("^([^\\" .. split .. "]+)")
r[#r] = r[#r] .. f
line = line:sub(#f + 1)
end
-- internal
-- development tests
function Vcard:_test()
- local vcf = Vcard:parse([=[
+ 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"].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" )
+
+ 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