From 5635807bc0f9839387df4717e3a096dab596a91b Mon Sep 17 00:00:00 2001 From: Romain Deltour Date: Thu, 17 Nov 2022 10:09:21 +0100 Subject: [PATCH] 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 --- .../idpf/epubcheck/util/css/CssGrammar.java | 122 +++++++++++++++--- .../idpf/epubcheck/util/css/CssParser.java | 100 +------------- .../util/css/ForgivingErrorHandler.java | 17 +++ .../epub3/00-minimal/minimal.feature | 1 - .../EPUB/style.css | 20 +++ 5 files changed, 151 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/idpf/epubcheck/util/css/ForgivingErrorHandler.java diff --git a/src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java b/src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java index 76ad868a2..75f46221d 100644 --- a/src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java +++ b/src/main/java/org/idpf/epubcheck/util/css/CssGrammar.java @@ -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); @@ -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 createSelectorList(CssToken start, CssTokenIterator iter, + CssErrorHandler err) + throws CssException + { + return createSelectorList(start, iter, err, false, false, MATCH_OPENBRACE); + } + + private List createSelectorList(CssToken start, CssTokenIterator iter, + CssErrorHandler err, boolean forgiving, boolean relative, Predicate endMatcher) + throws CssException + { + + List 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 @@ -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) @@ -1021,9 +1110,9 @@ 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); @@ -1031,15 +1120,18 @@ CssConstruct createNegationPseudo(final CssToken start, 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 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; } diff --git a/src/main/java/org/idpf/epubcheck/util/css/CssParser.java b/src/main/java/org/idpf/epubcheck/util/css/CssParser.java index e766a5fbe..1dc9bb7ae 100644 --- a/src/main/java/org/idpf/epubcheck/util/css/CssParser.java +++ b/src/main/java/org/idpf/epubcheck/util/css/CssParser.java @@ -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. @@ -227,7 +224,7 @@ private void handleRuleSet(CssToken start, final CssTokenIterator iter, final Cs char errChar = '{'; try { - List selectors = handleSelectors(start, iter, err); + List selectors = cssSelectorFactory.createSelectorList(start, iter, err); errChar = '}'; if (selectors == null) { @@ -235,6 +232,12 @@ private void handleRuleSet(CssToken start, final CssTokenIterator iter, final Cs 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() == '{'); @@ -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 handleSelectors(CssToken start, CssTokenIterator iter, - CssErrorHandler err) throws - CssException - { - - List 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. @@ -759,23 +690,6 @@ public boolean apply(CssConstruct cc) ; } }; - - /** - * A context restriction for elements inside a negation pseudo. - */ - static final Predicate PSEUDO_NEGATION = new Predicate() - { - 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) - ; - } - }; } } diff --git a/src/main/java/org/idpf/epubcheck/util/css/ForgivingErrorHandler.java b/src/main/java/org/idpf/epubcheck/util/css/ForgivingErrorHandler.java new file mode 100644 index 000000000..5e8d6c165 --- /dev/null +++ b/src/main/java/org/idpf/epubcheck/util/css/ForgivingErrorHandler.java @@ -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 + } + +} diff --git a/src/test/resources/epub3/00-minimal/minimal.feature b/src/test/resources/epub3/00-minimal/minimal.feature index 4a37141ba..efb520943 100644 --- a/src/test/resources/epub3/00-minimal/minimal.feature +++ b/src/test/resources/epub3/00-minimal/minimal.feature @@ -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 diff --git a/src/test/resources/epub3/06-content-document/files/content-css-selectors-valid/EPUB/style.css b/src/test/resources/epub3/06-content-document/files/content-css-selectors-valid/EPUB/style.css index 63fbe07f7..4cc15e780 100644 --- a/src/test/resources/epub3/06-content-document/files/content-css-selectors-valid/EPUB/style.css +++ b/src/test/resources/epub3/06-content-document/files/content-css-selectors-valid/EPUB/style.css @@ -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; +} \ No newline at end of file