summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2025-11-15 00:54:20 +0800
committerruki <[email protected]>2025-11-15 00:54:20 +0800
commitd6b3bc59cad34b6279f3df8a2066505ca82a2733 (patch)
treefc5b93ceb41573e14c96c54cc99e1d9f9ec7c11f
parent29fbfdef99861931d81578eac3434f452efddb8e (diff)
improve xml module
-rw-r--r--tests/modules/xml/test.lua30
-rw-r--r--xmake/core/base/xml.lua91
-rw-r--r--xmake/core/sandbox/modules/import/core/base/xml.lua4
3 files changed, 110 insertions, 15 deletions
diff --git a/tests/modules/xml/test.lua b/tests/modules/xml/test.lua
index eb01aad86..80ce486f6 100644
--- a/tests/modules/xml/test.lua
+++ b/tests/modules/xml/test.lua
@@ -7,13 +7,18 @@ function test_parse_basic(t)
t:are_equal(#doc.children, 2)
t:are_equal(doc.children[1].name, "item")
t:are_equal(xml.text_of(doc.children[1]), "foo")
+ t:are_equal(doc.children[1].attrs, nil)
t:are_equal(doc.children[2].attrs.id, "2")
end
function test_encode(t)
- local doc = xml.new("root", {id = "1"}, {
- xml.new("item", {}, {xml.text("foo")}),
- xml.new("item", {id = "2"})
+ local doc = xml.new({
+ name = "root",
+ attrs = {id = "1"},
+ children = {
+ xml.new({name = "item", children = {xml.text("foo")}}),
+ xml.new({name = "item", attrs = {id = "2"}})
+ }
})
local compact = xml.encode(doc)
t:are_equal(compact, '<root id="1"><item>foo</item><item id="2"/></root>')
@@ -27,3 +32,22 @@ function test_encode(t)
t:are_equal(pretty, expected)
end
+function test_special_nodes(t)
+ t:are_equal(xml.encode(xml.comment("note")), "<!--note-->")
+ t:are_equal(xml.encode(xml.cdata("a < b")), "<![CDATA[a < b]]>")
+ t:are_equal(xml.encode(xml.doctype("note SYSTEM \"note.dtd\"")), "<!DOCTYPE note SYSTEM \"note.dtd\">")
+ t:are_equal(xml.encode(xml.empty("br")), "<br/>")
+end
+
+function test_parse_special_nodes(t)
+ local doc = xml.decode([[<root><!--note--><![CDATA[a < b]]><child/></root>]])
+ t:are_equal(doc.children[1].name, "__comment__")
+ t:are_equal(doc.children[1].children[1], "note")
+ t:are_equal(doc.children[2].name, "__cdata__")
+ t:are_equal(doc.children[2].children[1], "a < b")
+ t:are_equal(doc.children[3].name, "child")
+ local nodes = xml.decode([[<!DOCTYPE note SYSTEM "note.dtd"><root/>]])
+ t:are_equal(nodes[1].name, "__doctype__")
+ t:are_equal(nodes[2].name, "root")
+end
+
diff --git a/xmake/core/base/xml.lua b/xmake/core/base/xml.lua
index 7ac12f133..a3f2c96f0 100644
--- a/xmake/core/base/xml.lua
+++ b/xmake/core/base/xml.lua
@@ -45,25 +45,55 @@ function xml._encode_attr(str)
end
function xml._parse_attrs(attrstr)
- local attrs = {}
+ local attrs
attrstr:gsub("([%w_:%-%.]+)%s*=%s*([\"'])(.-)%2", function(key, quote, value)
+ attrs = attrs or {}
attrs[key] = xml._decode_entities(value)
end)
return attrs
end
-- create an xml element node
-function xml.new(name, attrs, children)
+-- e.g. `local node = xml.new({name = "item", attrs = {id = "1"}, children = {xml.text("value")}})`
+function xml.new(opt)
+ opt = opt or {}
return {
- name = name,
- attrs = attrs or {},
- children = children or {}
+ name = opt.name,
+ attrs = opt.attrs,
+ kind = opt.kind or "element",
+ text = opt.text,
+ children = opt.children or {}
}
end
-- create a text node
+-- e.g. `local textnode = xml.text("hello")`
function xml.text(value)
- return {type = "text", text = value or ""}
+ return xml.new({kind = "text", text = value or ""})
+end
+
+-- create an empty element node
+-- e.g. `local br = xml.empty("br", {class = "line"})`
+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")`
+function xml.comment(value)
+ return xml.new({kind = "comment", text = value or ""})
+end
+
+-- create a CDATA node
+-- e.g. `local cdata = xml.cdata("if (a < b) { ... }")`
+function xml.cdata(value)
+ return xml.new({kind = "cdata", text = value or ""})
+end
+
+-- create a doctype node
+-- e.g. `local doc = xml.doctype('html')`
+function xml.doctype(value)
+ return xml.new({kind = "doctype", text = value or ""})
end
function xml._append_text(stack, text, opt)
@@ -74,7 +104,7 @@ function xml._append_text(stack, text, opt)
if text ~= "" then
local top = stack[#stack]
top.children = top.children or {}
- table.insert(top.children, {type = "text", text = xml._decode_entities(text)})
+ table.insert(top.children, xml.text(xml._decode_entities(text)))
end
end
@@ -88,6 +118,7 @@ function xml._handle_closing(stack, tagname)
end
-- decode xml string to tree node(s)
+-- e.g. `local doc, err = xml.decode("<root><item>foo</item></root>")`
function xml.decode(data, opt)
opt = opt or {}
local root = {name = "__root__", attrs = {}, children = {}}
@@ -110,7 +141,31 @@ function xml.decode(data, opt)
if not close then
return nil, "unterminated xml comment"
end
+ local value = data:sub(lt + 4, close - 1)
+ local top = stack[#stack]
+ top.children = top.children or {}
+ table.insert(top.children, xml.comment(value))
+ 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]
+ top.children = top.children or {}
+ table.insert(top.children, xml.cdata(value))
i = close + 3
+ elseif data:sub(lt + 1, lt + 9):upper() == "!DOCTYPE" then
+ local close = data:find(">", lt + 9)
+ if not close then
+ return nil, "unterminated doctype declaration"
+ end
+ local value = data:sub(lt + 10, close - 1)
+ local top = stack[#stack]
+ top.children = top.children or {}
+ table.insert(top.children, xml.doctype(value))
+ i = close + 1
elseif data:sub(lt + 1, lt + 1) == "?" then
local close = data:find("?>", lt + 2, true)
if not close then
@@ -146,7 +201,7 @@ function xml.decode(data, opt)
end
local tagname, attrstr = inside:match("^%s*([^%s>]+)%s*(.-)%s*$")
local attrs = xml._parse_attrs(attrstr or "")
- local node = {name = tagname, attrs = attrs, children = {}}
+ local node = xml.empty(tagname, attrs)
local top = stack[#stack]
top.children = top.children or {}
table.insert(top.children, node)
@@ -180,13 +235,19 @@ end
function xml._encode_node(node, opt, level)
opt = opt or {}
level = level or 0
- if node.type == "text" then
+ if 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
@@ -201,7 +262,7 @@ function xml._encode_node(node, opt, level)
return xml._indent(opt, level) .. open .. "/>"
end
local newline = opt.pretty and "\n" or ""
- if #node.children == 1 and node.children[1].type == "text" then
+ 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
@@ -215,12 +276,14 @@ function xml._encode_node(node, opt, level)
end
-- encode xml node to string
+-- e.g. `local xmlstr = xml.encode(node, {pretty = true, indent = 2})`
function xml.encode(node, opt)
opt = opt or {}
return xml._encode_node(node, opt, 0)
end
-- load xml file
+-- e.g. `local doc, err = xml.load("foo.xml")`
function xml.load(filepath, opt)
local data, err = io.readfile(filepath, opt)
if not data then
@@ -230,6 +293,7 @@ function xml.load(filepath, opt)
end
-- save xml node to file
+-- e.g. `assert(xml.save("foo.xml", node, {pretty = true}))`
function xml.save(filepath, node, opt)
local data = xml.encode(node, opt)
if not data then
@@ -239,6 +303,8 @@ function xml.save(filepath, node, opt)
end
-- find the first child node with the given name
+-- e.g. `local doc = xml.decode("<root><item id='1'/></root>")`
+-- `local item = xml.find(doc, "item")`
function xml.find(node, name)
if not node or not node.children then
return nil
@@ -251,14 +317,15 @@ function xml.find(node, name)
end
-- get concatenated text from child nodes
+-- e.g. `local text = xml.text_of(xml.decode("<item>foo</item>"))`
function xml.text_of(node)
if not node or not node.children then
return ""
end
local buffer = {}
for _, child in ipairs(node.children) do
- if child.type == "text" then
- table.insert(buffer, child.text)
+ if child.kind == "text" then
+ table.insert(buffer, child.text or "")
end
end
return table.concat(buffer, "")
diff --git a/xmake/core/sandbox/modules/import/core/base/xml.lua b/xmake/core/sandbox/modules/import/core/base/xml.lua
index 7867c3295..6af763e47 100644
--- a/xmake/core/sandbox/modules/import/core/base/xml.lua
+++ b/xmake/core/sandbox/modules/import/core/base/xml.lua
@@ -31,6 +31,10 @@ sandbox_core_base_xml.find = xml.find
sandbox_core_base_xml.text_of = xml.text_of
sandbox_core_base_xml.text = xml.text
sandbox_core_base_xml.new = xml.new
+sandbox_core_base_xml.empty = xml.empty
+sandbox_core_base_xml.comment = xml.comment
+sandbox_core_base_xml.cdata = xml.cdata
+sandbox_core_base_xml.doctype = xml.doctype
-- decode xml data
function sandbox_core_base_xml.decode(data, opt)