summaryrefslogtreecommitdiff
path: root/xml.lua
blob: 35d28046e9f126c649f139bb4d0ace4f8722eb0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
-- originally from http://lua-users.org/wiki/LuaXml
-- modified by me a bit


local function parseargs(s)
  local arg = {}
  string.gsub(s, "([%-%w]+)=([\"'])(.-)%2", function (w, _, a)
    arg[w] = a
  end)
  return arg
end

local function tag(x)
	return setmetatable(x, {__call=function(x, label)
		-- search for child with given label
		for _,c in ipairs(x) do
			if c.label == label then
				return c
			end
		end
		return nil
	end})
end

local psingle

local function pmulti(s, i, parent)
	::again::
	local nexti, child = psingle(s, i)
	if child.close and child.label == parent.label then
		return nexti, parent
	else
		table.insert(parent, child)
		i = nexti
		goto again
	end
end

psingle = function(s, i)
	i = i or 1
	local ts,j,c,label,xarg,empty = s:find("<(%/?)([%w:]+)(.-)(%/?)>", i)
	if not ts then
		local rest = s:sub(i)
		if rest:find("<",i) then
			error('ill formed (eof?)')
		elseif #rest == 0 then
			error('empty string')
		else
			return i+#rest, rest
		end
	end
	local nexti = j+1

	local pretext = s:sub(i, ts-1)
	if not pretext:find("^%s*$") then -- not entirely whitespace
		return ts, pretext
	end

	if empty == "/" then
		return nexti, tag{label=label, xarg=parseargs(xarg), empty=true}
	elseif c == "" then -- start tag
		return pmulti(s, nexti, tag{label=label, xarg=parseargs(xarg)})
	else -- end tag
		return nexti, tag{label=label, close=true}
	end
end

local wrap
do
	local _, cqa = pcall(require, 'cqueues.auxlib')
	wrap = cqa and cqa.wrap or coroutine.wrap
end

local function stanzae(getmore)
	return wrap(function()
		local buf = ''
		while true do
			local ok, ni, el = pcall(psingle, buf)
			if ok then
				coroutine.yield(el)
				buf = buf:sub(ni)
			else
				local more = getmore()
				buf = buf .. more
			end
		end
	end)
end

local entity_escapes = {
		["<"]="&lt;",
		[">"]="&gt;",
		["&"]="&amp;",
		['"']="&quot;",
		["'"]="&apos;"
}

local function escape(s)
	return s:gsub("[<>&'\"]",entity_escapes)
end

local safestr_mt = {name='SAFESTR', __tostring=function(x) return x.s end}
local function safestr(s) return setmetatable({s=s}, safestr_mt) end

local function _xmlify(x)
	if getmetatable(x) == safestr_mt then
		return x
	elseif type(x) == 'string' then
		return safestr(escape(x))
	elseif type(x) == 'table' then -- must be a tag
		local function argstr(t)
			local argstring = ''
			for k,v in pairs(t) do
				argstring = argstring .. (" %s='%s'"):format(k, _xmlify(v))
			end
			return argstring
		end
		if x.empty then
			return safestr(("<%s%s/>"):format(x.label, argstr(x.xarg)))
		else
			local open = ("<%s%s>"):format(x.label, argstr(x.xarg))
			local close = ("</%s>"):format(x.label)
			local children = {}
			for _,child in ipairs(x) do
				table.insert(children, tostring(_xmlify(child)))
			end
			return safestr(open .. table.concat(children, '') .. close)
		end
	end
end

local function xmlify(...)
	local n = select('#',...)
	if n <= 1 then return _xmlify(...) end

	local out = {}
	for i = 1, n do
		out[i] = tostring(_xmlify(select(i, ...)))
	end
	return table.concat(out, '\n')
end

-- dsl for input to xmlify. doesn't do escaping or anything.
-- THERE IS NO ESCAPE
local X = setmetatable({}, {__index=function(_,label)
	return function(tab)
		local out = {}
		local xarg = {}
		local empty = true

		for k, v in pairs(tab) do
			if type(k) == 'number' then -- child
				empty = nil
				out[k] = v
			elseif type(k) == 'string' then -- attrib
				xarg[k] = v
			end
		end

		out.label = label
		out.xarg = xarg
		out.empty = empty
		return out
	end
end})

return { psingle = psingle, stanzae = stanzae, wrap=wrap, xmlify=xmlify, X=X }