Skip to content

Commit

Permalink
fix: support CSS logical combination pseudo-classes
Browse files Browse the repository at this point in the history
This commit update the CSS grammar parser to support funcitonal pseudo-
classes taking selector listrs as argument.

In effect, this impacts the parsing of the following pseudo-classes:
- `:is()` (taking a forgiving selector list)
- `:not()` (taking a selector list)
- `:where()` (taking a forgiving selector list)
- `:has()` (taking a forgiving relative selector list)

Fixes #1289, Fixes #1354
  • Loading branch information
rdeltour committed Nov 27, 2022
1 parent 0f6b509 commit 5635807
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 109 deletions.
122 changes: 107 additions & 15 deletions src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ public CssSimpleSelectorSequence createSimpleSelectorSequence(final CssToken sta
while (next.type != CssToken.Type.S
&& !MATCH_COMMA.apply(next)
&& !MATCH_OPENBRACE.apply(next)
&& !MATCH_CLOSEPAREN.apply(next)
&& !MATCH_COMBINATOR_CHAR.apply(next))
{
seqItem = createSimpleSelector(iter.next(FILTER_NONE), iter, err);
Expand All @@ -838,9 +839,89 @@ public CssSimpleSelectorSequence createSimpleSelectorSequence(final CssToken sta
}

/**
* Create one item in a simple selector sequence. If creation fails,
* errors are issued, and null is returned. On return, the iterator
* will return the next token after the constructs last token.
* With start inparam being the first significant token in a selector, build
* the selector group (aka comma separated selectors), expected return when
* iter.last is '{'. On error, issue to errorlistener, and return (caller
* will forward).
*
* @return A syntactically valid CssSelector list, or null if fail.
* @throws CssException
*/

public List<CssSelector> createSelectorList(CssToken start, CssTokenIterator iter,
CssErrorHandler err)
throws CssException
{
return createSelectorList(start, iter, err, false, false, MATCH_OPENBRACE);
}

private List<CssSelector> createSelectorList(CssToken start, CssTokenIterator iter,
CssErrorHandler err, boolean forgiving, boolean relative, Predicate<CssToken> endMatcher)
throws CssException
{

List<CssSelector> selectors = Lists.newArrayList();
boolean end = false;
while (true)
{ // comma loop
CssSelector selector = new CssSelector(start.location);
int selectorIndex = iter.index();
while (true)
{ // combinator loop
if (!relative || iter.index() > selectorIndex)
{
CssSimpleSelectorSequence seq = createSimpleSelectorSequence(start, iter,
(forgiving) ? ForgivingErrorHandler.INSTANCE : err);
if (seq == null)
{
// errors already issued
return null;
}
selector.components.add(seq);
start = iter.next();
}
int idx = iter.index();
if (endMatcher.apply(start))
{
end = true;
break;
}
if (MATCH_COMMA.apply(start))
{
break;
}

CssSelectorCombinator comb = createCombinator(start, iter, err);
if (comb != null)
{
selector.components.add(comb);
start = iter.next();
}
else if (iter.list.get(idx - 1).type == CssToken.Type.S)
{
selector.components.add(new CssSelectorCombinator(' ', start.location));
}
else
{
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
messages.getLocale(), iter.last.chars));
return null;
}
} // combinator loop
selectors.add(selector);
if (end)
{
break;
}
start = iter.next();
} // comma loop
return selectors;
}

/**
* Create one item in a simple selector sequence. If creation fails, errors
* are issued, and null is returned. On return, the iterator will return the
* next token after the constructs last token.
*/
CssConstruct createSimpleSelector(final CssToken start, final CssTokenIterator iter,
final CssErrorHandler err) throws
Expand Down Expand Up @@ -971,13 +1052,21 @@ else if (next.type == CssToken.Type.FUNCTION)
//pseudo: type_selector | universal | HASH | class | attrib | pseudo

CssConstruct func;
if (Ascii.toLowerCase(next.getChars()).startsWith("not"))
{
func = createNegationPseudo(tk, iter, err);
}
else
switch (Ascii.toLowerCase(next.getChars()))
{
case "not(":
func = createFunctionalSelectorListPseudo(tk, iter, err, false, false);
break;
case "is(":
case "where(":
func = createFunctionalSelectorListPseudo(tk, iter, err, true, false);
break;
case "has(":
func = createFunctionalSelectorListPseudo(tk, iter, err, true, true);
break;
default:
func = createFunctionalPseudo(tk, iter, MATCH_OPENBRACE, err);
break;
}

if (func == null)
Expand Down Expand Up @@ -1021,25 +1110,28 @@ CssConstruct createFunctionalPseudo(final CssToken start,
return function;
}

CssConstruct createNegationPseudo(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) throws
CssException
CssConstruct createFunctionalSelectorListPseudo(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err, boolean forgiving, boolean relative)
throws CssException
{

String name = start.getChars().substring(0, start.getChars().length() - 1);

CssFunction negation = new CssFunction(name, start.location);

CssToken tk = iter.next();
CssConstruct cc = createSimpleSelector(tk, iter, err);
if (cc == null || !ContextRestrictions.PSEUDO_NEGATION.apply(cc))
List<CssSelector> selectors = createSelectorList(tk, iter, err, forgiving, relative,
MATCH_CLOSEPAREN);
if (selectors == null)
{
return null;
}
else
{
negation.components.add(cc);
iter.next();
for (CssSelector selector : selectors)
{
negation.components.add(selector);
}
}
return negation;
}
Expand Down
100 changes: 7 additions & 93 deletions src/main/java/org/idpf/epubcheck/util/css/CssParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,13 @@
import org.idpf.epubcheck.util.css.CssGrammar.CssConstructFactory;
import org.idpf.epubcheck.util.css.CssGrammar.CssDeclaration;
import org.idpf.epubcheck.util.css.CssGrammar.CssSelector;
import org.idpf.epubcheck.util.css.CssGrammar.CssSelectorCombinator;
import org.idpf.epubcheck.util.css.CssGrammar.CssSelectorConstructFactory;
import org.idpf.epubcheck.util.css.CssGrammar.CssSimpleSelectorSequence;
import org.idpf.epubcheck.util.css.CssToken.CssTokenConsumer;
import org.idpf.epubcheck.util.css.CssTokenList.CssTokenIterator;
import org.idpf.epubcheck.util.css.CssTokenList.PrematureEOFException;

import com.adobe.epubcheck.util.Messages;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;

/**
* A CSS parser.
Expand Down Expand Up @@ -227,14 +224,20 @@ private void handleRuleSet(CssToken start, final CssTokenIterator iter, final Cs
char errChar = '{';
try
{
List<CssSelector> selectors = handleSelectors(start, iter, err);
List<CssSelector> selectors = cssSelectorFactory.createSelectorList(start, iter, err);
errChar = '}';
if (selectors == null)
{
// handleSelectors() has issued errors, we forward
iter.next(MATCH_CLOSEBRACE);
return;
}
if (MATCH_CLOSEPAREN.apply(iter.last)) {
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
messages.getLocale(), iter.last.chars));
iter.next(MATCH_CLOSEBRACE);
return;
}
if (debug)
{
checkState(iter.last.getChar() == '{');
Expand Down Expand Up @@ -453,78 +456,6 @@ private boolean handlePropertyValue(CssDeclaration declaration, CssToken start,
}
}

/**
* With start inparam being the first significant token in a selector, build
* the selector group (aka comma separated selectors), expected return when
* iter.last is '{'. On error, issue to errorlistener, and return
* (caller will forward).
*
* @return A syntactically valid CssSelector list, or null if fail.
* @throws CssException
*/
private List<CssSelector> handleSelectors(CssToken start, CssTokenIterator iter,
CssErrorHandler err) throws
CssException
{

List<CssSelector> selectors = Lists.newArrayList();
boolean end = false;
while (true)
{ // comma loop
CssSelector selector = new CssSelector(start.location);
while (true)
{ //combinator loop
CssSimpleSelectorSequence seq = cssSelectorFactory.createSimpleSelectorSequence(start, iter,
err);
if (seq == null)
{
//errors already issued
return null;
}
selector.components.add(seq);
int idx = iter.index();
start = iter.next();
if (MATCH_OPENBRACE.apply(start))
{
end = true;
break;
}
if (MATCH_COMMA.apply(start))
{
break;
}

CssSelectorCombinator comb = cssSelectorFactory.createCombinator(start, iter, err);
if (comb != null)
{
selector.components.add(comb);
start = iter.next();
}
else if (iter.list.get(idx + 1).type == CssToken.Type.S)
{
selector.components.add(new CssSelectorCombinator(' ', start.location));
}
else
{
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
messages.getLocale(), iter.last.chars));
return null;
}
} //combinator loop
selectors.add(selector);
if (end)
{
break;
}
if (debug)
{
checkState(MATCH_COMMA.apply(start));
}
start = iter.next();
} // comma loop
return selectors;
}

/**
* With start token required to be an ATKEYWORD, collect at-rule parameters if
* any, and if the at-rule has a block, invoke those handlers.
Expand Down Expand Up @@ -759,23 +690,6 @@ public boolean apply(CssConstruct cc)
;
}
};

/**
* A context restriction for elements inside a negation pseudo.
*/
static final Predicate<CssConstruct> PSEUDO_NEGATION = new Predicate<CssConstruct>()
{
public boolean apply(final CssConstruct cc)
{
checkNotNull(cc);
return cc.type == CssConstruct.Type.TYPE_SELECTOR
|| cc.type == CssConstruct.Type.HASHNAME
|| cc.type == CssConstruct.Type.CLASSNAME
|| (cc.type == CssConstruct.Type.ATTRIBUTE_SELECTOR)
|| (cc.type == CssConstruct.Type.PSEUDO)
;
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.idpf.epubcheck.util.css;

import org.idpf.epubcheck.util.css.CssExceptions.CssException;

public final class ForgivingErrorHandler implements CssErrorHandler
{

public static final ForgivingErrorHandler INSTANCE = new ForgivingErrorHandler();

@Override
public void error(CssException e)
throws CssException
{
// do nothing
}

}
1 change: 0 additions & 1 deletion src/test/resources/epub3/00-minimal/minimal.feature
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
Given EPUB test files located at '/epub3/00-minimal/files/'
And EPUBCheck with default settings


Scenario: Verify a minimal expanded EPUB
When checking EPUB 'minimal'
Then no errors or warnings are reported
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,23 @@ p > span:nth-child(even) {
div[epub|type = "chapter"] {
padding: 2em;
}

:active {
font-size: 1em;
}

:is(h1,h2,h3) {
font-size: 1em;
}

:not(nav[epub|type="landmarks"]) {
font-size: 1em;
}

:not(nav.class) {
font-size: 1em;
}

:has(>img) {
font-size: 1em;
}

0 comments on commit 5635807

Please sign in to comment.