122

I'm running over all textnodes of my DOM and check if the nodeValue contains a certain string.

/html/body//text()[contains(.,'test')]

This is case sensitive. However, I also want to catch Test, TEST or TesT. Is that possible with XPath (in JavaScript)?

kjhughes
  • 106,133
  • 27
  • 181
  • 240
Aron Woost
  • 19,268
  • 13
  • 43
  • 51

6 Answers6

137

This is for XPath 1.0. If your environment supports XPath 2.0, see here.


Yes. Possible, but not beautiful.

/html/body//text()[
  contains(
    translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),
    'test'
  )
]

This would work for search strings where the alphabet is known beforehand. Add any accented characters you expect to see.


If you can, mark the text that interests you with some other means, like enclosing it in a <span> that has a certain class while building the HTML. Such things are much easier to locate with XPath than substrings in the element text.

If that's not an option, you can let JavaScript (or any other host language that you are using to execute XPath) help you with building an dynamic XPath expression:

function xpathPrepare(xpath, searchString) {
  return xpath.replace("$u", searchString.toUpperCase())
              .replace("$l", searchString.toLowerCase())
              .replace("$s", searchString.toLowerCase());
}

xp = xpathPrepare("//text()[contains(translate(., '$u', '$l'), '$s')]", "Test");
// -> "//text()[contains(translate(., 'TEST', 'test'), 'test')]"

(Hat tip to @KirillPolishchuk's answer - of course you only need to translate those characters you're actually searching for.)

This approach would work for any search string whatsoever, without requiring prior knowledge of the alphabet, which is a big plus.

Both of the methods above fail when search strings can contain single quotes, in which case things get more complicated.

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thanks! Also the addition is nice, translating only the needed chars. I'd be curious what the performance win is. Note that xpathPrepare() could handle more-than-once appearing chars differently (e.g. you get TEEEEEST and teeeeest). – Aron Woost Dec 12 '11 at 13:37
  • @AronWoost: Well, there might be some gain, just benchmark it if you are eager to find out. `translate()` itself does not care how often you repeat each character - `translate(., 'EE', 'ee')` is absolutely equivalent to `translate(., 'E', 'e')`. *P.S.: Don't forget to up-vote @KirillPolishchuk, the idea was his.* – Tomalak Dec 12 '11 at 14:19
  • 3
    System.Xml.XmlNodeList x = mydoc.SelectNodes("//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉÈÊÀÁÂÒÓÔÙÚÛÇÅÏÕÑŒ', 'abcdefghijklmnopqrstuvwxyzäöüéèêàáâòóôùúûçåïõñœ'),'foo')]"); – Stefan Steiger Nov 29 '13 at 09:34
  • 1
    No. See the *"of course you only need to translate those characters you're actually searching for"* part. – Tomalak Nov 29 '13 at 10:10
74

Modern XPath 2.0 (and higher) Solutions

  1. Use lower-case():

    /html/body//text()[contains(lower-case(.),'test')]

  2. Use matches() regex matching with its case-insensitive flag:

    /html/body//text()[matches(.,'test', 'i')]

For older XPath-1.0-limited environments, see the translate() technique described in @Tomalak's answer.

kjhughes
  • 106,133
  • 27
  • 181
  • 240
  • 1
    Is this syntax not supported in Firefox and Chrome? I just tried it in the console and they both return syntax error. – d-b Jun 08 '19 at 11:51
  • 8
    Firefox and Chrome only implement XPath 1.0. – kjhughes Aug 07 '19 at 12:17
  • where I can verify that this will work as expected? – Ankit Gupta Oct 13 '20 at 18:04
  • @AnkitGupta: Any online or offline tool that supports XPath 2.0 can be used to verify this answer, of course, but (1) tool recommendations are off-topic here on SO and (2) given the 56 upvotes, 0 downvotes, and no dissenting comments in over six years, you can be pretty confident that this answer is correct. ;-) – kjhughes Oct 13 '20 at 18:45
68

Case-insensitive contains

/html/body//text()[contains(translate(., 'EST', 'est'), 'test')]
Kirill Polishchuk
  • 54,804
  • 11
  • 122
  • 125
11

Yes. You can use translate to convert the text you want to match to lower case as follows:

/html/body//text()[contains(translate(., 
                                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                                      'abcdefghijklmnopqrstuvwxyz'),
                   'test')]
Andy
  • 8,870
  • 1
  • 31
  • 39
7

The way i always did this was by using the "translate" function in XPath. I won't say its very pretty but it works correctly.

/html/body//text()[contains(translate(.,'abcdefghijklmnopqrstuvwxyz',
                                        'ABCDEFGHIJKLMNOPQRSTUVWXYZ'),'TEST')]

hope this helps,

Endre Both
  • 5,540
  • 1
  • 26
  • 31
Marvin Smit
  • 4,088
  • 1
  • 22
  • 21
7

If you're using XPath 2.0 then you can specify a collation as the third argument to contains(). However, collation URIs are not standardized so the details depend on the product that you are using.

Note that the solutions given earlier using translate() all assume that you are only using the 26-letter English alphabet.

UPDATE: XPath 3.1 defines a standard collation URI for case-blind matching.

Michael Kay
  • 156,231
  • 11
  • 92
  • 164