diff --git a/flying-saucer-core/src/main/java/org/xhtmlrenderer/css/newmatch/Condition.java b/flying-saucer-core/src/main/java/org/xhtmlrenderer/css/newmatch/Condition.java index 854bde598..4b960dd88 100644 --- a/flying-saucer-core/src/main/java/org/xhtmlrenderer/css/newmatch/Condition.java +++ b/flying-saucer-core/src/main/java/org/xhtmlrenderer/css/newmatch/Condition.java @@ -271,11 +271,13 @@ protected boolean compare(String attrValue, String conditionValue) { } } - private static class ClassCondition extends Condition { - private final String _paddedClassName; + final static class ClassCondition extends Condition { + private final String className; + private final int classNameLength; private ClassCondition(String className) { - _paddedClassName = " " + className + " "; + this.className = className; + this.classNameLength = className.length(); } @Override @@ -284,14 +286,27 @@ boolean matches(Node e, AttributeResolver attRes, TreeResolver treeRes) { return false; } String c = attRes.getClass(e); - if (c == null) { - return false; - } + return c != null && containsClassName(c); + } + + boolean containsClassName(String classAttribute) { + return containsClassName(classAttribute, -1); + } + private boolean containsClassName(String classAttribute, int fromIndex) { // This is much faster than calling `split()` and comparing individual values in a loop. // NOTE: In jQuery, for example, the attribute value first has whitespace normalized to spaces. But // in an XML DOM, space normalization in attributes is supposed to have happened already. - return (" " + c + " ").contains(_paddedClassName); + int index = classAttribute.indexOf(className, fromIndex); + if (index == -1) return false; + + return isWhitespace(classAttribute, index - 1) + && isWhitespace(classAttribute, index + classNameLength) + || containsClassName(classAttribute, index + classNameLength); + } + + private boolean isWhitespace(String classAttribute, int index) { + return index < 0 || index >= classAttribute.length() || Character.isWhitespace(classAttribute.charAt(index)); } } diff --git a/flying-saucer-core/src/test/java/org/xhtmlrenderer/css/newmatch/ClassConditionTest.java b/flying-saucer-core/src/test/java/org/xhtmlrenderer/css/newmatch/ClassConditionTest.java new file mode 100644 index 000000000..6160b39c5 --- /dev/null +++ b/flying-saucer-core/src/test/java/org/xhtmlrenderer/css/newmatch/ClassConditionTest.java @@ -0,0 +1,58 @@ +package org.xhtmlrenderer.css.newmatch; + +import org.junit.jupiter.api.Test; +import org.xhtmlrenderer.css.newmatch.Condition.ClassCondition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.xhtmlrenderer.css.newmatch.Condition.createClassCondition; + +class ClassConditionTest { + private final ClassCondition condition = (ClassCondition) createClassCondition("active"); + + @Test + void classAttributeEqualsExpectedClassName() { + assertThat(condition.containsClassName("active")).isTrue(); + assertThat(condition.containsClassName("activex")).isFalse(); + } + + @Test + void classAttributeContainsExpectedClassName() { + assertThat(condition.containsClassName("foo active bar")).isTrue(); + assertThat(condition.containsClassName("foo active ")).isTrue(); + assertThat(condition.containsClassName(" active bar")).isTrue(); + assertThat(condition.containsClassName(" active ")).isTrue(); + } + + @Test + void classAttributeStartsWithExpectedClassName() { + assertThat(condition.containsClassName("active foo bar")).isTrue(); + assertThat(condition.containsClassName(" active foo bar")).isTrue(); + assertThat(condition.containsClassName("activex foo bar")).isFalse(); + assertThat(condition.containsClassName("inactive foo bar")).isFalse(); + } + + @Test + void classAttributeEndsWithExpectedClassName() { + assertThat(condition.containsClassName("foo bar active")).isTrue(); + assertThat(condition.containsClassName("foo bar active ")).isTrue(); + assertThat(condition.containsClassName("foo bar inactive")).isFalse(); + assertThat(condition.containsClassName("foo bar activeactive")).isFalse(); + assertThat(condition.containsClassName("foo bar active-active")).isFalse(); + assertThat(condition.containsClassName("foo bar active_active")).isFalse(); + assertThat(condition.containsClassName("foo bar activex")).isFalse(); + } + + @Test + void classAttributeContainsSimilarClassNames() { + assertThat(condition.containsClassName("activex active inactive")).isTrue(); + assertThat(condition.containsClassName("activex _active inactive")).isFalse(); + } + + @Test + void classNotMatches() { + assertThat(condition.containsClassName("")).isFalse(); + assertThat(condition.containsClassName("_active")).isFalse(); + assertThat(condition.containsClassName("active_")).isFalse(); + assertThat(condition.containsClassName("act ive")).isFalse(); + } +}