1mod parser;
2mod translate;
3
4pub use translate::{Error, Mode, Translator};
5
6pub const VERSION: &str = env!("CARGO_PKG_VERSION");
8
9pub fn css_to_xpath(css: &str, prefix: &str, mode: Mode) -> Result<String, Error> {
24 Translator::new(mode).css_to_xpath(css, prefix)
25}
26
27#[cfg(test)]
28mod tests {
29 use crate::translate::{Mode, Translator};
30
31 fn xpath(css: &str) -> String {
32 Translator::new(Mode::Generic)
33 .css_to_xpath(css, "")
34 .unwrap()
35 }
36
37 #[test]
39 fn simple_selectors() {
40 assert_eq!(xpath("*"), "*");
41 assert_eq!(xpath("e"), "e");
42 assert_eq!(xpath("*|e"), "*[local-name() = 'e']");
43 assert_eq!(xpath("|e"), "e");
44 assert_eq!(xpath("|*"), "*[namespace-uri() = '']");
45 assert_eq!(xpath("*|*"), "*");
46 assert_eq!(xpath("e|f"), "e:f");
47 assert_eq!(xpath("svg|*"), "svg:*");
48 assert_eq!(xpath("e[foo]"), "e[@foo]");
49 assert_eq!(xpath("e[foo|bar]"), "e[@foo:bar]");
50 assert_eq!(xpath("[*|foo]"), "*[@*[local-name() = 'foo']]");
51 assert_eq!(xpath("[|foo]"), "*[@foo]");
52 assert_eq!(xpath("e[foo=\"bar\"]"), "e[@foo = 'bar']");
53 assert_eq!(xpath("e[foo=\"\"]"), "e[@foo = '']");
54 assert_eq!(
55 xpath("e[foo|=\"\"]"),
56 "e[@foo and (@foo = '' or starts-with(@foo, '-'))]"
57 );
58 assert_eq!(
59 xpath("e[foo~=\"bar\"]"),
60 "e[@foo and contains(concat(' ', normalize-space(@foo), ' '), ' bar ')]"
61 );
62 assert_eq!(
63 xpath("e[foo^=\"bar\"]"),
64 "e[@foo and starts-with(@foo, 'bar')]"
65 );
66 assert_eq!(
67 xpath("e[foo$=\"bar\"]"),
68 "e[@foo and substring(@foo, string-length(@foo)-2) = 'bar']"
69 );
70 assert_eq!(
71 xpath("e[foo*=\"bar\"]"),
72 "e[@foo and contains(@foo, 'bar')]"
73 );
74 assert_eq!(
75 xpath("e[hreflang|=\"en\"]"),
76 "e[@hreflang and (@hreflang = 'en' or starts-with(@hreflang, 'en-'))]"
77 );
78 }
79
80 #[test]
81 fn class_id_combinators() {
82 assert_eq!(
83 xpath("e.warning"),
84 "e[@class and contains(concat(' ', normalize-space(@class), ' '), ' warning ')]"
85 );
86 assert_eq!(xpath("e#myid"), "e[@id = 'myid']");
87 assert_eq!(xpath("e f"), "e//f");
88 assert_eq!(xpath("e > f"), "e/f");
89 assert_eq!(xpath("e + f"), "e/following-sibling::*[1][self::f]");
90 assert_eq!(xpath("e ~ f"), "e/following-sibling::f");
91 assert_eq!(
92 xpath("e + f[bar]"),
93 "e/following-sibling::*[1][self::f][@bar]"
94 );
95 assert_eq!(xpath("e + *"), "e/following-sibling::*[1][self::*]");
96 assert_eq!(xpath("div#container p"), "div[@id = 'container']//p");
97 assert_eq!(xpath("a , b"), "a | b");
98 }
99
100 #[test]
101 fn unsafe_names_and_escapes() {
102 assert_eq!(xpath("di\\[v"), "*[name() = 'di[v']");
103 assert_eq!(xpath("[h\\]ref]"), "*[attribute::*[name() = 'h]ref']]");
104 assert_eq!(xpath("di\u{a0}v"), "*[name() = 'di\u{a0}v']");
105 assert_eq!(xpath("#\\31 23"), "*[@id = '123']");
108 assert_eq!(xpath("\\31 23"), "*[name() = '123']");
109 assert_eq!(xpath("[\\31 23]"), "*[attribute::*[name() = '123']]");
110 assert_eq!(xpath("e[foo='\\31 23']"), "e[@foo = '123']");
111 assert_eq!(xpath("e[foo='x\\79 z']"), "e[@foo = 'xyz']");
112 assert_eq!(xpath("*|di\\[v"), "*[local-name() = 'di[v']");
114 assert_eq!(xpath("[*|h\\]ref]"), "*[@*[local-name() = 'h]ref']]");
115 assert_eq!(
118 xpath("|di\\[v"),
119 "*[name() = 'di[v' and namespace-uri() = '']"
120 );
121 assert_eq!(xpath("|é"), "*[name() = 'é' and namespace-uri() = '']");
122 }
123
124 #[test]
125 fn case_sensitivity_flags() {
126 const LOWER_FOO: &str = "translate(@foo, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', \
127 'abcdefghijklmnopqrstuvwxyz')";
128 assert_eq!(xpath("e[foo=\"Bar\" i]"), format!("e[{LOWER_FOO} = 'bar']"));
129 assert_eq!(xpath("e[foo=\"Bar\" I]"), format!("e[{LOWER_FOO} = 'bar']"));
131 assert_eq!(
132 xpath("e[foo^=\"Bar\" i]"),
133 format!("e[{LOWER_FOO} and starts-with({LOWER_FOO}, 'bar')]")
134 );
135 assert_eq!(
136 xpath("e[foo$=\"Bar\" i]"),
137 format!(
138 "e[{LOWER_FOO} and substring({LOWER_FOO}, \
139 string-length({LOWER_FOO})-2) = 'bar']"
140 )
141 );
142 assert_eq!(
144 xpath("e[foo=\"B\u{e4}r\" i]"),
145 format!("e[{LOWER_FOO} = 'b\u{e4}r']")
146 );
147 assert_eq!(xpath("e[foo=\"\" i]"), "e[@foo = '']");
149 assert_eq!(xpath("e[foo=\"Bar\" s]"), "e[@foo = 'Bar']");
151 assert_eq!(
153 xpath("e[*|foo=\"Bar\" i]"),
154 format!(
155 "e[translate(@*[local-name() = 'foo'], \
156 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', \
157 'abcdefghijklmnopqrstuvwxyz') = 'bar']"
158 )
159 );
160 }
161
162 #[test]
163 fn unsupported_errors() {
164 let t = Translator::new(Mode::Generic);
165 assert!(t.css_to_xpath("e[foo!=\"bar\"]", "").is_err());
167 assert!(t.css_to_xpath("e:contains(\"foo\")", "").is_err());
168 assert!(t.css_to_xpath("e::before", "").is_err());
169 assert!(t.css_to_xpath("e:", "").is_err());
170 assert!(t.css_to_xpath("", "").is_err());
171 assert!(t.css_to_xpath("[rel i]", "").is_err());
173 assert!(t.css_to_xpath("[rel=stylesheet k]", "").is_err());
174 assert!(t.css_to_xpath("[rel=stylesheet i i]", "").is_err());
175 assert!(t.css_to_xpath("e:unknown-pseudo", "").is_err());
177 assert!(t.css_to_xpath("e:first-line", "").is_err()); assert!(t.css_to_xpath("col || td", "").is_err());
183 assert!(t.css_to_xpath("col||td", "").is_err());
184 assert!(t.css_to_xpath("e:nth-col(2)", "").is_err());
185 assert!(t.css_to_xpath("e:nth-last-col(2n)", "").is_err());
186 assert!(t.css_to_xpath("[foo=\"a||b\"]", "").is_ok());
188 assert!(t.css_to_xpath("a\\|\\|b", "").is_ok());
189 assert!(t.css_to_xpath("a /* || */ b", "").is_ok());
190 assert!(t.css_to_xpath("e:valid", "").is_err());
195 assert!(t.css_to_xpath("e:user-invalid", "").is_err());
196 assert!(t.css_to_xpath("e:read-only", "").is_err());
197 assert!(t.css_to_xpath("e:placeholder-shown", "").is_err());
198 assert!(t.css_to_xpath("e:defined", "").is_err());
199 assert!(t.css_to_xpath("a :scope", "").is_err());
203 assert!(t.css_to_xpath("a > :scope", "").is_err());
204 assert!(t.css_to_xpath(":scope :scope", "").is_err());
205 assert!(t.css_to_xpath("e:is(:scope)", "").is_err());
206 assert!(t.css_to_xpath("e:not(:scope)", "").is_err());
207 assert!(t.css_to_xpath("e:has(:scope)", "").is_err());
208 assert!(t.css_to_xpath("e:nth-child(2 of :scope)", "").is_err());
209 assert!(t.css_to_xpath("e:is(> a)", "").is_err());
212 assert!(t.css_to_xpath("e:has(> > a)", "").is_err());
213 assert!(t.css_to_xpath("e:has(>)", "").is_err());
214 assert!(t.css_to_xpath("e:has(a >)", "").is_err());
215 assert!(t.css_to_xpath("e:has(a:has(b))", "").is_err());
217 assert!(t.css_to_xpath("e:has(> a:has(b))", "").is_err());
218 assert!(t.css_to_xpath("*:first-of-type", "").is_err());
224 assert!(t.css_to_xpath("*:nth-last-of-type(2)", "").is_err());
225 assert!(t.css_to_xpath("*:only-of-type", "").is_err());
226 assert!(t.css_to_xpath(".foo:first-of-type", "").is_err());
227 assert!(t.css_to_xpath("[bar]:nth-of-type(2)", "").is_err());
228 assert!(t.css_to_xpath(":is(e):first-of-type", "").is_err());
229 assert!(t.css_to_xpath(":lang()", "").is_err());
232 assert!(t.css_to_xpath(":lang(5)", "").is_err());
233 assert!(t.css_to_xpath(":lang(-)", "").is_err());
234 assert!(t.css_to_xpath("e:nth-child(3 7)", "").is_err());
236 assert!(t.css_to_xpath("e:nth-child(2 n)", "").is_err());
237 assert!(t.css_to_xpath("e:nth-child(2.5)", "").is_err());
238 assert!(t.css_to_xpath("e:nth-child(2e1)", "").is_err());
239 }
240
241 #[test]
243 fn nth_family() {
244 assert_eq!(
245 xpath("e:nth-child(1)"),
246 "e[count(preceding-sibling::*) = 0]"
247 );
248 assert_eq!(
249 xpath("e:nth-child(3n+2)"),
250 "e[count(preceding-sibling::*) >= 1 and (count(preceding-sibling::*) +2) mod 3 = 0]"
251 );
252 assert_eq!(
253 xpath("e:nth-child(3n-2)"),
254 "e[count(preceding-sibling::*) mod 3 = 0]"
255 );
256 assert_eq!(
257 xpath("e:nth-child(-n+6)"),
258 "e[count(preceding-sibling::*) <= 5]"
259 );
260 assert_eq!(xpath("e:nth-child(n)"), "e");
261 assert_eq!(xpath("e:nth-child(odd)"), xpath("e:nth-child(2n+1)"));
262 assert_eq!(xpath("e:nth-child(even)"), xpath("e:nth-child(2n)"));
263 assert_eq!(xpath("e:nth-child(2N)"), xpath("e:nth-child(2n)"));
266 assert_eq!(xpath("e:nth-child(ODD)"), xpath("e:nth-child(odd)"));
267 assert_eq!(xpath("e:nth-child(EVEN)"), xpath("e:nth-child(even)"));
268 assert_eq!(xpath("e:nth-child(-N+3)"), xpath("e:nth-child(-n+3)"));
269 assert_eq!(
270 xpath("e:nth-last-child(1)"),
271 "e[count(following-sibling::*) = 0]"
272 );
273 assert_eq!(
274 xpath("e:nth-last-child(2n)"),
275 "e[(count(following-sibling::*) +1) mod 2 = 0]"
276 );
277 assert_eq!(
278 xpath("e:nth-last-child(2n+1)"),
279 "e[count(following-sibling::*) mod 2 = 0]"
280 );
281 assert_eq!(
282 xpath("e:nth-last-child(2n+2)"),
283 "e[count(following-sibling::*) >= 1 and (count(following-sibling::*) +1) mod 2 = 0]"
284 );
285 assert_eq!(
286 xpath("e:nth-last-child(3n+1)"),
287 "e[count(following-sibling::*) mod 3 = 0]"
288 );
289 assert_eq!(
290 xpath("e:nth-last-child(-n+2)"),
291 "e[count(following-sibling::*) <= 1]"
292 );
293 assert_eq!(
294 xpath("e:nth-of-type(1)"),
295 "e[count(preceding-sibling::e) = 0]"
296 );
297 assert_eq!(
298 xpath("e:nth-last-of-type(1)"),
299 "e[count(following-sibling::e) = 0]"
300 );
301 assert_eq!(
302 xpath("div e:nth-last-of-type(1) .aclass"),
303 "div//e[count(following-sibling::e) = 0]//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' aclass ')]"
304 );
305 assert_eq!(xpath("e:first-child"), "e[count(preceding-sibling::*) = 0]");
308 assert_eq!(xpath("e:last-child"), "e[count(following-sibling::*) = 0]");
309 assert_eq!(
310 xpath("e:first-of-type"),
311 "e[count(preceding-sibling::e) = 0]"
312 );
313 assert_eq!(
314 xpath("e:last-of-type"),
315 "e[count(following-sibling::e) = 0]"
316 );
317 assert_eq!(
318 xpath("e:only-child"),
319 "e[count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0]"
320 );
321 assert_eq!(
322 xpath("e:only-of-type"),
323 "e[count(preceding-sibling::e) = 0 and count(following-sibling::e) = 0]"
324 );
325 assert_eq!(
328 xpath("é:first-of-type"),
329 "*[name() = 'é' and count(preceding-sibling::*[name() = 'é']) = 0]"
330 );
331 assert_eq!(
332 xpath("é:nth-of-type(2)"),
333 "*[name() = 'é' and count(preceding-sibling::*[name() = 'é']) = 1]"
334 );
335 assert_eq!(
336 xpath("é:nth-last-of-type(1)"),
337 "*[name() = 'é' and count(following-sibling::*[name() = 'é']) = 0]"
338 );
339 assert_eq!(
340 xpath("é:only-of-type"),
341 "*[name() = 'é' and count(preceding-sibling::*[name() = 'é']) = 0 and count(following-sibling::*[name() = 'é']) = 0]"
342 );
343 assert_eq!(
344 xpath("e ~ f:nth-child(3)"),
345 "e/following-sibling::f[count(preceding-sibling::*) = 2]"
346 );
347 assert_eq!(xpath("e:nth-child(n+1)"), "e");
350 assert_eq!(xpath("e:nth-child(n-5)"), "e");
351 assert_eq!(xpath("e:nth-child(-n)"), "e[0]");
352 assert_eq!(xpath("e:nth-child(-2n-1)"), "e[0]");
353 assert_eq!(xpath("e:nth-child(-n+0)"), "e[0]");
354 assert_eq!(
355 xpath("e:nth-child(-n+1)"),
356 "e[count(preceding-sibling::*) <= 0]"
357 );
358 assert_eq!(
359 xpath("e:nth-child(-2n+2)"),
360 "e[count(preceding-sibling::*) <= 1 and (count(preceding-sibling::*) +1) mod -2 = 0]"
361 );
362 }
363
364 #[test]
366 fn nth_child_of() {
367 assert_eq!(
368 xpath("div:nth-child(2 of .foo)"),
369 "div[count(preceding-sibling::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]) = 1 and @class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"
370 );
371 assert_eq!(
373 xpath("li:nth-child(n of .item)"),
374 "li[@class and contains(concat(' ', normalize-space(@class), ' '), ' item ')]"
375 );
376 assert_eq!(
378 xpath("li:nth-child(-n of .item)"),
379 "li[0 and @class and contains(concat(' ', normalize-space(@class), ' '), ' item ')]"
380 );
381 assert_eq!(
383 xpath("div:nth-child(2 of div.foo)"),
384 "div[count(preceding-sibling::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ') and name() = 'div']) = 1 and @class and contains(concat(' ', normalize-space(@class), ' '), ' foo ') and name() = 'div']"
385 );
386 assert_eq!(
389 xpath("li:nth-child(2 of .foo, *)"),
390 "li[count(preceding-sibling::*) = 1]"
391 );
392 }
393
394 #[test]
396 fn structural_and_never_match_pseudos() {
397 assert_eq!(xpath("e:empty"), "e[not(*) and not(string-length())]");
398 assert_eq!(xpath("e:EmPTY"), "e[not(*) and not(string-length())]");
399 assert_eq!(xpath("e:root"), "e[not(parent::*)]");
400 for pseudo in [
402 "any-link",
403 "link",
404 "visited",
405 "hover",
406 "active",
407 "focus",
408 "focus-within",
409 "focus-visible",
410 "target",
411 "target-within",
412 "local-link",
413 "enabled",
414 "disabled",
415 "checked",
416 "required",
417 "optional",
418 ] {
419 assert_eq!(xpath(&format!("a:{pseudo}")), "a[0]");
420 }
421 assert_eq!(xpath("a:dir(ltr)"), "a[0]");
422 }
423
424 #[test]
425 fn negation_matching_where_has() {
426 assert_eq!(
427 xpath("e:not(:nth-child(odd))"),
428 "e[not(count(preceding-sibling::*) mod 2 = 0)]"
429 );
430 assert_eq!(xpath("e:nOT(*)"), "e[0]");
431 assert_eq!(xpath("e:not(a)"), "e[not(name() = 'a')]");
432 assert_eq!(xpath("e:not(a, b)"), "e[not(name() = 'a' or name() = 'b')]");
433 assert_eq!(xpath("div:not(a, *)"), "div[0]");
435 assert_eq!(xpath("div:where(p)"), "div[name() = 'p']");
438 assert_eq!(
439 xpath("div:where(p, span)"),
440 "div[name() = 'p' or name() = 'span']"
441 );
442 assert_eq!(
443 xpath("*:where(div.content)"),
444 "*[@class and contains(concat(' ', normalize-space(@class), ' '), ' content ') and name() = 'div']"
445 );
446 assert_eq!(
447 xpath("div:where(p):where(span)"),
448 "div[name() = 'p' and name() = 'span']"
449 );
450 assert_eq!(xpath("div:is(p)"), "div[name() = 'p']");
451 assert_eq!(xpath("div:matches(p)"), "div[name() = 'p']");
453 assert_eq!(xpath("e:is(*)"), "e");
455 assert_eq!(xpath("div:is(a, *)"), "div");
456 assert_eq!(xpath("div:where(a, *)"), "div");
457 assert_eq!(xpath("div:has(p)"), "div[.//*[name() = 'p']]");
459 assert_eq!(
460 xpath("div:has(.foo)"),
461 "div[.//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"
462 );
463 assert_eq!(
464 xpath("div:has(p, span)"),
465 "div[.//*[name() = 'p'] | .//*[name() = 'span']]"
466 );
467 assert_eq!(
468 xpath("div:has(p):has(span)"),
469 "div[.//*[name() = 'p'] and .//*[name() = 'span']]"
470 );
471 assert_eq!(
472 xpath("section:has(div.content)"),
473 "section[.//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' content ') and name() = 'div']]"
474 );
475 assert_eq!(xpath("div:has(*)"), "div[.//*]");
476 assert_eq!(xpath("e:has(> img)"), "e[child::*[name() = 'img']]");
478 assert_eq!(xpath("e:has(~ p)"), "e[following-sibling::*[name() = 'p']]");
479 assert_eq!(
480 xpath("e:has(+ p)"),
481 "e[following-sibling::*[1][name() = 'p']]"
482 );
483 assert_eq!(
484 xpath("e:has(> a, ~ p)"),
485 "e[child::*[name() = 'a'] | following-sibling::*[name() = 'p']]"
486 );
487 assert_eq!(
488 xpath("e:has(> .foo)"),
489 "e[child::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"
490 );
491 assert_eq!(
492 xpath("e:has(+ p.foo)"),
493 "e[following-sibling::*[1][@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ') and name() = 'p']]"
494 );
495 assert_eq!(xpath(":not(:not(a))"), "*[not(not(name() = 'a'))]");
497 assert_eq!(xpath("e:is(:not(f))"), "e[not(name() = 'f')]");
498 assert_eq!(xpath("e:has(:not(f))"), "e[.//*[not(name() = 'f')]]");
499 assert_eq!(xpath("e:is(svg|g)"), "e[self::svg:g]");
503 assert_eq!(xpath("e:not(svg|g)"), "e[not(self::svg:g)]");
504 assert_eq!(xpath("e:is(svg|*)"), "e[self::svg:*]");
505 assert_eq!(xpath("e:has(svg|g)"), "e[.//svg:g]");
506 assert_eq!(xpath("e:has(> svg|g)"), "e[child::svg:g]");
507 assert_eq!(xpath("e:has(~ svg|g)"), "e[following-sibling::svg:g]");
508 assert_eq!(
509 xpath("e:has(+ svg|g)"),
510 "e[following-sibling::*[1][self::svg:g]]"
511 );
512 assert_eq!(
513 xpath("e:has(svg|g.foo)"),
514 "e[.//svg:g[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"
515 );
516 }
517
518 #[test]
525 fn complex_pseudo_arguments() {
526 assert_eq!(
528 xpath("e:is(a b)"),
529 "e[name() = 'b' and ancestor::*[name() = 'a']]"
530 );
531 assert_eq!(
532 xpath("e:is(a > b)"),
533 "e[name() = 'b' and parent::*[name() = 'a']]"
534 );
535 assert_eq!(
536 xpath("e:is(a + b)"),
537 "e[name() = 'b' and preceding-sibling::*[1][name() = 'a']]"
538 );
539 assert_eq!(
540 xpath("e:is(a ~ b)"),
541 "e[name() = 'b' and preceding-sibling::*[name() = 'a']]"
542 );
543 assert_eq!(
545 xpath("e:is(a b c)"),
546 "e[name() = 'c' and ancestor::*[name() = 'b' and ancestor::*[name() = 'a']]]"
547 );
548 assert_eq!(
549 xpath("e:is(a > b ~ c)"),
550 "e[name() = 'c' and preceding-sibling::*[name() = 'b' and parent::*[name() = 'a']]]"
551 );
552 assert_eq!(
553 xpath("e:is(a + b > c)"),
554 "e[name() = 'c' and parent::*[name() = 'b' and preceding-sibling::*[1][name() = 'a']]]"
555 );
556 assert_eq!(
559 xpath("e:not(a b)"),
560 "e[not(name() = 'b' and ancestor::*[name() = 'a'])]"
561 );
562 assert_eq!(
563 xpath("e:not(a > b + c)"),
564 "e[not(name() = 'c' and preceding-sibling::*[1][name() = 'b' and parent::*[name() = 'a']])]"
565 );
566 assert_eq!(
567 xpath("e:is(a b, c)"),
568 "e[name() = 'b' and ancestor::*[name() = 'a'] or name() = 'c']"
569 );
570 assert_eq!(
571 xpath("e:is(a, b c)"),
572 "e[name() = 'a' or name() = 'c' and ancestor::*[name() = 'b']]"
573 );
574 assert_eq!(xpath("e:is(* b)"), "e[name() = 'b' and ancestor::*]");
579 assert_eq!(xpath("e:is(a *)"), "e[ancestor::*[name() = 'a']]");
580 assert_eq!(xpath("e:not(a *)"), "e[not(ancestor::*[name() = 'a'])]");
581 assert_eq!(xpath("e:is(a b, *)"), "e");
582 assert_eq!(xpath("e:not(a b, *)"), "e[0]");
583 assert_eq!(
585 xpath("e:is(a.x b.y)"),
586 "e[@class and contains(concat(' ', normalize-space(@class), ' '), ' y ') and \
587 name() = 'b' and \
588 ancestor::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' x ') \
589 and name() = 'a']]"
590 );
591 assert_eq!(
592 xpath("e:is(a[foo='bar'] > b)"),
593 "e[name() = 'b' and parent::*[@foo = 'bar' and name() = 'a']]"
594 );
595 assert_eq!(
596 xpath("e:is(a:first-child b)"),
597 "e[name() = 'b' and ancestor::*[count(preceding-sibling::*) = 0 and name() = 'a']]"
598 );
599 assert_eq!(
600 xpath("e:is(a:hover b)"),
601 "e[name() = 'b' and ancestor::*[0 and name() = 'a']]"
602 );
603 assert_eq!(
606 xpath("e:is(:not(a) b)"),
607 "e[name() = 'b' and ancestor::*[not(name() = 'a')]]"
608 );
609 assert_eq!(
610 xpath("e:not(:is(a b))"),
611 "e[not(name() = 'b' and ancestor::*[name() = 'a'])]"
612 );
613 assert_eq!(
614 xpath("e:is(:not(a b) c)"),
615 "e[name() = 'c' and ancestor::*[not(name() = 'b' and ancestor::*[name() = 'a'])]]"
616 );
617 assert_eq!(
618 xpath("e:is(:is(a, b) c)"),
619 "e[name() = 'c' and ancestor::*[name() = 'a' or name() = 'b']]"
620 );
621 assert_eq!(
622 xpath("e:is(c :is(a, b))"),
623 "e[(name() = 'a' or name() = 'b') and ancestor::*[name() = 'c']]"
624 );
625 assert_eq!(
627 xpath("ns|e:is(a b)"),
628 "ns:e[name() = 'b' and ancestor::*[name() = 'a']]"
629 );
630 assert_eq!(
631 xpath("e:is(ns|a b)"),
632 "e[name() = 'b' and ancestor::*[self::ns:a]]"
633 );
634 assert_eq!(
635 xpath("e:is(a ns|b)"),
636 "e[self::ns:b and ancestor::*[name() = 'a']]"
637 );
638 assert_eq!(
641 xpath("e:has(a b)"),
642 "e[.//*[name() = 'a']//*[name() = 'b']]"
643 );
644 assert_eq!(
645 xpath("e:has(a > b)"),
646 "e[.//*[name() = 'a']/*[name() = 'b']]"
647 );
648 assert_eq!(
649 xpath("e:has(a + b)"),
650 "e[.//*[name() = 'a']/following-sibling::*[1][name() = 'b']]"
651 );
652 assert_eq!(
653 xpath("e:has(a ~ b)"),
654 "e[.//*[name() = 'a']/following-sibling::*[name() = 'b']]"
655 );
656 assert_eq!(
657 xpath("e:has(> a b)"),
658 "e[child::*[name() = 'a']//*[name() = 'b']]"
659 );
660 assert_eq!(
661 xpath("e:has(> a > b)"),
662 "e[child::*[name() = 'a']/*[name() = 'b']]"
663 );
664 assert_eq!(
665 xpath("e:has(+ a > b)"),
666 "e[following-sibling::*[1][name() = 'a']/*[name() = 'b']]"
667 );
668 assert_eq!(
669 xpath("e:has(~ a + b)"),
670 "e[following-sibling::*[name() = 'a']/following-sibling::*[1][name() = 'b']]"
671 );
672 assert_eq!(
673 xpath("e:has(a > b + c)"),
674 "e[.//*[name() = 'a']/*[name() = 'b']/following-sibling::*[1][name() = 'c']]"
675 );
676 assert_eq!(
677 xpath("e:has(> a:is(b c))"),
678 "e[child::*[name() = 'c' and ancestor::*[name() = 'b'] and name() = 'a']]"
679 );
680 assert_eq!(
681 xpath("e:has(a.x > b.y)"),
682 "e[.//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' x ') \
683 and name() = 'a']/*[@class and \
684 contains(concat(' ', normalize-space(@class), ' '), ' y ') and name() = 'b']]"
685 );
686 assert_eq!(xpath("e:has(ns|a > b)"), "e[.//ns:a/*[name() = 'b']]");
689 assert_eq!(
690 xpath("e:has(a + ns|b)"),
691 "e[.//*[name() = 'a']/following-sibling::*[1][self::ns:b]]"
692 );
693 assert_eq!(
696 xpath("e:nth-child(2n of a b)"),
697 "e[(count(preceding-sibling::*[name() = 'b' and ancestor::*[name() = 'a']]) +1) \
698 mod 2 = 0 and name() = 'b' and ancestor::*[name() = 'a']]"
699 );
700 assert_eq!(
701 xpath("e:nth-child(2n of a > b)"),
702 "e[(count(preceding-sibling::*[name() = 'b' and parent::*[name() = 'a']]) +1) \
703 mod 2 = 0 and name() = 'b' and parent::*[name() = 'a']]"
704 );
705 assert_eq!(
706 xpath("e:nth-last-child(3 of a b)"),
707 "e[count(following-sibling::*[name() = 'b' and ancestor::*[name() = 'a']]) = 2 \
708 and name() = 'b' and ancestor::*[name() = 'a']]"
709 );
710 }
711
712 #[test]
716 fn scope_pseudo() {
717 let t = Translator::new(Mode::Generic);
718 assert_eq!(xpath(":scope"), "self::*");
719 assert_eq!(xpath(":ScoPE"), "self::*");
720 assert_eq!(xpath(":scope > a"), "self::*/a");
721 assert_eq!(xpath(":scope a"), "self::*//a");
722 assert_eq!(
723 xpath(":scope + a"),
724 "self::*/following-sibling::*[1][self::a]"
725 );
726 assert_eq!(xpath(":scope ~ a"), "self::*/following-sibling::a");
727 assert_eq!(xpath("div:scope"), "self::div");
730 assert_eq!(xpath("svg|g:scope"), "self::svg:g");
731 assert_eq!(
732 xpath(":scope.foo > a"),
733 "self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]/a"
734 );
735 assert_eq!(
736 xpath(":scope:first-child"),
737 "self::*[count(preceding-sibling::*) = 0]"
738 );
739 assert_eq!(
741 t.css_to_xpath(":scope > a", "descendant-or-self::")
742 .unwrap(),
743 "self::*/a"
744 );
745 assert_eq!(
746 t.css_to_xpath("a, :scope > b", "descendant-or-self::")
747 .unwrap(),
748 "descendant-or-self::a | self::*/b"
749 );
750 }
751
752 #[test]
753 fn lang_and_dir() {
754 assert_eq!(xpath("e:lang(en)"), "e[lang('en')]");
756 assert_eq!(xpath("e:lang(\"en\")"), "e[lang('en')]");
757 assert_eq!(xpath("e:lang(en-*)"), "e[lang('en')]");
758 assert_eq!(xpath("e:lang(*)"), "e[true()]");
759 assert_eq!(xpath("e:lang(en, fr)"), "e[lang('en') or lang('fr')]");
760 assert_eq!(xpath("e:lang(en fr)"), "e[lang('en') or lang('fr')]");
762 assert_eq!(xpath("e:lang(*, fr)"), "e[true() or lang('fr')]");
765 let html = Translator::new(Mode::Html);
767 assert_eq!(
768 html.css_to_xpath("e:lang(EN)", "").unwrap(),
769 "e[ancestor-or-self::*[@lang][1][starts-with(concat(translate(@lang, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-'), 'en-')]]"
770 );
771 assert_eq!(
772 html.css_to_xpath("e:lang(*)", "").unwrap(),
773 "e[ancestor-or-self::*[@lang]]"
774 );
775 let xhtml = Translator::new(Mode::Xhtml);
777 assert_eq!(
778 xhtml.css_to_xpath("E:lang(*)", "").unwrap(),
779 "E[ancestor-or-self::*[@lang]]"
780 );
781 let t = Translator::new(Mode::Generic);
785 for sel in [
786 "e:lang(*-CH)",
787 "e:lang(\"*-CH\")",
788 "e:lang(de-*-DE)",
789 "e:lang(\"de-*-DE\")",
790 ] {
791 assert!(t.css_to_xpath(sel, "").is_err(), "{sel} should error");
792 assert!(
793 html.css_to_xpath(sel, "").is_err(),
794 "{sel} should error (html)"
795 );
796 }
797 assert_eq!(xpath("e:dir(rtl)"), "e[0]");
803 assert_eq!(html.css_to_xpath("e:dir(rtl)", "").unwrap(), "e[0]");
804 assert_eq!(xhtml.css_to_xpath("e:dir(ltr)", "").unwrap(), "e[0]");
805 assert!(t.css_to_xpath("e:dir()", "").is_err());
806 assert!(t.css_to_xpath("e:dir(ltr rtl)", "").is_err());
807 assert!(t.css_to_xpath("e:dir(ltr, rtl)", "").is_err());
808 assert!(t.css_to_xpath("e:dir(\"ltr\")", "").is_err());
809 assert!(t.css_to_xpath("e:dir(*)", "").is_err());
810 }
811
812 #[test]
814 fn html_pseudo_overrides() {
815 let html = Translator::new(Mode::Html);
816 let h = |css: &str| html.css_to_xpath(css, "").unwrap();
817 assert_eq!(
818 h("a:link"),
819 "a[@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')]"
820 );
821 assert_eq!(h("a:any-link"), h("a:link"));
824 assert_eq!(h("a:ANY-link"), h("a:link"));
825 let t_lc = "translate(@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
829 assert_eq!(
830 h("input:checked"),
831 format!(
832 "input[(@selected and name(.) = 'option') or (@checked and \
833 (name(.) = 'input' or name(.) = 'command')and \
834 ({t_lc} = 'checkbox' or {t_lc} = 'radio'))]"
835 )
836 );
837 assert_eq!(
841 h("input:required"),
842 format!(
843 "input[@required and ((name(.) = 'input' and not(\
844 {t_lc} = 'hidden' or {t_lc} = 'range' or {t_lc} = 'color' or \
845 {t_lc} = 'submit' or {t_lc} = 'image' or {t_lc} = 'reset' or \
846 {t_lc} = 'button')) or name(.) = 'select' or name(.) = 'textarea')]"
847 )
848 );
849 assert_eq!(
850 h("select:optional"),
851 format!(
852 "select[not(@required) and ((name(.) = 'input' and not(\
853 {t_lc} = 'hidden' or {t_lc} = 'range' or {t_lc} = 'color' or \
854 {t_lc} = 'submit' or {t_lc} = 'image' or {t_lc} = 'reset' or \
855 {t_lc} = 'button')) or name(.) = 'select' or name(.) = 'textarea')]"
856 )
857 );
858 let fd = "count(ancestor::fieldset[@disabled]) > \
863 count(ancestor::legend[not(preceding-sibling::legend)]\
864 [parent::fieldset[@disabled]])";
865 assert_eq!(
866 h("input:disabled"),
867 format!(
868 "input[( @disabled and ( \
869 (name(.) = 'input' and not({t_lc} = 'hidden')) or \
870 name(.) = 'button' or name(.) = 'select' or \
871 name(.) = 'textarea' or name(.) = 'command' or \
872 name(.) = 'fieldset' or name(.) = 'optgroup' or \
873 name(.) = 'option' \
874 ) ) or ( ( \
875 (name(.) = 'input' and not({t_lc} = 'hidden')) or \
876 name(.) = 'button' or name(.) = 'select' or \
877 name(.) = 'textarea' \
878 ) \
879 and {fd} \
880 )]"
881 )
882 );
883 assert_eq!(
884 h("input:enabled"),
885 format!(
886 "input[(@href and (name(.) = 'a' or name(.) = 'link' or \
887 name(.) = 'area')) or \
888 ((name(.) = 'command' or name(.) = 'fieldset' or \
889 name(.) = 'optgroup') and not(@disabled)) or \
890 (((name(.) = 'input' and not({t_lc} = 'hidden')) \
891 or name(.) = 'button' or name(.) = 'select' \
892 or name(.) = 'textarea' or name(.) = 'keygen') \
893 and not (@disabled or {fd})) \
894 or (name(.) = 'option' and not(@disabled or \
895 ancestor::optgroup[@disabled]))]"
896 )
897 );
898 assert_eq!(h("a:hover"), "a[0]");
900 assert_eq!(h("a:visited"), "a[0]");
901 assert_eq!(h("a:focus-within"), "a[0]");
902 assert_eq!(h("a:focus-visible"), "a[0]");
903 }
904
905 #[test]
906 fn html_translator_lowercases_names_not_values() {
907 let html = Translator::new(Mode::Html);
908 assert_eq!(html.css_to_xpath("DIV", "").unwrap(), "div");
909 assert_eq!(html.css_to_xpath("[FOO]", "").unwrap(), "*[@foo]");
910 assert_eq!(
912 html.css_to_xpath("DIV[Value=\"Mixed Case\"]", "").unwrap(),
913 "div[@value = 'Mixed Case']"
914 );
915 assert_eq!(
917 html.css_to_xpath("*|DIV", "").unwrap(),
918 "*[local-name() = 'div']"
919 );
920 let xhtml = Translator::new(Mode::Xhtml);
922 assert_eq!(xhtml.css_to_xpath("DIV", "").unwrap(), "DIV");
923 }
924
925 #[test]
926 fn prefix_applied_per_branch() {
927 let t = Translator::new(Mode::Generic);
928 assert_eq!(
929 t.css_to_xpath("a, b", "descendant-or-self::").unwrap(),
930 "descendant-or-self::a | descendant-or-self::b"
931 );
932 }
933}