diff options
author | steve donovan <steve.j.donovan@gmail.com> | 2011-10-04 16:42:36 +0400 |
---|---|---|
committer | steve donovan <steve.j.donovan@gmail.com> | 2011-10-04 16:42:36 +0400 |
commit | 6cbb458e38512c0623abcd31682bbbdd03d3074f (patch) | |
tree | 7c8e72c78fed882f18d35a95dd4b2ebb3cee2d59 | |
parent | c5196ecaebaa415dca880bbe12ee39eef3f77c31 (diff) |
initial commit of XML support
-rw-r--r-- | lua/pl/xml.lua | 677 | ||||
-rw-r--r-- | tests/test-xml.lua | 285 |
2 files changed, 962 insertions, 0 deletions
diff --git a/lua/pl/xml.lua b/lua/pl/xml.lua new file mode 100644 index 0000000..0cce5d8 --- /dev/null +++ b/lua/pl/xml.lua @@ -0,0 +1,677 @@ +-- XML LOM Utilities. +-- This implements some useful things on LOM documents, such as returned by lxp.lom.parse. +-- In particular, it can convert LOM back into XML text, with optional pretty-printing control. +-- It's based on stanza.lua from Prosody (http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua) +-- +-- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included +-- but the default is to use lxp.lom if it can be found. +-- +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- classic Lua XML parser by Roberto Ierusalimschy. +-- modified to output LOM format. +-- http://lua-users.org/wiki/LuaXml +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- @module pl.xml + +local t_insert = table.insert; +local t_concat = table.concat; +local t_remove = table.remove; +local s_format = string.format; +local s_match = string.match; +local tostring = tostring; +local setmetatable = setmetatable; +local getmetatable = getmetatable; +local pairs = pairs; +local ipairs = ipairs; +local type = type; +local next = next; +local print = print; +local unpack = unpack or table.unpack; +local s_gsub = string.gsub; +local s_char = string.char; +local s_find = string.find; +local os = os; +local pcall,require,io = pcall,require,io +local split = require 'pl.utils'.split + +local _M = {} +local Doc = { __type = "doc" }; +Doc.__index = Doc; + +--- create a new document node. +-- @param tag the tag name +-- @param attr optional attributes (table of name-value pairs) +function _M.new(tag, attr) + local doc = { tag = tag, attr = attr or {}, last_add = {}}; + return setmetatable(doc, Doc); +end + +--- parse an XML document. By default, this uses lxp.lom.parse, but +-- falls back to basic_parse, or if use_basic is true +-- @param text_or_file file or string representation +-- @param is_file whether text_or_file is a file name or not +-- @param use_basic do a basic parse +-- @return a parsed LOM document with the document metatatables set +-- @return nil, error the error can either be a file error or a parse error +function _M.parse(text_or_file, is_file, use_basic) + local parser,status,lom + if use_basic then parser = _M.basic_parse + else + status,lom = pcall(require,'lxp.lom') + if not status then parser = _M.basic_parse else parser = lom.parse end + end + if is_file then + local f,err = io.open(text_or_file) + if not f then return nil,err end + text_or_file = f:read '*a' + f:close() + end + local doc,err = parser(text_or_file) + if not doc then return nil,err end + if lom then + _M.walk(doc,false,function(_,d) + setmetatable(d,Doc) + end) + end + return doc +end + +---- convenient function to add a document node, This updates the last inserted position. +-- @param tag a tag name +-- @param attrs optional set of attributes (name-string pairs) +function Doc:addtag(tag, attrs) + local s = _M.new(tag, attrs); + (self.last_add[#self.last_add] or self):add_direct_child(s); + t_insert(self.last_add, s); + return self; +end + +--- convenient function to add a text node. This updates the last inserted position. +-- @param text a string +function Doc:text(text) + (self.last_add[#self.last_add] or self):add_direct_child(text); + return self; +end + +---- go up one level in a document +function Doc:up() + t_remove(self.last_add); + return self; +end + +function Doc:reset() + local last_add = self.last_add; + for i = 1,#last_add do + last_add[i] = nil; + end + return self; +end + +--- append a child to a document directly. +-- @param child a child node (either text or a document) +function Doc:add_direct_child(child) + t_insert(self, child); +end + +--- append a child to a document at the last element added +-- @param child a child node (either text or a document) +function Doc:add_child(child) + (self.last_add[#self.last_add] or self):add_direct_child(child); + return self; +end + +--accessing attributes: useful not to have to expose implementation (attr) +--but also can allow attr to be nil in any future optimizations + +--- set attributes of a document node. +-- @param t a table containing attribute/value pairs +function Doc:set_attribs (t) + for k,v in pairs(t) do + self.attr[k] = v + end +end + +--- set a single attribute of a document node. +-- @param a attribute +-- @param v its value +function Doc:set_attrib(a,v) + self.attr[a] = v +end + +--- access the attributes of a document node. +function Doc:get_attribs() + return self.attr +end + +--- function to create an element with a given tag name and a set of children. +-- @param tag a tag name +-- @param items either text or a table where the hash part is the attributes and the list part is the children. +function _M.elem(tag,items) + local s = _M.new(tag) + if type(items) == 'string' then items = {items} end + if _M.is_tag(items) then + t_insert(s,items) + elseif type(items) == 'table' then + for k,v in pairs(items) do + if type(k) == 'string' then + s.attr[k] = v + t_insert(s.attr,k) + else + s[k] = v + end + end + end + return s +end + +--- given a list of names, return a number of element constructors. +-- @param list a list of names, or a comma-separated string. +-- @usage local parent,children = doc.tags 'parent,children' <br> +-- doc = parent {child 'one', child 'two'} +function _M.tags(list) + local ctors = {} + local elem = _M.elem + if type(list) == 'string' then list = split(list,'%s*,%s*') end + for _,tag in ipairs(list) do + local ctor = function(items) return _M.elem(tag,items) end + t_insert(ctors,ctor) + end + return unpack(ctors) +end + +local templ_cache = {} + +local function is_data(data) + return #data == 0 or type(data[1]) ~= 'table' +end + +local function prepare_data(data) + -- a hack for ensuring that $1 maps to first element of data, etc. + -- Either this or could change the gsub call just below. + for i,v in ipairs(data) do + data[tostring(i)] = v + end +end + +--- create a substituted copy of a document, +-- @param templ may be a document or a string representation which will be parsed and cached +-- @param data a table of name-value pairs or a list of such tables +-- @return an XML document +function Doc.subst(templ, data) + if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end + if is_data(data) then + prepare_data(data) + end + if type(templ) == 'string' then + if templ_cache[templ] then + templ = templ_cache[templ] + else + local str,err = templ + templ,err = _M.parse(str) + if not templ then return nil,err end + templ_cache[str] = templ + end + end + local function _subst(item) + return _M.clone(templ,function(s) + return s:gsub('%$(%w+)',item) + end) + end + if is_data(data) then return _subst(data) end + local list = {} + for _,item in ipairs(data) do + prepare_data(item) + t_insert(list,_subst(item)) + end + if data.tag then + list = _M.elem(data.tag,list) + end + return list +end + + +--- get the first child with a given tag name. +-- @param tag the tag name +function Doc:child_with_name(tag) + for _, child in ipairs(self) do + if child.tag == tag then return child; end + end +end + +local _children_with_name +function _children_with_name(self,tag,list,recurse) + for _, child in ipairs(self) do if type(child) == 'table' then + if child.tag == tag then t_insert(list,child) end + if recurse then _children_with_name(child,tag,list,recurse) end + end end +end + +--- get all elements in a document that have a given tag. +-- @param tag a tag name +-- @param dont_recurse optionally only return the immediate children with this tag name +-- @return a list of elements +function Doc:get_elements_with_name(tag,dont_recurse) + local res = {} + _children_with_name(self,tag,res,not dont_recurse) + return res +end + +-- iterate over all children of a document node, including text nodes. +function Doc:children() + local i = 0; + return function (a) + i = i + 1 + return a[i]; + end, self, i; +end + +-- return the first child element of a node, if it exists. +function Doc:first_childtag() + if #self == 0 then return end + for _,t in ipairs(self) do + if type(t) == 'table' then return t end + end +end + +function Doc:matching_tags(tag, xmlns) + xmlns = xmlns or self.attr.xmlns; + local tags = self; + local start_i, max_i = 1, #tags; + return function () + for i=start_i,max_i do + v = tags[i]; + if (not tag or v.tag == tag) + and (not xmlns or xmlns == v.attr.xmlns) then + start_i = i+1; + return v; + end + end + end, tags, i; +end + +--- iterate over all child elements of a document node. +function Doc:childtags() + local i = 0; + return function (a) + local v + repeat + i = i + 1 + v = self[i] + if v and type(v) == 'table' then return v; end + until not v + end, self[1], i; +end + +--- visit child element of a node and call a function, possibility modifying the document. +-- @param callback a function passed the node (text or element). If it returns nil, that node will be removed. +-- If it returns a value, that will replace the current node. +function Doc:maptags(callback) + local is_tag = _M.is_tag + local i = 1; + while i <= #self do + if is_tag(self[i]) then + local ret = callback(self[i]); + if ret == nil then + t_remove(self, i); + else + self[i] = ret; + i = i + 1; + end + end + end + return self; +end + +local xml_escape +do + local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; + function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end + _M.xml_escape = xml_escape; +end + +-- pretty printing +-- if indent, then put each new tag on its own line +-- if attr_indent, put each new attribute on its own line +local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent) + local nsid = 0; + local tag = t.tag + local lf,alf = ""," " + if indent then lf = '\n'..idn end + if attr_indent then alf = '\n'..idn..attr_indent end + t_insert(buf, lf.."<"..tag); + for k, v in pairs(t.attr) do + if type(k) ~= 'number' then -- LOM attr table has list-like part + if s_find(k, "\1", 1, true) then + local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$"); + nsid = nsid + 1; + t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'"); + elseif not(k == "xmlns" and v == parentns) then + t_insert(buf, alf..k.."='"..xml_escape(v).."'"); + end + end + end + local len,has_children = #t; + if len == 0 then + local out = "/>" + if attr_indent then out = '\n'..idn..out end + t_insert(buf, out); + else + t_insert(buf, ">"); + for n=1,len do + local child = t[n]; + if child.tag then + self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent ); + has_children = true + else -- text element + t_insert(buf, xml_escape(child)); + end + end + t_insert(buf, (has_children and lf or '').."</"..tag..">"); + end +end + +---- pretty-print an XML document +--- @param idn an initial indent (indents are all strings) +--- @param indent an indent for each level +--- @param attr_indent if given, indent each attribute pair and put on a separate line +--- @return a string representation +function _M.tostring(t,idn,indent, attr_indent) + local buf = {}; + _dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent); + return t_concat(buf); +end + +Doc.__tostring = _M.tostring + +--- get the full text value of an element +function Doc:get_text() + local res = {} + for i,el in ipairs(self) do + if type(el) == 'string' then t_insert(res,el) end + end + return t_concat(res); +end + +--- make a copy of a document +-- @param doc the original document +-- @param strsubst an optional function for handling string copying which could do substitution, etc. +function _M.clone(doc, strsubst) + local lookup_table = {}; + local function _copy(object) + if type(object) ~= "table" then + if strsubst and type(object) == 'string' then return strsubst(object) + else return object; + end + elseif lookup_table[object] then + return lookup_table[object]; + end + local new_table = {}; + lookup_table[object] = new_table; + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value); -- is cloning keys much use, hm? + end + return setmetatable(new_table, getmetatable(object)); + end + + return _copy(doc) +end + +--- compare two documents. +-- @param t1 any value +-- @param t2 any value +function _M.compare(t1,t2) + local ty1 = type(t1) + local ty2 = type(t2) + if ty1 ~= ty2 then return false, 'type mismatch' end + if ty1 == 'string' then + return t1 == t2 and true or 'text '..t1..' ~= text '..t2 + end + if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end + if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end + if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end + -- compare attributes + for k,v in pairs(t1.attr) do + if t2.attr[k] ~= v then return false, 'mismatch attrib' end + end + for k,v in pairs(t2.attr) do + if t1.attr[k] ~= v then return false, 'mismatch attrib' end + end + -- compare children + for i = 1,#t1 do + local yes,err = _M.compare(t1[i],t2[i]) + if not yes then return err end + end + return true +end + +--- is this value a document element? +-- @param d any value +function _M.is_tag(d) + return type(d) == 'table' and type(d.tag) == 'string' +end + +--- call the desired function recursively over the document. +-- @param depth_first visit child notes first, then the current node +-- @param operation a function which will receive the current tag name and current node. +function _M.walk (doc, depth_first, operation) + if not depth_first then operation(doc.tag,doc) end + for _,d in ipairs(doc) do + if _M.is_tag(d) then + _M.walk(d,depth_first,operation) + end + end + if depth_first then operation(doc.tag,doc) end +end + +local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" } +local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end + +local function parseargs(s) + local arg = {} + s:gsub("([%w:]+)%s*=%s*([\"'])(.-)%2", function (w, _, a) + arg[w] = unescape(a) + end) + return arg +end + +--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version. +-- @param s the XML document to be parsed. +-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included. +function _M.basic_parse(s,all_text) + local t_insert,t_remove = table.insert,table.remove + local s_find,s_sub = string.find,string.sub + local stack = {} + local top = {} + t_insert(stack, top) + local ni,c,label,xarg, empty + local i, j = 1, 1 + -- we're not interested in <?xml version="1.0"?> + local _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*') + if istart then i = istart+1 end + while true do + ni,j,c,label,xarg, empty = s_find(s, "<(%/?)([%w:%-_]+)(.-)(%/?)>", i) + if not ni then break end + local text = s_sub(s, i, ni-1) + if all_text or not s_find(text, "^%s*$") then + t_insert(top, unescape(text)) + end + if empty == "/" then -- empty element tag + t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc)) + elseif c == "" then -- start tag + top = setmetatable({tag=label, attr=parseargs(xarg)},Doc) + t_insert(stack, top) -- new level + else -- end tag + local toclose = t_remove(stack) -- remove top + top = stack[#stack] + if #stack < 1 then + error("nothing to close with "..label) + end + if toclose.tag ~= label then + error("trying to close "..toclose.tag.." with "..label) + end + t_insert(top, toclose) + end + i = j+1 + end + local text = s_sub(s, i) + if all_text or not s_find(text, "^%s*$") then + t_insert(stack[#stack], unescape(text)) + end + if #stack > 1 then + error("unclosed "..stack[#stack].tag) + end + local res = stack[1] + return type(res[1])=='string' and res[2] or res[1] +end + +local function empty(attr) return not attr or not next(attr) end +local function is_text(s) return type(s) == 'string' end +local function is_element(d) return type(d) == 'table' and d.tag ~= nil end + +-- returns the key,value pair from a table if it has exactly one entry +local function has_one_element(t) + local key,value = next(t) + if next(t,key) ~= nil then return false end + return key,value +end + +local function append_capture(res,tbl) + if not empty(tbl) then -- no point in capturing empty tables... + local key + if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table + key = tbl._ + tbl._ = nil + if empty(tbl) then return end + end + -- a table with only one pair {[0]=value} shall be reduced to that value + local numkey,val = has_one_element(tbl) + if numkey == 0 then tbl = val end + if key then + res[key] = tbl + else -- otherwise, we append the captured table + t_insert(res,tbl) + end + end +end + +local function make_number(pat) + if pat:find '^%d+$' then -- $1 etc means use this as an array location + pat = tonumber(pat) + end + return pat +end + +local function capture_attrib(res,pat,value) + pat = make_number(pat:sub(2)) + res[pat] = value + return true +end + +local match +function match(d,pat,res,keep_going) + local ret = true + if d == nil then return false end + -- attribute string matching is straight equality, except if the pattern is a $ capture, + -- which always succeeds. + if type(d) == 'string' then + if type(pat) ~= 'string' then return false end + if _M.debug then print(d,pat) end + if pat:find '^%$' then + return capture_attrib(res,pat,d) + else + return d == pat + end + else + if _M.debug then print(d.tag,pat.tag) end + -- this is an element node. For a match to succeed, the attributes must + -- match as well. + -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute + local tagpat = pat.tag:match '^(.-)%-$' + if tagpat then + tagpat = make_number(tagpat) + res[tagpat] = d.tag + end + if d.tag == pat.tag or tagpat then + + if not empty(pat.attr) then + if empty(d.attr) then ret = false + else + for prop,pval in pairs(pat.attr) do + local dval = d.attr[prop] + if not match(dval,pval,res) then ret = false; break end + end + end + end + -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..} + if ret and #pat > 0 then + local i,j = 1,1 + local function next_elem() + j = j + 1 -- next child element of data + if is_text(d[j]) then j = j + 1 end + return j <= #d + end + repeat + local p = pat[i] + -- repeated {{<...>}} patterns shall match one or more elements + -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X} + if is_element(p) and p.repeated then + local found + repeat + local tbl = {} + ret = match(d[j],p,tbl,false) + if ret then + found = false --true + append_capture(res,tbl) + end + until not next_elem() or (found and not ret) + i = i + 1 + else + ret = match(d[j],p,res,false) + if ret then i = i + 1 end + end + until not next_elem() or i > #pat -- run out of elements or patterns to match + -- if every element in our pattern matched ok, then it's been a successful match + if i > #pat then return true end + end + if ret then return true end + else + ret = false + end + -- keep going anyway - look at the children! + if keep_going then + for child in d:childtags() do + ret = match(child,pat,res,keep_going) + if ret then break end + end + end + end + return ret +end + +function Doc:match(pat) + if is_text(pat) then + pat = _M.parse(pat,false,true) + end + _M.walk(pat,false,function(_,d) + if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and + d[1]:find '%s*{{' and d[3]:find '}}%s*' then + t_remove(d,1) + t_remove(d,2) + d[1].repeated = true + end + end) + + local res = {} + local ret = match(self,pat,res,true) + return res,ret +end + + +return _M + diff --git a/tests/test-xml.lua b/tests/test-xml.lua new file mode 100644 index 0000000..5db85c9 --- /dev/null +++ b/tests/test-xml.lua @@ -0,0 +1,285 @@ +local xml = require 'pl.xml' +local asserteq = require 'pl.test'.asserteq +local dump = require 'pl.pretty'.dump + +-- Prosody stanza.lua style XML building + +d = xml.new 'top' : addtag 'child' : text 'alice' : up() : addtag 'child' : text 'bob' + +d = xml.new 'children' : + addtag 'child' : + addtag 'name' : text 'alice' : up() : addtag 'age' : text '5' : up() : addtag('toy',{type='fluffy'}) : up() : + up() : + addtag 'child': + addtag 'name' : text 'bob' : up() : addtag 'age' : text '6' : up() : addtag('toy',{type='squeaky'}) + +asserteq( +xml.tostring(d,'',' '), +[[ + +<children> + <child> + <name>alice</name> + <age>5</age> + <toy type='fluffy'/> + </child> + <child> + <name>bob</name> + <age>6</age> + <toy type='squeaky'/> + </child> +</children>]]) + +-- Orbit-style 'xmlification' + +local children,child,toy,name,age = xml.tags 'children, child, toy, name, age' + +d1 = children { + child {name 'alice', age '5', toy {type='fluffy'}}, + child {name 'bob', age '6', toy {type='squeaky'}} +} + +assert(xml.compare(d,d1)) + +-- or we can use a template document to convert Lua data to LOM + +templ = child {name '$name', age '$age', toy{type='$toy'}} + +d2 = children(templ:subst{ + {name='alice',age='5',toy='fluffy'}, + {name='bob',age='6',toy='squeaky'} +}) + +assert(xml.compare(d1,d2)) + +-- Parsing Google Weather service results -- + +local joburg = [[ +<xml_api_reply version='1'> + <weather module_id='0' tab_id='0' mobile_zipped='1' section='0' row='0' mobile_row='0'> + <forecast_information> + <city data='Johannesburg, Gauteng'/> + <postal_code data='Johannesburg,ZA'/> + <latitude_e6 data=''/> + <longitude_e6 data=''/> + <forecast_date data='2010-10-02'/> + <current_date_time data='2010-10-02 18:30:00 +0000'/> + <unit_system data='US'/> + </forecast_information> + <current_conditions> + <condition data='Clear'/> + <temp_f data='75'/> + <temp_c data='24'/> + <humidity data='Humidity: 19%'/> + <icon data='/ig/images/weather/sunny.gif'/> + <wind_condition data='Wind: NW at 7 mph'/> + </current_conditions> + <forecast_conditions> + <day_of_week data='Sat'/> + <low data='60'/> + <high data='89'/> + <icon data='/ig/images/weather/sunny.gif'/> + <condition data='Clear'/> + </forecast_conditions> + <forecast_conditions> + <day_of_week data='Sun'/> + <low data='53'/> + <high data='86'/> + <icon data='/ig/images/weather/sunny.gif'/> + <condition data='Clear'/> + </forecast_conditions> + <forecast_conditions> + <day_of_week data='Mon'/> + <low data='57'/> + <high data='87'/> + <icon data='/ig/images/weather/sunny.gif'/> + <condition data='Clear'/> + </forecast_conditions> + <forecast_conditions> + <day_of_week data='Tue'/> + <low data='60'/> + <high data='84'/> + <icon data='/ig/images/weather/sunny.gif'/> + <condition data='Clear'/> + </forecast_conditions> + </weather> +</xml_api_reply> + +]] + +local d = xml.parse(joburg) + +function match(t,xpect) + local res,ret = d:match(t) + asserteq(res,xpect) +end + +t1 = [[ + <weather> + <current_conditions> + <condition data='$condition'/> + <temp_c data='$temp'/> + </current_conditions> + </weather> +]] + +match(t1,{ + condition = "Clear", + temp = "24", +} ) + +t2 = [[ + <weather> + {{<forecast_conditions> + <day_of_week data='$day'/> + <low data='$low'/> + <high data='$high'/> + <condition data='$condition'/> + </forecast_conditions>}} + </weather> +]] + +match(t2,{ + { + low = "60", + high = "89", + day = "Sat", + condition = "Clear", + }, + { + low = "53", + high = "86", + day = "Sun", + condition = "Clear", + }, + { + low = "57", + high = "87", + day = "Mon", + condition = "Clear", + }, + { + low = "60", + high = "84", + day = "Tue", + condition = "Clear", + } +}) + +config = [[ +<config> + <alpha>1.3</alpha> + <beta>10</beta> + <name>bozo</name> +</config> +]] +d,err = xml.parse(config) +if not d then print(err); os.exit(1) end + + +-- can match against wildcard tag names (end with -) +-- can be names +match([[ +<config> + {{<key->$value</key->}} +</config> +]],{ + {key="alpha", value = "1.3"}, + {key="beta", value = "10"}, + {key="name",value = "bozo"}, +}) + +-- can be numerical indices +match([[ +<config> + {{<1->$2</1->}} +</config> +]],{ + {"alpha","1.3"}, + {"beta","10"}, + {"name","bozo"}, +}) + +-- _ is special; means 'this value is key of captured table' +match([[ +<config> + {{<_->$1</_->}} +</config> +]],{ + alpha = {"1.3"}, + beta = {"10"}, + name = {"bozo"}, +}) + +-- the numerical index 0 is special: a capture of {[0]=val} becomes simply the value val +match([[ +<config> + {{<_->$0</_->}} +</config> +]],{ + alpha = "1.3", + name = "bozo", + beta = "10" +}) + +-- this can of course also work with attributes, but then we don't want to collapse! + +config = [[ +<config> + <alpha type='number'>1.3</alpha> + <beta type='number'>10</beta> + <name type='string'>bozo</name> +</config> +]] +d,err = xml.parse(config) +if not d then print(err); os.exit(1) end + +match([[ +<config> + {{<_- type='$1'>$2</_->}} +</config> +]],{ + alpha = {"number","1.3"}, + beta = {"number","10"}, + name = {"string","bozo"}, +}) + +d,err = xml.parse [[ + +<configuremap> + <configure name="NAME" value="ImageMagick"/> + <configure name="LIB_VERSION" value="0x651"/> + <configure name="LIB_VERSION_NUMBER" value="6,5,1,3"/> + <configure name="RELEASE_DATE" value="2009-05-01"/> + <configure name="VERSION" value="6.5.1"/> + <configure name="CC" value="vs7"/> + <configure name="HOST" value="windows-unknown-linux-gnu"/> + <configure name="DELEGATES" value="bzlib freetype jpeg jp2 lcms png tiff x11 xml wmf zlib"/> + <configure name="COPYRIGHT" value="Copyright (C) 1999-2009 ImageMagick Studio LLC"/> + <configure name="WEBSITE" value="http://www.imagemagick.org"/> + +</configuremap> +]] +if not d then print(err); os.exit(1) end +--xml.debug = true + +res,err = d:match [[ +<configuremap> + {{<configure name="$_" value="$0"/>}} +</configuremap> +]] + +asserteq(res,{ + HOST = "windows-unknown-linux-gnu", + COPYRIGHT = "Copyright (C) 1999-2009 ImageMagick Studio LLC", + NAME = "ImageMagick", + LIB_VERSION = "0x651", + VERSION = "6.5.1", + RELEASE_DATE = "2009-05-01", + WEBSITE = "http://www.imagemagick.org", + LIB_VERSION_NUMBER = "6,5,1,3", + CC = "vs7", + DELEGATES = "bzlib freetype jpeg jp2 lcms png tiff x11 xml wmf zlib" +}) + + |