Skip to content

Commit 5635807

Browse files
committed
fix: support CSS logical combination pseudo-classes
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
1 parent 0f6b509 commit 5635807

File tree

5 files changed

+151
-109
lines changed

5 files changed

+151
-109
lines changed

src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ public CssSimpleSelectorSequence createSimpleSelectorSequence(final CssToken sta
823823
while (next.type != CssToken.Type.S
824824
&& !MATCH_COMMA.apply(next)
825825
&& !MATCH_OPENBRACE.apply(next)
826+
&& !MATCH_CLOSEPAREN.apply(next)
826827
&& !MATCH_COMBINATOR_CHAR.apply(next))
827828
{
828829
seqItem = createSimpleSelector(iter.next(FILTER_NONE), iter, err);
@@ -838,9 +839,89 @@ public CssSimpleSelectorSequence createSimpleSelectorSequence(final CssToken sta
838839
}
839840

840841
/**
841-
* Create one item in a simple selector sequence. If creation fails,
842-
* errors are issued, and null is returned. On return, the iterator
843-
* will return the next token after the constructs last token.
842+
* With start inparam being the first significant token in a selector, build
843+
* the selector group (aka comma separated selectors), expected return when
844+
* iter.last is '{'. On error, issue to errorlistener, and return (caller
845+
* will forward).
846+
*
847+
* @return A syntactically valid CssSelector list, or null if fail.
848+
* @throws CssException
849+
*/
850+
851+
public List<CssSelector> createSelectorList(CssToken start, CssTokenIterator iter,
852+
CssErrorHandler err)
853+
throws CssException
854+
{
855+
return createSelectorList(start, iter, err, false, false, MATCH_OPENBRACE);
856+
}
857+
858+
private List<CssSelector> createSelectorList(CssToken start, CssTokenIterator iter,
859+
CssErrorHandler err, boolean forgiving, boolean relative, Predicate<CssToken> endMatcher)
860+
throws CssException
861+
{
862+
863+
List<CssSelector> selectors = Lists.newArrayList();
864+
boolean end = false;
865+
while (true)
866+
{ // comma loop
867+
CssSelector selector = new CssSelector(start.location);
868+
int selectorIndex = iter.index();
869+
while (true)
870+
{ // combinator loop
871+
if (!relative || iter.index() > selectorIndex)
872+
{
873+
CssSimpleSelectorSequence seq = createSimpleSelectorSequence(start, iter,
874+
(forgiving) ? ForgivingErrorHandler.INSTANCE : err);
875+
if (seq == null)
876+
{
877+
// errors already issued
878+
return null;
879+
}
880+
selector.components.add(seq);
881+
start = iter.next();
882+
}
883+
int idx = iter.index();
884+
if (endMatcher.apply(start))
885+
{
886+
end = true;
887+
break;
888+
}
889+
if (MATCH_COMMA.apply(start))
890+
{
891+
break;
892+
}
893+
894+
CssSelectorCombinator comb = createCombinator(start, iter, err);
895+
if (comb != null)
896+
{
897+
selector.components.add(comb);
898+
start = iter.next();
899+
}
900+
else if (iter.list.get(idx - 1).type == CssToken.Type.S)
901+
{
902+
selector.components.add(new CssSelectorCombinator(' ', start.location));
903+
}
904+
else
905+
{
906+
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
907+
messages.getLocale(), iter.last.chars));
908+
return null;
909+
}
910+
} // combinator loop
911+
selectors.add(selector);
912+
if (end)
913+
{
914+
break;
915+
}
916+
start = iter.next();
917+
} // comma loop
918+
return selectors;
919+
}
920+
921+
/**
922+
* Create one item in a simple selector sequence. If creation fails, errors
923+
* are issued, and null is returned. On return, the iterator will return the
924+
* next token after the constructs last token.
844925
*/
845926
CssConstruct createSimpleSelector(final CssToken start, final CssTokenIterator iter,
846927
final CssErrorHandler err) throws
@@ -971,13 +1052,21 @@ else if (next.type == CssToken.Type.FUNCTION)
9711052
//pseudo: type_selector | universal | HASH | class | attrib | pseudo
9721053

9731054
CssConstruct func;
974-
if (Ascii.toLowerCase(next.getChars()).startsWith("not"))
975-
{
976-
func = createNegationPseudo(tk, iter, err);
977-
}
978-
else
1055+
switch (Ascii.toLowerCase(next.getChars()))
9791056
{
1057+
case "not(":
1058+
func = createFunctionalSelectorListPseudo(tk, iter, err, false, false);
1059+
break;
1060+
case "is(":
1061+
case "where(":
1062+
func = createFunctionalSelectorListPseudo(tk, iter, err, true, false);
1063+
break;
1064+
case "has(":
1065+
func = createFunctionalSelectorListPseudo(tk, iter, err, true, true);
1066+
break;
1067+
default:
9801068
func = createFunctionalPseudo(tk, iter, MATCH_OPENBRACE, err);
1069+
break;
9811070
}
9821071

9831072
if (func == null)
@@ -1021,25 +1110,28 @@ CssConstruct createFunctionalPseudo(final CssToken start,
10211110
return function;
10221111
}
10231112

1024-
CssConstruct createNegationPseudo(final CssToken start,
1025-
final CssTokenIterator iter, final CssErrorHandler err) throws
1026-
CssException
1113+
CssConstruct createFunctionalSelectorListPseudo(final CssToken start,
1114+
final CssTokenIterator iter, final CssErrorHandler err, boolean forgiving, boolean relative)
1115+
throws CssException
10271116
{
10281117

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

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

10331122
CssToken tk = iter.next();
1034-
CssConstruct cc = createSimpleSelector(tk, iter, err);
1035-
if (cc == null || !ContextRestrictions.PSEUDO_NEGATION.apply(cc))
1123+
List<CssSelector> selectors = createSelectorList(tk, iter, err, forgiving, relative,
1124+
MATCH_CLOSEPAREN);
1125+
if (selectors == null)
10361126
{
10371127
return null;
10381128
}
10391129
else
10401130
{
1041-
negation.components.add(cc);
1042-
iter.next();
1131+
for (CssSelector selector : selectors)
1132+
{
1133+
negation.components.add(selector);
1134+
}
10431135
}
10441136
return negation;
10451137
}

src/main/java/org/idpf/epubcheck/util/css/CssParser.java

Lines changed: 7 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,13 @@
4040
import org.idpf.epubcheck.util.css.CssGrammar.CssConstructFactory;
4141
import org.idpf.epubcheck.util.css.CssGrammar.CssDeclaration;
4242
import org.idpf.epubcheck.util.css.CssGrammar.CssSelector;
43-
import org.idpf.epubcheck.util.css.CssGrammar.CssSelectorCombinator;
4443
import org.idpf.epubcheck.util.css.CssGrammar.CssSelectorConstructFactory;
45-
import org.idpf.epubcheck.util.css.CssGrammar.CssSimpleSelectorSequence;
4644
import org.idpf.epubcheck.util.css.CssToken.CssTokenConsumer;
4745
import org.idpf.epubcheck.util.css.CssTokenList.CssTokenIterator;
4846
import org.idpf.epubcheck.util.css.CssTokenList.PrematureEOFException;
4947

5048
import com.adobe.epubcheck.util.Messages;
5149
import com.google.common.base.Predicate;
52-
import com.google.common.collect.Lists;
5350

5451
/**
5552
* A CSS parser.
@@ -227,14 +224,20 @@ private void handleRuleSet(CssToken start, final CssTokenIterator iter, final Cs
227224
char errChar = '{';
228225
try
229226
{
230-
List<CssSelector> selectors = handleSelectors(start, iter, err);
227+
List<CssSelector> selectors = cssSelectorFactory.createSelectorList(start, iter, err);
231228
errChar = '}';
232229
if (selectors == null)
233230
{
234231
// handleSelectors() has issued errors, we forward
235232
iter.next(MATCH_CLOSEBRACE);
236233
return;
237234
}
235+
if (MATCH_CLOSEPAREN.apply(iter.last)) {
236+
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
237+
messages.getLocale(), iter.last.chars));
238+
iter.next(MATCH_CLOSEBRACE);
239+
return;
240+
}
238241
if (debug)
239242
{
240243
checkState(iter.last.getChar() == '{');
@@ -453,78 +456,6 @@ private boolean handlePropertyValue(CssDeclaration declaration, CssToken start,
453456
}
454457
}
455458

456-
/**
457-
* With start inparam being the first significant token in a selector, build
458-
* the selector group (aka comma separated selectors), expected return when
459-
* iter.last is '{'. On error, issue to errorlistener, and return
460-
* (caller will forward).
461-
*
462-
* @return A syntactically valid CssSelector list, or null if fail.
463-
* @throws CssException
464-
*/
465-
private List<CssSelector> handleSelectors(CssToken start, CssTokenIterator iter,
466-
CssErrorHandler err) throws
467-
CssException
468-
{
469-
470-
List<CssSelector> selectors = Lists.newArrayList();
471-
boolean end = false;
472-
while (true)
473-
{ // comma loop
474-
CssSelector selector = new CssSelector(start.location);
475-
while (true)
476-
{ //combinator loop
477-
CssSimpleSelectorSequence seq = cssSelectorFactory.createSimpleSelectorSequence(start, iter,
478-
err);
479-
if (seq == null)
480-
{
481-
//errors already issued
482-
return null;
483-
}
484-
selector.components.add(seq);
485-
int idx = iter.index();
486-
start = iter.next();
487-
if (MATCH_OPENBRACE.apply(start))
488-
{
489-
end = true;
490-
break;
491-
}
492-
if (MATCH_COMMA.apply(start))
493-
{
494-
break;
495-
}
496-
497-
CssSelectorCombinator comb = cssSelectorFactory.createCombinator(start, iter, err);
498-
if (comb != null)
499-
{
500-
selector.components.add(comb);
501-
start = iter.next();
502-
}
503-
else if (iter.list.get(idx + 1).type == CssToken.Type.S)
504-
{
505-
selector.components.add(new CssSelectorCombinator(' ', start.location));
506-
}
507-
else
508-
{
509-
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, iter.last.location,
510-
messages.getLocale(), iter.last.chars));
511-
return null;
512-
}
513-
} //combinator loop
514-
selectors.add(selector);
515-
if (end)
516-
{
517-
break;
518-
}
519-
if (debug)
520-
{
521-
checkState(MATCH_COMMA.apply(start));
522-
}
523-
start = iter.next();
524-
} // comma loop
525-
return selectors;
526-
}
527-
528459
/**
529460
* With start token required to be an ATKEYWORD, collect at-rule parameters if
530461
* any, and if the at-rule has a block, invoke those handlers.
@@ -759,23 +690,6 @@ public boolean apply(CssConstruct cc)
759690
;
760691
}
761692
};
762-
763-
/**
764-
* A context restriction for elements inside a negation pseudo.
765-
*/
766-
static final Predicate<CssConstruct> PSEUDO_NEGATION = new Predicate<CssConstruct>()
767-
{
768-
public boolean apply(final CssConstruct cc)
769-
{
770-
checkNotNull(cc);
771-
return cc.type == CssConstruct.Type.TYPE_SELECTOR
772-
|| cc.type == CssConstruct.Type.HASHNAME
773-
|| cc.type == CssConstruct.Type.CLASSNAME
774-
|| (cc.type == CssConstruct.Type.ATTRIBUTE_SELECTOR)
775-
|| (cc.type == CssConstruct.Type.PSEUDO)
776-
;
777-
}
778-
};
779693
}
780694

781695
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.idpf.epubcheck.util.css;
2+
3+
import org.idpf.epubcheck.util.css.CssExceptions.CssException;
4+
5+
public final class ForgivingErrorHandler implements CssErrorHandler
6+
{
7+
8+
public static final ForgivingErrorHandler INSTANCE = new ForgivingErrorHandler();
9+
10+
@Override
11+
public void error(CssException e)
12+
throws CssException
13+
{
14+
// do nothing
15+
}
16+
17+
}

src/test/resources/epub3/00-minimal/minimal.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
Given EPUB test files located at '/epub3/00-minimal/files/'
99
And EPUBCheck with default settings
1010

11-
1211
Scenario: Verify a minimal expanded EPUB
1312
When checking EPUB 'minimal'
1413
Then no errors or warnings are reported

src/test/resources/epub3/06-content-document/files/content-css-selectors-valid/EPUB/style.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,23 @@ p > span:nth-child(even) {
2929
div[epub|type = "chapter"] {
3030
padding: 2em;
3131
}
32+
33+
:active {
34+
font-size: 1em;
35+
}
36+
37+
:is(h1,h2,h3) {
38+
font-size: 1em;
39+
}
40+
41+
:not(nav[epub|type="landmarks"]) {
42+
font-size: 1em;
43+
}
44+
45+
:not(nav.class) {
46+
font-size: 1em;
47+
}
48+
49+
:has(>img) {
50+
font-size: 1em;
51+
}

0 commit comments

Comments
 (0)