slaxdom.lua 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. -- Optional parser that creates a flat DOM from parsing
  2. local modpath = minetest.get_modpath(minetest.get_current_modname())
  3. local SLAXML = dofile(modpath .. "/slaxml/slaxml.lua")
  4. function SLAXML:dom(xml,opts)
  5. if not opts then opts={} end
  6. local rich = not opts.simple
  7. local push, pop = table.insert, table.remove
  8. local doc = {type="document", name="#doc", kids={}}
  9. local current,stack = doc, {doc}
  10. local builder = SLAXML:parser{
  11. startElement = function(name,nsURI,nsPrefix)
  12. local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, nsPrefix=nsPrefix, parent=rich and current or nil }
  13. if current==doc then
  14. if doc.root then error(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) end
  15. doc.root = rich and el or nil
  16. end
  17. push(current.kids,el)
  18. if current.el then push(current.el,el) end
  19. current = el
  20. push(stack,el)
  21. end,
  22. attribute = function(name,value,nsURI,nsPrefix)
  23. if not current or current.type~="element" then error(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) end
  24. local attr = {type='attribute',name=name,nsURI=nsURI,nsPrefix=nsPrefix,value=value,parent=rich and current or nil}
  25. if rich then current.attr[name] = value end
  26. push(current.attr,attr)
  27. end,
  28. closeElement = function(name)
  29. if current.name~=name or current.type~="element" then error(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) end
  30. pop(stack)
  31. current = stack[#stack]
  32. end,
  33. text = function(value,cdata)
  34. -- documents may only have text node children that are whitespace: https://www.w3.org/TR/xml/#NT-Misc
  35. if current.type=='document' and not value:find('^%s+$') then error(("Document has non-whitespace text at root: '%s'"):format(value:gsub('[\r\n\t]',{['\r']='\\r', ['\n']='\\n', ['\t']='\\t'}))) end
  36. push(current.kids,{type='text',name='#text',cdata=cdata and true or nil,value=value,parent=rich and current or nil})
  37. end,
  38. comment = function(value)
  39. push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil})
  40. end,
  41. pi = function(name,value)
  42. push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil})
  43. end
  44. }
  45. builder:parse(xml,opts)
  46. return doc
  47. end
  48. local escmap = {["<"]="&lt;", [">"]="&gt;", ["&"]="&amp;", ['"']="&quot;", ["'"]="&apos;"}
  49. local function esc(s) return s:gsub('[<>&"]', escmap) end
  50. -- opts.indent: number of spaces, or string
  51. function SLAXML:xml(n,opts)
  52. opts = opts or {}
  53. local out = {}
  54. local tab = opts.indent and (type(opts.indent)=="number" and string.rep(" ",opts.indent) or opts.indent) or ""
  55. local ser = {}
  56. local omit = {}
  57. if opts.omit then for _,s in ipairs(opts.omit) do omit[s]=true end end
  58. function ser.document(n)
  59. for _,kid in ipairs(n.kids) do
  60. if ser[kid.type] then ser[kid.type](kid,0) end
  61. end
  62. end
  63. function ser.pi(n,depth)
  64. depth = depth or 0
  65. table.insert(out, tab:rep(depth)..'<?'..n.name..' '..n.value..'?>')
  66. end
  67. function ser.element(n,depth)
  68. if n.nsURI and omit[n.nsURI] then return end
  69. depth = depth or 0
  70. local indent = tab:rep(depth)
  71. local name = n.nsPrefix and n.nsPrefix..':'..n.name or n.name
  72. local result = indent..'<'..name
  73. if n.attr and n.attr[1] then
  74. local sorted = n.attr
  75. if opts.sort then
  76. sorted = {}
  77. for i,a in ipairs(n.attr) do sorted[i]=a end
  78. table.sort(sorted,function(a,b)
  79. if a.nsPrefix and b.nsPrefix then
  80. return a.nsPrefix==b.nsPrefix and a.name<b.name or a.nsPrefix<b.nsPrefix
  81. elseif not (a.nsPrefix or b.nsPrefix) then
  82. return a.name<b.name
  83. elseif b.nsPrefix then
  84. return true
  85. else
  86. return false
  87. end
  88. end)
  89. end
  90. local attrs = {}
  91. for _,a in ipairs(sorted) do
  92. if (not a.nsURI or not omit[a.nsURI]) and not (omit[a.value] and a.name:find('^xmlns:')) then
  93. attrs[#attrs+1] = ' '..(a.nsPrefix and (a.nsPrefix..':') or '')..a.name..'="'..esc(a.value)..'"'
  94. end
  95. end
  96. result = result..table.concat(attrs,'')
  97. end
  98. result = result .. (n.kids and n.kids[1] and '>' or '/>')
  99. table.insert(out, result)
  100. if n.kids and n.kids[1] then
  101. for _,kid in ipairs(n.kids) do
  102. if ser[kid.type] then ser[kid.type](kid,depth+1) end
  103. end
  104. table.insert(out, indent..'</'..name..'>')
  105. end
  106. end
  107. function ser.text(n,depth)
  108. if n.cdata then
  109. table.insert(out, tab:rep(depth)..'<![[CDATA['..n.value..']]>')
  110. else
  111. table.insert(out, tab:rep(depth)..esc(n.value))
  112. end
  113. end
  114. function ser.comment(n,depth)
  115. table.insert(out, tab:rep(depth)..'<!--'..n.value..'-->')
  116. end
  117. ser[n.type](n,0)
  118. return table.concat(out, opts.indent and '\n' or '')
  119. end
  120. return SLAXML