Skip to content

Commit 062d190

Browse files
PhuNHjhy
andauthored
Restore the behavior of cssSelector before v1.16.2 for elements with an id (#2283)
* Restore the behavior of cssSelector before v1.16.2 * Optimized out id() and ownerDoc() calls Fixes #2282 --------- Co-authored-by: Jonathan Hedley <jonathan@hedley.net>
1 parent ae4dfd2 commit 062d190

File tree

3 files changed

+62
-18
lines changed

3 files changed

+62
-18
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# jsoup Changelog
22

3+
## 1.19.2 (PENDING)
4+
5+
### Changes
6+
7+
### Improvements
8+
* `Element.cssSelector()` will prefer to return shorter selectors by using ancestor IDs when available and unique. E.g. `#id > div > p` instead of `html > body > div > div > p` [#2283](https://github.com/jhy/jsoup/pull/2283).
9+
310
## 1.19.1 (2025-03-04)
411

512
### Changes

src/main/java/org/jsoup/nodes/Element.java

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -938,32 +938,47 @@ public Element wrap(String html) {
938938
}
939939

940940
/**
941-
* Get a CSS selector that will uniquely select this element.
942-
* <p>
943-
* If the element has an ID, returns #id;
944-
* otherwise returns the parent (if any) CSS selector, followed by {@literal '>'},
945-
* followed by a unique selector for the element (tag.class.class:nth-child(n)).
946-
* </p>
947-
*
948-
* @return the CSS Path that can be used to retrieve the element in a selector.
941+
Gets an #id selector for this element, if it has a unique ID. Otherwise, returns an empty string.
942+
943+
@param ownerDoc the document that owns this element, if there is one
949944
*/
950-
public String cssSelector() {
951-
if (id().length() > 0) {
952-
// prefer to return the ID - but check that it's actually unique first!
953-
String idSel = "#" + escapeCssIdentifier(id());
954-
Document doc = ownerDocument();
955-
if (doc != null) {
956-
Elements els = doc.select(idSel);
957-
if (els.size() == 1 && els.get(0) == this) // otherwise, continue to the nth-child impl
958-
return idSel;
945+
private String uniqueIdSelector(@Nullable Document ownerDoc) {
946+
String id = id();
947+
if (!id.isEmpty()) { // check if the ID is unique and matches this
948+
String idSel = "#" + escapeCssIdentifier(id);
949+
if (ownerDoc != null) {
950+
Elements els = ownerDoc.select(idSel);
951+
if (els.size() == 1 && els.get(0) == this) return idSel;
959952
} else {
960-
return idSel; // no ownerdoc, return the ID selector
953+
return idSel;
961954
}
962955
}
956+
return EmptyString;
957+
}
958+
959+
/**
960+
Get a CSS selector that will uniquely select this element.
961+
<p>
962+
If the element has an ID, returns #id; otherwise returns the parent (if any) CSS selector, followed by
963+
{@literal '>'}, followed by a unique selector for the element (tag.class.class:nth-child(n)).
964+
</p>
963965
966+
@return the CSS Path that can be used to retrieve the element in a selector.
967+
*/
968+
public String cssSelector() {
969+
Document ownerDoc = ownerDocument();
970+
String idSel = uniqueIdSelector(ownerDoc);
971+
if (!idSel.isEmpty()) return idSel;
972+
973+
// No unique ID, work up the parent stack and find either a unique ID to hang from, or just a GP > Parent > Child chain
964974
StringBuilder selector = StringUtil.borrowBuilder();
965975
Element el = this;
966976
while (el != null && !(el instanceof Document)) {
977+
idSel = el.uniqueIdSelector(ownerDoc);
978+
if (!idSel.isEmpty()) {
979+
selector.insert(0, idSel);
980+
break; // found a unique ID to use as ancestor; stop
981+
}
967982
selector.insert(0, el.cssSelectorComponent());
968983
el = el.parent();
969984
}

src/test/java/org/jsoup/nodes/ElementTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,28 @@ void prettySerializationRoundTrips(Document.OutputSettings settings) {
25922592
assertEquals("div", el.cssSelector());
25932593
}
25942594

2595+
@Test void cssSelectorParentWithId() {
2596+
// https://github.com/jhy/jsoup/issues/2282
2597+
Document doc = Jsoup.parse("<div><div id=id1><p>A</p></div><div><p>B</p></div><div class='c1 c2'><p>C</p></div></div>");
2598+
Elements els = doc.select("p");
2599+
Element pA = els.get(0);
2600+
Element pB = els.get(1);
2601+
Element pC = els.get(2);
2602+
assertEquals("#id1 > p", pA.cssSelector());
2603+
assertEquals("html > body > div > div:nth-child(2) > p", pB.cssSelector());
2604+
assertEquals("html > body > div > div.c1.c2 > p", pC.cssSelector());
2605+
}
2606+
2607+
@Test void cssSelectorWithNonUniqueId() {
2608+
Document doc = Jsoup.parse("<main id=out><div><div id=in>One</div><div id=in>Two</div></div></main>");
2609+
Element two = doc.expectFirst("div:containsOwn(Two)");
2610+
String selector = two.cssSelector();
2611+
assertEquals("#out > div > div:nth-child(2)", selector);
2612+
Elements found = doc.select(selector);
2613+
assertEquals(1, found.size());
2614+
assertEquals(two, found.first());
2615+
}
2616+
25952617
@Test void cssSelectorDoesntStackOverflow() {
25962618
// https://github.com/jhy/jsoup/issues/2001
25972619
Element element = new Element("element");

0 commit comments

Comments
 (0)