diff options
| author | ruki <[email protected]> | 2025-11-15 00:40:08 +0800 |
|---|---|---|
| committer | ruki <[email protected]> | 2025-11-15 00:40:08 +0800 |
| commit | dc6a23d0752243d950b81628124fcaa9b907dabe (patch) | |
| tree | 04d65b63f6062c0e15636da283edfd04f65df6be /xmake/core/base/xml.lua | |
| parent | e8aa6bd86dcfc67fb2fd5d9bb4319a956097f9c1 (diff) | |
add new apis
Diffstat (limited to 'xmake/core/base/xml.lua')
| -rw-r--r-- | xmake/core/base/xml.lua | 311 |
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 "" |
