From 5e1e3260ea4c23c3af91352651c02f7bdfbe48c3 Mon Sep 17 00:00:00 2001 From: Stefan Bechtold Date: Thu, 12 Dec 2024 22:25:39 +0100 Subject: [PATCH] add support for nth-last pseudo class selectors With this commit the following CSS3 pseudo classes will be supported: - :nth-last-child(an+b) and - :nth-last-of-type(2a+b) This is especially helpful to format tables with a fixed number of columns, by using a combination of :nth-child and :nth-last-child. Example, given the following markup: ```
First Column Second Column Third Column Fourth Column Fifth Column
Line 1a Line 1b Line 1c Line 1d Line 1e
First Column Second Column Third Column Fourth Column
Line 1a Line 1b Line 1c Line 1d
``` The following styles can be used to format the tables, such that the four-column table's last three columns will align with the last three columns of the five-column table: ``` table tr th:nth-child(1):nth-last-child(5) { width: 30%; } table tr th:nth-child(2):nth-last-child(4) { width: 10%; } table tr th:nth-child(3):nth-last-child(3) { width: 20%; } table tr th:nth-child(4):nth-last-child(2) { width: 20%; } table tr th:nth-child(5):nth-last-child(1) { width: 20%; } table tr th:nth-child(1):nth-last-child(4) { width: 40%; } table tr th:nth-child(2):nth-last-child(3) { width: 20%; } table tr th:nth-child(3):nth-last-child(2) { width: 20%; } table tr th:nth-child(4):nth-last-child(1) { width: 20%; } ``` --- ...ssPseudoClassNthLastChildSelectorItem.java | 44 +++++ ...sPseudoClassNthLastOfTypeSelectorItem.java | 48 +++++ .../CssPseudoClassNthOfTypeSelectorItem.java | 8 +- .../item/CssPseudoClassSelectorItem.java | 4 + .../css/selector/item/CssMatchesTest.java | 180 ++++++++++++++++++ 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastChildSelectorItem.java create mode 100644 styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastOfTypeSelectorItem.java diff --git a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastChildSelectorItem.java b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastChildSelectorItem.java new file mode 100644 index 0000000000..d3a11a83f1 --- /dev/null +++ b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastChildSelectorItem.java @@ -0,0 +1,44 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2024 Apryse Group NV + Authors: Apryse Software. + + This program is offered under a commercial and under the AGPL license. + For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below. + + AGPL licensing: + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + */ +package com.itextpdf.styledxmlparser.css.selector.item; + +import com.itextpdf.styledxmlparser.css.CommonCssConstants; +import com.itextpdf.styledxmlparser.node.INode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class CssPseudoClassNthLastChildSelectorItem extends CssPseudoClassNthSelectorItem { + + CssPseudoClassNthLastChildSelectorItem(String arguments) { + super(CommonCssConstants.NTH_LAST_CHILD, arguments); + } + + @Override + protected boolean resolveNth(INode node, List children) { + final List reversedChildren = new ArrayList<>(children); + Collections.reverse(reversedChildren); + return super.resolveNth(node, reversedChildren); + } +} diff --git a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastOfTypeSelectorItem.java b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastOfTypeSelectorItem.java new file mode 100644 index 0000000000..0370fec8a1 --- /dev/null +++ b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthLastOfTypeSelectorItem.java @@ -0,0 +1,48 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2024 Apryse Group NV + Authors: Apryse Software. + + This program is offered under a commercial and under the AGPL license. + For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below. + + AGPL licensing: + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + */ +package com.itextpdf.styledxmlparser.css.selector.item; + +import com.itextpdf.styledxmlparser.css.CommonCssConstants; +import com.itextpdf.styledxmlparser.node.ICustomElementNode; +import com.itextpdf.styledxmlparser.node.IDocumentNode; +import com.itextpdf.styledxmlparser.node.IElementNode; +import com.itextpdf.styledxmlparser.node.INode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class CssPseudoClassNthLastOfTypeSelectorItem extends CssPseudoClassNthOfTypeSelectorItem { + + CssPseudoClassNthLastOfTypeSelectorItem(String arguments) { + super(CommonCssConstants.NTH_LAST_OF_TYPE, arguments); + } + + @Override + protected boolean resolveNth(INode node, List children) { + final List reversedChildren = new ArrayList<>(children); + Collections.reverse(reversedChildren); + return super.resolveNth(node, reversedChildren); + } + +} diff --git a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthOfTypeSelectorItem.java b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthOfTypeSelectorItem.java index 975ed2ed17..f3e2f75e16 100644 --- a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthOfTypeSelectorItem.java +++ b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassNthOfTypeSelectorItem.java @@ -31,8 +31,12 @@ This file is part of the iText (R) project. class CssPseudoClassNthOfTypeSelectorItem extends CssPseudoClassNthSelectorItem { - public CssPseudoClassNthOfTypeSelectorItem(String arguments) { - super(CommonCssConstants.NTH_OF_TYPE, arguments); + CssPseudoClassNthOfTypeSelectorItem(String arguments) { + this(CommonCssConstants.NTH_OF_TYPE, arguments); + } + + CssPseudoClassNthOfTypeSelectorItem(String pseudoClass, String arguments) { + super(pseudoClass, arguments); } @Override diff --git a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassSelectorItem.java b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassSelectorItem.java index 2e76a504cb..2191566925 100644 --- a/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassSelectorItem.java +++ b/styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/selector/item/CssPseudoClassSelectorItem.java @@ -82,8 +82,12 @@ public static CssPseudoClassSelectorItem create(String pseudoClass, String argum return CssPseudoClassLastOfTypeSelectorItem.getInstance(); case CommonCssConstants.NTH_CHILD: return new CssPseudoClassNthChildSelectorItem(arguments); + case CommonCssConstants.NTH_LAST_CHILD: + return new CssPseudoClassNthLastChildSelectorItem(arguments); case CommonCssConstants.NTH_OF_TYPE: return new CssPseudoClassNthOfTypeSelectorItem(arguments); + case CommonCssConstants.NTH_LAST_OF_TYPE: + return new CssPseudoClassNthLastOfTypeSelectorItem(arguments); case CommonCssConstants.NOT: CssSelector selector = new CssSelector(arguments); for (ICssSelectorItem item : selector.getSelectorItems()) { diff --git a/styled-xml-parser/src/test/java/com/itextpdf/styledxmlparser/css/selector/item/CssMatchesTest.java b/styled-xml-parser/src/test/java/com/itextpdf/styledxmlparser/css/selector/item/CssMatchesTest.java index 6ced8b4bd5..3dc5263d18 100644 --- a/styled-xml-parser/src/test/java/com/itextpdf/styledxmlparser/css/selector/item/CssMatchesTest.java +++ b/styled-xml-parser/src/test/java/com/itextpdf/styledxmlparser/css/selector/item/CssMatchesTest.java @@ -81,6 +81,126 @@ public void matchesEmptySelectorItemSpaceTest() { Assertions.assertFalse(item.matches(divNode)); } + @Test + public void matchesNthChildFixSelectorItemTest() { + CssPseudoClassNthChildSelectorItem item = new CssPseudoClassNthChildSelectorItem("2"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(second), "Second paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(third), "Third paragraph should NOT be matched, but matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be matched, but matched!"); + } + + @Test + public void matchesNthChildEvenSelectorItemTest() { + CssPseudoClassNthChildSelectorItem item = new CssPseudoClassNthChildSelectorItem("2n"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(second), "Second paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(third), "Third paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(fourth), "Fourth paragraph should be be matched, but WAS NOT matched!"); + } + + @Test + public void matchesNthChildOddSelectorItemTest() { + CssPseudoClassNthChildSelectorItem item = new CssPseudoClassNthChildSelectorItem("2n-1"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertTrue(item.matches(first), "First paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(second), "Second paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(third), "Third paragraph should be be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be matched, but matched!"); + } + + @Test + public void matchesNthLastChildFixSelectorItemTest() { + CssPseudoClassNthLastChildSelectorItem item = new CssPseudoClassNthLastChildSelectorItem("2"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertFalse(item.matches(second), "Second paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(third), "Third paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be matched, but matched!"); + } + + @Test + public void matchesNthLastChildEvenSelectorItemTest() { + CssPseudoClassNthLastChildSelectorItem item = new CssPseudoClassNthLastChildSelectorItem("2n"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertTrue(item.matches(first), "First paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(second), "Second paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(third), "Third paragraph should be be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be matched, but matched!"); + } + + @Test + public void matchesNthLastChildOddSelectorItemTest() { + CssPseudoClassNthLastChildSelectorItem item = new CssPseudoClassNthLastChildSelectorItem("2n-1"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Second

Third

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(1); + INode third = bodyNode.childNodes().get(2); + INode fourth = bodyNode.childNodes().get(3); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(second), "Second paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(third), "Third paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(fourth), "Fourth paragraph should be be matched, but WAS NOT matched!"); + } + @Test public void matchesFirstOfTypeSelectorItemTest() { CssPseudoClassFirstOfTypeSelectorItem item = CssPseudoClassFirstOfTypeSelectorItem.getInstance(); @@ -128,6 +248,66 @@ public void matchesLastOfTypeSelectorItemTest() { Assertions.assertTrue(item.matches(divNode)); } + @Test + public void matchesNthLastOfTypeFixSelectorItemTest() { + CssPseudoClassNthLastOfTypeSelectorItem item = new CssPseudoClassNthLastOfTypeSelectorItem("2"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Headline

Second

Headline

Third

Headline

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(2); + INode third = bodyNode.childNodes().get(4); + INode fourth = bodyNode.childNodes().get(6); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertFalse(item.matches(second), "Second paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(third), "Third paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be be matched, but matched!"); + } + + @Test + public void matchesNthLastOfTypeEvenSelectorItemTest() { + CssPseudoClassNthLastOfTypeSelectorItem item = new CssPseudoClassNthLastOfTypeSelectorItem("2n"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Headline

Second

Headline

Third

Headline

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(2); + INode third = bodyNode.childNodes().get(4); + INode fourth = bodyNode.childNodes().get(6); + + Assertions.assertTrue(item.matches(first), "First paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(second), "Second paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(third), "Third paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(fourth), "Fourth paragraph should NOT be be matched, but matched!"); + } + + @Test + public void matchesNthLastOfTypeOddSelectorItemTest() { + CssPseudoClassNthLastOfTypeSelectorItem item = new CssPseudoClassNthLastOfTypeSelectorItem("2n-1"); + IXmlParser htmlParser = new JsoupHtmlParser(); + IDocumentNode documentNode = htmlParser.parse("

First

Headline

Second

Headline

Third

Headline

Fourth

"); + + INode bodyNode = documentNode + .childNodes().get(0) + .childNodes().get(1); + INode first = bodyNode.childNodes().get(0); + INode second = bodyNode.childNodes().get(2); + INode third = bodyNode.childNodes().get(4); + INode fourth = bodyNode.childNodes().get(6); + + Assertions.assertFalse(item.matches(first), "First paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(second), "Second paragraph should be matched, but WAS NOT matched!"); + Assertions.assertFalse(item.matches(third), "Third paragraph should NOT be matched, but matched!"); + Assertions.assertTrue(item.matches(fourth), "Fourth paragraph should be be matched, but WAS NOT matched!"); + } + @Test public void matchesLastOfTypeSelectorItemTestNotTaggedText() { CssPseudoClassLastOfTypeSelectorItem item = CssPseudoClassLastOfTypeSelectorItem.getInstance();