LegacyQueryParser.java
package org.jsoup.select;
import org.jsoup.helper.Regex;
import org.jsoup.helper.Validate;
import org.jsoup.internal.CssMatching.AttributeOperator;
import org.jsoup.internal.CssMatching.CaseSensitivity;
import org.jsoup.internal.LegacyMatching;
import org.jsoup.parser.TokenQueue;
import org.jspecify.annotations.Nullable;
import static org.jsoup.internal.Normalizer.asciiLowerCase;
import static org.jsoup.internal.StringUtil.trimAsciiWhitespace;
/**
Parses attribute selectors in legacy query mode.
Scheduled for removal with legacy mode in 1.26.1.
*/
final class LegacyQueryParser {
private static final String[] AttributeOperators;
static {
AttributeOperator[] operators = AttributeOperator.values();
AttributeOperators = new String[operators.length];
for (int i = 0; i < operators.length; i++) AttributeOperators[i] = operators[i].symbol;
}
private LegacyQueryParser() {}
/** Parses legacy attribute selectors, also accepting CSS case flags and {@code |=}. */
static Evaluator attribute(TokenQueue tq, String query) {
Evaluator css = tryCssAttribute(tq, query);
if (css != null) return css;
try (TokenQueue cq = new TokenQueue(tq.chompBalanced('[', ']'))) {
return rawAttribute(cq, query);
}
}
/** Tries CSS case flags or {@code |=}, leaving the query untouched if legacy parsing is needed. */
private static @Nullable Evaluator tryCssAttribute(TokenQueue tq, String query) {
String attribute;
try {
attribute = QueryParser.peekCssAttribute(query, tq.pos(), false);
} catch (Selector.SelectorParseException e) {
return null; // raw regexes may contain delimiters that aren't balanced CSS
}
try (TokenQueue probe = new TokenQueue(attribute)) {
probe.consumeToAny(AttributeOperators);
boolean dashPrefix = probe.matches("|=");
if (probe.matches("~=") || !(dashPrefix || LegacyMatching.hasAttributeFlag(attribute))) return null;
try (TokenQueue css = new TokenQueue(attribute)) {
Evaluator eval = QueryParser.cssAttribute(css, false);
if (dashPrefix || eval instanceof Evaluator.AttributeKeyPair &&
((Evaluator.AttributeKeyPair) eval).flag != CaseSensitivity.Default) {
QueryParser.consumeAttribute(tq, attribute);
return eval;
}
// [attr= i] has no flag; preserve the leading space in its legacy value
} catch (IllegalArgumentException e) {
if (dashPrefix) throw e;
// a trailing i or s may be part of an unquoted legacy value
}
}
return null;
}
/** Parses legacy attribute names and values without decoding CSS escapes. */
private static Evaluator rawAttribute(TokenQueue cq, String query) {
String key = cq.consumeToAny(AttributeOperators); // eq, not, start, end, contain, match, (no val)
key = trimAsciiWhitespace(key);
Validate.notEmpty(key);
Validate.isFalse(asciiLowerCase(key).equals("abs:"), "Absolute attribute key must have a name");
cq.consumeWhitespace();
final Evaluator eval;
if (cq.isEmpty()) {
if (key.startsWith("^"))
eval = new Evaluator.AttributeStarting(key.substring(1));
else if (key.equals("*")) // any attribute
eval = new Evaluator.AttributeStarting("");
else
eval = new Evaluator.Attribute(key);
} else {
if (cq.matchChomp('='))
eval = new Evaluator.AttributeWithValue(key, cq.remainder());
else if (cq.matchChomp("!="))
eval = new Evaluator.AttributeWithValueNot(key, cq.remainder());
else if (cq.matchChomp("^="))
eval = new Evaluator.AttributeWithValueStarting(key, cq.remainder());
else if (cq.matchChomp("$="))
eval = new Evaluator.AttributeWithValueEnding(key, cq.remainder());
else if (cq.matchChomp("*="))
eval = new Evaluator.AttributeWithValueContaining(key, cq.remainder());
else if (cq.matchChomp("~="))
eval = new Evaluator.AttributeWithValueMatching(key, Regex.compile(cq.remainder()));
else
throw new Selector.SelectorParseException(
"Could not parse attribute query '%s': unexpected token at '%s'", query, cq.remainder());
}
return eval;
}
}