Quick actions

cmd+k|ctrl+k

Navigation

Languages

XmlParser

Snippet info

Language

Lua

Visibility

public

Author

thosewhoknow

Created

2023-08-21T08:52:05.262384Z

Updated

2024-01-11T19:08:41.852291Z

-- Generated using the... (v1.1.3-private)

local CompiledModule = {}
CompiledModule.exports = {}

-- dom.server.luau (240 lines)

do
local function init()
	return {
		options = {commentNode=1, piNode=1, dtdNode=1, declNode=1},
		current = { _children = {}, _type = "ROOT" },
		_stack = {}
	}
end

local dom = init()

function dom:new()
	local obj = init()

	obj.__index = self
	setmetatable(obj, self)

	return obj
end

---Parses a start tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the attributes of the tag
function dom:starttag(tag)
	local node = { _type = 'ELEMENT',
		_name = tag.name,
		_attr = tag.attrs,
		_children = {}
	}

	if not self.root then
		self.root = node
	end

	table.insert(self._stack, node)

	table.insert(self.current._children, node)
	self.current = node
end

---Parses an end tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the attributes of the tag
function dom:endtag(tag)
	--Table representing the containing tag of the current tag
	local prev = self._stack[#self._stack]

	if tag.name ~= prev._name then
		error("XML Error - Unmatched Tag")
	end

	table.remove(self._stack)
	self.current = self._stack[#self._stack]
	if not self.current then
		local node = { _children = {}, _type = "ROOT" }
		if self.decl then
			table.insert(node._children, self.decl)
			self.decl = nil
		end
		if self.dtd then
			table.insert(node._children, self.dtd)
			self.dtd = nil
		end
		if self.root then
			table.insert(node._children, self.root)
			self.root = node
		end
		self.current = node
	end
end

---Parses a tag content.
-- @param text text to process
function dom:text(text)
	local node = { _type = "TEXT",
		_text = text
	}
	table.insert(self.current._children, node)
end

---Parses a comment tag.
-- @param text comment text
function dom:comment(text)
	if self.options.commentNode then
		local node = { _type = "COMMENT",
			_text = text
		}
		table.insert(self.current._children, node)
	end
end

--- Parses a XML processing instruction (PI) tag
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the attributes of the tag
function dom:pi(tag)
	if self.options.piNode then
		local node = { _type = "PI",
			_name = tag.name,
			_attr = tag.attrs,
		}
		table.insert(self.current._children, node)
	end
end

---Parse the XML declaration line (the line that indicates the XML version).
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the attributes of the tag
function dom:decl(tag)
	if self.options.declNode then
		self.decl = { _type = "DECL",
			_name = tag.name,
			_attr = tag.attrs,
		}
	end
end

---Parses a DTD tag.
-- @param tag a {name, value} table
-- where name is the name of the tag and value
-- is a table containing the attributes of the tag
function dom:dtd(tag)
	if self.options.dtdNode then
		self.dtd = { _type = "DTD",
			_name = tag.name,
			_text = tag.value
		}
	end
end

--- XML escape characters for a TEXT node.
-- @param s a string
-- @return @p s XML escaped.
local function xmlEscape(s)
	s = string.gsub(s, '&', '&')
	s = string.gsub(s, '<', '&lt;')
	return string.gsub(s, '>', '&gt;')
end

--- return a string of XML attributes
-- @param tab table with XML attribute pairs. key and value are supposed to be strings.
-- @return a string.
local function attrsToStr(tab)
	if not tab then
		return ''
	end
	if type(tab) == 'table' then
		local s = ''
		for n,v in pairs(tab) do
			-- determine a safe quote character
			local val = tostring(v)
			local found_single_quote = string.find(val, "'")
			local found_double_quote = string.find(val, '"')
			local quot = '"'
			if found_single_quote and found_double_quote then
				-- XML escape both quote characters
				val = string.gsub(val, '"', '&quot;')
				val = string.gsub(val, "'", '&apos;')
			elseif found_double_quote then
				quot = "'"
			end
			s = ' ' .. tostring(n) .. '=' .. quot .. val .. quot
		end
		return s
	end
	return 'BUG:unknown type:' .. type(tab)
end

--- return a XML formatted string of @p node.
-- @param node a Node object (table) of the xml2lua DOM tree structure.
-- @return a string.
local function toXmlStr(node, indentLevel)
	if not node then
		return 'BUG:node==nil'
	end
	if not node._type then
		return 'BUG:node._type==nil'
	end

	local indent = ''
	for i=0, indentLevel+1, 1 do
		indent = indent .. ' '
	end

	if node._type == 'ROOT' then
		local s = ''
		for i, n in pairs(node._children) do
			s = s .. toXmlStr(n, indentLevel+2)
		end
		return s
	elseif node._type == 'ELEMENT' then
		local s = indent .. '<' .. node._name .. attrsToStr(node._attr)

		-- check if ELEMENT has no children
		if not node._children or
			#node._children == 0 then
			return s .. '/>\n'
		end

		s = s .. '>\n'

		for i, n in pairs(node._children) do
			local xx = toXmlStr(n, indentLevel+2)
			if not xx then
				print('BUG:xx==nil')
			else
				s = s .. xx
			end
		end

		return s .. indent .. '</' .. node._name .. '>\n'

	elseif node._type == 'TEXT' then
		return indent .. xmlEscape(node._text) .. '\n'
	elseif node._type == 'COMMENT' then
		return indent .. '<!--' .. node._text .. '-->\n'
	elseif node._type == 'PI' then
		return indent .. '<?' .. node._name .. ' ' .. node._attr._text .. '?>\n'
	elseif node._type == 'DECL' then
		return indent .. '<?' .. node._name .. attrsToStr(node._attr) .. '?>\n'
	elseif node._type == 'DTD' then
		return indent .. '<!' .. node._name .. ' ' .. node._text .. '>\n'
	end
	return 'BUG:unknown type:' .. tostring(node._type)
end

---create a string in XML format from the dom root object @p node.
-- @param node a root object, typically created with `dom` XML parser handler.
-- @return a string, XML formatted.
function dom:toXml(node)
	return toXmlStr(node, -4)
end

---Parses CDATA tag content.
dom.cdata = dom.text
dom.__index = dom
table.insert(CompiledModule.exports, dom)

end
-- print.server.luau (110 lines)

do
---@module Handler to generate a simple event trace which 
--outputs messages to the terminal during the XML
--parsing, usually for debugging purposes.
--
--  License:
--  ========
--
--      This code is freely distributable under the terms of the [MIT license](LICENSE).
--
--@author Paul Chakravarti ([email protected])
--@author Manoel Campos da Silva Filho
local _print = print
local io = {write = _print}
local print = {}

---Parses a start tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs 
-- is a table containing the atributtes of the tag
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:starttag(tag, s, e) 
	io.write("Start    : "..tag.name.."\n") 
	if tag.attrs then 
		for k,v in pairs(tag.attrs) do 
			io.write(string.format(" + %s='%s'\n", k, v))
		end 
	end
end

---Parses an end tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs 
-- is a table containing the atributtes of the tag
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:endtag(tag, s, e) 
	io.write("End      : "..tag.name.."\n") 
end

---Parses a tag content.
-- @param text text to process
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:text(text, s, e)
	io.write("Text     : "..text.."\n") 
end

---Parses CDATA tag content.
-- @param text CDATA content to be processed
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:cdata(text, s, e)
	io.write("CDATA    : "..text.."\n") 
end

---Parses a comment tag.
-- @param text comment text
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:comment(text, s, e)
	io.write("Comment  : "..text.."\n") 
end

---Parses a DTD tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs 
-- is a table containing the atributtes of the tag
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:dtd(tag, s, e)     
	io.write("DTD      : "..tag.name.."\n") 
	if tag.attrs then 
		for k,v in pairs(tag.attrs) do 
			io.write(string.format(" + %s='%s'\n", k, v))
		end 
	end
end

--- Parse a XML processing instructions (PI) tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs 
-- is a table containing the atributtes of the tag
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:pi(tag, s, e) 
	io.write("PI       : "..tag.name.."\n")
	if tag.attrs then 
		for k,v in pairs(tag.attrs) do 
			io. write(string.format(" + %s='%s'\n",k,v))
		end 
	end
end

---Parse the XML declaration line (the line that indicates the XML version).
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs 
-- is a table containing the atributtes of the tag
-- @param s position where the tag starts
-- @param e position where the tag ends
function print:decl(tag, s, e) 
	io.write("XML Decl : "..tag.name.."\n")
	if tag.attrs then 
		for k,v in pairs(tag.attrs) do 
			io.write(string.format(" + %s='%s'\n", k, v))
		end 
	end
end

table.insert(CompiledModule.exports, print)
end
-- tree.server.luau (171 lines)

do
local function init()
	local obj = {
		root = {},
		options = {noreduce = {}}
	}

	obj._stack = {obj.root}  
	return obj  
end

--- @module XML Tree Handler.
-- Generates a lua table from an XML content string.
-- It is a simplified handler which attempts
-- to generate a more 'natural' table based structure which
-- supports many common XML formats.
--
-- The XML tree structure is mapped directly into a recursive
-- table structure with node names as keys and child elements
-- as either a table of values or directly as a string value
-- for text. Where there is only a single child element this
-- is inserted as a named key - if there are multiple
-- elements these are inserted as a vector (in some cases it
-- may be preferable to always insert elements as a vector
-- which can be specified on a per element basis in the
-- options).  Attributes are inserted as a child element with
-- a key of '_attr'.
--
-- Only Tag/Text & CDATA elements are processed - all others
-- are ignored.
--
-- This format has some limitations - primarily
-- 
-- * Mixed-Content behaves unpredictably - the relationship
--   between text elements and embedded tags is lost and
--   multiple levels of mixed content does not work
-- * If a leaf element has both a text element and attributes
--   then the text must be accessed through a vector (to
--   provide a container for the attribute)
--
-- In general however this format is relatively useful.
--
-- It is much easier to understand by running some test
-- data through 'testxml.lua -simpletree' than to read this)
--
-- Options
-- =======
--    options.noreduce = { <tag> = bool,.. }
--        - Nodes not to reduce children vector even if only
--          one child
--
--  License:
--  ========
--
--  This code is freely distributable under the terms of the [MIT license](LICENSE).
--
--@author Paul Chakravarti ([email protected])
--@author Manoel Campos da Silva Filho
local tree = init()

---Instantiates a new handler object.
--Each instance can handle a single XML.
--By using such a constructor, you can parse
--multiple XML files in the same application.
--@return the handler instance
function tree:new()
	local obj = init()

	obj.__index = self
	setmetatable(obj, self)

	return obj
end

--- Recursively removes redundant vectors for nodes
-- with single child elements
function tree:reduce(node, key, parent)
	for k,v in pairs(node) do
		if type(v) == 'table' then
			self:reduce(v,k,node)
		end
	end
	if #node == 1 and not self.options.noreduce[key] and 
		node._attr == nil then
		parent[key] = node[1]
	end
end


--- If an object is not an array,
-- creates an empty array and insert that object as the 1st element.
--
-- It's a workaround for duplicated XML tags outside an inner tag. Check issue #55 for details.
-- It checks if a given tag already exists on the parsing stack.
-- In such a case, if that tag is represented as a single element,
-- an array is created and that element is inserted on it.
-- The existing tag is then replaced by the created array.
-- For instance, if we have a tag x = {attr1=1, attr2=2}
-- and another x tag is found, the previous entry will be changed to an array
-- x = {{attr1=1, attr2=2}}. This way, the duplicated tag will be
-- inserted into this array as x = {{attr1=1, attr2=2}, {attr1=3, attr2=4}}
-- https://github.com/manoelcampos/xml2lua/issues/55
--
-- @param obj the object to try to convert to an array
-- @return the same object if it's already an array or a new array with the object
--         as the 1st element.
local function convertObjectToArray(obj)
	--#obj == 0 verifies if the field is not an array
	if #obj == 0 then
		local array = {}
		table.insert(array, obj)
		return array
	end

	return obj
end

---Parses a start tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the atributtes of the tag
function tree:starttag(tag)
	local node = {}
	if self.parseAttributes == true then
		node._attr=tag.attrs
	end

	--Table in the stack representing the tag being processed
	local current = self._stack[#self._stack]

	if current[tag.name] then
		local array = convertObjectToArray(current[tag.name])
		table.insert(array, node)
		current[tag.name] = array
	else
		current[tag.name] = {node}
	end

	table.insert(self._stack, node)
end

---Parses an end tag.
-- @param tag a {name, attrs} table
-- where name is the name of the tag and attrs
-- is a table containing the atributtes of the tag
function tree:endtag(tag, s)
	--Table in the stack representing the tag being processed
	--Table in the stack representing the containing tag of the current tag
	local prev = self._stack[#self._stack-1]
	if not prev[tag.name] then
		error("XML Error - Unmatched Tag ["..s..":"..tag.name.."]\n")
	end
	if prev == self.root then
		-- Once parsing complete, recursively reduce tree
		self:reduce(prev, nil, nil)
	end

	table.remove(self._stack)
end

---Parses a tag content.
-- @param t text to process
function tree:text(text)
	local current = self._stack[#self._stack]
	table.insert(current, text)
end

---Parses CDATA tag content.
tree.cdata = tree.text
tree.__index = tree
table.insert(CompiledModule.exports, tree)

end

return unpack(CompiledModule.exports)
INFO