-- SLAXML parser -- Adapted for Anomaly usage by demonized --[=====================================================================[ v0.8 Copyright © 2013-2018 Gavin Kistner ; 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("", target, content)) end, comment = function(content) print(string.format("", 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("") end, }, _defcall = { startElement = function(name, nsURI, nsPrefix) end, -- When "" or 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. "" }, } 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, "^", 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, "^", 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, "^", 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) .. "") 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 .. "") end end function ser.text(n, depth) if n.cdata then table.insert(out, tab:rep(depth) .. "") else table.insert(out, tab:rep(depth) .. esc(n.value)) end end function ser.comment(n, depth) table.insert(out, tab:rep(depth) .. "") 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) .. "") 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 .. "") 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