diff --git a/checkstyle_suppressions.xml b/checkstyle_suppressions.xml
index a87ce46..ef3c989 100644
--- a/checkstyle_suppressions.xml
+++ b/checkstyle_suppressions.xml
@@ -15,5 +15,7 @@
+
+
diff --git a/src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java b/src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java
index b152df0..5255d2b 100644
--- a/src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java
+++ b/src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java
@@ -20,6 +20,7 @@
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
import org.htmlunit.cssparser.parser.javacc.CharStream;
import org.htmlunit.cssparser.parser.javacc.ParseException;
@@ -40,6 +41,15 @@ public abstract class AbstractCSSParser {
private InputSource source_;
private static final HashMap PARSER_MESSAGES_ = new HashMap<>();
+ private static final ConcurrentHashMap ERROR_MESSAGE_CACHE_ = new ConcurrentHashMap<>();
+ private static final int MAX_CACHE_SIZE = 100;
+
+ /**
+ * Thread-local StringBuilder cache to reduce allocations.
+ * Automatically cleared after use to prevent memory leaks.
+ */
+ private static final ThreadLocal STRING_BUILDER_CACHE =
+ ThreadLocal.withInitial(() -> new StringBuilder(256));
static {
PARSER_MESSAGES_.put("invalidExpectingOne", "Invalid token \"{0}\". Was expecting: {1}.");
@@ -170,20 +180,49 @@ protected InputSource getInputSource() {
* @return the parser message
*/
protected String getParserMessage(final String key) {
+ final String cached = ERROR_MESSAGE_CACHE_.get(key);
+ if (cached != null) {
+ return cached;
+ }
+
final String msg = PARSER_MESSAGES_.get(key);
if (msg == null) {
return "[[" + key + "]]";
}
+
+ if (ERROR_MESSAGE_CACHE_.size() < MAX_CACHE_SIZE) {
+ ERROR_MESSAGE_CACHE_.put(key, msg);
+ }
+
return msg;
}
+ /**
+ * Gets a cached StringBuilder, cleared and ready to use.
+ * @return A cleared StringBuilder instance
+ */
+ protected StringBuilder getCachedStringBuilder() {
+ final StringBuilder sb = STRING_BUILDER_CACHE.get();
+ sb.setLength(0);
+ return sb;
+ }
+
+ /**
+ * Returns a cached StringBuilder after extracting its content.
+ * @param sb The StringBuilder to extract from
+ * @return The string content
+ */
+ protected String returnCachedStringBuilder(final StringBuilder sb) {
+ return sb.toString();
+ }
+
/**
* Returns a new locator for the given token.
* @param t the token to generate the locator for
* @return a new locator
*/
protected Locator createLocator(final Token t) {
- return new Locator(getInputSource().getURI(),
+ return LocatorPool.acquire(getInputSource().getURI(),
t == null ? 0 : t.beginLine,
t == null ? 0 : t.beginColumn);
}
@@ -194,7 +233,7 @@ protected Locator createLocator(final Token t) {
* @return a new string with the escaped values
*/
protected String addEscapes(final String str) {
- final StringBuilder sb = new StringBuilder();
+ final StringBuilder sb = getCachedStringBuilder();
char ch;
for (int i = 0; i < str.length(); i++) {
ch = str.charAt(i);
@@ -236,7 +275,7 @@ protected String addEscapes(final String str) {
continue;
}
}
- return sb.toString();
+ return returnCachedStringBuilder(sb);
}
/**
diff --git a/src/main/java/org/htmlunit/cssparser/parser/Locator.java b/src/main/java/org/htmlunit/cssparser/parser/Locator.java
index e795e96..b42ecdc 100644
--- a/src/main/java/org/htmlunit/cssparser/parser/Locator.java
+++ b/src/main/java/org/htmlunit/cssparser/parser/Locator.java
@@ -97,6 +97,15 @@ public void setLineNumber(final int line) {
lineNumber_ = line;
}
+ /**
+ * Clears all fields (for object pooling).
+ */
+ public void clear() {
+ uri_ = null;
+ lineNumber_ = 0;
+ columnNumber_ = 0;
+ }
+
/** {@inheritDoc} */
@Override
public boolean equals(final Object obj) {
diff --git a/src/main/java/org/htmlunit/cssparser/parser/LocatorPool.java b/src/main/java/org/htmlunit/cssparser/parser/LocatorPool.java
new file mode 100644
index 0000000..3cfe102
--- /dev/null
+++ b/src/main/java/org/htmlunit/cssparser/parser/LocatorPool.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Simple object pool for Locator instances to reduce allocations.
+ * Uses ThreadLocal to avoid synchronization overhead.
+ *
+ * @author Ronald Brill
+ */
+public final class LocatorPool {
+
+ private static final int MAX_POOL_SIZE = 32;
+
+ private static final ThreadLocal> POOL =
+ ThreadLocal.withInitial(() -> new ArrayDeque<>(MAX_POOL_SIZE));
+
+ private LocatorPool() {
+ }
+
+ /**
+ * Acquires a Locator from the pool or creates a new one.
+ *
+ * @param uri The URI
+ * @param line The line number
+ * @param column The column number
+ * @return A Locator instance
+ */
+ public static Locator acquire(final String uri, final int line, final int column) {
+ final Deque pool = POOL.get();
+ Locator locator = pool.poll();
+
+ if (locator == null) {
+ locator = new Locator(uri, line, column);
+ }
+ else {
+ locator.setUri(uri);
+ locator.setLineNumber(line);
+ locator.setColumnNumber(column);
+ }
+
+ return locator;
+ }
+
+ /**
+ * Returns a Locator to the pool for reuse.
+ * Note: This method is provided for completeness but typically
+ * Locator objects are not explicitly released in the parser.
+ *
+ * @param locator The locator to return
+ */
+ public static void release(final Locator locator) {
+ if (locator != null) {
+ final Deque pool = POOL.get();
+ if (pool.size() < MAX_POOL_SIZE) {
+ locator.clear();
+ pool.offer(locator);
+ }
+ }
+ }
+
+ /**
+ * Clears the pool (useful for testing).
+ */
+ public static void clear() {
+ POOL.get().clear();
+ }
+}
diff --git a/src/main/java/org/htmlunit/cssparser/parser/PerformanceMetrics.java b/src/main/java/org/htmlunit/cssparser/parser/PerformanceMetrics.java
new file mode 100644
index 0000000..3117450
--- /dev/null
+++ b/src/main/java/org/htmlunit/cssparser/parser/PerformanceMetrics.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+/**
+ * Optional performance metrics for the CSS parser.
+ * Enable with -Dhtmlunit.cssparser.metrics=true
+ *
+ * @author Ronald Brill
+ */
+public class PerformanceMetrics {
+
+ private static final boolean ENABLED =
+ Boolean.getBoolean("htmlunit.cssparser.metrics");
+
+ private long parseTimeMs_;
+ private int tokenCount_;
+ private int ruleCount_;
+ private int propertyCount_;
+
+ /**
+ * Creates a new PerformanceMetrics instance if enabled.
+ *
+ * @return a new instance or null if disabled
+ */
+ public static PerformanceMetrics start() {
+ return ENABLED ? new PerformanceMetrics() : null;
+ }
+
+ /**
+ * Records the parse time.
+ *
+ * @param ms the time in milliseconds
+ */
+ public void recordParseTime(final long ms) {
+ if (ENABLED) {
+ parseTimeMs_ = ms;
+ }
+ }
+
+ /**
+ * Increments the token count.
+ */
+ public void incrementTokens() {
+ if (ENABLED) {
+ tokenCount_++;
+ }
+ }
+
+ /**
+ * Increments the rule count.
+ */
+ public void incrementRules() {
+ if (ENABLED) {
+ ruleCount_++;
+ }
+ }
+
+ /**
+ * Increments the property count.
+ */
+ public void incrementProperties() {
+ if (ENABLED) {
+ propertyCount_++;
+ }
+ }
+
+ /**
+ * Prints the metrics report to System.out.
+ */
+ public void report() {
+ if (ENABLED) {
+ System.out.println("=== CSS Parser Performance Metrics ===");
+ System.out.println("Parse time: " + parseTimeMs_ + "ms");
+ System.out.println("Tokens: " + tokenCount_);
+ System.out.println("Rules: " + ruleCount_);
+ System.out.println("Properties: " + propertyCount_);
+ System.out.println("=====================================");
+ }
+ }
+
+ /**
+ * @return the parse time in milliseconds
+ */
+ public long getParseTimeMs() {
+ return parseTimeMs_;
+ }
+
+ /**
+ * @return the token count
+ */
+ public int getTokenCount() {
+ return tokenCount_;
+ }
+
+ /**
+ * @return the rule count
+ */
+ public int getRuleCount() {
+ return ruleCount_;
+ }
+
+ /**
+ * @return the property count
+ */
+ public int getPropertyCount() {
+ return propertyCount_;
+ }
+}
diff --git a/src/main/java/org/htmlunit/cssparser/util/ParserUtils.java b/src/main/java/org/htmlunit/cssparser/util/ParserUtils.java
index ab14a24..d5f2faa 100644
--- a/src/main/java/org/htmlunit/cssparser/util/ParserUtils.java
+++ b/src/main/java/org/htmlunit/cssparser/util/ParserUtils.java
@@ -79,6 +79,33 @@ public static String trimBy(final StringBuilder s, final int left, final int rig
return s.substring(left, s.length() - right);
}
+ /**
+ * Remove the given number of chars from start and end.
+ * Optimized version with bounds checking for String input.
+ *
+ * @param s the String
+ * @param left no of chars to be removed from start
+ * @param right no of chars to be removed from end
+ * @return the trimmed string
+ */
+ public static String trimBy(final String s, final int left, final int right) {
+ if (s == null) {
+ return null;
+ }
+
+ final int length = s.length();
+
+ if (left < 0 || right < 0 || left + right >= length) {
+ return s;
+ }
+
+ if (left == 0 && right == 0) {
+ return s;
+ }
+
+ return s.substring(left, length - right);
+ }
+
/**
* Helper that removes the leading "url(", the trailing ")"
* and surrounding quotes from the given string builder.
@@ -101,4 +128,34 @@ public static String trimUrl(final StringBuilder s) {
return s1;
}
+ /**
+ * Compare CharSequence without creating String objects.
+ * Case-insensitive comparison.
+ *
+ * @param cs1 the first CharSequence
+ * @param cs2 the second CharSequence
+ * @return true if the CharSequences are equal ignoring case
+ */
+ public static boolean equalsIgnoreCase(final CharSequence cs1, final CharSequence cs2) {
+ if (cs1 == cs2) {
+ return true;
+ }
+ if (cs1 == null || cs2 == null) {
+ return false;
+ }
+ if (cs1.length() != cs2.length()) {
+ return false;
+ }
+
+ for (int i = 0; i < cs1.length(); i++) {
+ final char c1 = cs1.charAt(i);
+ final char c2 = cs2.charAt(i);
+ if (c1 != c2 && Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/LocatorPoolTest.java b/src/test/java/org/htmlunit/cssparser/parser/LocatorPoolTest.java
new file mode 100644
index 0000000..cd47032
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/parser/LocatorPoolTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Testcases for {@link LocatorPool}.
+ *
+ * @author Ronald Brill
+ */
+public class LocatorPoolTest {
+
+ @AfterEach
+ public void tearDown() {
+ LocatorPool.clear();
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void acquireCreatesNewLocator() throws Exception {
+ final Locator locator = LocatorPool.acquire("test.css", 10, 20);
+
+ assertNotNull(locator);
+ assertEquals("test.css", locator.getUri());
+ assertEquals(10, locator.getLineNumber());
+ assertEquals(20, locator.getColumnNumber());
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void releaseAndReacquireReusesLocator() throws Exception {
+ final Locator locator1 = LocatorPool.acquire("test.css", 10, 20);
+ LocatorPool.release(locator1);
+
+ final Locator locator2 = LocatorPool.acquire("test2.css", 30, 40);
+
+ assertSame(locator1, locator2);
+ assertEquals("test2.css", locator2.getUri());
+ assertEquals(30, locator2.getLineNumber());
+ assertEquals(40, locator2.getColumnNumber());
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void clearResetsFields() throws Exception {
+ final Locator locator = LocatorPool.acquire("test.css", 10, 20);
+ locator.clear();
+
+ assertNull(locator.getUri());
+ assertEquals(0, locator.getLineNumber());
+ assertEquals(0, locator.getColumnNumber());
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void poolSizeLimit() throws Exception {
+ // Fill the pool beyond its capacity
+ for (int i = 0; i < 40; i++) {
+ final Locator locator = LocatorPool.acquire("test" + i + ".css", i, i);
+ LocatorPool.release(locator);
+ }
+
+ // Clear and verify pool is empty
+ LocatorPool.clear();
+
+ // Acquire should create new locators since pool was cleared
+ final Locator locator1 = LocatorPool.acquire("test.css", 1, 1);
+ final Locator locator2 = LocatorPool.acquire("test.css", 2, 2);
+
+ assertNotSame(locator1, locator2);
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void releaseNullDoesNotThrow() throws Exception {
+ LocatorPool.release(null);
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void threadLocalIsolation() throws Exception {
+ final Locator locator1 = LocatorPool.acquire("main.css", 1, 1);
+ LocatorPool.release(locator1);
+
+ final Thread thread = new Thread(() -> {
+ final Locator locator2 = LocatorPool.acquire("thread.css", 2, 2);
+ // In a different thread, should get a different locator
+ assertNotSame(locator1, locator2);
+ LocatorPool.release(locator2);
+ });
+
+ thread.start();
+ thread.join();
+
+ // Back in main thread, should get the original locator
+ final Locator locator3 = LocatorPool.acquire("main2.css", 3, 3);
+ assertSame(locator1, locator3);
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/PerformanceTest.java b/src/test/java/org/htmlunit/cssparser/parser/PerformanceTest.java
new file mode 100644
index 0000000..b76c08b
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/parser/PerformanceTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.StringReader;
+
+import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Performance tests for CSS parser optimizations.
+ *
+ * @author Ronald Brill
+ */
+public class PerformanceTest {
+
+ private static final String SIMPLE_CSS = ".test { color: red; margin: 10px; padding: 5px; }";
+
+ private static final String MEDIUM_CSS =
+ "body { margin: 0; padding: 0; font-family: Arial, sans-serif; }\n"
+ + "h1, h2, h3 { color: #333; font-weight: bold; }\n"
+ + ".container { width: 100%; max-width: 1200px; margin: 0 auto; }\n"
+ + ".header { background: #f0f0f0; padding: 20px; border-bottom: 1px solid #ccc; }\n"
+ + ".nav { display: flex; justify-content: space-between; }\n"
+ + ".nav a { color: #007bff; text-decoration: none; padding: 10px; }\n"
+ + ".nav a:hover { background: #e0e0e0; }\n"
+ + ".content { padding: 20px; }\n"
+ + ".footer { background: #333; color: #fff; padding: 20px; text-align: center; }\n"
+ + "@media (max-width: 768px) {\n"
+ + " .container { width: 95%; }\n"
+ + " .nav { flex-direction: column; }\n"
+ + "}\n";
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void parseSimpleCSS() throws Exception {
+ final CSSOMParser parser = new CSSOMParser();
+ final InputSource source = new InputSource(new StringReader(SIMPLE_CSS));
+
+ final CSSStyleSheetImpl styleSheet = parser.parseStyleSheet(source, null);
+
+ assertNotNull(styleSheet);
+ assertTrue(styleSheet.getCssRules().getLength() > 0);
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void parseMediumCSS() throws Exception {
+ final CSSOMParser parser = new CSSOMParser();
+ final InputSource source = new InputSource(new StringReader(MEDIUM_CSS));
+
+ final CSSStyleSheetImpl styleSheet = parser.parseStyleSheet(source, null);
+
+ assertNotNull(styleSheet);
+ assertTrue(styleSheet.getCssRules().getLength() > 0);
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void benchmarkParseSpeed() throws Exception {
+ final CSSOMParser parser = new CSSOMParser();
+
+ // Warmup
+ for (int i = 0; i < 10; i++) {
+ final InputSource source = new InputSource(new StringReader(MEDIUM_CSS));
+ parser.parseStyleSheet(source, null);
+ }
+
+ // Benchmark
+ final long start = System.nanoTime();
+ for (int i = 0; i < 100; i++) {
+ final InputSource source = new InputSource(new StringReader(MEDIUM_CSS));
+ parser.parseStyleSheet(source, null);
+ }
+ final long end = System.nanoTime();
+
+ final long avgTimeMs = (end - start) / 100 / 1_000_000;
+ System.out.println("Average parse time for medium CSS: " + avgTimeMs + "ms");
+
+ // Should parse reasonably fast (this is a sanity check, not a strict requirement)
+ assertTrue(avgTimeMs < 100, "Parse time too slow: " + avgTimeMs + "ms");
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testStringBuilderCaching() throws Exception {
+ final CSSOMParser parser = new CSSOMParser();
+
+ // Parse multiple times to test StringBuilder reuse
+ for (int i = 0; i < 50; i++) {
+ final InputSource source = new InputSource(new StringReader(SIMPLE_CSS));
+ final CSSStyleSheetImpl styleSheet = parser.parseStyleSheet(source, null);
+ assertNotNull(styleSheet);
+ }
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testLocatorPooling() throws Exception {
+ LocatorPool.clear();
+
+ final CSSOMParser parser = new CSSOMParser();
+ final InputSource source = new InputSource(new StringReader(MEDIUM_CSS));
+
+ final CSSStyleSheetImpl styleSheet = parser.parseStyleSheet(source, null);
+
+ assertNotNull(styleSheet);
+ assertTrue(styleSheet.getCssRules().getLength() > 0);
+
+ LocatorPool.clear();
+ }
+
+ /**
+ * @throws Exception in case of failure
+ */
+ @Test
+ public void testPerformanceMetrics() throws Exception {
+ final PerformanceMetrics metrics = PerformanceMetrics.start();
+
+ if (metrics != null) {
+ metrics.recordParseTime(100);
+ metrics.incrementTokens();
+ metrics.incrementRules();
+ metrics.incrementProperties();
+ metrics.report();
+ }
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/util/ParserUtilsTest.java b/src/test/java/org/htmlunit/cssparser/parser/util/ParserUtilsTest.java
index eec4997..2927eb8 100644
--- a/src/test/java/org/htmlunit/cssparser/parser/util/ParserUtilsTest.java
+++ b/src/test/java/org/htmlunit/cssparser/parser/util/ParserUtilsTest.java
@@ -15,6 +15,9 @@
package org.htmlunit.cssparser.parser.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.htmlunit.cssparser.util.ParserUtils;
import org.junit.jupiter.api.Test;
@@ -40,6 +43,30 @@ public void trimBy() {
assertEquals("e", ParserUtils.trimBy(new StringBuilder("test"), 1, 2));
}
+ /**
+ * @throws Exception on failure
+ */
+ @Test
+ public void trimByString() {
+ assertEquals("test", ParserUtils.trimBy("test", 0, 0));
+
+ assertEquals("est", ParserUtils.trimBy("test", 1, 0));
+ assertEquals("st", ParserUtils.trimBy("test", 2, 0));
+
+ assertEquals("tes", ParserUtils.trimBy("test", 0, 1));
+ assertEquals("te", ParserUtils.trimBy("test", 0, 2));
+
+ assertEquals("e", ParserUtils.trimBy("test", 1, 2));
+
+ // Test edge cases with explicit String type
+ final String nullString = null;
+ assertNull(ParserUtils.trimBy(nullString, 1, 1));
+ final String testString = "test";
+ assertEquals("test", ParserUtils.trimBy(testString, -1, 0));
+ assertEquals("test", ParserUtils.trimBy(testString, 0, -1));
+ assertEquals("test", ParserUtils.trimBy(testString, 3, 2));
+ }
+
/**
* @throws Exception on failure
*/
@@ -53,4 +80,26 @@ public void trimUrl() {
assertEquals("test", ParserUtils.trimUrl(new StringBuilder("url( test \t )")));
}
+
+ /**
+ * @throws Exception on failure
+ */
+ @Test
+ public void equalsIgnoreCase() {
+ assertTrue(ParserUtils.equalsIgnoreCase("test", "TEST"));
+ assertTrue(ParserUtils.equalsIgnoreCase("Test", "test"));
+ assertTrue(ParserUtils.equalsIgnoreCase("test", "test"));
+ assertTrue(ParserUtils.equalsIgnoreCase("", ""));
+
+ assertFalse(ParserUtils.equalsIgnoreCase("test", "other"));
+ assertFalse(ParserUtils.equalsIgnoreCase("test", "TEST123"));
+
+ assertTrue(ParserUtils.equalsIgnoreCase(null, null));
+ assertFalse(ParserUtils.equalsIgnoreCase("test", null));
+ assertFalse(ParserUtils.equalsIgnoreCase(null, "test"));
+
+ // Same reference
+ final String s = "test";
+ assertTrue(ParserUtils.equalsIgnoreCase(s, s));
+ }
}