lxml-aux.lua /size: 33 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['lxml-aux'] = {
2    version   = 1.001,
3    comment   = "this module is the basis for the lxml-* ones",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- not all functions here make sense anymore vbut we keep them for
10-- compatibility reasons
11
12local trace_manipulations = false  trackers.register("lxml.manipulations", function(v) trace_manipulations = v end)
13local trace_inclusions    = false  trackers.register("lxml.inclusions",    function(v) trace_inclusions    = v end)
14
15local report_xml = logs.reporter("xml")
16
17local xml = xml
18
19local xmlcopy, xmlname = xml.copy, xml.name
20local xmlinheritedconvert = xml.inheritedconvert
21local xmlapplylpath = xml.applylpath
22
23local type, next, setmetatable, getmetatable = type, next, setmetatable, getmetatable
24local insert, remove, fastcopy, concat = table.insert, table.remove, table.fastcopy, table.concat
25local gmatch, gsub, format, find, strip, match = string.gmatch, string.gsub, string.format, string.find, string.strip, string.match
26local utfbyte = utf.byte
27local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
28local striplinepatterns = utilities.strings.striplinepatterns
29
30local function report(what,pattern,c,e)
31    report_xml("%s element %a, root %a, position %a, index %a, pattern %a",what,xmlname(e),xmlname(e.__p__),c,e.ni,pattern)
32end
33
34local function withelements(e,handle,depth)
35    if e and handle then
36        local edt = e.dt
37        if edt then
38            depth = depth or 0
39            for i=1,#edt do
40                local e = edt[i]
41                if type(e) == "table" then
42                    handle(e,depth)
43                    withelements(e,handle,depth+1)
44                end
45            end
46        end
47    end
48end
49
50xml.withelements = withelements
51
52function xml.withelement(e,n,handle) -- slow
53    if e and n ~= 0 and handle then
54        local edt = e.dt
55        if edt then
56            if n > 0 then
57                for i=1,#edt do
58                    local ei = edt[i]
59                    if type(ei) == "table" then
60                        if n == 1 then
61                            handle(ei)
62                            return
63                        else
64                            n = n - 1
65                        end
66                    end
67                end
68            elseif n < 0 then
69                for i=#edt,1,-1 do
70                    local ei = edt[i]
71                    if type(ei) == "table" then
72                        if n == -1 then
73                            handle(ei)
74                            return
75                        else
76                            n = n + 1
77                        end
78                    end
79                end
80            end
81        end
82    end
83end
84
85function xml.each(root,pattern,handle,reverse)
86    local collected = xmlapplylpath(root,pattern)
87    if collected then
88        if handle then
89            if reverse then
90                for c=#collected,1,-1 do
91                    handle(collected[c])
92                end
93            else
94                for c=1,#collected do
95                    handle(collected[c])
96                end
97            end
98        end
99        return collected
100    end
101end
102
103function xml.processattributes(root,pattern,handle)
104    local collected = xmlapplylpath(root,pattern)
105    if collected and handle then
106        for c=1,#collected do
107            handle(collected[c].at)
108        end
109    end
110    return collected
111end
112
113-- The following functions collect elements and texts.
114
115function xml.collect(root, pattern)
116    return xmlapplylpath(root,pattern)
117end
118
119function xml.collecttexts(root, pattern, flatten) -- todo: variant with handle
120    local collected = xmlapplylpath(root,pattern)
121    if collected and flatten then
122        local xmltostring = xml.tostring
123        for c=1,#collected do
124            collected[c] = xmltostring(collected[c].dt)
125        end
126    end
127    return collected or { }
128end
129
130function xml.collect_tags(root, pattern, nonamespace)
131    local collected = xmlapplylpath(root,pattern)
132    if collected then
133        local t = { }
134        local n = 0
135        for c=1,#collected do
136            local e  = collected[c]
137            local ns = e.ns
138            local tg = e.tg
139            n = n + 1
140            if nonamespace then
141                t[n] = tg
142            elseif ns == "" then
143                t[n] = tg
144            else
145                t[n] = ns .. ":" .. tg
146            end
147        end
148        return t
149    end
150end
151
152-- We've now arrived at the functions that manipulate the tree.
153
154local no_root = { no_root = true }
155
156local function redo_ni(d)
157    for k=1,#d do
158        local dk = d[k]
159        if type(dk) == "table" then
160            dk.ni = k
161        end
162    end
163end
164
165xml.reindex = redo_ni
166
167local function xmltoelement(whatever,root)
168    if not whatever then
169        return nil
170    end
171    local element
172    if type(whatever) == "string" then
173        element = xmlinheritedconvert(whatever,root,true) -- beware, not really a root
174    else
175        element = whatever -- we assume a table
176    end
177    if element.error then
178        return whatever -- string
179    end
180    if element then
181    --~ if element.ri then
182    --~     element = element.dt[element.ri].dt
183    --~ else
184    --~     element = element.dt
185    --~ end
186    end
187    return element
188end
189
190xml.toelement = xmltoelement
191
192-- local function copiedelement(element,newparent)
193--     if type(element) ~= "string" then
194--         element = xmlcopy(element).dt
195--         if newparent and type(element) == "table" then
196--             element.__p__ = newparent
197--         end
198--     end
199--     return element
200-- end
201
202local function copiedelement(element,newparent)
203    if type(element) ~= "string" then
204        element = xmlcopy(element).dt
205        if newparent and type(element) == "table" then
206            for i=1,#element do
207                local e = element[i]
208                if type(e) == "table" then
209                    e.__p__ = newparent
210                end
211            end
212        end
213    end
214    return element
215end
216
217function xml.delete(root,pattern)
218    if not pattern or pattern == "" then
219        local p = root.__p__
220        if p then
221            if trace_manipulations then
222                report('deleting',"--",c,root)
223            end
224            local d = p.dt
225            remove(d,root.ni)
226            redo_ni(d) -- can be made faster and inlined
227        end
228    else
229        local collected = xmlapplylpath(root,pattern)
230        if collected then
231            for c=1,#collected do
232                local e = collected[c]
233                local p = e.__p__
234                if p then
235                    if trace_manipulations then
236                        report('deleting',pattern,c,e)
237                    end
238                    local d  = p.dt
239                    local ni = e.ni
240                    if ni <= #d then
241                        if false then
242                            p.dt[ni] = ""
243                        else
244                            -- what if multiple deleted in one set
245                            remove(d,ni)
246                            redo_ni(d) -- can be made faster and inlined
247                        end
248                    else
249                        -- disturbing
250                    end
251                end
252            end
253        end
254    end
255end
256
257function xml.wipe(root,pattern) -- not yet in manual
258    local collected = xmlapplylpath(root,pattern)
259    if collected then
260        for c=1,#collected do
261            local e = collected[c]
262            local p = e.__p__
263            if p then
264                local d  = p.dt
265                local ni = e.ni
266                if ni <= #d then
267                    local dt = e.dt
268                    if #dt == 1 then
269                        local d1 = dt[1]
270                        if type(d1) == "string" and match(d1,"^%s*$") then
271                            if trace_manipulations then
272                                report('wiping',pattern,c,e)
273                            end
274                            remove(d,ni)
275                            redo_ni(d) -- can be made faster and inlined
276                        end
277                    end
278                end
279            end
280        end
281    end
282end
283
284function xml.replace(root,pattern,whatever)
285    local element = root and xmltoelement(whatever,root)
286    local collected = element and xmlapplylpath(root,pattern)
287    if collected then
288        for c=1,#collected do
289            local e = collected[c]
290            local p = e.__p__
291            if p then
292                if trace_manipulations then
293                    report('replacing',pattern,c,e)
294                end
295                local d = p.dt
296                local n = e.ni
297                local t = copiedelement(element,p)
298                if type(t) == "table" then
299                    d[n] = t[1]
300                    for i=2,#t do
301                        n = n + 1
302                        insert(d,n,t[i])
303                    end
304                else
305                    d[n] = t
306                end
307                redo_ni(d) -- probably not needed
308            end
309        end
310    end
311end
312
313function xml.expand(root,pattern,whatever)
314    local collected = root and xmlapplylpath(root,pattern)
315    if collected then
316        for c=1,#collected do
317            local e = collected[c]
318            local p = e.__p__
319            if p then
320                if trace_manipulations then
321                    report('expanding',pattern,c,e)
322                end
323                local d = p.dt
324                local n = e.ni
325                local t = whatever(e,p)
326                if t then
327                    if type(t) == "table" then
328                        t = xmlcopy(t)
329                        d[n] = t[1]
330                        for i=2,#t do
331                            n = n + 1
332                            insert(d,n,t[i])
333                        end
334                    else
335                        d[n] = t
336                    end
337                    redo_ni(d) -- probably not needed
338                end
339            end
340        end
341    end
342end
343
344local function wrap(e,wrapper)
345    local t = {
346        rn = e.rn,
347        tg = e.tg,
348        ns = e.ns,
349        at = e.at,
350        dt = e.dt,
351        __p__ = e,
352    }
353    setmetatable(t,getmetatable(e))
354    e.rn = wrapper.rn or e.rn or ""
355    e.tg = wrapper.tg or e.tg or ""
356    e.ns = wrapper.ns or e.ns or ""
357    e.at = fastcopy(wrapper.at)
358    e.dt = { t }
359end
360
361function xml.wrap(root,pattern,whatever)
362    if whatever then
363        local wrapper = xmltoelement(whatever,root)
364        local collected = xmlapplylpath(root,pattern)
365        if collected then
366            for c=1,#collected do
367                local e = collected[c]
368                if trace_manipulations then
369                    report('wrapping',pattern,c,e)
370                end
371                wrap(e,wrapper)
372            end
373        end
374    else
375        wrap(root,xmltoelement(pattern))
376    end
377end
378
379local function inject_element(root,pattern,whatever,prepend)
380    local element = root and xmltoelement(whatever,root)
381    local collected = element and xmlapplylpath(root,pattern)
382    local function inject_e(e)
383        local r   = e.__p__
384        local d   = r.dt
385        local k   = e.ni
386        local rri = r.ri
387        local edt = (rri and d[rri].dt) or (d and d[k] and d[k].dt)
388        if edt then
389            local be, af
390            local cp = copiedelement(element,e)
391            if prepend then
392                be, af = cp, edt
393            else
394                be, af = edt, cp
395            end
396            local bn = #be
397            for i=1,#af do
398                bn = bn + 1
399                be[bn] = af[i]
400            end
401            if rri then
402                r.dt[rri].dt = be
403            else
404                d[k].dt = be
405            end
406            redo_ni(d)
407        end
408    end
409    if not collected then
410        -- nothing
411    elseif collected.tg then
412        -- first or so
413        inject_e(collected)
414    else
415        for c=1,#collected do
416            inject_e(collected[c])
417        end
418    end
419end
420
421local function insert_element(root,pattern,whatever,before) -- todo: element als functie
422    local element = root and xmltoelement(whatever,root)
423    local collected = element and xmlapplylpath(root,pattern)
424    local function insert_e(e)
425        local r = e.__p__
426        local d = r.dt
427        local k = e.ni
428        if not before then
429            k = k + 1
430        end
431        insert(d,k,copiedelement(element,r))
432        redo_ni(d)
433    end
434    if not collected then
435        -- nothing
436    elseif collected.tg then
437        -- first or so
438        insert_e(collected)
439    else
440        for c=1,#collected do
441            insert_e(collected[c])
442        end
443    end
444end
445
446xml.insert_element  =                 insert_element
447xml.insertafter     =                 insert_element
448xml.insertbefore    = function(r,p,e) insert_element(r,p,e,true) end
449xml.injectafter     =                 inject_element
450xml.injectbefore    = function(r,p,e) inject_element(r,p,e,true) end
451
452-- loaddata can restrict loading
453
454local function include(xmldata,pattern,attribute,recursive,loaddata,level)
455 -- attribute = attribute or 'href'
456    pattern   = pattern or 'include'
457    loaddata  = loaddata or io.loaddata
458    local collected = xmlapplylpath(xmldata,pattern)
459    if collected then
460        if not level then
461            level = 1
462        end
463        for c=1,#collected do
464            local ek = collected[c]
465            local name = nil
466            local ekdt = ek.dt
467            if ekdt then
468                local ekat = ek.at
469                local ekrt = ek.__p__
470                if ekrt then
471                    local epdt = ekrt.dt
472                    if not attribute or attribute == "" then
473                        name = (type(ekdt) == "table" and ekdt[1]) or ekdt -- check, probably always tab or str
474                    end
475                    if not name then
476                        for a in gmatch(attribute or "href","([^|]+)") do
477                            name = ekat[a]
478                            if name then
479                                break
480                            end
481                        end
482                    end
483                    local data = nil
484                    if name and name ~= "" then
485                        local d, n = loaddata(name)
486                        data = d or ""
487                        name = n or name
488                        if trace_inclusions then
489                            report_xml("including %s bytes from %a at level %s by pattern %a and attribute %a (%srecursing)",#data,name,level,pattern,attribute or "",recursive and "" or "not ")
490                        end
491                    end
492                    if not data or data == "" then
493                        epdt[ek.ni] = "" -- xml.empty(d,k)
494                    elseif ekat["parse"] == "text" then
495                        -- for the moment hard coded
496                        epdt[ek.ni] = xml.escaped(data) -- d[k] = xml.escaped(data)
497                    else
498                        local settings = xmldata.settings
499                        local savedresource = settings.currentresource
500                        settings.currentresource = name
501                        local xi = xmlinheritedconvert(data,xmldata,true)
502                        if not xi then
503                            epdt[ek.ni] = "" -- xml.empty(d,k)
504                        else
505                            if recursive then
506                                include(xi,pattern,attribute,recursive,loaddata,level+1)
507                            end
508                            local child = xml.body(xi) -- xml.assign(d,k,xi)
509                            child.__p__ = ekrt
510                            child.__f__ = name -- handy for tracing
511                            child.cf = name
512                            epdt[ek.ni] = child
513                            local settings   = xmldata.settings
514                            local inclusions = settings and settings.inclusions
515                            if inclusions then
516                                inclusions[#inclusions+1] = name
517                            elseif settings then
518                                settings.inclusions = { name }
519                            else
520                                settings = { inclusions = { name } }
521                                xmldata.settings = settings
522                            end
523                            if child.er then
524                                local badinclusions = settings.badinclusions
525                                if badinclusions then
526                                    badinclusions[#badinclusions+1] = name
527                                else
528                                    settings.badinclusions = { name }
529                                end
530                            end
531                        end
532settings.currentresource = savedresource
533                    end
534                end
535            end
536        end
537    end
538end
539
540xml.include = include
541
542function xml.inclusion(e,default)
543    while e do
544        local f = e.__f__
545        if f then
546            return f
547        else
548            e = e.__p__
549        end
550    end
551    return default
552end
553
554local function getinclusions(key,e,sorted)
555    while e do
556        local settings = e.settings
557        if settings then
558            local inclusions = settings[key]
559            if inclusions then
560                inclusions = table.unique(inclusions) -- a copy
561                if sorted then
562                    table.sort(inclusions) -- so we sort the copy
563                end
564                return inclusions -- and return the copy
565            else
566                e = e.__p__
567            end
568        else
569            e = e.__p__
570        end
571    end
572end
573
574function xml.inclusions(e,sorted)
575    return getinclusions("inclusions",e,sorted)
576end
577
578function xml.badinclusions(e,sorted)
579    return getinclusions("badinclusions",e,sorted)
580end
581
582local b_collapser  = lpegpatterns.b_collapser
583local m_collapser  = lpegpatterns.m_collapser
584local e_collapser  = lpegpatterns.e_collapser
585local x_collapser  = lpegpatterns.x_collapser
586
587local b_stripper   = lpegpatterns.b_stripper
588local m_stripper   = lpegpatterns.m_stripper
589local e_stripper   = lpegpatterns.e_stripper
590local x_stripper   = lpegpatterns.x_stripper
591
592local function stripelement(e,nolines,anywhere,everything)
593    local edt = e.dt
594    if edt then
595        local n = #edt
596        if n == 0 then
597            return e -- convenient
598        elseif everything then
599            local t = { }
600            local m = 0
601            for i=1,n do
602                local str = edt[i]
603                if type(str) ~= "string" then
604                    m = m + 1
605                    t[m] = str
606                elseif str ~= "" then
607                    str = lpegmatch(x_collapser,str)
608                    if str ~= "" then
609                        m = m + 1
610                        t[m] = str
611                    end
612                end
613            end
614            e.dt = t
615        elseif anywhere then
616            local t = { }
617            local m = 0
618            for i=1,n do
619                local str = edt[i]
620                if type(str) ~= "string" then
621                    m = m + 1
622                    t[m] = str
623                elseif str ~= "" then
624                    if nolines then
625                        str = lpegmatch((i == 1 and b_collapser) or (i == m and e_collapser) or m_collapser,str)
626                    else
627                        str = lpegmatch((i == 1 and b_stripper) or (i == m and e_stripper) or m_stripper,str)
628                    end
629                    if str ~= "" then
630                        m = m + 1
631                        t[m] = str
632                    end
633                end
634            end
635            e.dt = t
636        else
637            local str = edt[1]
638            if type(str) == "string" then
639                if str ~= "" then
640                    str = lpegmatch(nolines and b_collapser or b_stripper,str)
641                end
642                if str == "" then
643                    remove(edt,1)
644                    n = n - 1
645                else
646                    edt[1] = str
647                end
648            end
649            if n > 0 then
650                str = edt[n]
651                if type(str) == "string" then
652                    if str == "" then
653                        remove(edt)
654                    else
655                        str = lpegmatch(nolines and e_collapser or e_stripper,str)
656                        if str == "" then
657                            remove(edt)
658                        else
659                            edt[n] = str
660                        end
661                    end
662                end
663            end
664        end
665    end
666    return e -- convenient
667end
668
669xml.stripelement = stripelement
670
671function xml.strip(root,pattern,nolines,anywhere,everything) -- strips all leading and trailing spacing
672    local collected = xmlapplylpath(root,pattern) -- beware, indices no longer are valid now
673    if collected then
674        for i=1,#collected do
675            stripelement(collected[i],nolines,anywhere,everything)
676        end
677    end
678--  return root
679end
680
681-- local function compactelement(e)
682--     local edt = e.dt
683--     if edt then
684--         local t = { }
685--         local m = 0
686--         for e=1,#edt do
687--             local str = edt[e]
688--             if type(str) ~= "string" then
689--                 m = m + 1
690--                 t[m] = str
691--             elseif str ~= "" and find(str,"%S") then
692--                 m = m + 1
693--                 t[m] = str
694--             end
695--         end
696--         e.dt = t
697--     end
698--     return e -- convenient
699-- end
700
701local function compactelement(e)
702    local edt = e.dt
703    if edt then
704        for e=1,#edt do
705            local str = edt[e]
706            if type(str) == "string" and not find(str,"%S") then
707                edt[e] = ""
708            end
709        end
710    end
711    return e -- convenient
712end
713
714xml.compactelement = compactelement
715
716local function renamespace(root, oldspace, newspace) -- fast variant
717    local ndt = #root.dt
718    for i=1,ndt or 0 do
719        local e = root[i]
720        if type(e) == "table" then
721            if e.ns == oldspace then
722                e.ns = newspace
723                if e.rn then
724                    e.rn = newspace
725                end
726            end
727            local edt = e.dt
728            if edt then
729                renamespace(edt, oldspace, newspace)
730            end
731        end
732    end
733end
734
735xml.renamespace = renamespace
736
737function xml.remaptag(root, pattern, newtg)
738    local collected = xmlapplylpath(root,pattern)
739    if collected then
740        for c=1,#collected do
741            collected[c].tg = newtg
742        end
743    end
744end
745
746function xml.remapnamespace(root, pattern, newns)
747    local collected = xmlapplylpath(root,pattern)
748    if collected then
749        for c=1,#collected do
750            collected[c].ns = newns
751        end
752    end
753end
754
755function xml.checknamespace(root, pattern, newns)
756    local collected = xmlapplylpath(root,pattern)
757    if collected then
758        for c=1,#collected do
759            local e = collected[c]
760            if (not e.rn or e.rn == "") and e.ns == "" then
761                e.rn = newns
762            end
763        end
764    end
765end
766
767function xml.remapname(root, pattern, newtg, newns, newrn)
768    local collected = xmlapplylpath(root,pattern)
769    if collected then
770        for c=1,#collected do
771            local e = collected[c]
772            e.tg, e.ns, e.rn = newtg, newns, newrn
773        end
774    end
775end
776
777-- Helper (for q2p).
778
779function xml.cdatatotext(e)
780    local dt = e.dt
781    if #dt == 1 then
782        local first = dt[1]
783        if first.tg == "@cd@" then
784            e.dt = first.dt
785        end
786    else
787        -- maybe option
788    end
789end
790
791-- local x = xml.convert("<x><a>1<b>2</b>3</a></x>")
792-- xml.texttocdata(xml.first(x,"a"))
793-- print(x) -- <x><![CDATA[1<b>2</b>3]]></x>
794
795function xml.texttocdata(e) -- could be a finalizer
796    local dt = e.dt
797    local s = xml.tostring(dt) -- no shortcut?
798    e.tg = "@cd@"
799    e.special = true
800    e.ns = ""
801    e.rn = ""
802    e.dt = { s }
803    e.at = nil
804end
805
806-- local x = xml.convert("<x><a>1<b>2</b>3</a></x>")
807-- xml.tocdata(xml.first(x,"a"))
808-- print(x) -- <x><![CDATA[<a>1<b>2</b>3</a>]]></x>
809
810function xml.elementtocdata(e) -- could be a finalizer
811    local dt = e.dt
812    local s = xml.tostring(e) -- no shortcut?
813    e.tg = "@cd@"
814    e.special = true
815    e.ns = ""
816    e.rn = ""
817    e.dt = { s }
818    e.at = nil
819end
820
821xml.builtinentities = table.tohash { "amp", "quot", "apos", "lt", "gt" } -- used often so share
822
823local entities        = characters and characters.entities or nil
824local builtinentities = xml.builtinentities
825
826function xml.addentitiesdoctype(root,option) -- we could also have a 'resolve' i.e. inline hex
827    if not entities then
828        require("char-ent")
829        entities = characters.entities
830    end
831    if entities and root and root.tg == "@rt@" and root.statistics then
832        local list = { }
833        local hexify = option == "hexadecimal"
834        for k, v in table.sortedhash(root.statistics.entities.names) do
835            if not builtinentities[k] then
836                local e = entities[k]
837                if not e then
838                    e = format("[%s]",k)
839                elseif hexify then
840                    e = format("&#%05X;",utfbyte(k))
841                end
842                list[#list+1] = format("  <!ENTITY %s %q >",k,e)
843            end
844        end
845        local dt = root.dt
846        local n = dt[1].tg == "@pi@" and 2 or 1
847        if #list > 0 then
848            insert(dt, n, { "\n" })
849            insert(dt, n, {
850               tg      = "@dt@", -- beware, doctype is unparsed
851               dt      = { format("Something [\n%s\n] ",concat(list)) },
852               ns      = "",
853               special = true,
854            })
855            insert(dt, n, { "\n\n" })
856        else
857         -- insert(dt, n, { table.serialize(root.statistics) })
858        end
859    end
860end
861
862-- local str = [==[
863-- <?xml version='1.0' standalone='yes' ?>
864-- <root>
865-- <a>test &nbsp; test &#123; test</a>
866-- <b><![CDATA[oeps]]></b>
867-- </root>
868-- ]==]
869--
870-- local x = xml.convert(str)
871-- xml.addentitiesdoctype(x,"hexadecimal")
872-- print(x)
873
874-- Here are a few synonyms:
875
876xml.all     = xml.each
877xml.insert  = xml.insertafter
878xml.inject  = xml.injectafter
879xml.after   = xml.insertafter
880xml.before  = xml.insertbefore
881xml.process = xml.each
882
883-- obsolete
884
885xml.obsolete   = xml.obsolete or { }
886local obsolete = xml.obsolete
887
888xml.strip_whitespace           = xml.strip                 obsolete.strip_whitespace      = xml.strip
889xml.collect_elements           = xml.collect               obsolete.collect_elements      = xml.collect
890xml.delete_element             = xml.delete                obsolete.delete_element        = xml.delete
891xml.replace_element            = xml.replace               obsolete.replace_element       = xml.replace
892xml.each_element               = xml.each                  obsolete.each_element          = xml.each
893xml.process_elements           = xml.process               obsolete.process_elements      = xml.process
894xml.insert_element_after       = xml.insertafter           obsolete.insert_element_after  = xml.insertafter
895xml.insert_element_before      = xml.insertbefore          obsolete.insert_element_before = xml.insertbefore
896xml.inject_element_after       = xml.injectafter           obsolete.inject_element_after  = xml.injectafter
897xml.inject_element_before      = xml.injectbefore          obsolete.inject_element_before = xml.injectbefore
898xml.process_attributes         = xml.processattributes     obsolete.process_attributes    = xml.processattributes
899xml.collect_texts              = xml.collecttexts          obsolete.collect_texts         = xml.collecttexts
900xml.inject_element             = xml.inject                obsolete.inject_element        = xml.inject
901xml.remap_tag                  = xml.remaptag              obsolete.remap_tag             = xml.remaptag
902xml.remap_name                 = xml.remapname             obsolete.remap_name            = xml.remapname
903xml.remap_namespace            = xml.remapnamespace        obsolete.remap_namespace       = xml.remapnamespace
904
905-- new (probably ok)
906
907function xml.cdata(e)
908    if e then
909        local dt = e.dt
910        if dt and #dt == 1 then
911            local first = dt[1]
912            return first.tg == "@cd@" and first.dt[1] or ""
913        end
914    end
915    return ""
916end
917
918function xml.finalizers.xml.cdata(collected)
919    if collected then
920        local e = collected[1]
921        if e then
922            local dt = e.dt
923            if dt and #dt == 1 then
924                local first = dt[1]
925                return first.tg == "@cd@" and first.dt[1] or ""
926            end
927        end
928    end
929    return ""
930end
931
932function xml.insertcomment(e,str,n)
933    insert(e.dt,n or 1,{
934        tg      = "@cm@",
935        ns      = "",
936        special = true,
937        at      = { },
938        dt      = { str },
939    })
940end
941
942function xml.insertcdata(e,str,n)
943    insert(e.dt,n or 1,{
944        tg      = "@cd@",
945        ns      = "",
946        special = true,
947        at      = { },
948        dt      = { str },
949    })
950end
951
952function xml.setcomment(e,str,n)
953    e.dt = { {
954        tg      = "@cm@",
955        ns      = "",
956        special = true,
957        at      = { },
958        dt      = { str },
959    } }
960end
961
962function xml.setcdata(e,str)
963    e.dt = { {
964        tg      = "@cd@",
965        ns      = "",
966        special = true,
967        at      = { },
968        dt      = { str },
969    } }
970end
971
972-- maybe helpers like this will move to an autoloader
973
974function xml.separate(x,pattern)
975    local collected = xmlapplylpath(x,pattern)
976    if collected then
977        for c=1,#collected do
978            local e = collected[c]
979            local d = e.dt
980            if d == x then
981                report_xml("warning: xml.separate changes root")
982                x = d
983            end
984            local t  = { "\n" }
985            local n  = 1
986            local i  = 1
987            local nd = #d
988            while i <= nd do
989                while i <= nd do
990                    local di = d[i]
991                    if type(di) == "string" then
992                        if di == "\n" or find(di,"^%s+$") then -- first test is speedup
993                            i = i + 1
994                        else
995                            d[i] = strip(di)
996                            break
997                        end
998                    else
999                        break
1000                    end
1001                end
1002                if i > nd then
1003                    break
1004                end
1005                t[n+1] = "\n"
1006                t[n+2] = d[i]
1007                t[n+3] = "\n"
1008                n = n + 3
1009                i = i + 1
1010            end
1011            t[n+1] = "\n"
1012            setmetatable(t,getmetatable(d))
1013            e.dt = t
1014        end
1015    end
1016    return x
1017end
1018
1019--
1020
1021local helpers = xml.helpers or { }
1022xml.helpers   = helpers
1023
1024local function normal(e,action)
1025    local edt = e.dt
1026    if edt then
1027        for i=1,#edt do
1028            local str = edt[i]
1029            if type(str) == "string" and str ~= "" then
1030                edt[i] = action(str)
1031            end
1032        end
1033    end
1034end
1035
1036local function recurse(e,action)
1037    local edt = e.dt
1038    if edt then
1039        for i=1,#edt do
1040            local str = edt[i]
1041            if type(str) ~= "string" then
1042                recurse(str,action) -- ,recursive
1043            elseif str ~= "" then
1044                edt[i] = action(str)
1045            end
1046        end
1047    end
1048end
1049
1050function helpers.recursetext(collected,action,recursive)
1051    if recursive then
1052        for i=1,#collected do
1053            recurse(collected[i],action)
1054        end
1055    else
1056        for i=1,#collected do
1057           normal(collected[i],action)
1058        end
1059    end
1060end
1061
1062-- on request ... undocumented ...
1063--
1064-- _tag       : element name
1065-- _type      : node type (_element can be an option)
1066-- _namespace : only if given
1067--
1068-- [1..n]     : text or table
1069-- key        : value or attribite 'key'
1070--
1071-- local str = [[
1072-- <?xml version="1.0" ?>
1073-- <a one="1">
1074--     <!-- rubish -->
1075--   <b two="1"/>
1076--   <b two="2">
1077--     c &gt; d
1078--   </b>
1079-- </a>
1080-- ]]
1081--
1082-- inspect(xml.totable(xml.convert(str)))
1083-- inspect(xml.totable(xml.convert(str),true))
1084-- inspect(xml.totable(xml.convert(str),true,true))
1085
1086local specials = {
1087    ["@rt@"] = "root",
1088    ["@pi@"] = "instruction",
1089    ["@cm@"] = "comment",
1090    ["@dt@"] = "declaration",
1091    ["@cd@"] = "cdata",
1092}
1093
1094local function convert(x,strip,flat)
1095    local ns = x.ns
1096    local tg = x.tg
1097    local at = x.at
1098    local dt = x.dt
1099    local node = flat and {
1100        [0] = (not x.special and (ns ~= "" and ns .. ":" .. tg or tg)) or nil,
1101    } or {
1102        _namespace = ns ~= "" and ns or nil,
1103        _tag       = not x.special and tg or nil,
1104        _type      = specials[tg] or "_element",
1105    }
1106    if at then
1107        for k, v in next, at do
1108            node[k] = v
1109        end
1110    end
1111    local n = 0
1112    for i=1,#dt do
1113        local di = dt[i]
1114        if type(di) == "table" then
1115            if flat and di.special then
1116                -- ignore
1117            else
1118                di = convert(di,strip,flat)
1119                if di then
1120                    n = n + 1
1121                    node[n] = di
1122                end
1123            end
1124        elseif strip then
1125            di = lpegmatch(strip,di)
1126            if di ~= "" then
1127                n = n + 1
1128                node[n] = di
1129            end
1130        else
1131            n = n + 1
1132            node[n] = di
1133        end
1134    end
1135    if next(node) then
1136        return node
1137    end
1138end
1139
1140function xml.totable(x,strip,flat)
1141    if type(x) == "table" then
1142        if strip then
1143            strip = striplinepatterns[strip]
1144        end
1145        return convert(x,strip,flat)
1146    end
1147end
1148
1149-- namespace, name, attributes
1150-- name, attributes
1151-- name
1152
1153function xml.rename(e,namespace,name,attributes)
1154    if type(e) ~= "table" or not e.tg then
1155        return
1156    end
1157    if type(name) == "table" then
1158        attributes = name
1159        name       = namespace
1160        namespace  = ""
1161    elseif type(name) ~= "string" then
1162        attributes = { }
1163        name       = namespace
1164        namespace  = ""
1165    end
1166    if type(attributes) ~= "table" then
1167        attributes = { }
1168    end
1169    e.ns = namespace
1170    e.rn = namespace
1171    e.tg = name
1172    e.at = attributes
1173end
1174