Skip to content

Commit

Permalink
Fix parse of :nth-child(-n+2)
Browse files Browse the repository at this point in the history
The `-` sign of the nth-child step was ignored if there was no digit for the step.

Also cleaned up the function a bit.

Fixes #1147
  • Loading branch information
jhy committed Jan 14, 2025
1 parent d5acae9 commit 62674f9
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
* A `template` tag containing an `li` within an open `li` would be parsed incorrectly, as it was not recognized as a
"special" tag (which have additional processing rules). Also, added the SVG and MathML namespace tags to the list of
special tags. [2258](https://github.com/jhy/jsoup/issues/2258)
* An `:nth-child` selector with a negative digit-less step, such as `:nth-child(-n+2)`, would be parsed incorrectly as a
positive step, and so would not match as expected. [1147](https://github.com/jhy/jsoup/issues/1147)

## 1.18.3 (2024-Dec-02)

Expand Down
69 changes: 32 additions & 37 deletions src/main/java/org/jsoup/select/QueryParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -318,44 +318,39 @@ else if (cq.matchChomp("~="))
}

//pseudo selectors :first-child, :last-child, :nth-child, ...
private static final Pattern NTH_AB = Pattern.compile("(([+-])?(\\d+)?)n(\\s*([+-])?\\s*\\d+)?", Pattern.CASE_INSENSITIVE);
private static final Pattern NTH_B = Pattern.compile("([+-])?(\\d+)");

private Evaluator cssNthChild(boolean backwards, boolean ofType) {
String arg = normalize(consumeParens());
Matcher mAB = NTH_AB.matcher(arg);
Matcher mB = NTH_B.matcher(arg);
final int a, b;
if ("odd".equals(arg)) {
a = 2;
b = 1;
} else if ("even".equals(arg)) {
a = 2;
b = 0;
} else if (mAB.matches()) {
a = mAB.group(3) != null ? Integer.parseInt(mAB.group(1).replaceFirst("^\\+", "")) : 1;
b = mAB.group(4) != null ? Integer.parseInt(mAB.group(4).replaceFirst("^\\+", "")) : 0;
} else if (mB.matches()) {
a = 0;
b = Integer.parseInt(mB.group().replaceFirst("^\\+", ""));
} else {
throw new Selector.SelectorParseException("Could not parse nth-index '%s': unexpected format", arg);
}
private static final Pattern NthStepOffset = Pattern.compile("(([+-])?(\\d+)?)n(\\s*([+-])?\\s*\\d+)?", Pattern.CASE_INSENSITIVE);
private static final Pattern NthOffset = Pattern.compile("([+-])?(\\d+)");

private Evaluator cssNthChild(boolean last, boolean ofType) {
String arg = normalize(consumeParens()); // arg is like "odd", or "-n+2", within nth-child(odd)
final int step, offset;
if ("odd".equals(arg)) {
step = 2;
offset = 1;
} else if ("even".equals(arg)) {
step = 2;
offset = 0;
} else {
Matcher stepOffsetM, stepM;
if ((stepOffsetM = NthStepOffset.matcher(arg)).matches()) {
if (stepOffsetM.group(3) != null) // has digits, like 3n+2 or -3n+2
step = Integer.parseInt(stepOffsetM.group(1).replaceFirst("^\\+", ""));
else // no digits, might be like n+2, or -n+2. if group(2) == "-", it’s -1;
step = "-".equals(stepOffsetM.group(2)) ? -1 : 1;
offset =
stepOffsetM.group(4) != null ? Integer.parseInt(stepOffsetM.group(4).replaceFirst("^\\+", "")) : 0;
} else if ((stepM = NthOffset.matcher(arg)).matches()) {
step = 0;
offset = Integer.parseInt(stepM.group().replaceFirst("^\\+", ""));
} else {
throw new Selector.SelectorParseException("Could not parse nth-index '%s': unexpected format", arg);
}
}

final Evaluator eval;
if (ofType)
if (backwards)
eval = new Evaluator.IsNthLastOfType(a, b);
else
eval = new Evaluator.IsNthOfType(a, b);
else {
if (backwards)
eval = (new Evaluator.IsNthLastChild(a, b));
else
eval = new Evaluator.IsNthChild(a, b);
}
return eval;
}
return ofType
? (last ? new Evaluator.IsNthLastOfType(step, offset) : new Evaluator.IsNthOfType(step, offset))
: (last ? new Evaluator.IsNthLastChild(step, offset) : new Evaluator.IsNthChild(step, offset));
}

private String consumeParens() {
return tq.chompBalanced('(', ')');
Expand Down
23 changes: 23 additions & 0 deletions src/test/java/org/jsoup/select/SelectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1358,4 +1358,27 @@ public void emptyPseudo() {
assertEquals(1, els.size());
assertEquals("o", els.get(0).id());
}

@Test void negativeNthChild() {
// https://github.com/jhy/jsoup/issues/1147
String html = "<p>1</p> <p>2</p> <p>3</p> <p>4</p>";
Document doc = Jsoup.parse(html);

// Digitless
Elements pos = doc.select("p:nth-child(n+2)");
assertSelectedOwnText(pos, "2", "3", "4");

Elements neg = doc.select("p:nth-child(-n+2)");
assertSelectedOwnText(neg, "1", "2");

Elements combo = doc.select("p:nth-child(n+2):nth-child(-n+2)");
assertSelectedOwnText(combo, "2");

// Digitful, 2n+2 or -1n+2
Elements pos2 = doc.select("p:nth-child(2n+2)");
assertSelectedOwnText(pos2, "2", "4");

Elements neg2 = doc.select("p:nth-child(-1n+2)");
assertSelectedOwnText(neg2, "1", "2");
}
}

0 comments on commit 62674f9

Please sign in to comment.