File

util/vcard.lua @ 279:7a0aa3d055f4

verse: Accept a file object as a log handler, and automatically call :write() on it with a formatted message
author Matthew Wild <mwild1@gmail.com>
date Sun, 12 Feb 2012 20:21:52 +0000
parent 227:31019cb93d59
child 298:1897dc6a07bb
line wrap: on
line source

-- Copyright (C) 2011 Kim Alvefur
-- 
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

-- TODO
-- function lua_to_xep54()
-- function lua_to_text()
-- replace text_to_xep54() and xep54_to_text() with intermediate lua?

local st = verse or require "util.stanza";
local t_insert, t_concat = table.insert, table.concat;
local type = type;
local next, pairs, ipairs = next, pairs, ipairs;

module "vcard"

local vCard_dtd;

local function vCard_esc(s)
	return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
end

local function vCard_unesc(s)
	return s:gsub("\\?[\\nt:;,]", {
		["\\\\"] = "\\",
		["\\n"] = "\n",
		["\\t"] = "\t",
		["\\:"] = ":",
		["\\;"] = ";",
		["\\,"] = ",",
		[":"] = "\29",
		[";"] = "\30",
		[","] = "\31",
	});
end

function text_to_xep54(data)
	--[[ TODO
	return lua_to_xep54(text_to_lua(data));
	--]]
	data = data
		:gsub("\r\n","\n")
		:gsub("\n ", "")
		:gsub("\n\n+","\n");
	local c = st.stanza("xCard", { xmlns = "vcard-temp" });
	for line in data:gmatch("[^\n]+") do
		local line = vCard_unesc(line);
		local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
		value = value:gsub("\29",":");
		line = nil;
		if params and #params > 0 then
			local _params = {};
			for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
				local _vt = {};
				for _p in v:gmatch("[^\31]*") do
					_vt[#_vt]=_p
					_vt[_p]=true;
				end
				_params[k]=isval == "" or _vt;
			end
			params = _params;
		end
		if name == "BEGIN" and value == "VCARD" then
			c:tag("vCard", { xmlns = "vcard-temp" });
		elseif name == "END" and value == "VCARD" then
			c:up();
		elseif vCard_dtd[name] then
			local dtd = vCard_dtd[name];
			c:tag(name);
			if dtd.types then
				for _, t in ipairs(dtd.types) do
					if ( params.TYPE and params.TYPE[t] == true)
							or params[t] == true then
						c:tag(t):up();
					end
				end
			end
			if dtd.props then
				for _, p in ipairs(dtd.props) do
					if params[p] then
						if params[p] == true then
							c:tag(p):up();
						else
							for _, prop in ipairs(params[p]) do
								c:tag(p):text(prop):up();
							end
						end
					end
				end
			end
			if dtd == "text" then
				c:text(value);
			elseif dtd.value then
				c:tag(dtd.value):text(value):up();
			elseif dtd.values then
				local values = dtd.values;
				local i = 1;
				local value = "\30"..value;
				for p in value:gmatch("\30([^\30]*)") do
					c:tag(values[i]):text(p):up();
					if i < #values then
						i = i + 1;
					end
				end
			end
			c:up();
		end
	end
	return c;
end

function text_to_lua(data) --table
	data = data
		:gsub("\r\n","\n")
		:gsub("\n ", "")
		:gsub("\n\n+","\n");
	local vCards = {};
	local c; -- current item
	for line in data:gmatch("[^\n]+") do
		local line = vCard_unesc(line);
		local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
		value = value:gsub("\29",":");
		line = nil;
		if #params > 0 then
			local _params = {};
			for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
				local _vt = {};
				for _p in v:gmatch("[^\31]*") do
					_vt[#_vt]=_p
					_vt[_p]=true;
				end
				_params[k]=isval == "" or _vt;
			end
			params = _params;
		end
		if name == "BEGIN" and value == "VCARD" then
			c = {};
			vCards[#vCards+1] = c;
		elseif name == "END" and value == "VCARD" then
			c = nil;
		elseif vCard_dtd[name] then
			local dtd = vCard_dtd[name];
			local p = { name = name };
			c[#c+1]=p;
			--c[name]=p;
			local up = c;
			c = p;
			if dtd.types then
				for _, t in ipairs(dtd.types) do
					if ( params.TYPE and params.TYPE[t] == true)
							or params[t] == true then
						c.TYPE=t;
					end
				end
			end
			if dtd.props then
				for _, p in ipairs(dtd.props) do
					if params[p] then
						if params[p] == true then
							c[p]=true;
						else
							for _, prop in ipairs(params[p]) do
								c[p]=prop;
							end
						end
					end
				end
			end
			if dtd == "text" or dtd.value then
				t_insert(c, value);
			elseif dtd.values then
				local value = "\30"..value;
				for p in value:gmatch("\30([^\30]*)") do
					t_insert(c, p);
				end
			end
			c = up;
		end
	end
	return vCards;
end

local function vCard_prop(item) -- single item staza object to text line
	local prop_name = item.name;
	local prop_def = vCard_dtd[prop_name];
	if not prop_def then return nil end

	local value, params = "", {};

	if prop_def == "text" then
		value = item:get_text();
	elseif type(prop_def) == "table" then
		if prop_def.value then --single item
			value = item:get_child_text(prop_def.value) or "";
		elseif prop_def.values then --array
			local value_names = prop_def.values;
			value = {};
			if value_names.behaviour == "repeat-last" then
				for i=1,#item do
					t_insert(value, item[i]:get_text() or "");
				end
			else
				for i=1,#value_names do
					t_insert(value, item:get_child_text(value_names[i]) or "");
				end
			end
		elseif prop_def.names then
			local names = prop_def.names;
			for i=1,#names do
				if item:get_child(names[i]) then
					value = names[i];
					break;
				end
			end
		end
		
		if prop_def.props_verbatim then
			for k,v in pairs(prop_def.props_verbatim) do
				params[k] = v;
			end
		end

		if prop_def.types then
			local types = prop_def.types;
			params.TYPE = {};
			for i=1,#types do
				if item:get_child(types[i]) then
					t_insert(params.TYPE, types[i]:lower());
				end
			end
			if #params.TYPE == 0 then
				params.TYPE = nil;
			end
		end

		if prop_def.props then
			local props = prop_def.props;
			for i=1,#props do
				local prop = props[i]
				local p = item:get_child_text(prop);
				if p then
					params[prop] = params[prop] or {};
					t_insert(params[prop], p);
				end
			end
		end
	else
		return nil
	end

	if type(value) == "table" then
		for i=1,#value do
			value[i]=vCard_esc(value[i]);
		end
		value = t_concat(value, ";");
	else
		value = vCard_esc(value);
	end

	if next(params) then
		local sparams = "";
		for k,v in pairs(params) do
			sparams = sparams .. (";%s=%s"):format(k, t_concat(v,","));
		end
		params = sparams;
	else
		params = "";
	end

	return ("%s%s:%s"):format(item.name, params, value)
		:gsub(("."):rep(75), "%0\r\n "):gsub("\r\n $","");
end

function xep54_to_text(vCard)
	--[[ TODO
	return lua_to_text(xep54_to_lua(vCard))
	--]]
	local r = {};
	t_insert(r, "BEGIN:VCARD");
	for i = 1,#vCard do
		local item = vCard[i];
		if item.name then
			local s = vCard_prop(item);
			if s then
				t_insert(r, s);
			end
		end
	end
	t_insert(r, "END:VCARD");
	return t_concat(r, "\r\n");
end

local function xep54_item_to_lua(item)
	local prop_name = item.name;
	local prop_def = vCard_dtd[prop_name];
	if not prop_def then return nil end

	local prop = { name = prop_name };

	if prop_def == "text" then
		prop[1] = item:get_text();
	elseif type(prop_def) == "table" then
		if prop_def.value then --single item
			prop[1] = item:get_child_text(prop_def.value) or "";
		elseif prop_def.values then --array
			local value_names = prop_def.values;
			if value_names.behaviour == "repeat-last" then
				for i=1,#item do
					t_insert(prop, item[i]:get_text() or "");
				end
			else
				for i=1,#value_names do
					t_insert(prop, item:get_child_text(value_names[i]) or "");
				end
			end
		elseif prop_def.names then
			local names = prop_def.names;
			for i=1,#names do
				if item:get_child(names[i]) then
					prop[1] = names[i];
					break;
				end
			end
		end
		
		if prop_def.props_verbatim then
			for k,v in pairs(prop_def.props_verbatim) do
				prop[k] = v;
			end
		end

		if prop_def.types then
			local types = prop_def.types;
			prop.TYPE = {};
			for i=1,#types do
				if item:get_child(types[i]) then
					t_insert(prop.TYPE, types[i]:lower());
				end
			end
			if #prop.TYPE == 0 then
				prop.TYPE = nil;
			end
		end

		-- A key-value pair, within a key-value pair?
		if prop_def.props then
			local params = prop_def.props;
			for i=1,#params do
				local name = params[i]
				local data = item:get_child_text(prop_name);
				if prop_text then
					prop[prop_name] = prop[prop_name] or {};
					t_insert(prop[prop_name], prop_text);
				end
			end
		end
	else
		return nil
	end

	return prop;
end

local function xep54_vCard_to_lua(vCard)
	local tags = vCard.tags;
	local t = {};
	for i=1,#tags do
		t[i] = xep54_item_to_lua(tags[i]);
	end
	return t
end

function xep54_to_lua(vCard)
	if vCard.attr.xmlns ~= "vcard-temp" then
		return false
	end
	if vCard.name == "xCard" then
		local t = {};
		local vCards = vCard.tags;
		for i=1,#vCards do
			local ti = xep54_vCard_to_lua(vCards[i]);
			t[i] = ti;
			--t[ti.name] = ti;
		end
		return t
	elseif vCard.name == "vCard" then
		return xep54_vCard_to_lua(vCard)
	end
end

-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
vCard_dtd = {
	VERSION = "text", --MUST be 3.0, so parsing is redundant
	FN = "text",
	N = {
		values = {
			"FAMILY",
			"GIVEN",
			"MIDDLE",
			"PREFIX",
			"SUFFIX",
		},
	},
	NICKNAME = "text",
	PHOTO = {
		props_verbatim = { ENCODING = { "b" } },
		props = { "TYPE" },
		value = "BINVAL", --{ "EXTVAL", },
	},
	BDAY = "text",
	ADR = {
		types = {
			"HOME",
			"WORK", 
			"POSTAL", 
			"PARCEL", 
			"DOM",
			"INTL",
			"PREF", 
		},
		values = {
			"POBOX",
			"EXTADD",
			"STREET",
			"LOCALITY",
			"REGION",
			"PCODE",
			"CTRY",
		}
	},
	LABEL = {
		types = {
			"HOME", 
			"WORK", 
			"POSTAL", 
			"PARCEL", 
			"DOM",
			"INTL", 
			"PREF", 
		},
		value = "LINE",
	},
	TEL = {
		types = {
			"HOME", 
			"WORK", 
			"VOICE", 
			"FAX", 
			"PAGER", 
			"MSG", 
			"CELL", 
			"VIDEO", 
			"BBS", 
			"MODEM", 
			"ISDN", 
			"PCS", 
			"PREF", 
		},
		value = "NUMBER",
	},
	EMAIL = {
		types = {
			"HOME", 
			"WORK", 
			"INTERNET", 
			"PREF", 
			"X400", 
		},
		value = "USERID",
	},
	JABBERID = "text",
	MAILER = "text",
	TZ = "text",
	GEO = {
		values = {
			"LAT",
			"LON",
		},
	},
	TITLE = "text",
	ROLE = "text",
	LOGO = "copy of PHOTO",
	AGENT = "text",
	ORG = {
		values = {
			behaviour = "repeat-last",
			"ORGNAME",
			"ORGUNIT",
		}
	},
	CATEGORIES = {
		values = "KEYWORD",
	},
	NOTE = "text",
	PRODID = "text",
	REV = "text",
	SORTSTRING = "text",
	SOUND = "copy of PHOTO",
	UID = "text",
	URL = "text",
	CLASS = {
		names = { -- The item.name is the value if it's one of these.
			"PUBLIC",
			"PRIVATE",
			"CONFIDENTIAL",
		},
	},
	KEY = {
		props = { "TYPE" },
		value = "CRED",
	},
	DESC = "text",
};
vCard_dtd.LOGO = vCard_dtd.PHOTO;
vCard_dtd.SOUND = vCard_dtd.PHOTO;
return _M