lxml-css.lmt /size: 34 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['lxml-css'] = {
2    version   = 1.001,
3    comment   = "companion to lxml-css.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local tonumber, rawset, type, select = tonumber, rawset, type, select
10local lower, format, find, gmatch = string.lower, string.format, string.find, string.gmatch
11local topattern, is_empty =  string.topattern, string.is_empty
12local P, S, C, R, Cb, Cg, Carg, Ct, Cc, Cs = lpeg.P, lpeg.S, lpeg.C, lpeg.R, lpeg.Cb, lpeg.Cg, lpeg.Carg, lpeg.Ct, lpeg.Cc, lpeg.Cs
13----- Cf = lpeg.Cf
14local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
15local sort = table.sort
16local setmetatableindex = table.setmetatableindex
17
18xml.css            = xml.css or { }
19local css          = xml.css
20
21local report_css   = logs and logs.reporter("xml","css") or function(...) print(string.format(...)) end
22
23local getid        = lxml and lxml.getid
24
25if not number.dimenfactors then
26    require("util-dim.lua")
27end
28
29local dimenfactors = number.dimenfactors
30local bpf          = 1/dimenfactors.bp
31local cmf          = 1/dimenfactors.cm
32local mmf          = 1/dimenfactors.mm
33local inf          = 1/dimenfactors["in"]
34
35local whitespace   = lpegpatterns.whitespace
36local skipspace    = whitespace^0
37
38local percentage, exheight, emwidth, pixels
39
40if tex then
41
42    local exheights = fonts.hashes.exheights
43    local emwidths  = fonts.hashes.emwidths
44    local texget    = tex.get
45
46    percentage = function(s,pcf) return tonumber(s) * (pcf or texget("hsize"))    end
47    exheight   = function(s,exf) return tonumber(s) * (exf or exheights[true])    end
48    emwidth    = function(s,emf) return tonumber(s) * (emf or emwidths[true])     end
49    pixels     = function(s,pxf) return tonumber(s) * (pxf or emwidths[true]/300) end
50
51else
52
53    local function generic(s,unit) return tonumber(s) * unit end
54
55    percentage = generic
56    exheight   = generic
57    emwidth    = generic
58    pixels     = generic
59
60end
61
62local validdimen = Cg(lpegpatterns.number,'a') * (
63        Cb('a') * P("pt") / function(s) return tonumber(s) * bpf end
64      + Cb('a') * P("cm") / function(s) return tonumber(s) * cmf end
65      + Cb('a') * P("mm") / function(s) return tonumber(s) * mmf end
66      + Cb('a') * P("in") / function(s) return tonumber(s) * inf end
67      + Cb('a') * P("px") * Carg(1) / pixels
68      + Cb('a') * P("%")  * Carg(2) / percentage
69      + Cb('a') * P("ex") * Carg(3) / exheight
70      + Cb('a') * P("em") * Carg(4) / emwidth
71      + Cb('a')           * Carg(1) / pixels
72    )
73
74local pattern = (validdimen * skipspace)^1
75
76-- todo: default if ""
77
78local function dimension(str,pixel,percent,exheight,emwidth)
79    return (lpegmatch(pattern,str,1,pixel,percent,exheight,emwidth))
80end
81
82local function padding(str,pixel,percent,exheight,emwidth)
83    local top, bottom, left, right = lpegmatch(pattern,str,1,pixel,percent,exheight,emwidth)
84    if not bottom then
85        bottom, left, right = top, top, top
86    elseif not left then
87        bottom, left, right = top, bottom, bottom
88    elseif not right then
89        bottom, left, right = left, bottom, bottom
90    end
91    return top, bottom, left, right
92end
93
94css.dimension = dimension
95css.padding   = padding
96
97-- local hsize    = 655360*100
98-- local exheight = 65536*4
99-- local emwidth  = 65536*10
100-- local pixel    = emwidth/100
101--
102-- print(padding("10px",pixel,hsize,exheight,emwidth))
103-- print(padding("10px 20px",pixel,hsize,exheight,emwidth))
104-- print(padding("10px 20px 30px",pixel,hsize,exheight,emwidth))
105-- print(padding("10px 20px 30px 40px",pixel,hsize,exheight,emwidth))
106--
107-- print(padding("10%",pixel,hsize,exheight,emwidth))
108-- print(padding("10% 20%",pixel,hsize,exheight,emwidth))
109-- print(padding("10% 20% 30%",pixel,hsize,exheight,emwidth))
110-- print(padding("10% 20% 30% 40%",pixel,hsize,exheight,emwidth))
111--
112-- print(padding("10",pixel,hsize,exheight,emwidth))
113-- print(padding("10 20",pixel,hsize,exheight,emwidth))
114-- print(padding("10 20 30",pixel,hsize,exheight,emwidth))
115-- print(padding("10 20 30 40",pixel,hsize,exheight,emwidth))
116--
117-- print(padding("10pt",pixel,hsize,exheight,emwidth))
118-- print(padding("10pt 20pt",pixel,hsize,exheight,emwidth))
119-- print(padding("10pt 20pt 30pt",pixel,hsize,exheight,emwidth))
120-- print(padding("10pt 20pt 30pt 40pt",pixel,hsize,exheight,emwidth))
121
122-- print(padding("0",pixel,hsize,exheight,emwidth))
123
124local context = context
125
126if context then
127
128    local currentfont = font.current
129    local texget      = tex.get
130    local hashes      = fonts.hashes
131    local quads       = hashes.quads
132    local xheights    = hashes.xheights
133
134    local function todimension(str)
135        local font     = currentfont()
136        local exheight = xheights[font]
137        local emwidth  = quads[font]
138        local hsize    = texget("hsize")/100
139        local pixel    = emwidth/100
140        return dimension(str,pixel,hsize,exheight,emwidth)
141    end
142
143    css.todimension = todimension
144
145    function context.cssdimension(str)
146     -- context("%ssp",todimension(str))
147        context(todimension(str) .. "sp")
148    end
149
150end
151
152
153do
154
155    local p_digit    = lpegpatterns.digit
156    local p_unquoted = Cs(lpegpatterns.unquoted)
157    local p_size     = (S("+-")^0 * (p_digit^0 * P(".") * p_digit^1 + p_digit^1 * P(".") + p_digit^1)) / tonumber
158                     * C(P("p") * S("txc") + P("e") * S("xm") + S("mc") * P("m") + P("in") + P("%"))
159
160--     local pattern = Cf( Ct("") * (
161--         Cg(
162--             Cc("style") * (
163--                 C("italic")
164--               + C("oblique")
165--               + C("slanted") / "oblique"
166--             )
167--           + Cc("variant") * (
168--                 (C("smallcaps") + C("caps")) / "small-caps"
169--             )
170--           + Cc("weight") * (
171--                 C("bold")
172--             )
173--           + Cc("family") * (
174--                 (C("mono")      + C("type")) / "monospace"  -- just ignore the "space(d)"
175--               + (C("sansserif") + C("sans")) / "sans-serif" -- match before serif
176--               +  C("serif")
177--             )
178--           + Cc("size") * Ct(p_size)
179--         )
180--       + P(1)
181--     )^0 , rawset)
182
183    local pattern = Ct("") * (
184        Cg(
185            Cc("style") * (
186                C("italic")
187              + C("oblique")
188              + C("slanted") / "oblique"
189            )
190          + Cc("variant") * (
191                (C("smallcaps") + C("caps")) / "small-caps"
192            )
193          + Cc("weight") * (
194                C("bold")
195            )
196          + Cc("family") * (
197                (C("mono")      + C("type")) / "monospace"  -- just ignore the "space(d)"
198              + (C("sansserif") + C("sans")) / "sans-serif" -- match before serif
199              +  C("serif")
200            )
201          + Cc("size") * Ct(p_size)
202        ) % rawset
203      + P(1)
204    )^0
205
206    function css.fontspecification(str)
207        return str and lpegmatch(pattern,lower(str))
208    end
209
210    -- These map onto context!
211
212    function css.style(str)
213        if str and str ~= "" then
214            str = lower(str)
215            if str == "italic" then
216                return "italic"
217            elseif str == "slanted" or str == "oblique" then
218                return "slanted"
219            end
220        end
221        return "normal"
222    end
223
224    function css.variant(str) -- will change to a feature
225        if str and str ~= "" then
226            str = lower(str)
227            if str == "small-caps" or str == "caps" or str == "smallcaps" then
228                return "caps"
229            end
230        end
231        return "normal"
232    end
233
234    function css.weight(str)
235        if str and str ~= "" then
236            str = lower(str)
237            if str == "bold" then
238                return "bold"
239            end
240        end
241        return "normal"
242    end
243
244    function css.family(str)
245        if str and str ~= "" then
246            str = lower(str)
247            if str == "mono" or str == "type" or str == "monospace" then
248                return "mono"
249            elseif str == "sansserif" or str == "sans" then
250                return "sans"
251            elseif str == "serif" then
252                return "serif"
253            else
254                -- what if multiple ...
255                return lpegmatch(p_unquoted,str) or str
256            end
257        end
258    end
259
260    function css.size(str,factors, pct)
261        local size, unit
262        if type(str) == "table" then
263            size, unit = str[1], str[2]
264        elseif str and str ~= "" then
265            size, unit = lpegmatch(p_size,lower(str))
266        end
267        if size and unit then
268            if unit == "%" and pct then
269                return size * pct
270            elseif factors then
271                return (factors[unit] or 1) * size
272            else
273                return size, unit
274            end
275        end
276    end
277
278    function css.colorspecification(str)
279        if str and str ~= "" then
280            local c = attributes.colors.values[tonumber(str)]
281            if c then
282                return format("rgb(%s%%,%s%%,%s%%)",c[3]*100,c[4]*100,c[5]*100)
283            end
284        end
285    end
286
287end
288
289-- The following might be handy. It hooks into the normal parser as <selector>
290-- and should work ok with the rest. It's sometimes even a bit faster but that might
291-- change. It's somewhat optimized but not too aggressively.
292
293-- element-1 > element-2 : element-2 with parent element-1
294
295local function s_element_a(list,collected,c,negate,str,dummy,dummy,n)
296    local all = str == "*"
297    for l=1,#list do
298        local ll = list[l]
299        local dt = ll.dt
300        if dt then
301            local ok = all or ll.tg == str
302            if negate then
303                ok = not ok
304            end
305            if ok then
306                c = c + 1
307                collected[c] = ll
308            end
309            if (not n or n > 1) and dt then
310                c = s_element_a(dt,collected,c,negate,str,dummy,dummy,n and n+1 or 1)
311            end
312        end
313    end
314    return c
315end
316
317-- element-1 + element-2 : element-2 preceded by element-1
318
319local function s_element_b(list,collected,c,negate,str)
320    local all = str == "*"
321    for l=1,#list do
322        local ll = list[l]
323        local pp = ll.__p__
324        if pp then
325            local dd = pp.dt
326            if dd then
327                local ni = ll.ni
328                local d = dd[ni+1]
329                local dt = d and d.dt
330                if not dt then
331                    d = dd[ni+2]
332                    dt = d and d.dt
333                end
334                if dt then
335                    local ok = all or d.tg == str
336                    if negate then
337                        ok = not ok
338                    end
339                    if ok then
340                        c = c + 1
341                        collected[c] = d
342                    end
343                end
344            end
345        end
346    end
347    return c
348end
349
350-- element-1 ~ element-2 : element-2 preceded by element-1 -- ?
351
352local function s_element_c(list,collected,c,negate,str)
353    local all = str == "*"
354    for l=1,#list do
355        local ll = list[l]
356        local pp = ll.__p__
357        if pp then
358            local dt = pp.dt
359            if dt then
360                local ni = ll.ni
361                for i=ni+1,#dt do
362                    local d = dt[i]
363                    local dt = d.dt
364                    if dt then
365                        local ok = all or d.tg == str
366                        if negate then
367                            ok = not ok
368                        end
369                        if ok then
370                            c = c + 1
371                            collected[c] = d
372                        end
373                    end
374                end
375            end
376        end
377    end
378    return c
379end
380
381-- element
382-- element-1   element-2 : element-2 inside element-1
383
384local function s_element_d(list,collected,c,negate,str)
385    if str == "*" then
386        if not negate then
387            for l=1,#list do
388                local ll = list[l]
389                local dt = ll.dt
390                if dt then
391                    if not ll.special then
392                        c = c + 1
393                        collected[c] = ll
394                    end
395                    c = s_element_d(dt,collected,c,negate,str)
396                end
397            end
398        end
399    else
400        for l=1,#list do
401            local ll = list[l]
402            local dt = ll.dt
403            if dt then
404                if not ll.special then
405                    local ok = ll.tg == str
406                    if negate then
407                        ok = not ok
408                    end
409                    if ok then
410                        c = c + 1
411                        collected[c] = ll
412                    end
413                end
414                c = s_element_d(dt,collected,c,negate,str)
415            end
416        end
417    end
418    return c
419end
420
421-- [attribute]
422-- [attribute=value]     equals
423-- [attribute~=value]    contains word
424-- [attribute^="value"]  starts with
425-- [attribute$="value"]  ends with
426-- [attribute*="value"]  contains
427
428-- .class    (no need to optimize)
429-- #id       (no need to optimize)
430
431local function s_attribute(list,collected,c,negate,str,what,value)
432    for l=1,#list do
433        local ll = list[l]
434        local dt = ll.dt
435        if dt then
436            local at = ll.at
437            if at then
438                local v  = at[str]
439                local ok = negate
440                if v then
441                    if not what then
442                        ok = not negate
443                    elseif what == 1 then
444                        if v == value then
445                            ok = not negate
446                        end
447                    elseif what == 2 then
448                        -- todo: lpeg
449                        if find(v,value) then -- value can be a pattern
450                            ok = not negate
451                        end
452                    elseif what == 3 then
453                        -- todo: lpeg
454                        if find(v," ",1,true) then
455                            for s in gmatch(v,"[^ ]+") do
456                                if s == value then
457                                    ok = not negate
458                                    break
459                                end
460                            end
461                        elseif v == value then
462                            ok = not negate
463                        end
464                    end
465                end
466                if ok then
467                    c = c + 1
468                    collected[c] = ll
469                end
470            end
471            c = s_attribute(dt,collected,c,negate,str,what,value)
472        end
473    end
474    return c
475end
476
477-- :nth-child(n)
478-- :nth-last-child(n)
479-- :first-child
480-- :last-child
481
482local function filter_down(collected,c,negate,dt,a,b)
483    local t = { }
484    local n = 0
485    for i=1,#dt do
486        local d = dt[i]
487        if type(d) == "table" then
488            n = n + 1
489            t[n] = i
490        end
491    end
492    if n == 0 then
493        return 0
494    end
495    local m = a
496    while true do
497        if m > n then
498            break
499        end
500        if m > 0 then
501            t[m] = -t[m] -- sign signals match
502        end
503        m = m + b
504    end
505    if negate then
506        for i=n,1-1 do
507            local ti = t[i]
508            if ti > 0 then
509                local di = dt[ti]
510                c = c + 1
511                collected[c] = di
512            end
513        end
514    else
515        for i=n,1,-1 do
516            local ti = t[i]
517            if ti < 0 then
518                ti = - ti
519                local di = dt[ti]
520                c = c + 1
521                collected[c] = di
522            end
523        end
524    end
525    return c
526end
527
528local function filter_up(collected,c,negate,dt,a,b)
529    local t = { }
530    local n = 0
531    for i=1,#dt do
532        local d = dt[i]
533        if type(d) == "table" then
534            n = n + 1
535            t[n] = i
536        end
537    end
538    if n == 0 then
539        return 0
540    end
541    if not b then
542        b = 0
543    end
544    local m = n - a
545    while true do
546        if m < 1 then
547            break
548        end
549        if m < n then
550            t[m] = -t[m] -- sign signals match
551        end
552        m = m - b
553    end
554    if negate then
555        for i=1,n do
556            local ti = t[i]
557            if ti > 0 then
558                local di = dt[ti]
559                c = c + 1
560                collected[c] = di
561            end
562        end
563    else
564        for i=1,n do
565            local ti = t[i]
566            if ti < 0 then
567                ti = - ti
568                local di = dt[ti]
569                c = c + 1
570                collected[c] = di
571            end
572        end
573    end
574    return c
575end
576
577local function just(collected,c,negate,dt,a,start,stop,step)
578    local m = 0
579    for i=start,stop,step do
580        local d = dt[i]
581        if type(d) == "table" then
582            m = m + 1
583            if negate then
584                if a ~= m then
585                    c = c + 1
586                    collected[c] = d
587                end
588            else
589                if a == m then
590                    c = c + 1
591                    collected[c] = d
592                    break
593                end
594            end
595        end
596    end
597    return c
598end
599
600local function s_nth_child(list,collected,c,negate,a,n,b)
601    if n == "n" then
602        for l=1,#list do
603            local ll = list[l]
604            local dt = ll.dt
605            if dt then
606                c = filter_up(collected,c,negate,dt,a,b)
607            end
608        end
609    else
610        for l=1,#list do
611            local ll = list[l]
612            local dt = ll.dt
613            if dt then
614                c = just(collected,c,negate,dt,a,1,#dt,1)
615            end
616        end
617    end
618    return c
619end
620
621local function s_nth_last_child(list,collected,c,negate,a,n,b)
622    if n == "n" then
623        for l=1,#list do
624            local ll = list[l]
625            local dt = ll.dt
626            if dt then
627                c = filter_down(collected,c,negate,dt,a,b)
628            end
629        end
630    else
631        for l=1,#list do
632            local ll = list[l]
633            local dt = ll.dt
634            if dt then
635                c = just(collected,c,negate,dt,a,#dt,1,-1)
636            end
637        end
638    end
639    return c
640end
641
642-- :nth-of-type(n)
643-- :nth-last-of-type(n)
644-- :first-of-type
645-- :last-of-type
646
647local function s_nth_of_type(list,collected,c,negate,a,n,b)
648    if n == "n" then
649        return filter_up(collected,c,negate,list,a,b)
650    else
651        return just(collected,c,negate,list,a,1,#list,1)
652    end
653end
654
655local function s_nth_last_of_type(list,collected,c,negate,a,n,b)
656    if n == "n" then
657        return filter_down(collected,c,negate,list,a,b)
658    else
659        return just(collected,c,negate,list,a,#list,1,-1)
660    end
661end
662
663-- :only-of-type
664
665local function s_only_of_type(list,collected,c,negate)
666    if negate then
667        for i=1,#list do
668            c = c + 1
669            collected[c] = list[i]
670        end
671    else
672        if #list == 1 then
673            c = c + 1
674            collected[c] = list[1]
675        end
676    end
677    return c
678end
679
680-- :only-child
681
682local function s_only_child(list,collected,c,negate)
683    if negate then
684        for l=1,#list do
685            local ll = list[l]
686            local dt = ll.dt
687            if dt then
688                for i=1,#dt do
689                    local di = dt[i]
690                    if type(di) == "table" then
691                        c = c + 1
692                        collected[c] = di
693                    end
694                end
695            end
696        end
697    else
698        for l=1,#list do
699            local ll = list[l]
700            local dt = ll.dt
701            if dt and #dt == 1 then
702                local di = dt[1]
703                if type(di) == "table" then
704                    c = c + 1
705                    collected[c] = di
706                end
707            end
708        end
709    end
710    return c
711end
712
713-- :empty
714
715local function s_empty(list,collected,c,negate)
716    for l=1,#list do
717        local ll = list[l]
718        local dt = ll.dt
719        if dt then
720            local dn = #dt
721            local ok = dn == 0
722            if not ok and dn == 1 then
723                local d = dt[1]
724                if type(d) == "string" and is_empty(d) then
725                    ok = true
726                end
727            end
728            if negate then
729                ok = not ok
730            end
731            if ok then
732                c = c + 1
733                collected[c] = ll
734            end
735        end
736    end
737    return c
738end
739
740-- :root
741
742local function s_root(list,collected,c,negate)
743    for l=1,#list do
744        local ll = list[l]
745        if type(ll) == "table" then
746            local r = xml.root(ll)
747            if r then
748                if r.special and r.tg == "@rt@" then
749                    r = r.dt[r.ri]
750                end
751                c = c + 1
752                collected[c] = r
753                break
754            end
755        end
756    end
757    return c
758end
759
760local P, R, S, C, Cs, Ct, Cc, Carg, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.Cc, lpeg.Carg, lpeg.match
761
762local p_number           = lpegpatterns.integer / tonumber
763
764local p_key              = C((R("az","AZ","09") + S("_-"))^1)
765local p_left             = S("#.[],:()")
766local p_right            = S("#.[],:() ")
767local p_tag              = C((1-p_left) * (1-p_right)^0)
768local p_value            = C((1-P("]"))^0)
769local p_unquoted         = (P('"')/"") * C((1-P('"'))^0) * (P('"')/"")
770                         + (1-P("]"))^1
771local p_element          =          Ct( (
772                               P(">") * skipspace * Cc(s_element_a) +
773                               P("+") * skipspace * Cc(s_element_b) +
774                               P("~") * skipspace * Cc(s_element_c) +
775                                                    Cc(s_element_d)
776                           ) * p_tag )
777local p_attribute        = P("[") * Ct(Cc(s_attribute) * p_key * (
778                               P("=" ) * Cc(1) * Cs(           p_unquoted)
779                             + P("^=") * Cc(2) * Cs(Cc("^") * (p_unquoted / topattern))
780                             + P("$=") * Cc(2) * Cs(           p_unquoted / topattern * Cc("$"))
781                             + P("*=") * Cc(2) * Cs(           p_unquoted / topattern)
782                             + P("~=") * Cc(3) * Cs(           p_unquoted)
783                           )^0 * P("]"))
784
785local p_separator        = skipspace * P(",") * skipspace
786
787local p_formula          = skipspace * P("(")
788                         * skipspace
789                         * (
790                                p_number * skipspace * (C("n") * skipspace * (p_number + Cc(0)))^-1
791                              + P("even") * Cc(0)  * Cc("n") * Cc(2)
792                              + P("odd")  * Cc(-1) * Cc("n") * Cc(2)
793                           )
794                         * skipspace
795                         * P(")")
796
797local p_step             = P(".") * Ct(Cc(s_attribute) * Cc("class") * Cc(3) * p_tag)
798                         + P("#") * Ct(Cc(s_attribute) * Cc("id")    * Cc(1) * p_tag)
799                         + p_attribute
800                         + p_element
801                         + P(":nth-child")        * Ct(Cc(s_nth_child)        * p_formula)
802                         + P(":nth-last-child")   * Ct(Cc(s_nth_last_child)   * p_formula)
803                         + P(":first-child")      * Ct(Cc(s_nth_child)        * Cc(1))
804                         + P(":last-child")       * Ct(Cc(s_nth_last_child)   * Cc(1))
805                         + P(":only-child")       * Ct(Cc(s_only_child)       )
806                         + P(":nth-of-type")      * Ct(Cc(s_nth_of_type)      * p_formula)
807                         + P(":nth-last-of-type") * Ct(Cc(s_nth_last_of_type) * p_formula)
808                         + P(":first-of-type")    * Ct(Cc(s_nth_of_type)      * Cc(1))
809                         + P(":last-of-type")     * Ct(Cc(s_nth_last_of_type) * Cc(1))
810                         + P(":only-of-type")     * Ct(Cc(s_only_of_type)     )
811                         + P(":empty")            * Ct(Cc(s_empty)            )
812                         + P(":root")             * Ct(Cc(s_root)             )
813
814local p_not              = P(":not") * Cc(true) * skipspace * P("(") * skipspace * p_step * skipspace * P(")")
815local p_yes              =             Cc(false)                     * skipspace * p_step
816
817local p_stepper          = Ct((skipspace * (p_not+p_yes))^1)
818local p_steps            = Ct((p_stepper * p_separator^0)^1) * skipspace * (P(-1) + function() report_css("recovering from error") end)
819
820local cache = setmetatableindex(function(t,k)
821    local v = lpegmatch(p_steps,k) or false
822    t[k] = v
823    return v
824end)
825
826local function selector(root,s)
827 -- local steps = lpegmatch(p_steps,s)
828    local steps = cache[s]
829    if steps then
830        local done         = { }
831        local collected    = { }
832        local nofcollected = 0
833        local nofsteps     = #steps
834        for i=1,nofsteps do
835            local step = steps[i]
836            local n    = #step
837            if n > 0 then
838                local r = root
839                local m = 0
840                local c = { }
841                for i=1,n,2 do
842                    local s = step[i+1] -- function + data
843                    m = s[1](r,c,0,step[i],s[2],s[3],s[4])
844                    if m == 0 then
845                        break
846                    else
847                        r = c
848                        c = { }
849                    end
850                end
851                if m > 0 then
852                    if nofsteps > 1 then
853                        for i=1,m do
854                            local ri = r[i]
855                            if done[ri] then
856                             -- print("duplicate",i)
857                         -- elseif ri.special then
858                         --     done[ri] = true
859                            else
860                                nofcollected = nofcollected + 1
861                                collected[nofcollected] = ri
862                                done[ri] = true
863                            end
864                        end
865                    else
866                        return r
867                    end
868                end
869            end
870        end
871        if nofcollected > 1 then
872         -- local n = 0
873         -- local function traverse(e)
874         --     if done[e] then
875         --         n = n + 1
876         --         done[e] = n
877         --     end
878         --     local dt = e.dt
879         --     if dt then
880         --         for i=1,#dt do
881         --             local e = dt[i]
882         --             if type(e) == "table" then
883         --                 traverse(e)
884         --             end
885         --         end
886         --     end
887         -- end
888         -- traverse(root[1])
889            --
890            local n = 0
891            local function traverse(dt)
892                for i=1,#dt do
893                    local e = dt[i]
894                    if done[e] then
895                        n = n + 1
896                        done[e] = n
897                        if n == nofcollected then
898                            return
899                        end
900                    end
901                    local d = e.dt
902                    if d then
903                        traverse(d)
904                        if n == nofcollected then
905                            return
906                        end
907                    end
908                end
909            end
910            local r = root[1]
911            if done[r] then
912                n = n + 1
913                done[r] = n
914            end
915            traverse(r.dt)
916            --
917            sort(collected,function(a,b) return done[a] < done[b] end)
918        end
919        return collected
920    else
921        return { }
922    end
923end
924
925xml.applyselector= selector
926
927-- local t = [[
928-- <?xml version="1.0" ?>
929--
930-- <a>
931--     <b class="one">   </b>
932--     <b class="two">   </b>
933--     <b class="one">   </b>
934--     <b class="three"> </b>
935--     <b id="first">    </b>
936--     <c>               </c>
937--     <d>   d e         </d>
938--     <e>   d e         </e>
939--     <e>   d e e       </e>
940--     <d>   d f         </d>
941--     <f foo="bar">     </f>
942--     <f bar="foo">     </f>
943--     <f bar="foo1">     </f>
944--     <f bar="foo2">     </f>
945--     <f bar="foo3">     </f>
946--     <f bar="foo+4">     </f>
947--     <g> </g>
948--     <?crap ?>
949--     <!-- crap -->
950--     <g> <gg> <d> </d> </gg> </g>
951--     <g> <gg> <f> </f> </gg> </g>
952--     <g> <gg> <f class="one"> g gg f </f> </gg> </g>
953--     <g> </g>
954--     <g> <gg> <f class="two"> g gg f </f> </gg> </g>
955--     <g> <gg> <f class="three"> g gg f </f> </gg> </g>
956--     <g> <f class="one"> g f </f> </g>
957--     <g> <f class="three"> g f </f> </g>
958--     <h whatever="four five six"> </h>
959-- </a>
960-- ]]
961--
962-- local s = [[ .one ]]
963-- local s = [[ .one, .two ]]
964-- local s = [[ .one, .two, #first ]]
965-- local s = [[ .one, .two, #first, c, e, [foo], [bar=foo] ]]
966-- local s = [[ .one, .two, #first, c, e, [foo], [bar=foo], [bar~=foo] [bar^="foo"] ]]
967-- local s = [[ [bar^="foo"] ]]
968-- local s = [[ g f .one, g f .three ]]
969-- local s = [[ g > f .one, g > f .three ]]
970-- local s = [[ * ]]
971-- local s = [[ d + e ]]
972-- local s = [[ d ~ e ]]
973-- local s = [[ d ~ e, g f .one, g f .three ]]
974-- local s = [[ :not(d) ]]
975-- local s = [[ [whatever~="five"] ]]
976-- local s = [[ :not([whatever~="five"]) ]]
977-- local s = [[ e ]]
978-- local s = [[ :not ( e ) ]]
979-- local s = [[ a:nth-child(3) ]]
980-- local s = [[ a:nth-child(3n+1) ]]
981-- local s = [[ a:nth-child(2n+8) ]]
982-- local s = [[ g:nth-of-type(3) ]]
983-- local s = [[ a:first-child ]]
984-- local s = [[ a:last-child ]]
985-- local s = [[ e:first-of-type ]]
986-- local s = [[gg d:only-of-type ]]
987-- local s = [[ a:nth-child(even) ]]
988-- local s = [[ a:nth-child(odd) ]]
989-- local s = [[ g:empty ]]
990-- local s = [[ g:root ]]
991
992-- local c = css.applyselector(xml.convert(t),s) for i=1,#c do print(xml.tostring(c[i])) end
993
994function css.applyselector(x,str)
995    -- the wrapping needs checking so this is a placeholder
996    return applyselector({ x },str)
997end
998
999-- -- Some helpers to map e.g. style attributes:
1000--
1001-- -- string based (2.52):
1002--
1003-- local match     = string.match
1004-- local topattern = string.topattern
1005--
1006-- function css.stylevalue(root,name)
1007--     local list = getid(root).at.style
1008--     if list then
1009--         local pattern = topattern(name) .. ":%s*([^;]+)"
1010--         local value   = match(list,pattern)
1011--         if value then
1012--             context(value)
1013--         end
1014--     end
1015-- end
1016--
1017-- -- string based, cached (2.28 / 2.17 interfaced):
1018--
1019-- local match     = string.match
1020-- local topattern = string.topattern
1021--
1022-- local patterns = table.setmetatableindex(function(t,k)
1023--     local v = topattern(k) .. ":%s*([^;]+)"
1024--     t[k] = v
1025--     return v
1026-- end)
1027--
1028-- function css.stylevalue(root,name)
1029--     local list = getid(root).at.style
1030--     if list then
1031--         local value   = match(list,patterns[name])
1032--         if value then
1033--             context(value)
1034--         end
1035--     end
1036-- end
1037--
1038-- -- lpeg based (4.26):
1039--
1040-- the lpeg variant also removes trailing spaces and accepts spaces before a colon
1041
1042local ctx_sprint   = context.sprint
1043local ctx_xmlvalue = context.xmlvalue
1044
1045local colon        = P(":")
1046local semicolon    = P(";")
1047local eos          = P(-1)
1048local somevalue    = (1 - (skipspace * (semicolon + eos)))^1
1049local someaction   = skipspace * colon * skipspace * (somevalue/ctx_sprint)
1050
1051-- function css.stylevalue(root,name)
1052--     local list = getid(root).at.style
1053--     if list then
1054--         lpegmatch(P(name * someaction + 1)^0,list)
1055--     end
1056-- end
1057
1058-- -- cache patterns (2.13):
1059
1060local patterns = setmetatableindex(function(t,k)
1061    local v = P(k * someaction + 1)^0
1062    t[k] = v
1063    return v
1064end)
1065
1066function css.stylevalue(root,name)
1067    local list = getid(root).at.style -- hard coded style
1068    if list then
1069        lpegmatch(patterns[name],list)
1070    end
1071end
1072
1073local somevalue  = (1 - whitespace - semicolon - eos)^1
1074local someaction = skipspace * colon * (skipspace * Carg(1) * C(somevalue)/function(m,s)
1075    ctx_xmlvalue(m,s,"") -- use one with two args
1076end)^1
1077
1078local patterns= setmetatableindex(function(t,k)
1079    local v = P(k * someaction + 1)^0
1080    t[k] = v
1081    return v
1082end)
1083
1084function css.mappedstylevalue(root,map,name)
1085    local list = getid(root).at.style -- hard coded style
1086    if list then
1087        lpegmatch(patterns[name],list,1,map)
1088    end
1089end
1090
1091-- -- faster interface (1.02):
1092
1093interfaces.implement {
1094    name      = "xmlcssstylevalue",
1095    public    = true,
1096    actions   = css.stylevalue,
1097    arguments = "2 strings",
1098}
1099
1100interfaces.implement {
1101    name      = "xmlcssmappedstylevalue",
1102    public    = true,
1103    actions   = css.mappedstylevalue,
1104    arguments = "3 strings",
1105}
1106
1107-- more (for mm)
1108
1109local containsws    = string.containsws
1110local classsplitter = lpeg.tsplitat(whitespace^1)
1111
1112function xml.functions.classes(e,class) -- cache
1113    if class then
1114        local at = e.at
1115        local data = at[class] or at.class
1116        if data then
1117            return lpegmatch(classsplitter,data) or { }
1118        end
1119    end
1120    return { }
1121end
1122
1123-- function xml.functions.hasclass(e,class,name)
1124--     if class then
1125--         local at = e.at
1126--         local data = at[class] or at.class
1127--         if data then
1128--             return data == name or containsws(data,name)
1129--         end
1130--     end
1131--     return false
1132-- end
1133--
1134-- function xml.expressions.hasclass(attribute,name)
1135--     if attribute then
1136--         return attribute == name or containsws(attribute,name)
1137--     end
1138--     return false
1139-- end
1140
1141function xml.functions.hasclass(e,class,name,more,...)
1142    if class and name then
1143        local at = e.at
1144        local data = at[class] or at.class
1145        if not data or data == "" then
1146            return false
1147        end
1148        if data == name or data == more then
1149            return true
1150        end
1151        if containsws(data,name) then
1152            return true
1153        end
1154        if not more then
1155            return false
1156        end
1157        if containsws(data,more) then
1158            return true
1159        end
1160        for i=1,select("#",...) do
1161            if containsws(data,select(i,...)) then
1162                return true
1163            end
1164        end
1165    end
1166    return false
1167end
1168
1169function xml.expressions.hasclass(data,name,more,...)
1170    if data then
1171        if not data or data == "" then
1172            return false
1173        end
1174        if data == name or data == more then
1175            return true
1176        end
1177        if containsws(data,name) then
1178            return true
1179        end
1180        if not more then
1181            return false
1182        end
1183        if containsws(data,more) then
1184            return true
1185        end
1186        for i=1,select("#",...) do
1187            if containsws(data,select(i,...)) then
1188                return true
1189            end
1190        end
1191    end
1192    return false
1193end
1194