Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stevedonovan/Penlight.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsteve donovan <steve.j.donovan@gmail.com>2011-10-04 16:42:36 +0400
committersteve donovan <steve.j.donovan@gmail.com>2011-10-04 16:42:36 +0400
commit6cbb458e38512c0623abcd31682bbbdd03d3074f (patch)
tree7c8e72c78fed882f18d35a95dd4b2ebb3cee2d59
parentc5196ecaebaa415dca880bbe12ee39eef3f77c31 (diff)
initial commit of XML support
-rw-r--r--lua/pl/xml.lua677
-rw-r--r--tests/test-xml.lua285
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 = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
+ 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"
+})
+
+