summaryrefslogtreecommitdiff
path: root/xmake/core/base/xml.lua
diff options
context:
space:
mode:
authorruki <[email protected]>2025-11-15 00:40:08 +0800
committerruki <[email protected]>2025-11-15 00:40:08 +0800
commitdc6a23d0752243d950b81628124fcaa9b907dabe (patch)
tree04d65b63f6062c0e15636da283edfd04f65df6be /xmake/core/base/xml.lua
parente8aa6bd86dcfc67fb2fd5d9bb4319a956097f9c1 (diff)
add new apis
Diffstat (limited to 'xmake/core/base/xml.lua')
-rw-r--r--xmake/core/base/xml.lua311
1 files changed, 249 insertions, 62 deletions
diff --git a/xmake/core/base/xml.lua b/xmake/core/base/xml.lua
index 84da8e16b..07260959c 100644
--- a/xmake/core/base/xml.lua
+++ b/xmake/core/base/xml.lua
@@ -97,11 +97,81 @@ function xml._handle_closing(stack, tagname)
return nil, string.format("malformed xml: unexpected closing </%s>", tagname)
end
table.remove(stack)
- return true
+ return top
+end
+
+-- compute indent string for pretty output
+function xml._indent(opt, level)
+ if not opt.pretty then
+ return ""
+ end
+ local indent = opt.indent or 4
+ local indentchar = opt.indentchar or " "
+ if type(indent) == "number" then
+ return string.rep(indentchar, indent * level)
+ end
+ return indent:rep(level)
+end
+
+function xml._encode_node(node, opt, level)
+ opt = opt or {}
+ level = level or 0
+ if node.kind == "document" then
+ local parts = {}
+ if node.children then
+ for _, child in ipairs(node.children) do
+ table.insert(parts, xml._encode_node(child, opt, level))
+ end
+ end
+ return table.concat(parts, opt.pretty and "\n" or "")
+ elseif node.kind == "text" then
+ local indent = xml._indent(opt, level)
+ local text = xml._encode_text(tostring(node.text or ""))
+ if opt.pretty then
+ return indent .. text
+ end
+ return text
+ elseif node.kind == "comment" then
+ return xml._indent(opt, level) .. string.format("<!--%s-->", tostring(node.text or ""))
+ elseif node.kind == "cdata" then
+ return xml._indent(opt, level) .. string.format("<![CDATA[%s]]>", tostring(node.text or ""))
+ elseif node.kind == "doctype" then
+ return xml._indent(opt, level) .. string.format("<!DOCTYPE %s>", tostring(node.text or ""))
+ end
+ local attrs = {}
+ for k, v in pairs(node.attrs or {}) do
+ table.insert(attrs, string.format('%s="%s"', k, xml._encode_attr(tostring(v))))
+ end
+ table.sort(attrs)
+ local open = "<" .. node.name
+ if #attrs > 0 then
+ open = open .. " " .. table.concat(attrs, " ")
+ end
+ if not node.children or #node.children == 0 then
+ return xml._indent(opt, level) .. open .. "/>"
+ end
+ local newline = opt.pretty and "\n" or ""
+ if #node.children == 1 and node.children[1].kind == "text" then
+ local text = xml._encode_text(tostring(node.children[1].text or ""))
+ return string.format("%s%s>%s</%s>", xml._indent(opt, level), open, text, node.name)
+ end
+ local result = {}
+ table.insert(result, xml._indent(opt, level) .. open .. ">")
+ if node.children then
+ for _, child in ipairs(node.children) do
+ table.insert(result, xml._encode_node(child, opt, level + 1))
+ end
+ end
+ table.insert(result, xml._indent(opt, level) .. "</" .. node.name .. ">")
+ return table.concat(result, newline ~= "" and newline or "")
end
-- create an xml element node
-- e.g. `local node = xml.new({name = "item", attrs = {id = "1"}, children = {xml.text("value")}})`
+--
+-- @param opt table with name/attrs/children/kind/text fields
+-- @return node table
+--
function xml.new(opt)
opt = opt or {}
return {
@@ -115,36 +185,62 @@ end
-- create a text node
-- e.g. `local textnode = xml.text("hello")`
+--
+-- @param value string content
+-- @return text node
+--
function xml.text(value)
return xml.new({kind = "text", text = value or ""})
end
-- create an empty element node
-- e.g. `local br = xml.empty("br", {class = "line"})`
+--
+-- @param name element name
+-- @param attrs attribute table
+-- @return element node
+--
function xml.empty(name, attrs)
return xml.new({name = name, attrs = attrs})
end
-- create a comment node
-- e.g. `local comment = xml.comment("generated by xmake")`
+--
+-- @param value comment text
+-- @return comment node
+--
function xml.comment(value)
return xml.new({kind = "comment", text = value or ""})
end
--- create a CDATA node
+-- create a cdata node
-- e.g. `local cdata = xml.cdata("if (a < b) { ... }")`
+--
+-- @param value cdata text
+-- @return cdata node
+--
function xml.cdata(value)
return xml.new({kind = "cdata", text = value or ""})
end
-- create a doctype node
-- e.g. `local doc = xml.doctype('html')`
+--
+-- @param value doctype payload
+-- @return doctype node
+--
function xml.doctype(value)
return xml.new({kind = "doctype", text = value or ""})
end
-- decode xml string to tree node(s)
-- e.g. `local doc, err = xml.decode("<root><item>foo</item></root>")`
+--
+-- @param data xml string
+-- @param opt options (trim_text, etc.)
+-- @return root node or list on success, nil + error on failure
+--
function xml.decode(data, opt)
opt = opt or {}
local root_children = {}
@@ -221,8 +317,8 @@ function xml.decode(data, opt)
return nil, "unterminated closing tag"
end
local tagname = data:sub(lt + 2, close - 1):match("^%s*([^%s>]+)")
- local ok, err = xml._handle_closing(stack, tagname)
- if not ok then
+ local node, err = xml._handle_closing(stack, tagname)
+ if not node then
return nil, err
end
i = close + 1
@@ -273,74 +369,146 @@ function xml.decode(data, opt)
return root_children
end
--- compute indent string for pretty output
-function xml._indent(opt, level)
- if not opt.pretty then
- return ""
- end
- local indent = opt.indent or 4
- local indentchar = opt.indentchar or " "
- if type(indent) == "number" then
- return string.rep(indentchar, indent * level)
- end
- return indent:rep(level)
-end
-
-function xml._encode_node(node, opt, level)
+-- stream parse xml data
+--
+-- @param data xml string
+-- @param callback function(node) -> true|false (return false to stop scanning)
+-- @param opt options (trim_text, etc.)
+-- @return true on success or nil, error on failure
+--
+function xml.scan(data, callback, opt)
opt = opt or {}
- level = level or 0
- if node.kind == "document" then
- local parts = {}
- if node.children then
- for _, child in ipairs(node.children) do
- table.insert(parts, xml._encode_node(child, opt, level))
+ local root_children = {}
+ local doc_node = {kind = "document", children = root_children}
+ local stack = {doc_node}
+ local stop = false
+ local function ensure_children(node)
+ if not node.children then
+ if node == doc_node then
+ node.children = root_children
+ else
+ node.children = {}
end
end
- return table.concat(parts, opt.pretty and "\n" or "")
- elseif node.kind == "text" then
- local indent = xml._indent(opt, level)
- local text = xml._encode_text(tostring(node.text or ""))
- if opt.pretty then
- return indent .. text
- end
- return text
- elseif node.kind == "comment" then
- return xml._indent(opt, level) .. string.format("<!--%s-->", tostring(node.text or ""))
- elseif node.kind == "cdata" then
- return xml._indent(opt, level) .. string.format("<![CDATA[%s]]>", tostring(node.text or ""))
- elseif node.kind == "doctype" then
- return xml._indent(opt, level) .. string.format("<!DOCTYPE %s>", tostring(node.text or ""))
- end
- local attrs = {}
- for k, v in pairs(node.attrs or {}) do
- table.insert(attrs, string.format('%s="%s"', k, xml._encode_attr(tostring(v))))
- end
- table.sort(attrs)
- local open = "<" .. node.name
- if #attrs > 0 then
- open = open .. " " .. table.concat(attrs, " ")
- end
- if not node.children or #node.children == 0 then
- return xml._indent(opt, level) .. open .. "/>"
+ return node.children
end
- local newline = opt.pretty and "\n" or ""
- if #node.children == 1 and node.children[1].kind == "text" then
- local text = xml._encode_text(tostring(node.children[1].text or ""))
- return string.format("%s%s>%s</%s>", xml._indent(opt, level), open, text, node.name)
+ local function emit(node)
+ if callback and node.kind ~= "document" then
+ if callback(node) == false then
+ stop = true
+ end
+ end
end
- local result = {}
- table.insert(result, xml._indent(opt, level) .. open .. ">")
- if node.children then
- for _, child in ipairs(node.children) do
- table.insert(result, xml._encode_node(child, opt, level + 1))
+ local i = 1
+ local len = #data
+ while i <= len do
+ if stop then
+ break
+ end
+ local lt = data:find("<", i, true)
+ if not lt then
+ local text = data:sub(i)
+ xml._append_text(stack, text, opt)
+ break
+ end
+ if lt > i then
+ local text = data:sub(i, lt - 1)
+ xml._append_text(stack, text, opt)
+ end
+ if data:sub(lt + 1, lt + 3) == "!--" then
+ local close = data:find("-->", lt + 4, true)
+ if not close then
+ return nil, "unterminated xml comment"
+ end
+ local value = data:sub(lt + 4, close - 1)
+ local top = stack[#stack]
+ local children = ensure_children(top)
+ local node = xml.comment(value)
+ table.insert(children, node)
+ emit(node)
+ i = close + 3
+ elseif data:sub(lt + 1, lt + 8) == "![CDATA[" then
+ local close = data:find("]]>", lt + 9, true)
+ if not close then
+ return nil, "unterminated cdata section"
+ end
+ local value = data:sub(lt + 9, close - 1)
+ local top = stack[#stack]
+ local children = ensure_children(top)
+ local node = xml.cdata(value)
+ table.insert(children, node)
+ emit(node)
+ i = close + 3
+ elseif data:sub(lt + 1, lt + 8):upper() == "!DOCTYPE" then
+ local close = data:find(">", lt + 8)
+ if not close then
+ return nil, "unterminated doctype declaration"
+ end
+ local value = data:sub(lt + 9, close - 1)
+ local top = stack[#stack]
+ local children = ensure_children(top)
+ local node = xml.doctype(value)
+ table.insert(children, node)
+ emit(node)
+ i = close + 1
+ elseif data:sub(lt + 1, lt + 1) == "?" then
+ local close = data:find("?>", lt + 2, true)
+ if not close then
+ return nil, "unterminated xml declaration"
+ end
+ i = close + 2
+ elseif data:sub(lt + 1, lt + 1) == "!" then
+ local close = data:find(">", lt + 2)
+ if not close then
+ return nil, "unterminated xml declaration"
+ end
+ i = close + 1
+ elseif data:sub(lt + 1, lt + 1) == "/" then
+ local close = data:find(">", lt + 1)
+ if not close then
+ return nil, "unterminated closing tag"
+ end
+ local tagname = data:sub(lt + 2, close - 1):match("^%s*([^%s>]+)")
+ local node, err = xml._handle_closing(stack, tagname)
+ if not node then
+ return nil, err
+ end
+ emit(node)
+ i = close + 1
+ else
+ local close = data:find(">", lt + 1)
+ if not close then
+ return nil, "unterminated opening tag"
+ end
+ local inside = data:sub(lt + 1, close - 1)
+ local selfclose = inside:find("/%s*$")
+ if selfclose then
+ inside = inside:gsub("/%s*$", "")
+ end
+ local tagname, attrstr = inside:match("^%s*([^%s>]+)%s*(.-)%s*$")
+ local attrs = xml._parse_attrs(attrstr or "")
+ local node = xml.empty(tagname, attrs)
+ local top = stack[#stack]
+ local children = ensure_children(top)
+ table.insert(children, node)
+ if not selfclose then
+ table.insert(stack, node)
+ else
+ emit(node)
+ end
+ i = close + 1
end
end
- table.insert(result, xml._indent(opt, level) .. "</" .. node.name .. ">")
- return table.concat(result, newline ~= "" and newline or "")
+ return true
end
-- encode xml node to string
-- e.g. `local xmlstr = xml.encode(node, {pretty = true, indent = 2})`
+--
+-- @param node xml node
+-- @param opt options (pretty, indent, etc.)
+-- @return xml string or nil, err
+--
function xml.encode(node, opt)
opt = opt or {}
local fragments = {}
@@ -359,6 +527,11 @@ end
-- load xml file
-- e.g. `local doc, err = xml.load("foo.xml")`
+--
+-- @param filepath file path
+-- @param opt read/decode options
+-- @return node or nil + error
+--
function xml.load(filepath, opt)
local data, err = io.readfile(filepath, opt)
if not data then
@@ -369,6 +542,12 @@ end
-- save xml node to file
-- e.g. `assert(xml.save("foo.xml", node, {pretty = true}))`
+--
+-- @param filepath destination file
+-- @param node xml node
+-- @param opt encode/write options
+-- @return true on success or nil + error message
+--
function xml.save(filepath, node, opt)
local data = xml.encode(node, opt)
if not data then
@@ -378,7 +557,11 @@ function xml.save(filepath, node, opt)
end
-- find the first matching node by name or path (e.g. "root/item/subitem")
--- returns nil if not found
+--
+-- @param node root node
+-- @param path slash separated string
+-- @return first matched node or nil
+--
function xml.find(node, path)
if not node or not path or path == "" then
return nil
@@ -416,6 +599,10 @@ end
-- get concatenated text from child nodes
-- e.g. `local text = xml.text_of(xml.decode("<item>foo</item>"))`
+--
+-- @param node xml node
+-- @return concatenated string
+--
function xml.text_of(node)
if not node or not node.children then
return ""