| 1 | local assert , ipairs , pairs , setmetatable , rawget , rawset , tostring = |
| 2 | assert , ipairs , pairs , setmetatable , rawget , rawset , tostring |
| 3 | local strchar , strgmatch , strgsub , strsub , strmatch = string.char , string.gmatch , string.gsub , string.sub , string.match |
| 4 | local tblconcat = table.concat |
| 5 | local tblinsert = table.insert |
| 6 | |
| 7 | local stanza_methods = require "util.stanza".stanza_mt; |
| 8 | |
| 9 | local entities = setmetatable ( { |
| 10 | amp = "&" ; |
| 11 | lt = "<" ; |
| 12 | gt = ">" ; |
| 13 | apos = "'" ; |
| 14 | quot = '"' ; |
| 15 | } , { __index = function ( entity ) |
| 16 | return strchar ( tonumber ( entity:match ( "^#%d+" ) ) or error ( "invalid entity " .. entity ) ) |
| 17 | end } ) |
| 18 | local function xml_unescape ( str ) |
| 19 | return ( strgsub ( str , "&([^;]*);" , entities ) ) |
| 20 | end |
| 21 | |
| 22 | local function getstring ( msgs , startpos , finishpos ) |
| 23 | if #msgs == 1 then --All originated in same string |
| 24 | return strsub ( msgs[1] , startpos , finishpos ) |
| 25 | else -- Over multiple source strings |
| 26 | return strsub ( msgs[1] , startpos , -1 ) |
| 27 | .. tblconcat ( msgs , "" , 2 , #msgs - 1 ) |
| 28 | .. strsub ( msgs[#msgs] , 1 , finishpos ) |
| 29 | end |
| 30 | end |
| 31 | |
| 32 | local m_mt = { |
| 33 | __tostring = function ( v ) |
| 34 | local str = v.stringform |
| 35 | if str then |
| 36 | return str |
| 37 | else |
| 38 | str = getstring ( v.msgs , v.start , v.finish ) |
| 39 | if v.type == "text" then |
| 40 | str = xml_unescape ( str ) |
| 41 | end |
| 42 | v.stringform = str |
| 43 | return str |
| 44 | end |
| 45 | end |
| 46 | } |
| 47 | |
| 48 | local function handleoutside ( str , r , initial ) |
| 49 | local a , b , close = str:find ( "<(/?)" , initial ) |
| 50 | |
| 51 | if not a then |
| 52 | r.state = "outside" |
| 53 | return false |
| 54 | end |
| 55 | |
| 56 | --Finalise text object |
| 57 | local m = r[#r] |
| 58 | m.finish = a - 1 |
| 59 | m.type = "text" |
| 60 | |
| 61 | local m = setmetatable ( { |
| 62 | msgs = { str } ; |
| 63 | start = a ; |
| 64 | starte = b + 1 ; |
| 65 | } , m_mt ) |
| 66 | |
| 67 | if close ~= "/" then |
| 68 | r.depth = r.depth + 1 |
| 69 | m.type = "open" |
| 70 | else |
| 71 | r.depth = r.depth - 1 |
| 72 | m.type = "close" |
| 73 | end |
| 74 | |
| 75 | tblinsert ( r , m ) |
| 76 | |
| 77 | r.state = "inside" |
| 78 | return true |
| 79 | end |
| 80 | |
| 81 | local function handleinside ( str, r , initial ) |
| 82 | local c , d , selfclosing = str:find ( "([/?]?)>" , initial ) |
| 83 | if not c then |
| 84 | r.state = "inside" |
| 85 | return false |
| 86 | end |
| 87 | |
| 88 | local m = r[#r] |
| 89 | m.finish = d |
| 90 | m.finishs = c - 1 |
| 91 | if selfclosing == "/" or selfclosing == "?" then |
| 92 | m.type = "selfclosing" |
| 93 | r.depth = r.depth - 1 |
| 94 | end |
| 95 | |
| 96 | local m = setmetatable ( { |
| 97 | msgs = { str } ; |
| 98 | start = d + 1 ; |
| 99 | type = "text" ; |
| 100 | } , m_mt ) |
| 101 | tblinsert ( r , m ) |
| 102 | |
| 103 | r.state = "outside" |
| 104 | return true |
| 105 | end |
| 106 | |
| 107 | local function index ( str , r ) |
| 108 | r = r or { depth = 0, state = "outside" } |
| 109 | |
| 110 | local initial = 1 |
| 111 | |
| 112 | if not r[#r] then |
| 113 | r[1] = setmetatable ( { |
| 114 | msgs = { str } ; |
| 115 | type = "text" ; |
| 116 | start = 1 ; |
| 117 | } , m_mt ) |
| 118 | else |
| 119 | tblinsert ( r[#r].msgs , str ) |
| 120 | end |
| 121 | |
| 122 | while true do |
| 123 | if r.state == "outside" then |
| 124 | if not handleoutside ( str , r , initial ) then |
| 125 | break |
| 126 | end |
| 127 | else |
| 128 | if not handleinside ( str , r , initial ) then |
| 129 | break |
| 130 | end |
| 131 | end |
| 132 | initial = r[#r].start |
| 133 | end |
| 134 | |
| 135 | return r |
| 136 | end |
| 137 | |
| 138 | local function get_name ( str ) |
| 139 | return strmatch ( str , "^<([^%s>/]+)" ) |
| 140 | end |
| 141 | |
| 142 | local function get_attr ( str ) |
| 143 | local attr = { } |
| 144 | for name , quote, attvalue in strgmatch ( str , [=[([^%s=/<]+)%s*=%s*(["'])([^'"]*)%2]=] ) do |
| 145 | attr [ name ] = xml_unescape ( attvalue ) |
| 146 | end |
| 147 | return attr |
| 148 | end |
| 149 | |
| 150 | function resolve_attr_namespaces ( attr ) |
| 151 | local namespace = { } |
| 152 | local prefixattr = { } |
| 153 | for k , attr_value in pairs ( attr ) do |
| 154 | local attr_prefix , attr_name = k:match ( "^([^:\1]+):?([^\1]-)$" ) |
| 155 | |
| 156 | if attr_prefix == nil then |
| 157 | error ( "already resolved" ) |
| 158 | elseif attr_prefix == "xmlns" then |
| 159 | namespace [ attr_name ] = attr_value |
| 160 | elseif #attr_name ~= 0 and attr_prefix ~= "xml" then |
| 161 | local t = prefixattr [ attr_prefix ] |
| 162 | if not t then |
| 163 | t = { } |
| 164 | prefixattr [ attr_prefix ] = t |
| 165 | end |
| 166 | t [ attr_name ] = attr_value |
| 167 | end |
| 168 | end |
| 169 | |
| 170 | for k , v in pairs ( prefixattr ) do |
| 171 | for name , value in pairs ( v ) do |
| 172 | attr [ namespace [ k ] .. "\1" .. name ] = value |
| 173 | end |
| 174 | end |
| 175 | return namespace |
| 176 | end |
| 177 | |
| 178 | local currentindex = 1 |
| 179 | |
| 180 | local function resolve_namespace ( element ) |
| 181 | local parent = element.parent |
| 182 | |
| 183 | local prefix = get_name ( element.str ):match ( "^([^:]+):" ) |
| 184 | |
| 185 | local namespace = setmetatable ( resolve_attr_namespaces ( element.attr ) , { __index = parent.namespace } ) |
| 186 | |
| 187 | local current_namespace = prefix and ( namespace [ prefix ] or error ("unbound prefix: "..prefix) ) |
| 188 | or rawget(element.attr, "xmlns") |
| 189 | or parent.attr.xmlns |
| 190 | or parent.namespace [ currentindex ] |
| 191 | namespace [ currentindex ] = current_namespace |
| 192 | |
| 193 | return namespace |
| 194 | end |
| 195 | |
| 196 | local dynamic_properties = { |
| 197 | name = function ( t ) |
| 198 | return get_name ( t.str ):match("[^:]+$") |
| 199 | end; |
| 200 | attr = function ( t , k ) |
| 201 | return setmetatable ( get_attr ( t.str ) , { __index = function ( attr_table , attr ) |
| 202 | local _ = t.namespace -- DO NOT OPTIMISE AWAY WAQAS |
| 203 | setmetatable ( attr_table , { __index = { xmlns = t.namespace[currentindex] } } ) |
| 204 | return attr_table [ attr ] |
| 205 | end } ) |
| 206 | end; |
| 207 | str = function ( t , k ) |
| 208 | return tostring ( t.opentag ) |
| 209 | end; |
| 210 | namespace = resolve_namespace ; |
| 211 | } |
| 212 | |
| 213 | local stanza_mt = { |
| 214 | __index = function ( t , k ) |
| 215 | local f = dynamic_properties [ k ] |
| 216 | if f then |
| 217 | local v = f ( t ) |
| 218 | |
| 219 | rawset ( t , k , v ) |
| 220 | return v |
| 221 | else |
| 222 | return stanza_methods[k] |
| 223 | end |
| 224 | end ; |
| 225 | |
| 226 | __tostring = function ( t ) |
| 227 | if t.modified then |
| 228 | return stanza_methods.__tostring ( t ) |
| 229 | end |
| 230 | local opentag = t.opentag |
| 231 | local endtag = assert ( rawget ( t , "endtag" ) or rawget ( t , "selfclosing" ) and t.opentag ) |
| 232 | return getstring ( opentag.msgs , opentag.start , endtag.finish ) |
| 233 | end ; |
| 234 | |
| 235 | __newindex = function ( t , k , v ) |
| 236 | rawset ( t , "modified", true ) |
| 237 | rawset ( t , k , v ) |
| 238 | end ; |
| 239 | } |
| 240 | |
| 241 | local function new_stanza ( parent ) |
| 242 | return setmetatable ( { tags = { } , parent = parent } , stanza_mt ) |
| 243 | end |
| 244 | |
| 245 | local function tagindex_to_tree(indices, start, finish,root) |
| 246 | if not start then |
| 247 | start = 1 |
| 248 | finish = #indices |
| 249 | end |
| 250 | |
| 251 | root = root or { attr = { } } |
| 252 | root.namespace = resolve_attr_namespaces ( root.attr ) |
| 253 | root.root = true |
| 254 | root.tags = { } |
| 255 | local leaf = root |
| 256 | |
| 257 | for i = start , finish do |
| 258 | local v = indices [ i ] |
| 259 | |
| 260 | if v.type == "selfclosing" then |
| 261 | local newleaf = new_stanza ( leaf ) |
| 262 | newleaf.opentag = v |
| 263 | newleaf.selfclosing = true |
| 264 | |
| 265 | tblinsert ( leaf , newleaf ) |
| 266 | tblinsert ( leaf.tags , newleaf ) |
| 267 | elseif v.type == "close" then -- Close tag |
| 268 | leaf.endtag = v |
| 269 | leaf = leaf.parent |
| 270 | elseif v.type == "text" then |
| 271 | tblinsert ( leaf, v ) |
| 272 | else -- Open tag |
| 273 | local newleaf = new_stanza ( leaf ) |
| 274 | newleaf.opentag = v |
| 275 | |
| 276 | tblinsert ( leaf , newleaf ) |
| 277 | tblinsert ( leaf.tags , newleaf ) |
| 278 | |
| 279 | leaf = newleaf |
| 280 | end |
| 281 | end |
| 282 | |
| 283 | assert ( leaf == root , "Mismatched opening/closing tags" ) |
| 284 | return root; |
| 285 | end |
| 286 | |
| 287 | return { |
| 288 | index = index ; |
| 289 | tagindex_to_tree = tagindex_to_tree ; |
| 290 | get_name = get_name ; |
| 291 | get_attr = get_attr ; |
| 292 | resolve_attr_namespaces = resolve_attr_namespaces ; |
| 293 | }; |