# coding=utf-8 """ Test CSS Selectors Implementation This unit test is a heavily modified version, adapted to SVG, originating from CSSSelect2 (BSD). """ import pytest from lxml import etree from inkex.elements._parser import load_svg from inkex.styles import ConditionalStyle document = load_svg("tests/data/svg/ids.svg").getroot() ALL_IDS = [el.get("id") for el in document.iter() if not isinstance(el, etree._Comment)] @pytest.mark.parametrize( "selector, result", ( ("*", ALL_IDS), ("g", ["layer", "outer-g", "tspan-g", "foobar-g"]), ("g g", ["outer-g", "tspan-g", "foobar-g"]), ("g, g g", ["layer", "outer-g", "tspan-g", "foobar-g"]), ("g , g g", ["layer", "outer-g", "tspan-g", "foobar-g"]), ("rect[width]", ["rect1"]), ('rect[width="5"]', ["rect1"]), ('rect[style*="black"]', ["rect1"]), ('rect[style*=""]', []), ('rect[style^="fill"]', ["rect1", "rect2"]), ('rect[style^=""]', []), ('rect[style$=";"]', ["rect1", "rect2"]), ('rect[style$=""]', []), ('g[foobar~="bc"]', ["foobar-g"]), ('g[foobar~="cde"]', ["foobar-g"]), ('[foobar~="ab bc"]', []), ('[foobar~=""]', []), ('[foobar~=" \t"]', []), ('g[foobar~="cd"]', []), ('rect[style="fill:BlAck;"]', []), ('rect[style="fill:BlAck;" s]', []), ('rect[style="fill:BlAck;" i]', ["rect1"]), ('rect[style*="BlAck"]', []), ('rect[style*="BlAck" s]', []), ('rect[style*="BlAck" i]', ["rect1"]), ('rect[style^="fILl"]', []), ('rect[style^="fiLl" s]', []), ('rect[style^="fiLl" i]', ["rect1", "rect2"]), ('rect[style$="Black;"]', []), ('rect[style$="Black;" S]', []), ('rect[style$="Black;" I]', ["rect1"]), ('g[foobar~="BC"]', []), ('g[foobar~="BC" s]', []), ('g[foobar~="BC" i]', ["foobar-g"]), # Attribute values are case sensitive… ('*[lang|="En"]', ["second-tspan"]), ('[lang|="En-us"]', ["second-tspan"]), ('*[lang|="en"]', []), ('[lang|="en-US"]', []), ('*[lang|="e"]', []), # … but :lang() is not. (":lang(EN)", ["second-tspan", "tspan-g"]), ("*:lang(en-US)", ["second-tspan", "tspan-g"]), (":lang(En)", ["second-tspan", "tspan-g"]), (":lang(e)", []), (':lang("en-US")', ["second-tspan", "tspan-g"]), # pytest.param( # ':lang("*-US")', ['second-tspan', 'tspan-g'], marks=pytest.mark.xfail), # pytest.param( # ':lang(\\*-US)', ['second-tspan', 'tspan-g'], marks=pytest.mark.xfail), (":lang(en /* English */, fr /* French */)", ["second-tspan", "tspan-g"]), ("tspan:nth-child(3)", ["third-tspan"]), ("tspan:nth-child(10)", []), ("tspan:nth-child(2n)", ["second-tspan", "fourth-tspan", "sixth-tspan"]), ("tspan:nth-child(even)", ["second-tspan", "fourth-tspan", "sixth-tspan"]), ("tspan:nth-child(+2n+0)", ["second-tspan", "fourth-tspan", "sixth-tspan"]), ( "tspan:nth-child(2n+1)", ["first-tspan", "third-tspan", "fifth-tspan", "seventh-tspan"], ), ( "tspan:nth-child(odd)", ["first-tspan", "third-tspan", "fifth-tspan", "seventh-tspan"], ), ("tspan:nth-child(2n+4)", ["fourth-tspan", "sixth-tspan"]), ("tspan:nth-child(3n+1)", ["first-tspan", "fourth-tspan", "seventh-tspan"]), ( "text > tspan:nth-child(2n of text tspan[my-attr=test])", ["third-tspan", "fifth-tspan"], ), ("tspan:nth-last-child(1)", ["seventh-tspan"]), ("tspan:nth-last-child(0)", []), ("tspan:nth-last-child(2n+2)", ["second-tspan", "fourth-tspan", "sixth-tspan"]), ("tspan:nth-last-child(even)", ["second-tspan", "fourth-tspan", "sixth-tspan"]), ("tspan:nth-last-child(2n+4)", ["second-tspan", "fourth-tspan"]), (":nth-last-child(1 of [my-attr=test])", ["fifth-tspan"]), ("text:first-of-type", ["first-text"]), ("text:nth-child(1)", []), ("text:nth-of-type(2)", ["second-text"]), (":nth-of-type(1 of .e)", ["first-text"]), ("text:nth-last-of-type(2)", ["first-text"]), (":nth-last-of-type(1 of .e)", ["second-text"]), ("mask:only-child", ["foobar-mask"]), ("g:only-child", ["tspan-g"]), ("g *:only-child", ["tspan-g", "link-element", "foobar-mask"]), ( "pattern *:only-of-type", ["pattern-textPath", "link-element", "radialGradient"], ), ("pattern:only-of-type", ["paragraph"]), ( "tspan:empty", [ "third-tspan", "fourth-tspan", "fifth-tspan", "sixth-tspan", "seventh-tspan", ], ), ( "tspan:EMpty", [ "third-tspan", "fourth-tspan", "fifth-tspan", "sixth-tspan", "seventh-tspan", ], ), (":root", ["svg"]), ("svg:root", ["svg"]), ("tspan:root", []), ("* :root", []), (".a", ["first-text"]), (".b", ["first-text"]), ("*.a", ["first-text"]), ("text.a", ["first-text"]), (".c", ["first-text", "third-tspan", "fourth-tspan"]), ("*.c", ["first-text", "third-tspan", "fourth-tspan"]), ("text *.c", ["third-tspan", "fourth-tspan"]), ("text tspan.c", ["third-tspan", "fourth-tspan"]), ("tspan ~ tspan.c", ["third-tspan", "fourth-tspan"]), ("text > tspan.c", ["third-tspan", "fourth-tspan"]), ("#first-tspan", ["first-tspan"]), ("tspan#first-tspan", ["first-tspan"]), ("*#first-tspan", ["first-tspan"]), ("tspan g", ["tspan-g"]), ("tspan > g", ["tspan-g"]), ("g > g", ["outer-g", "foobar-g"]), ("g>.c", ["first-text"]), ("g > .c", ["first-text"]), ("g + g", ["foobar-g"]), ("stop ~ stop", ["stop2-radialGradient"]), ('circle[shape="circle"] ~ circle', ["circle-nohref"]), ("text#first-text tspan:last-child", ["seventh-tspan"]), ("text#first-text *:last-child", ["tspan-g", "seventh-tspan"]), ("#outer-g:first-child", ["outer-g"]), ( "#outer-g :first-child", [ "rect1", "first-tspan", "tspan-g", "pattern-p", "link-element", "stop-radialGradient", "circle-href", ], ), (":not(*)", []), ("linearGradient:not([href])", ["linearGradient-nohref"]), ( "text :Not([class])", [ "first-tspan", "second-tspan", "tspan-g", "fifth-tspan", "sixth-tspan", "seventh-tspan", ], ), ("tspan:not(:nth-child(odd), #second-tspan)", ["fourth-tspan", "sixth-tspan"]), ("tspan:not(tspan)", []), (":is(*)", ALL_IDS), (":is(g)", ["layer", "outer-g", "tspan-g", "foobar-g"]), ( ":is(g, radialGradient)", ["layer", "outer-g", "tspan-g", "radialGradient", "foobar-g"], ), (":is(:::wrong)", []), ( ":is(g, :::wrong, radialGradient)", ["layer", "outer-g", "tspan-g", "radialGradient", "foobar-g"], ), ("g :is(g, g)", ["outer-g", "tspan-g", "foobar-g"]), ("tspan:is(.c)", ["third-tspan", "fourth-tspan"]), ('stop:is([offset="1"])', ["stop2-radialGradient"]), ("g:is(:not(#outer-g))", ["layer", "tspan-g", "foobar-g"]), ("g:is(g::before)", []), (":where(*)", ALL_IDS), (":where(g)", ["layer", "outer-g", "tspan-g", "foobar-g"]), ( ":where(g, radialGradient)", ["layer", "outer-g", "tspan-g", "radialGradient", "foobar-g"], ), (":where(:::wrong)", []), ( ":where(g, :::wrong, radialGradient)", ["layer", "outer-g", "tspan-g", "radialGradient", "foobar-g"], ), ("g :where(g, g)", ["outer-g", "tspan-g", "foobar-g"]), ("tspan:where(.c)", ["third-tspan", "fourth-tspan"]), ('stop:where([offset="1"])', ["stop2-radialGradient"]), ("g:where(:not(#outer-g))", ["layer", "tspan-g", "foobar-g"]), ("g:where(g::before)", []), ("pattern:has(stop)", ["paragraph"]), ("pattern:has(radialGradient stop)", ["paragraph"]), ("pattern:has(> radialGradient)", ["paragraph"]), ("text:has(> g)", []), ("text:has(stop, tspan)", ["first-text"]), ("text:has(stop, radialGradient)", []), ("text:has(+ pattern)", ["first-text"]), ("text:has(~ text)", ["first-text"]), ("text:has(>a, ~ text)", ["first-text"]), ("text:has(a,text, tspan )", ["first-text"]), ("text:has(*)", ["first-text"]), ("text:has(:not(tspan))", ["first-text"]), ("text:has( > :not( tspan ))", []), ("text:has(:not(tspan, g))", []), # Invalid characters in XPath element names, should not crash (r"di\a0 v", []), (r"g\[", []), (r"[h\a0 ref]", []), (r"[h\]ref]", []), (":link", ["link", "link-element"]), (":any-link", ["link", "link-element"]), (":local-link", ["link", "link-element"]), (":visited", []), (":hover", []), (":active", []), (":focus", []), (":target", []), (":enabled", []), (":disabled", []), (":checked", []), # Check that comments inside the selector are not a problem ("a:not([href]), g g", ["outer-g", "tspan-g", "nolink", "foobar-g"]), ("a:not([href]) /* test */, g g", ["outer-g", "tspan-g", "nolink", "foobar-g"]), ("a:not([href]), /* test */ g g", ["outer-g", "tspan-g", "nolink", "foobar-g"]), ("/* test */a:not([href]),g g", ["outer-g", "tspan-g", "nolink", "foobar-g"]), ("a:not([href]) , g g/* test */", ["outer-g", "tspan-g", "nolink", "foobar-g"]), ( "/* test */a:not([href]), /* test */ g g", ["outer-g", "tspan-g", "nolink", "foobar-g"], ), ( "/* test */a:not([href])/* test */,g g", ["outer-g", "tspan-g", "nolink", "foobar-g"], ), ( "/* test */ a:not([href]), g/* test */ g", ["outer-g", "tspan-g", "nolink", "foobar-g"], ), ( "a:not([href]) /* test */,/* test */g g", ["outer-g", "tspan-g", "nolink", "foobar-g"], ), (":is() p", []), ("*~rect", ["rect2", "rect3"]), ("radialGradient *", ["stop-radialGradient", "stop2-radialGradient"]), ("radialGradient > :is()", []), ("pattern:has(stop, :is())", ["paragraph"]), ("svg|pattern:has(stop, :is())", ["paragraph"]), ('g[inkscape|groupmode="layer"][svg|id="layer"]', ["layer"]), ('g[inkscape|groupmode="layer"][id="layer"]', ["layer"]), ('g[groupmode="layer"][id="layer"]', []), ( "#paragraph :last-of-type", [ "pattern-textPath", "pattern-p2", "link-element", "nolink", "radialGradient", "stop2-radialGradient", ], ), ), ) def test_select(selector, result): """Main selectors test""" style = ConditionalStyle(selector) found = [el.get("id", "nil") for el in style.all_matches(document)] assert found == result, found