Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,16 @@ public class CommonCssConstants {
*/
public static final String HAS = "has";

/**
* The Constant IS.
*/
public static final String IS = "is";

/**
* The Constant WHERE.
*/
public static final String WHERE = "where";

/**
* The Constant DISABLED.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static List<ICssSelectorItem> parseSelectorItems(String selector) {
* @return a list of {@link ICssSelectorItem} objects representing the parsed components
* of the CSS selector
*/
static List<ICssSelectorItem> parseSelectorItems(String selector, boolean allowRelativeSelectorAtStart) {
public static List<ICssSelectorItem> parseSelectorItems(String selector, boolean allowRelativeSelectorAtStart) {
List<ICssSelectorItem> selectorItems = new ArrayList<>();
State state = new NoneState();
for (int i = 0; i < selector.length(); ++i) {
Expand Down Expand Up @@ -140,7 +140,7 @@ static List<ICssSelectorItem> parseSelectorItems(String selector, boolean allowR
* @param input the string to split
* @return a list of string parts split by top-level commas
*/
static List<String> splitByTopLevelComma(String input) {
public static List<String> splitByTopLevelComma(String input) {
return CssUtils.splitString(input, ',',
new EscapeGroup('(', ')'),
new EscapeGroup('"'),
Expand Down Expand Up @@ -503,6 +503,9 @@ private static abstract class FunctionState implements State {
protected char closure;
protected boolean inString = false;
protected boolean isReadyForSwitch = false;
// Tracks nested parentheses for pseudo-class functions like :is(...), :not(...), :where(...), :has(...)
// Only used when closure == ')'.
protected int parenthesesDepth = 0;

/**
* Constructs a new FunctionState object with the specified closure character.
Expand Down Expand Up @@ -533,7 +536,27 @@ public void addChar(char c, boolean isEscaped) {
if ((c == '"' || c == '\'') && !isEscaped) {
inString = !inString;
}
if (c == closure && !isEscaped && !inString) {

if (isEscaped || inString) {
return;
}

if (closure == ')') {
if (c == '(') {
parenthesesDepth++;
} else if (c == ')') {
// Close one nesting level; function ends when we close the initial '('.
if (parenthesesDepth > 0) {
parenthesesDepth--;
}
if (parenthesesDepth == 0) {
isReadyForSwitch = true;
}
}
return;
}

if (c == closure) {
isReadyForSwitch = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2026 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 <https://www.gnu.org/licenses/>.
*/

package com.itextpdf.styledxmlparser.css.selector.item;

import com.itextpdf.styledxmlparser.css.parse.CssSelectorParser;
import com.itextpdf.styledxmlparser.css.selector.CssSelector;
import com.itextpdf.styledxmlparser.css.selector.ICssSelector;
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;

/**
* Base class for pseudo-classes that accept a forgiving selector list (e.g. :is(), :where()).
*/
abstract class CssPseudoClassForgivingSelectorListSelectorItem extends CssPseudoClassSelectorItem {
protected final List<ICssSelector> selectorList;

protected CssPseudoClassForgivingSelectorListSelectorItem(String pseudoClass, List<ICssSelector> selectorList,
String argumentsString) {
super(pseudoClass, argumentsString);
this.selectorList = selectorList;
}

@Override
public boolean matches(INode node) {
if (!(node instanceof IElementNode) || node instanceof ICustomElementNode || node instanceof IDocumentNode) {
return false;
}
for (ICssSelector sel : selectorList) {
if (sel != null && sel.matches(node)) {
return true;
}
}
return false;
}

/**
* Parses a forgiving selector list for :is() / :where().
* <p>
* Per Selectors Level 4, :is() and :where() accept a forgiving selector list:
* invalid selectors are ignored rather than invalidating the whole pseudo-class.
*
* @param arguments selector list as written inside parentheses
* @return list of valid selectors (possibly empty), or null if arguments are syntactically missing
*/
static List<ICssSelector> parseForgivingSelectorListWithoutPseudoElements(String arguments) {
if (arguments == null || arguments.trim().isEmpty()) {
// :is() / :where() with empty arguments is invalid.
return null;
}

List<String> parts = CssSelectorParser.splitByTopLevelComma(arguments);
if (parts.isEmpty()) {
return null;
}

List<ICssSelector> selectors = new ArrayList<>();
for (String rawPart : parts) {
String part = rawPart == null ? "" : rawPart.trim();
if (part.isEmpty()) {
// Empty entries like :is(.a,,.b) are invalid selectors in the list; ignore (forgiving).
continue;
}

try {
CssSelector sel = new CssSelector(CssSelectorParser.parseSelectorItems(part, false));
if (!containsPseudoElement(Collections.singletonList(sel))) {
selectors.add(sel);
}
} catch (IllegalArgumentException ex) {
// Invalid/unsupported selector in the list; ignore (forgiving).
}
}

return selectors;
}

static boolean containsPseudoElement(List<ICssSelector> selectors) {
for (ICssSelector sel : selectors) {
if (sel instanceof CssSelector) {
for (ICssSelectorItem item : ((CssSelector) sel).getSelectorItems()) {
if (item instanceof CssPseudoElementSelectorItem) {
return true;
}
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.itextpdf.styledxmlparser.css.selector.item;

import com.itextpdf.styledxmlparser.css.CommonCssConstants;
import com.itextpdf.styledxmlparser.css.selector.ICssSelector;

import java.util.List;

class CssPseudoClassIsSelectorItem extends CssPseudoClassForgivingSelectorListSelectorItem {

CssPseudoClassIsSelectorItem(List<ICssSelector> selectorList, String argumentsString) {
super(CommonCssConstants.IS, selectorList, argumentsString);
}

@Override
public int getSpecificity() {
int max = 0;
for (ICssSelector sel : selectorList) {
if (sel != null) {
max = Math.max(max, sel.calculateSpecificity());
}
}
return max;
}

public static CssPseudoClassIsSelectorItem createIsSelectorItem(String arguments) {
List<ICssSelector> selectors = parseForgivingSelectorListWithoutPseudoElements(arguments);
if (selectors == null) {
return null;
}
return new CssPseudoClassIsSelectorItem(selectors, arguments);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ class CssPseudoClassNotSelectorItem extends CssPseudoClassSelectorItem {
this.argumentsSelector = argumentsSelector;
}

@Override
public int getSpecificity() {
// Per Selectors Level 4: :not() specificity is replaced by the specificity
// of the most specific complex selector in its selector list argument.
return argumentsSelector != null ? argumentsSelector.calculateSpecificity() : 0;
}

public List<ICssSelectorItem> getArgumentsSelector() {
return CssSelectorParser.parseSelectorItems(arguments);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ public static CssPseudoClassSelectorItem create(String pseudoClass, String argum
return createHasSelectorItem(arguments);
case CommonCssConstants.NOT:
return createNotSelectorItem(arguments);
case CommonCssConstants.IS:
return CssPseudoClassIsSelectorItem.createIsSelectorItem(arguments);
case CommonCssConstants.WHERE:
return CssPseudoClassWhereSelectorItem.createWhereSelectorItem(arguments);
case CommonCssConstants.ROOT:
return CssPseudoClassRootSelectorItem.getInstance();
case CommonCssConstants.LINK:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.itextpdf.styledxmlparser.css.selector.item;

import com.itextpdf.styledxmlparser.css.CommonCssConstants;
import com.itextpdf.styledxmlparser.css.selector.ICssSelector;

import java.util.List;

class CssPseudoClassWhereSelectorItem extends CssPseudoClassForgivingSelectorListSelectorItem {

CssPseudoClassWhereSelectorItem(List<ICssSelector> selectorList, String argumentsString) {
super(CommonCssConstants.WHERE, selectorList, argumentsString);
}

@Override
public int getSpecificity() {
// Per Selectors Level 4: :where() always contributes 0 specificity.
return 0;
}

public static CssPseudoClassWhereSelectorItem createWhereSelectorItem(String arguments) {
List<ICssSelector> selectors = parseForgivingSelectorListWithoutPseudoElements(arguments);
if (selectors == null) {
return null;
}
return new CssPseudoClassWhereSelectorItem(selectors, arguments);
}
}

Loading