util/xmllex.lua

1
local assert , ipairs , pairs , setmetatable , rawget , rawset , tostring =
2
	assert , ipairs , pairs , setmetatable , rawget , rawset , tostring
3
local strchar , strgmatch , strgsub , strsub , strmatch = string.char , string.gmatch , string.gsub , string.sub , string.match
4
local tblconcat = table.concat
5
local tblinsert = table.insert
6
 
7
local stanza_methods = require "util.stanza".stanza_mt;
8
 
9
local entities = setmetatable ( {
10
        amp = "&" ;
11
        lt = "<" ;
12
        gt = ">" ;
13
        apos = "'" ;
14
        quot = '"' ;
15
} , { __index = function ( entity )
16
        return strchar ( tonumber ( entity:match ( "^#%d+" ) ) or error ( "invalid entity " .. entity ) )
17
end  } )
18
local function xml_unescape ( str )
19
	return ( strgsub ( str , "&([^;]*);" , entities ) ) 
20
end
21
 
22
local function getstring ( msgs , startpos , finishpos )
23
	if #msgs == 1 then --All originated in same string
24
		return strsub ( msgs[1] , startpos , finishpos )
25
	else -- Over multiple source strings
26
		return strsub ( msgs[1] , startpos , -1 )
27
			.. tblconcat ( msgs , "" , 2 , #msgs - 1 )
28
			.. strsub ( msgs[#msgs] , 1 , finishpos )
29
	end
30
end
31
 
32
local m_mt = {
33
	__tostring = function ( v )
34
		local str = v.stringform
35
		if str then
36
			return str
37
		else
38
			str = getstring ( v.msgs , v.start , v.finish )
39
			if v.type == "text" then
40
				str = xml_unescape ( str )
41
			end
42
			v.stringform = str
43
			return str
44
		end
45
	end
46
}
47
 
48
local function handleoutside ( str , r , initial )
49
	local a , b , close = str:find ( "<(/?)" , initial )
50
 
51
	if not a then
52
		r.state = "outside"
53
		return false
54
	end
55
	
56
	--Finalise text object
57
	local m = r[#r]
58
	m.finish = a - 1
59
	m.type = "text"
60
	
61
	local m = setmetatable ( {
62
		msgs = { str } ;
63
		start = a ;
64
		starte = b + 1 ;
65
	} , m_mt )
66
	
67
	if close ~= "/" then
68
		r.depth = r.depth + 1
69
		m.type = "open"
70
	else
71
		r.depth = r.depth - 1
72
		m.type = "close"
73
	end
74
	
75
	tblinsert ( r , m )
76
 
77
	r.state = "inside"
78
	return true
79
end
80
 
81
local function handleinside ( str, r , initial )
82
	local c , d , selfclosing = str:find ( "([/?]?)>" , initial )
83
	if not c then
84
		r.state = "inside"
85
		return false
86
	end
87
	
88
	local m = r[#r]
89
	m.finish = d
90
	m.finishs = c - 1
91
	if selfclosing == "/" or selfclosing == "?" then
92
		m.type = "selfclosing"
93
		r.depth = r.depth - 1
94
	end
95
 
96
	local m = setmetatable ( {
97
		msgs = { str } ;
98
		start = d + 1 ;
99
		type = "text" ;
100
	} , m_mt )
101
	tblinsert ( r , m )
102
	
103
	r.state = "outside"
104
	return true
105
end
106
 
107
local function index ( str , r )
108
	r = r or { depth = 0, state = "outside" }
109
	
110
	local initial = 1
111
	
112
	if not r[#r] then
113
		r[1] = setmetatable ( {
114
			msgs = { str } ;
115
			type = "text" ;
116
			start = 1 ;
117
		} , m_mt )
118
	else
119
		tblinsert ( r[#r].msgs , str )
120
	end
121
	
122
	while true do
123
		if r.state == "outside" then
124
			if not handleoutside ( str , r , initial ) then
125
				break
126
			end
127
		else
128
			if not handleinside ( str , r , initial ) then
129
				break
130
			end
131
		end
132
		initial = r[#r].start
133
	end
134
 
135
	return r
136
end
137
 
138
local function get_name ( str )
139
	return strmatch ( str , "^<([^%s>/]+)" )
140
end
141
 
142
local function get_attr ( str  )
143
	local attr = { }
144
	for name , quote, attvalue in strgmatch ( str , [=[([^%s=/<]+)%s*=%s*(["'])([^'"]*)%2]=] ) do
145
		attr [ name ] = xml_unescape ( attvalue )
146
	end
147
	return attr
148
end
149
 
150
function resolve_attr_namespaces ( attr )
151
	local namespace = { }
152
	local prefixattr = { }
153
	for k , attr_value in pairs ( attr ) do
154
		local attr_prefix , attr_name = k:match ( "^([^:\1]+):?([^\1]-)$" )
155
		
156
		if attr_prefix == nil then
157
			error ( "already resolved" )
158
		elseif attr_prefix == "xmlns" then
159
			namespace [ attr_name ] = attr_value
160
		elseif #attr_name ~= 0 and attr_prefix ~= "xml" then
161
			local t = prefixattr [ attr_prefix ]
162
			if not t then
163
				t = { }
164
				prefixattr [ attr_prefix ] = t
165
			end
166
			t [ attr_name ] = attr_value
167
		end
168
	end
169
 
170
	for k , v in pairs ( prefixattr ) do 
171
		for name , value in pairs ( v ) do
172
			attr [ namespace [ k ] .. "\1" .. name ] = value
173
		end
174
	end
175
	return namespace
176
end
177
 
178
local currentindex = 1
179
 
180
local function resolve_namespace ( element )
181
	local parent = element.parent
182
 
183
	local prefix = get_name ( element.str ):match ( "^([^:]+):" )
184
 
185
	local namespace = setmetatable ( resolve_attr_namespaces ( element.attr ) , { __index = parent.namespace } )
186
	
187
	local current_namespace = prefix and ( namespace [ prefix ] or error ("unbound prefix: "..prefix) )
188
		or rawget(element.attr, "xmlns")
189
		or parent.attr.xmlns
190
		or parent.namespace [ currentindex ]
191
	namespace [ currentindex ] = current_namespace
192
	
193
	return namespace
194
end
195
 
196
local dynamic_properties = {
197
	name = function ( t )
198
		return get_name ( t.str ):match("[^:]+$")
199
	end;
200
	attr = function ( t , k )
201
		return setmetatable ( get_attr ( t.str ) , { __index = function ( attr_table , attr )
202
				local _ = t.namespace -- DO NOT OPTIMISE AWAY WAQAS
203
				setmetatable ( attr_table , { __index = { xmlns = t.namespace[currentindex] } } )
204
				return attr_table [ attr ]
205
			end } )
206
	end;
207
	str = function ( t , k )
208
		return tostring ( t.opentag )
209
	end;
210
	namespace = resolve_namespace ;
211
}
212
 
213
local stanza_mt = {
214
	__index = function ( t , k )
215
		local f = dynamic_properties [ k ]
216
		if f then
217
			local v = f ( t )
218
			
219
			rawset ( t , k , v )
220
			return v
221
		else
222
			return stanza_methods[k]
223
		end
224
	end ;
225
	
226
	__tostring = function ( t )
227
		if t.modified then
228
			return stanza_methods.__tostring ( t )
229
		end
230
		local opentag = t.opentag
231
		local endtag = assert ( rawget ( t , "endtag" ) or rawget ( t , "selfclosing" ) and t.opentag )
232
		return getstring ( opentag.msgs , opentag.start , endtag.finish )
233
	end ;
234
	
235
	__newindex = function ( t , k , v )
236
		rawset ( t , "modified", true )
237
		rawset ( t , k , v )
238
	end ;
239
}
240
 
241
local function new_stanza ( parent )
242
	return setmetatable ( { tags = { } , parent = parent } , stanza_mt )
243
end
244
 
245
local function tagindex_to_tree(indices, start, finish,root)
246
	if not start then
247
		start = 1
248
		finish = #indices
249
	end
250
	
251
	root = root or { attr = { } }
252
	root.namespace = resolve_attr_namespaces ( root.attr )
253
	root.root = true
254
	root.tags = { }
255
	local leaf = root
256
	
257
	for i = start , finish do
258
		local v = indices [ i ]
259
		
260
		if v.type == "selfclosing" then
261
			local newleaf = new_stanza ( leaf )
262
			newleaf.opentag = v
263
			newleaf.selfclosing = true
264
			
265
			tblinsert ( leaf , newleaf )
266
			tblinsert ( leaf.tags , newleaf )
267
		elseif v.type == "close" then -- Close tag
268
			leaf.endtag = v
269
			leaf = leaf.parent
270
		elseif v.type == "text" then
271
			tblinsert ( leaf, v )
272
		else -- Open tag
273
			local newleaf = new_stanza ( leaf )
274
			newleaf.opentag = v
275
			
276
			tblinsert ( leaf , newleaf )
277
			tblinsert ( leaf.tags , newleaf )
278
			
279
			leaf = newleaf
280
		end
281
	end
282
	
283
	assert ( leaf == root , "Mismatched opening/closing tags" )
284
	return root;
285
end
286
 
287
return {
288
	index = index ;
289
	tagindex_to_tree = tagindex_to_tree ;
290
	get_name = get_name ;
291
	get_attr = get_attr ;
292
	resolve_attr_namespaces = resolve_attr_namespaces ;
293
};