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