738 lines
19 KiB
Plaintext
738 lines
19 KiB
Plaintext
-- SLAXML parser
|
|
-- Adapted for Anomaly usage by demonized
|
|
|
|
--[=====================================================================[
|
|
v0.8 Copyright © 2013-2018 Gavin Kistner <!@phrogz.net>; MIT Licensed
|
|
See http://github.com/Phrogz/SLAXML for details.
|
|
--]=====================================================================]
|
|
function SLAXML()
|
|
local SLAXML = {
|
|
VERSION = "0.8",
|
|
_call = {
|
|
pi = function(target, content)
|
|
print(string.format("<?%s %s?>", target, content))
|
|
end,
|
|
comment = function(content)
|
|
print(string.format("<!-- %s -->", content))
|
|
end,
|
|
startElement = function(name, nsURI, nsPrefix)
|
|
io.write("<")
|
|
if nsPrefix then
|
|
io.write(nsPrefix, ":")
|
|
end
|
|
io.write(name)
|
|
if nsURI then
|
|
io.write(" (ns='", nsURI, "')")
|
|
end
|
|
print(">")
|
|
end,
|
|
attribute = function(name, value, nsURI, nsPrefix)
|
|
io.write(" ")
|
|
if nsPrefix then
|
|
io.write(nsPrefix, ":")
|
|
end
|
|
io.write(name, "=", string.format("%q", value))
|
|
if nsURI then
|
|
io.write(" (ns='", nsURI, "')")
|
|
end
|
|
io.write("\n")
|
|
end,
|
|
text = function(text, cdata)
|
|
print(string.format(" %s: %q", cdata and "cdata" or "text", text))
|
|
end,
|
|
closeElement = function(name, nsURI, nsPrefix)
|
|
io.write("</")
|
|
if nsPrefix then
|
|
io.write(nsPrefix, ":")
|
|
end
|
|
print(name .. ">")
|
|
end,
|
|
},
|
|
_defcall = {
|
|
startElement = function(name, nsURI, nsPrefix) end, -- When "<foo" or <x:foo is seen
|
|
attribute = function(name, value, nsURI, nsPrefix) end, -- attribute found on current element
|
|
closeElement = function(name, nsURI) end, -- When "</foo>" or </x:foo> or "/>" is seen
|
|
text = function(text, cdata) end, -- text and CDATA nodes (cdata is true for cdata nodes)
|
|
comment = function(content) end, -- comments
|
|
pi = function(target, content) end, -- processing instructions e.g. "<?yes mon?>"
|
|
},
|
|
}
|
|
|
|
function SLAXML:parser(callbacks)
|
|
return { _call = callbacks or self._defcall, parse = self.parse }
|
|
end
|
|
|
|
-- demonized
|
|
-- Sanitize xml before parsing
|
|
local function sanitize(s)
|
|
-- Add spaces between attributes
|
|
s = s:gsub("<([%a_][%w_.-]*)( )(.-)(/?>)", function(a, b, c, d)
|
|
local new_c = c:gsub('([:%a_][:%w_.-]*)%s*=%s*"(.-)"', function(a, b)
|
|
return string.format('%s="%s" ', a, b)
|
|
end)
|
|
new_c = one_space(new_c)
|
|
|
|
-- Add quotes to attributes where they are absent
|
|
new_c = new_c:gsub("([:%a_][:%w_.-]*=)([%w_]+)([ />]?)", [[%1"%2"%3]])
|
|
return one_space("<" .. a .. b .. " " .. new_c .. d)
|
|
end)
|
|
|
|
return s
|
|
end
|
|
|
|
local function loadFile(path)
|
|
local file_reader = getFS():r_open('$game_config$', path)
|
|
return file_reader
|
|
end
|
|
|
|
local parse_include_line
|
|
local parse_includes
|
|
|
|
parse_include_line = function(line)
|
|
if line:sub(1, 1) == "#" and line:find("#include") then
|
|
local include_file = line:match('"(.*)"')
|
|
if include_file then
|
|
-- printf("parsing include %s", include_file)
|
|
local include_file_xml = loadFile(include_file)
|
|
if include_file_xml then
|
|
line = parse_includes(include_file_xml, false)
|
|
end
|
|
end
|
|
end
|
|
return line
|
|
end
|
|
|
|
parse_includes = function(s, rootFile)
|
|
local res = ""
|
|
if rootFile then
|
|
for line in s:gmatch('[^\r\n]+') do
|
|
res = res .. parse_include_line(line) .. "\n"
|
|
end
|
|
else
|
|
local lua_s = ""
|
|
while not s:r_eof() do
|
|
lua_s = lua_s .. string.char(s:r_u8())
|
|
end
|
|
-- getFS():r_close(s)
|
|
for line in lua_s:gmatch('[^\r\n]+') do
|
|
res = res .. parse_include_line(line) .. "\n"
|
|
end
|
|
end
|
|
-- res = sanitize(res)
|
|
-- printf("%s", res)
|
|
return res
|
|
end
|
|
|
|
function SLAXML:parse(xml, options)
|
|
if not options then
|
|
options = { stripWhitespace = false, parseIncludes = false }
|
|
end
|
|
|
|
xml = sanitize(xml)
|
|
if options.parseIncludes then
|
|
xml = parse_includes(xml, true)
|
|
-- printf("%s", xml)
|
|
end
|
|
|
|
-- Cache references for maximum speed
|
|
local find, sub, gsub, char, push, pop, concat =
|
|
string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat
|
|
local first, last, match1, match2, match3, pos2, nsURI
|
|
local unpack = unpack or table.unpack
|
|
local pos = 1
|
|
local state = "text"
|
|
local textStart = 1
|
|
local currentElement = {}
|
|
local currentAttributes = {}
|
|
local currentAttributeCt -- manually track length since the table is re-used
|
|
local nsStack = {}
|
|
local anyElement = false
|
|
|
|
local utf8markers = { { 0x7FF, 192 }, { 0xFFFF, 224 }, { 0x1FFFFF, 240 } }
|
|
local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string
|
|
if decimal < 128 then
|
|
return char(decimal)
|
|
end
|
|
local charbytes = {}
|
|
for bytes, vals in ipairs(utf8markers) do
|
|
if decimal <= vals[1] then
|
|
for b = bytes + 1, 2, -1 do
|
|
local mod = decimal % 64
|
|
decimal = (decimal - mod) / 64
|
|
charbytes[b] = char(128 + mod)
|
|
end
|
|
charbytes[1] = char(vals[2] + decimal)
|
|
return concat(charbytes)
|
|
end
|
|
end
|
|
end
|
|
local entityMap = { ["lt"] = "<", ["gt"] = ">", ["amp"] = "&", ["quot"] = '"', ["apos"] = "'" }
|
|
local entitySwap = function(orig, n, s)
|
|
return entityMap[s] or n == "#" and utf8(tonumber("0" .. s)) or orig
|
|
end
|
|
local function unescape(str)
|
|
return gsub(str, "(&(#?)([%d%a]+);)", entitySwap)
|
|
end
|
|
|
|
local function finishText()
|
|
if first > textStart and self._call.text then
|
|
local text = sub(xml, textStart, first - 1)
|
|
if options.stripWhitespace then
|
|
text = gsub(text, "^%s+", "")
|
|
text = gsub(text, "%s+$", "")
|
|
if #text == 0 then
|
|
text = nil
|
|
end
|
|
end
|
|
if text then
|
|
self._call.text(unescape(text), false)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function findPI()
|
|
first, last, match1, match2 = find(xml, "^<%?([:%a_][:%w_.-]*) ?(.-)%?>", pos)
|
|
if first then
|
|
finishText()
|
|
if self._call.pi then
|
|
self._call.pi(match1, match2)
|
|
end
|
|
pos = last + 1
|
|
textStart = pos
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function findComment()
|
|
first, last, match1 = find(xml, "^<!%-%-(.-)%-%->", pos)
|
|
if first then
|
|
finishText()
|
|
if self._call.comment then
|
|
self._call.comment(match1)
|
|
end
|
|
pos = last + 1
|
|
textStart = pos
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function nsForPrefix(prefix)
|
|
if prefix == "xml" then
|
|
return "http://www.w3.org/XML/1998/namespace"
|
|
end -- http://www.w3.org/TR/xml-names/#ns-decl
|
|
for i = #nsStack, 1, -1 do
|
|
if nsStack[i][prefix] then
|
|
return nsStack[i][prefix]
|
|
end
|
|
end
|
|
error(("Cannot find namespace for prefix %s"):format(prefix))
|
|
end
|
|
|
|
local function startElement()
|
|
anyElement = true
|
|
first, last, match1 = find(xml, "^<([%a_][%w_.-]*)", pos)
|
|
if first then
|
|
currentElement[2] = nil -- reset the nsURI, since this table is re-used
|
|
currentElement[3] = nil -- reset the nsPrefix, since this table is re-used
|
|
finishText()
|
|
pos = last + 1
|
|
first, last, match2 = find(xml, "^:([%a_][%w_.-]*)", pos)
|
|
if first then
|
|
currentElement[1] = match2
|
|
currentElement[3] = match1 -- Save the prefix for later resolution
|
|
match1 = match2
|
|
pos = last + 1
|
|
else
|
|
currentElement[1] = match1
|
|
for i = #nsStack, 1, -1 do
|
|
if nsStack[i]["!"] then
|
|
currentElement[2] = nsStack[i]["!"]
|
|
break
|
|
end
|
|
end
|
|
end
|
|
currentAttributeCt = 0
|
|
push(nsStack, {})
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function findAttribute()
|
|
first, last, match1 = find(xml, "^%s+([:%a_][:%w_.-]*)%s*=%s*", pos)
|
|
if first then
|
|
pos2 = last + 1
|
|
first, last, match2 = find(xml, '^"([^<"]*)"', pos2) -- FIXME: disallow non-entity ampersands
|
|
if first then
|
|
pos = last + 1
|
|
match2 = unescape(match2)
|
|
else
|
|
first, last, match2 = find(xml, "^'([^<']*)'", pos2) -- FIXME: disallow non-entity ampersands
|
|
if first then
|
|
pos = last + 1
|
|
match2 = unescape(match2)
|
|
end
|
|
end
|
|
end
|
|
if match1 and match2 then
|
|
local currentAttribute = { match1, match2 }
|
|
local prefix, name = string.match(match1, "^([^:]+):([^:]+)$")
|
|
if prefix then
|
|
if prefix == "xmlns" then
|
|
nsStack[#nsStack][name] = match2
|
|
else
|
|
currentAttribute[1] = name
|
|
currentAttribute[4] = prefix
|
|
end
|
|
else
|
|
if match1 == "xmlns" then
|
|
nsStack[#nsStack]["!"] = match2
|
|
currentElement[2] = match2
|
|
end
|
|
end
|
|
currentAttributeCt = currentAttributeCt + 1
|
|
currentAttributes[currentAttributeCt] = currentAttribute
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function findCDATA()
|
|
first, last, match1 = find(xml, "^<!%[CDATA%[(.-)%]%]>", pos)
|
|
if first then
|
|
finishText()
|
|
if self._call.text then
|
|
self._call.text(match1, true)
|
|
end
|
|
pos = last + 1
|
|
textStart = pos
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function closeElement()
|
|
first, last, match1 = find(xml, "^%s*(/?)>", pos)
|
|
if first then
|
|
state = "text"
|
|
pos = last + 1
|
|
textStart = pos
|
|
|
|
-- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed
|
|
if currentElement[3] then
|
|
currentElement[2] = nsForPrefix(currentElement[3])
|
|
end
|
|
if self._call.startElement then
|
|
self._call.startElement(unpack(currentElement))
|
|
end
|
|
if self._call.attribute then
|
|
for i = 1, currentAttributeCt do
|
|
if currentAttributes[i][4] then
|
|
currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4])
|
|
end
|
|
self._call.attribute(unpack(currentAttributes[i]))
|
|
end
|
|
end
|
|
|
|
if match1 == "/" then
|
|
pop(nsStack)
|
|
if self._call.closeElement then
|
|
self._call.closeElement(unpack(currentElement))
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function findElementClose()
|
|
first, last, match1, match2 = find(xml, "^</([%a_][%w_.-]*)%s*>", pos)
|
|
if first then
|
|
nsURI = nil
|
|
for i = #nsStack, 1, -1 do
|
|
if nsStack[i]["!"] then
|
|
nsURI = nsStack[i]["!"]
|
|
break
|
|
end
|
|
end
|
|
else
|
|
first, last, match2, match1 = find(xml, "^</([%a_][%w_.-]*):([%a_][%w_.-]*)%s*>", pos)
|
|
if first then
|
|
nsURI = nsForPrefix(match2)
|
|
end
|
|
end
|
|
if first then
|
|
finishText()
|
|
if self._call.closeElement then
|
|
self._call.closeElement(match1, nsURI)
|
|
end
|
|
pos = last + 1
|
|
textStart = pos
|
|
pop(nsStack)
|
|
return true
|
|
end
|
|
end
|
|
|
|
while pos < #xml do
|
|
if state == "text" then
|
|
if not (findPI() or findComment() or findCDATA() or findElementClose()) then
|
|
if startElement() then
|
|
state = "attributes"
|
|
else
|
|
first, last = find(xml, "^[^<]+", pos)
|
|
pos = (first and last or pos) + 1
|
|
end
|
|
end
|
|
elseif state == "attributes" then
|
|
if not findAttribute() then
|
|
if not closeElement() then
|
|
error("Was in an element and couldn't find attributes or the close.")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not anyElement then
|
|
error("Parsing did not discover any elements")
|
|
end
|
|
if #nsStack > 0 then
|
|
error("Parsing ended with unclosed elements")
|
|
end
|
|
end
|
|
|
|
function SLAXML:dom(xml, opts)
|
|
if not opts then
|
|
opts = {}
|
|
end
|
|
local rich = not opts.simple
|
|
local push, pop = table.insert, table.remove
|
|
local doc = { type = "document", name = "#doc", kids = {} }
|
|
local current, stack = doc, { doc }
|
|
local builder = self:parser({
|
|
startElement = function(name, nsURI, nsPrefix)
|
|
local el = {
|
|
type = "element",
|
|
name = name,
|
|
kids = {},
|
|
el = rich and {} or nil,
|
|
attr = {},
|
|
nsURI = nsURI,
|
|
nsPrefix = nsPrefix,
|
|
parent = rich and current or nil,
|
|
}
|
|
if current == doc then
|
|
if doc.root then
|
|
printf(
|
|
("Encountered element '%s' when the document already has a root '%s' element"):format(
|
|
name,
|
|
doc.root.name
|
|
)
|
|
)
|
|
else
|
|
doc.root = rich and el or nil
|
|
end
|
|
end
|
|
push(current.kids, el)
|
|
if current.el then
|
|
push(current.el, el)
|
|
end
|
|
current = el
|
|
push(stack, el)
|
|
end,
|
|
attribute = function(name, value, nsURI, nsPrefix)
|
|
if not current or current.type ~= "element" then
|
|
error(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name, value))
|
|
end
|
|
local attr = {
|
|
type = "attribute",
|
|
name = name,
|
|
nsURI = nsURI,
|
|
nsPrefix = nsPrefix,
|
|
value = value,
|
|
parent = rich and current or nil,
|
|
}
|
|
if rich then
|
|
current.attr[name] = value
|
|
end
|
|
push(current.attr, attr)
|
|
end,
|
|
closeElement = function(name)
|
|
if current.name ~= name or current.type ~= "element" then
|
|
error(
|
|
("Received a close element notification for '%s' but was inside a '%s' %s"):format(
|
|
name,
|
|
current.name,
|
|
current.type
|
|
)
|
|
)
|
|
end
|
|
pop(stack)
|
|
current = stack[#stack]
|
|
end,
|
|
text = function(value, cdata)
|
|
-- documents may only have text node children that are whitespace: https://www.w3.org/TR/xml/#NT-Misc
|
|
if current.type == "document" and not value:find("^%s+$") then
|
|
error(
|
|
("Document has non-whitespace text at root: '%s'"):format(
|
|
value:gsub("[\r\n\t]", { ["\r"] = "\\r", ["\n"] = "\\n", ["\t"] = "\\t" })
|
|
)
|
|
)
|
|
end
|
|
push(current.kids, {
|
|
type = "text",
|
|
name = "#text",
|
|
cdata = cdata and true or nil,
|
|
value = value,
|
|
parent = rich and current or nil,
|
|
})
|
|
end,
|
|
comment = function(value)
|
|
push(
|
|
current.kids,
|
|
{ type = "comment", name = "#comment", value = value, parent = rich and current or nil }
|
|
)
|
|
end,
|
|
pi = function(name, value)
|
|
push(current.kids, { type = "pi", name = name, value = value, parent = rich and current or nil })
|
|
end,
|
|
})
|
|
builder:parse(xml, opts)
|
|
return doc
|
|
end
|
|
|
|
local escmap = {
|
|
["<"] = "<",
|
|
[">"] = ">",
|
|
["&"] = "&",
|
|
['"'] = """,
|
|
["'"] = "'",
|
|
}
|
|
local function esc(s)
|
|
return s:gsub('[<>&"]', escmap)
|
|
end
|
|
|
|
-- opts.indent: number of spaces, or string
|
|
function SLAXML:xml(n, opts)
|
|
opts = opts or {}
|
|
local out = {}
|
|
local tab = opts.indent and (type(opts.indent) == "number" and string.rep(" ", opts.indent) or opts.indent)
|
|
or ""
|
|
local ser = {}
|
|
local omit = {}
|
|
if opts.omit then
|
|
for _, s in ipairs(opts.omit) do
|
|
omit[s] = true
|
|
end
|
|
end
|
|
|
|
function ser.document(n)
|
|
for _, kid in ipairs(n.kids) do
|
|
if ser[kid.type] then
|
|
ser[kid.type](kid, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ser.pi(n, depth)
|
|
depth = depth or 0
|
|
table.insert(out, tab:rep(depth) .. "<?" .. n.name .. " " .. n.value .. "?>")
|
|
end
|
|
|
|
function ser.element(n, depth)
|
|
if n.nsURI and omit[n.nsURI] then
|
|
return
|
|
end
|
|
depth = depth or 0
|
|
local indent = tab:rep(depth)
|
|
local name = n.nsPrefix and n.nsPrefix .. ":" .. n.name or n.name
|
|
local result = indent .. "<" .. name
|
|
if n.attr and n.attr[1] then
|
|
local sorted = n.attr
|
|
if opts.sort then
|
|
sorted = {}
|
|
for i, a in ipairs(n.attr) do
|
|
sorted[i] = a
|
|
end
|
|
table.sort(sorted, function(a, b)
|
|
if a.nsPrefix and b.nsPrefix then
|
|
return a.nsPrefix == b.nsPrefix and a.name < b.name or a.nsPrefix < b.nsPrefix
|
|
elseif not (a.nsPrefix or b.nsPrefix) then
|
|
return a.name < b.name
|
|
elseif b.nsPrefix then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end)
|
|
end
|
|
|
|
local attrs = {}
|
|
for _, a in ipairs(sorted) do
|
|
if (not a.nsURI or not omit[a.nsURI]) and not (omit[a.value] and a.name:find("^xmlns:")) then
|
|
attrs[#attrs + 1] = " "
|
|
.. (a.nsPrefix and (a.nsPrefix .. ":") or "")
|
|
.. a.name
|
|
.. '="'
|
|
.. esc(a.value)
|
|
.. '"'
|
|
end
|
|
end
|
|
result = result .. table.concat(attrs, "")
|
|
end
|
|
result = result .. (n.kids and n.kids[1] and ">" or "/>")
|
|
table.insert(out, result)
|
|
if n.kids and n.kids[1] then
|
|
for _, kid in ipairs(n.kids) do
|
|
if ser[kid.type] then
|
|
ser[kid.type](kid, depth + 1)
|
|
end
|
|
end
|
|
table.insert(out, indent .. "</" .. name .. ">")
|
|
end
|
|
end
|
|
|
|
function ser.text(n, depth)
|
|
if n.cdata then
|
|
table.insert(out, tab:rep(depth) .. "<![CDATA[" .. n.value .. "]]>")
|
|
else
|
|
table.insert(out, tab:rep(depth) .. esc(n.value))
|
|
end
|
|
end
|
|
|
|
function ser.comment(n, depth)
|
|
table.insert(out, tab:rep(depth) .. "<!--" .. n.value .. "-->")
|
|
end
|
|
|
|
ser[n.type](n, 0)
|
|
|
|
return table.concat(out, opts.indent and "\n" or "")
|
|
end
|
|
|
|
-- demonized
|
|
-- Simple dom and xml for stalker needs
|
|
function SLAXML:simple_dom(xml, opts)
|
|
if not opts then
|
|
opts = {}
|
|
end
|
|
local push, pop = table.insert, table.remove
|
|
local doc = { el = "!doc", kids = {} }
|
|
local current, stack = doc, { doc }
|
|
local builder = self:parser({
|
|
startElement = function(name, nsURI, nsPrefix)
|
|
local el = {
|
|
el = "<" .. name,
|
|
kids = {},
|
|
parent = current,
|
|
}
|
|
push(current.kids, el)
|
|
push(stack, el)
|
|
current = el
|
|
end,
|
|
attribute = function(name, value, nsURI, nsPrefix)
|
|
current.el = current.el .. " " .. name .. "=" .. value
|
|
end,
|
|
closeElement = function(name)
|
|
pop(stack)
|
|
current = stack[#stack]
|
|
end,
|
|
text = function(value, cdata)
|
|
local el = {
|
|
el = "#" .. value,
|
|
parent = current,
|
|
}
|
|
push(current.kids, el)
|
|
end,
|
|
comment = function(value)
|
|
-- Strip comments
|
|
end,
|
|
pi = function(name, value)
|
|
local el = {
|
|
el = "?xml " .. trim(value),
|
|
}
|
|
push(current.kids, el)
|
|
end,
|
|
})
|
|
builder:parse(xml, opts)
|
|
return doc
|
|
end
|
|
|
|
function SLAXML:sanitize_attr_list(s)
|
|
local res
|
|
local t = str_explode_lim(s, " ", 1)
|
|
if not t[2] then
|
|
return s
|
|
end
|
|
|
|
if trim(t[2]) == "" then
|
|
return s
|
|
end
|
|
|
|
res = t[1]
|
|
t = str_explode(t[2], " ")
|
|
for i, v in ipairs(t) do
|
|
local attr = str_explode(v, "=")
|
|
if attr[2] then
|
|
res = res .. " " .. attr[1] .. '="' .. attr[2] .. '"'
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- opts.indent: number of spaces, or string
|
|
function SLAXML:simple_xml(n, opts)
|
|
opts = opts or {}
|
|
local out = {}
|
|
local tab = opts.indent and (type(opts.indent) == "number" and string.rep(" ", opts.indent) or opts.indent)
|
|
or ""
|
|
local ser = {}
|
|
local omit = {}
|
|
if opts.omit then
|
|
for _, s in ipairs(opts.omit) do
|
|
omit[s] = true
|
|
end
|
|
end
|
|
|
|
ser["!"] = function(n)
|
|
for _, kid in ipairs(n.kids) do
|
|
local t = kid.el:sub(1, 1)
|
|
if ser[t] then
|
|
ser[t](kid, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
ser["?"] = function(n, depth)
|
|
depth = depth or 0
|
|
local el = n.el:sub(2)
|
|
table.insert(out, tab:rep(depth) .. "<?" .. el .. "?>")
|
|
end
|
|
|
|
ser["<"] = function(n, depth)
|
|
depth = depth or 0
|
|
local indent = tab:rep(depth)
|
|
local el = self:sanitize_attr_list(n.el:sub(2))
|
|
local result = indent .. "<" .. el
|
|
result = result .. (n.kids and n.kids[1] and ">" or "/>")
|
|
table.insert(out, result)
|
|
if n.kids and n.kids[1] then
|
|
for _, kid in ipairs(n.kids) do
|
|
local t = kid.el:sub(1, 1)
|
|
if ser[t] then
|
|
ser[t](kid, depth + 1)
|
|
end
|
|
end
|
|
local tag = str_explode(el, " ")[1]
|
|
table.insert(out, indent .. "</" .. tag .. ">")
|
|
end
|
|
end
|
|
|
|
ser["#"] = function(n, depth)
|
|
local el = n.el:sub(2)
|
|
table.insert(out, tab:rep(depth) .. esc(el))
|
|
end
|
|
|
|
local t = n.el:sub(1, 1)
|
|
ser[t](n, 0)
|
|
|
|
return table.concat(out, opts.indent and "\n" or "")
|
|
end
|
|
|
|
return SLAXML
|
|
end
|