fallbacks/lxp.lua

1
local coroutine = coroutine;
2
local tonumber = tonumber;
3
local string = string;
4
local setmetatable, getmetatable = setmetatable, getmetatable;
5
local pairs = pairs;
6
 
7
local deadroutine = coroutine.create(function() end);
8
coroutine.resume(deadroutine);
9
 
10
module("lxp")
11
 
12
local entity_map = setmetatable({
13
	["amp"] = "&";
14
	["gt"] = ">";
15
	["lt"] = "<";
16
	["apos"] = "'";
17
	["quot"] = "\"";
18
}, {__index = function(_, s)
19
		if s:sub(1,1) == "#" then
20
			if s:sub(2,2) == "x" then
21
				return string.char(tonumber(s:sub(3), 16));
22
			else
23
				return string.char(tonumber(s:sub(2)));
24
			end
25
		end
26
	end
27
});
28
local function xml_unescape(str)
29
	return (str:gsub("&(.-);", entity_map));
30
end
31
local function parse_tag(s)
32
	local name,sattr=(s):gmatch("([^%s]+)(.*)")();
33
	local attr = {};
34
	for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
35
	return name, attr;
36
end
37
 
38
local function parser(data, handlers, ns_separator)
39
	local function read_until(str)
40
		local pos = data:find(str, nil, true);
41
		while not pos do
42
			data = data..coroutine.yield();
43
			pos = data:find(str, nil, true);
44
		end
45
		local r = data:sub(1, pos);
46
		data = data:sub(pos+1);
47
		return r;
48
	end
49
	local function read_before(str)
50
		local pos = data:find(str, nil, true);
51
		while not pos do
52
			data = data..coroutine.yield();
53
			pos = data:find(str, nil, true);
54
		end
55
		local r = data:sub(1, pos-1);
56
		data = data:sub(pos);
57
		return r;
58
	end
59
	local function peek()
60
		while #data == 0 do data = coroutine.yield(); end
61
		return data:sub(1,1);
62
	end
63
	
64
	local ns = { xml = "http://www.w3.org/XML/1998/namespace" };
65
	ns.__index = ns;
66
	local function apply_ns(name, dodefault)
67
		local prefix,n = name:match("^([^:]*):(.*)$");
68
		if prefix and ns[prefix] then
69
			return ns[prefix]..ns_separator..n;
70
		end
71
		if dodefault and ns[""] then
72
			return ns[""]..ns_separator..name;
73
		end
74
		return name;
75
	end
76
	local function push(tag, attr)
77
		ns = setmetatable({}, ns);
78
		for k,v in pairs(attr) do
79
			local xmlns = k == "xmlns" and "" or k:match("^xmlns:(.*)$");
80
			if xmlns then
81
				ns[xmlns] = v;
82
				attr[k] = nil;
83
			end
84
		end
85
		local newattr, n = {}, 0;
86
		for k,v in pairs(attr) do
87
			n = n+1;
88
			k = apply_ns(k);
89
			newattr[n] = k;
90
			newattr[k] = v;
91
		end
92
		tag = apply_ns(tag, true);
93
		ns[0] = tag;
94
		ns.__index = ns;
95
		return tag, newattr;
96
	end
97
	local function pop()
98
		local tag = ns[0];
99
		ns = getmetatable(ns);
100
		return tag;
101
	end
102
	
103
	while true do
104
		if peek() == "<" then
105
			local elem = read_until(">"):sub(2,-2);
106
			if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
107
			elseif elem:sub(1,1) == "/" then -- end tag
108
				elem = elem:sub(2);
109
				local name = pop();
110
				handlers:EndElement(name); -- TODO check for start-end tag name match
111
			elseif elem:sub(-1,-1) == "/" then -- empty tag
112
				elem = elem:sub(1,-2);
113
				local name,attr = parse_tag(elem);
114
				name,attr = push(name,attr);
115
				handlers:StartElement(name,attr);
116
				name = pop();
117
				handlers:EndElement(name);
118
			else -- start tag
119
				local name,attr = parse_tag(elem);
120
				name,attr = push(name,attr);
121
				handlers:StartElement(name,attr);
122
			end
123
		else
124
			local text = read_before("<");
125
			handlers:CharacterData(xml_unescape(text));
126
		end
127
	end
128
end
129
 
130
function new(handlers, ns_separator)
131
	local co = coroutine.create(parser);
132
	return {
133
		parse = function(self, data)
134
			if not data then
135
				co = deadroutine;
136
				return true; -- eof
137
			end
138
			local success, result = coroutine.resume(co, data, handlers, ns_separator);
139
			if result then
140
				co = deadroutine;
141
				return nil, result; -- error
142
			end
143
			return true; -- success
144
		end;
145
	};
146
end
147
 
148
return _M;