org.idpf.epubcheck.util.css.CssGrammar Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of epubcheck Show documentation
Show all versions of epubcheck Show documentation
EpubCheck is a tool to validate IDPF EPUB files. It can detect many types of errors in EPUB.
OCF container structure, OPF and OPS mark-up, and internal reference consistency are checked.
EpubCheck can be run as a standalone command-line tool, installed as a Java server-side web application
or used as a Java library.
The newest version!
/*
* Copyright (c) 2012 International Digital Publishing Forum
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
package org.idpf.epubcheck.util.css;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.idpf.epubcheck.util.css.CssExceptions.CssErrorCode.GRAMMAR_UNEXPECTED_TOKEN;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_ATTRIBUTE_SELECTOR_MATCHERS;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_CLOSEPAREN;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_CLOSESQUAREBRACKET;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_COLON;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_COMBINATOR_CHAR;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_COMMA;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_OPENBRACE;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_OPENPAREN;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_OPENSQUAREBRACKET;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_PIPE;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_STAR;
import static org.idpf.epubcheck.util.css.CssToken.Matchers.MATCH_STAR_PIPE;
import static org.idpf.epubcheck.util.css.CssTokenList.Filters.FILTER_NONE;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.idpf.epubcheck.util.css.CssExceptions.CssErrorCode;
import org.idpf.epubcheck.util.css.CssExceptions.CssException;
import org.idpf.epubcheck.util.css.CssExceptions.CssGrammarException;
import org.idpf.epubcheck.util.css.CssParser.ContextRestrictions;
import org.idpf.epubcheck.util.css.CssTokenList.CssTokenIterator;
import com.adobe.epubcheck.util.Messages;
import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
/**
* CSS grammar components.
*
* @author mgylling
*/
public class CssGrammar
{
/**
* Abstract base for all CssConstructs.
*/
public static abstract class CssConstruct
{
final CssLocation location;
final Type type;
public CssConstruct(final Type type, final CssLocation location)
{
this.location = checkNotNull(location);
this.type = checkNotNull(type);
}
public final CssLocation getLocation()
{
return location;
}
public final Type getType()
{
return type;
}
public abstract String toCssString();
public enum Type
{
//atomics
STRING,
KEYWORD,
COMBINATOR, //space, plus, gt, tilde
ATTRIBUTE_MATCH, // "~=", "|=", "^=","$=" "*="
HASHNAME, //#ident
CLASSNAME, //.ident
QUANTITY,
URANGE,
URI,
SYMBOL, //a single char, eg operators
TYPE_SELECTOR, //ns|E, *|E, |E, E, *
PSEUDO, //element and class
//composed
ATRULE,
FUNCTION,
DECLARATION,
SELECTOR,
SIMPLE_SELECTOR_SEQ,
ATTRIBUTE_SELECTOR, //[...]
SCOPEDGROUP, //(...) and [...], the latter when not an attr selector segment
}
}
/*********************************************
*
* atomics
*
********************************************/
/**
* A CssConstruct that is composed by a single token.
*/
static abstract class CssAtomicConstruct extends CssConstruct
{
final String value;
public CssAtomicConstruct(final Type type, final String value, final CssLocation location)
{
super(type, location);
this.value = checkNotNull(value);
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this).addValue(value).toString();
}
@Override
public String toCssString()
{
return value;
}
}
/**
* A CSS string. The returned value does not include start and end quotes.
*/
public final static class CssString extends CssAtomicConstruct
{
public CssString(final String value, final CssLocation location)
{
super(Type.STRING, value, location);
}
@Override
public final String toCssString()
{
return "'" + value + "'";
}
}
/**
* A CSS keyword.
*/
public final static class CssKeyword extends CssAtomicConstruct
{
public CssKeyword(final String value, final CssLocation location)
{
super(Type.KEYWORD, value, location);
}
}
/**
* A CSS hash name
*/
public final static class CssHashName extends CssAtomicConstruct
{
public CssHashName(final String value, final CssLocation location)
{
super(Type.HASHNAME, value, location);
}
}
/**
* A CSS class name
*/
public final static class CssClassName extends CssAtomicConstruct
{
public CssClassName(final String value, final CssLocation location)
{
super(Type.CLASSNAME, value, location);
}
}
/**
* A CSS unicode range
*/
public final static class CssUnicodeRange extends CssAtomicConstruct
{
public CssUnicodeRange(final String value, final CssLocation location)
{
super(Type.URANGE, value, location);
}
}
/**
* A CSS URI.
*/
public final static class CssURI extends CssAtomicConstruct
{
public CssURI(final String value, final CssLocation location)
{
super(Type.URI, value, location);
}
/**
* The URI string itself (leading/trailing whitespace+quotes stripped, url function removed)
*/
public String toUriString()
{
StringBuilder builder = new StringBuilder();
boolean inStart = false;
for (int i = 0; i < value.length(); i++)
{
if (i > 3 && i < value.length() - 1)
{
char ch = value.charAt(i);
if (CssScanner.QUOTES.matches(ch)
|| CssScanner.WHITESPACE.matches(ch))
{
if (inStart)
{
builder.append(ch);
}
}
else
{
inStart = true;
builder.append(ch);
}
}
}
while (true)
{
if (builder.length() == 0)
{
break;
}
int index = builder.length() - 1;
char ch = builder.charAt(index);
if (CssScanner.QUOTES.matches(ch)
|| CssScanner.WHITESPACE.matches(ch))
{
builder.deleteCharAt(index);
}
else
{
break;
}
}
return builder.toString();
}
}
/**
* A CSS single-character symbol (e.g. operator) that is not
* in the range of IDENT/KEYWORD and not '{', '}' or ';'. The value
* is interned.
*/
public final static class CssSymbol extends CssAtomicConstruct
{
public CssSymbol(final String value, final CssLocation location)
{
super(Type.SYMBOL, value.intern(), location);
checkArgument(value.length() == 1);
}
}
/**
* A CSS selector combinator (space, plus, greater or tilde). The value is interned.
*/
public final static class CssSelectorCombinator extends CssAtomicConstruct
{
final CssSelectorCombinator.Type subType;
public CssSelectorCombinator(final char value, final CssLocation location)
{
super(CssConstruct.Type.COMBINATOR, String.valueOf(value).intern(), location);
switch (value)
{
case ' ':
this.subType = Type.DESCENDANT;
break;
case '>':
this.subType = Type.CHILD;
break;
case '+':
this.subType = Type.ADJACENT_SIBLING;
break;
case '~':
this.subType = Type.GENERAL_SIBLING;
break;
default:
throw new IllegalStateException();
}
}
public final CssSelectorCombinator.Type getCombinatorType()
{
return subType;
}
public enum Type
{
DESCENDANT, //S
CHILD, //>
ADJACENT_SIBLING, //+
GENERAL_SIBLING //~
}
}
/**
* An attribute match selector ('=', '~=', '|=', '^=', '$=', '*=')
*/
public final static class CssAttributeMatchSelector extends CssAtomicConstruct
{
final CssAttributeMatchSelector.Type subType;
public CssAttributeMatchSelector(final String value,
final CssAttributeMatchSelector.Type type, final CssLocation location)
{
super(CssConstruct.Type.ATTRIBUTE_MATCH, value, location);
this.subType = type;
}
public final CssAttributeMatchSelector.Type getAttributeMatchType()
{
return subType;
}
public enum Type
{
EQUALS, //"="
INCLUDES, //"~="
DASHMATCH, //"|="
PREFIXMATCH, //"^="
SUFFIXMATCH, //"$="
SUBSTRINGMATCH //"*="
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this).addValue(subType.name()).addValue(value).toString();
}
}
/**
* A type or universal ('*') selector, possibly with namespace bindings.
*/
public static class CssTypeSelector extends CssAtomicConstruct
{
public CssTypeSelector(final String value, final CssLocation location)
{
super(Type.TYPE_SELECTOR, value, location);
}
}
/**
* A CSS quantity.
*/
public final static class CssQuantity extends CssAtomicConstruct
{
final CssQuantity.Unit subType;
public CssQuantity(final String value, final CssQuantity.Unit subtype, final CssLocation location)
{
super(CssConstruct.Type.QUANTITY, value, location);
this.subType = checkNotNull(subtype);
}
public final CssQuantity.Unit getUnit()
{
return subType;
}
public enum Unit
{
DIMEN,
PERCENTAGE,
LENGTH,
EMS,
EXS,
ANGLE,
TIME,
FREQ,
RESOLUTION,
NUMBER,
INTEGER,
REMS
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this).addValue(subType.name()).addValue(value).toString();
}
}
/*********************************************
*
* composed
*
********************************************/
/**
* A CssConstruct that is composed a list of atomic and/or composed CssConstructs,
* and optionally a name.
*/
static abstract class CssComposedConstruct extends CssConstruct
{
final List components;
final Optional name;
public CssComposedConstruct(final Type type, final String name, final CssLocation location)
{
super(type, location);
this.components = Lists.newArrayList();
this.name = name != null ? Optional.of(name) : absent;
}
public CssComposedConstruct(final Type type, final CssLocation location)
{
this(type, null, location);
}
/**
* Get the components. The list may be empty but is never null.
*/
public List getComponents()
{
return components;
}
public Optional getName()
{
return name;
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.addValue(type)
.addValue(name.isPresent() ? name.get() : "")
.addValue(components.isEmpty() ? "" : Joiner.on(" ").join(components))
.toString();
}
final Optional absent = Optional.absent();
}
/**
* An attribute selector ([name] or [name, match, ident/string] )
*/
public final static class CssAttributeSelector extends CssComposedConstruct
{
public CssAttributeSelector(final CssLocation location)
{
super(Type.ATTRIBUTE_SELECTOR, location);
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder().append('[');
for (CssConstruct cc : components)
{
sb.append(cc.toCssString());
}
return sb.append(']').toString();
}
}
/**
* A CSS function. The function name is interned, and ASCII characters in the name
* are guaranteed to be lower case.
*/
public final static class CssFunction extends CssComposedConstruct
{
public CssFunction(final String name, final CssLocation location)
{
super(Type.FUNCTION, Ascii.toLowerCase(checkNotNull(name)).intern(), location);
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder().append(name.get()).append('(');
for (CssConstruct cc : components)
{
sb.append(cc.toCssString());
}
return sb.append(')').toString();
}
}
/**
* A CSS at-rule. The at-rule name is interned, and ASCII characters in the name
* are guaranteed to be lowercase.
*/
public final static class CssAtRule extends CssComposedConstruct
{
boolean hasBlock;
public CssAtRule(final String name, final CssLocation location)
{
super(Type.ATRULE, checkNotNull(
Ascii.toLowerCase(name).intern()), location);
}
public final boolean hasBlock()
{
return hasBlock;
}
@Override
public String toCssString()
{
// Note that semi or braceblock is not included in the return
StringBuilder sb = new StringBuilder().append(name.get()).append(' ');
for (CssConstruct cc : components)
{
sb.append(cc.toCssString()).append(' '); //TODO get smarter re space, joint helper function
}
return sb.deleteCharAt(sb.length() - 1).toString();
}
}
/**
* A CSS declaration. The property name is interned, and ASCII characters in the name
* are guaranteed to be lowercase.
*/
public final static class CssDeclaration extends CssComposedConstruct
{
boolean important = false;
public CssDeclaration(final String name, final CssLocation location)
{
super(Type.DECLARATION, Ascii.toLowerCase(name).intern(), location);
}
/**
* Get the state of the important flag
*/
public final boolean getImportant()
{
return important;
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder().append(name.get()).append(" : ");
for (CssConstruct cc : components)
{
sb.append(cc.toCssString()).append(' '); //TODO get smarter re space, joint helper function
}
if (getImportant())
{
sb.append("!important ");
}
return sb.deleteCharAt(sb.length() - 1).append(" ;").toString();
}
}
/**
* A scoped group, aka (...) and [...].
*/
public final static class CssScopedGroup extends CssComposedConstruct
{
final CssScopedGroup.Type subType;
public CssScopedGroup(final CssScopedGroup.Type type, final CssLocation location)
{
super(CssConstruct.Type.SCOPEDGROUP, location);
this.subType = type;
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.addValue(type)
.addValue(subType)
.addValue(Joiner.on(" ").join(components))
.toString();
}
public final CssScopedGroup.Type getGroupType()
{
return subType;
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder();
sb.append(subType == Type.PAREN ? '(' : '[');
for (CssConstruct cc : components)
{
sb.append(cc.toCssString()).append(' '); //TODO get smarter re space, joint helper function
}
sb.deleteCharAt(sb.length() - 1);
sb.append(subType == Type.PAREN ? ')' : ']');
return sb.toString();
}
public enum Type
{
PAREN,
BRACKET
}
}
/**
* A CSS pseudo selector (pseudo-element and pseudo-class).
*/
public final static class CssPseudoSelector extends CssComposedConstruct
{
final CssPseudoSelector.Type subType;
CssFunction function = null; //may remain null
String name = null; //may remain null
public CssPseudoSelector(final CssPseudoSelector.Type type, final CssLocation location)
{
super(CssConstruct.Type.PSEUDO, location);
this.subType = type;
}
/*
* If this pseudo selector is a functional_pseudo,
* return the function name, else :(:)ident.
*/
@Override
public final Optional getName()
{
if (function != null)
{
return function.getName();
}
return Optional.of(name);
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.addValue(getSubType().name())
.addValue(getName().get())
.addValue(function != null ? Joiner.on(" ").join(function.components) : "")
.toString();
}
/**
* If this pseudo selector is a functional_pseudo,
* return it as a function, else return null.
*/
public final CssFunction getFunction()
{
return function;
}
public final CssPseudoSelector.Type getSubType()
{
return subType;
}
@Override
public String toCssString()
{
if (function != null)
{
return function.toCssString();
}
return name;
}
public enum Type
{
PSEUDO_ELEMENT,
PSEUDO_CLASS,
}
}
/**
* A CSS selector
*/
public final static class CssSelector extends CssComposedConstruct
{
public CssSelector(final CssLocation location)
{
super(Type.SELECTOR, location);
}
/**
* Get the list of selector constructs, consisting of at least one
* CssSimpleSelectorSequence, possibly followed by (CssSelectorCombinator,
* CssSimpleSelectorSequence)*
*/
public final List getComponents()
{
return super.getComponents();
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder();
for (CssConstruct cc : components)
{
sb.append(cc.toCssString());
}
return sb.toString();
}
}
/**
* A CSS simple selector sequence
* A sequence of simple selectors is a chain of simple selectors that are not
* separated by a combinator. It always begins with a type selector or a universal
* selector. No other type selector or universal selector is allowed in the sequence.
* A simple selector is either a type selector, universal selector, attribute selector,
* class selector, ID selector, or pseudo-class.
*/
public final static class CssSimpleSelectorSequence extends CssComposedConstruct
{
public CssSimpleSelectorSequence(final CssLocation location)
{
super(Type.SIMPLE_SELECTOR_SEQ, location);
}
@Override
public String toCssString()
{
StringBuilder sb = new StringBuilder();
for (CssConstruct cc : components)
{
sb.append(cc.toCssString());
}
return sb.toString();
}
}
/**
* ******************************************
*
* helpers
*
* ******************************************
*/
static final class CssSelectorConstructFactory
{
private final Locale locale;
private final Messages messages;
public CssSelectorConstructFactory(Locale locale) {
this.locale = locale;
this.messages = Messages.getInstance(locale, CssGrammar.class);
}
/**
* Create a simple selector sequence. If creation fails,
* errors are issued, and null is returned.
*
* @throws CssException
*/
public CssSimpleSelectorSequence createSimpleSelectorSequence(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) throws
CssException
{
CssSimpleSelectorSequence seq = new CssSimpleSelectorSequence(start.location);
CssConstruct seqItem = createSimpleSelector(start, iter, err);
if (seqItem == null)
{
//errors already issued
return null;
}
seq.components.add(seqItem);
CssToken next = iter.peek(FILTER_NONE);
while (next.type != CssToken.Type.S
&& !MATCH_COMMA.apply(next)
&& !MATCH_OPENBRACE.apply(next)
&& !MATCH_COMBINATOR_CHAR.apply(next))
{
seqItem = createSimpleSelector(iter.next(FILTER_NONE), iter, err);
if (seqItem == null)
{
//errors already issued
return null;
}
seq.components.add(seqItem);
next = iter.peek(FILTER_NONE);
}
return seq;
}
/**
* 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
CssException
{
// type and universal selector; ns|E, *|E, |E, E
if (start.type == CssToken.Type.IDENT || MATCH_STAR_PIPE.apply(start))
{
//note, we bundle universal selector in CssTypeSelector
return createTypeSelector(start, iter, err);
// hashname #{name}
}
else if (start.type == CssToken.Type.HASHNAME)
{
return new CssHashName(start.getChars(), start.location);
// classname .{name}
}
else if (start.type == CssToken.Type.CLASSNAME)
{
return new CssClassName(start.getChars(), start.location);
//attribute selector [...]
}
else if (MATCH_OPENSQUAREBRACKET.apply(start))
{
return createAttributeSelector(iter.next(), iter, err);
//pseudo selector
}
else if (MATCH_COLON.apply(start))
{
return createPseudoSelector(start, iter, err);
//keyframes percentage
} else if(start.type == CssToken.Type.QNTY_PERCENTAGE) {
//note, for now, "from" and "to" keywords become type selectors above,
//this handles only the percentage TODO FIX
CssSelector sel = new CssSelector(start.location);
sel.components.add(new CssQuantity(start.chars, CssQuantity.Unit.PERCENTAGE, start.location));
return sel;
}
else
{
err.error(new CssGrammarException(GRAMMAR_UNEXPECTED_TOKEN, start.location, locale, start.chars));
return null;
}
}
/**
* Create a combinator. Note that this method does not support the S combinator.
* This method also returns null without issuing errors
*/
CssSelectorCombinator createCombinator(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err)
{
char symbol;
if (start.type == CssToken.Type.CHAR)
{
char ch = start.getChar();
if (ch == '>')
{
symbol = ch;
}
else if (ch == '+')
{
symbol = ch;
}
else if (ch == '~')
{
symbol = ch;
}
else
{
return null;
}
}
else
{
return null;
}
return new CssSelectorCombinator(symbol, start.location);
}
CssPseudoSelector createPseudoSelector(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) throws
CssException
{
CssPseudoSelector.Type type;
CssPseudoSelector cps;
StringBuilder sb = new StringBuilder();
sb.append(start.getChars());
CssToken next = iter.next(FILTER_NONE);
if (MATCH_COLON.apply(next))
{
type = CssPseudoSelector.Type.PSEUDO_ELEMENT;
sb.append(next.getChars());
next = iter.next(FILTER_NONE);
}
else
{
type = CssPseudoSelector.Type.PSEUDO_CLASS;
}
cps = new CssPseudoSelector(type, start.location);
if (next.type == CssToken.Type.IDENT)
{
sb.append(next.getChars());
cps.name = sb.toString();
}
else if (next.type == CssToken.Type.FUNCTION)
{
//need to get the colons into the name so clone and mod the token
CssToken tk = new CssToken(next.type, next.location,
sb.toString() + next.chars, next.errors.isPresent() ? next.errors.get() : null);
//general functional pseudos and negation pseudos have different contentmodels
//functional: [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
//pseudo: type_selector | universal | HASH | class | attrib | pseudo
CssConstruct func;
if (Ascii.toLowerCase(next.getChars()).startsWith("not"))
{
func = createNegationPseudo(tk, iter, err);
}
else
{
func = createFunctionalPseudo(tk, iter, MATCH_OPENBRACE, err);
}
if (func == null)
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_UNEXPECTED_TOKEN, iter.last.location, locale, iter.last.chars,
next.getChars()));
return null;
}
cps.function = (CssFunction) func;
}
return cps;
}
CssConstruct createFunctionalPseudo(final CssToken start,
final CssTokenIterator iter, final Predicate limit,
final CssErrorHandler err)
{
String name = start.getChars().substring(0, start.getChars().length() - 1);
CssFunction function = new CssFunction(name, start.location);
CssToken tk = iter.next();
while (!MATCH_CLOSEPAREN.apply(tk))
{
if (limit.apply(tk))
{
return null;
}
CssConstruct cc = CssConstructFactory.create(tk, iter, limit, ContextRestrictions.PSEUDO_FUNCTIONAL);
if (cc == null)
{
return null;
}
else
{
function.components.add(cc);
}
tk = iter.next();
}
return function;
}
CssConstruct createNegationPseudo(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) 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))
{
return null;
}
else
{
negation.components.add(cc);
iter.next();
}
return negation;
}
CssAttributeSelector createAttributeSelector(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) throws
CssException
{
CssAttributeSelector cas = new CssAttributeSelector(start.location);
CssTypeSelector cts = createTypeSelector(start, iter, err);
if (cts == null)
{
//factory method has issued errors
return null;
}
cas.components.add(cts);
CssToken next = iter.next(); // ']' or string matcher
if (!MATCH_CLOSESQUAREBRACKET.apply(next))
{
if (MATCH_ATTRIBUTE_SELECTOR_MATCHERS.apply(next))
{
CssAttributeMatchSelector casm = createAttributeMatchSelector(next, iter, err);
cas.components.add(casm);
next = iter.next();
CssConstruct val = CssConstructFactory.create(next, iter, MATCH_CLOSESQUAREBRACKET,
ContextRestrictions.ATTRIBUTE_SELECTOR_VALUE);
if (val != null)
{
cas.components.add(val);
}
else
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_EXPECTING_TOKEN, next.location, locale, next.chars,
messages.get("a_string_or_dentifier")));
return null;
}
iter.next(); // ']'
}
else
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_EXPECTING_TOKEN, next.location, locale, next.chars,
messages.get("an_attribute_value_matcher")));
return null;
}
}
return cas;
}
CssAttributeMatchSelector createAttributeMatchSelector(final CssToken tk,
final CssTokenIterator iter, final CssErrorHandler err)
{
CssAttributeMatchSelector.Type type;
switch (tk.type)
{
case INCLUDES:
type = CssAttributeMatchSelector.Type.INCLUDES;
break;
case DASHMATCH:
type = CssAttributeMatchSelector.Type.DASHMATCH;
break;
case PREFIXMATCH:
type = CssAttributeMatchSelector.Type.PREFIXMATCH;
break;
case SUFFIXMATCH:
type = CssAttributeMatchSelector.Type.SUFFIXMATCH;
break;
case SUBSTRINGMATCH:
type = CssAttributeMatchSelector.Type.SUBSTRINGMATCH;
break;
default:
type = CssAttributeMatchSelector.Type.EQUALS;
break;
}
return new CssAttributeMatchSelector(tk.getChars(), type, tk.location);
}
CssTypeSelector createTypeSelector(final CssToken start,
final CssTokenIterator iter, final CssErrorHandler err) throws
CssException
{
if (start.type != CssToken.Type.IDENT && !MATCH_STAR_PIPE.apply(start))
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_EXPECTING_TOKEN, start.location, locale,
start.getChars(), messages.get("a_type_or_universal_selector")));
return null;
}
StringBuilder sb = new StringBuilder();
sb.append(start.getChars());
if (MATCH_PIPE.apply(start))
{
//|E
CssToken next = iter.peek(FILTER_NONE);
if (next.type != CssToken.Type.IDENT)
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_EXPECTING_TOKEN, next.location, locale,
next.getChars(), messages.get("a_type_or_universal_selector")));
return null;
}
else
{
sb.append(iter.next().getChars());
}
}
else if (MATCH_PIPE.apply(iter.peek(FILTER_NONE)))
{
//ns|E, *|E,
sb.append(iter.next().getChars());
CssToken next = iter.next(FILTER_NONE);
if (next.type != CssToken.Type.IDENT && !MATCH_STAR.apply(next))
{
err.error(new CssGrammarException(
CssErrorCode.GRAMMAR_EXPECTING_TOKEN, start.location, locale,
next.getChars(), messages.get("a_type_or_universal_selector")));
return null;
}
else
{
sb.append(next.getChars());
}
}
else if (iter.peek(FILTER_NONE).type == CssToken.Type.IDENT)
{
sb.append(iter.next().getChars());
}
return new CssTypeSelector(sb.toString(), start.location);
}
}
static final class CssConstructFactory
{
static CssConstruct create(final CssToken start, final CssTokenIterator iter,
final Predicate limit, final Predicate permitted)
{
CssTokenTransform transform = transformerMappings.get(start.type);
return transform == null ? null : transform.build(start, iter, limit, permitted);
}
static final CssTokenTransform BUILDER_FUNCTION = new CssTokenTransform()
{
public CssFunction build(CssToken start, CssTokenIterator iter,
Predicate limit, Predicate permitted)
{
String name = start.getChars().substring(0, start.getChars().length() - 1);
CssFunction function = new CssFunction(name, start.location);
CssToken tk = iter.next();
while (!MATCH_CLOSEPAREN.apply(tk))
{
if (limit.apply(tk))
{
return null;
}
CssConstruct cc = CssConstructFactory.create(tk, iter, limit,
ContextRestrictions.FUNCTION);
if (cc == null || !permitted.apply(cc))
{
return null;
}
else
{
function.components.add(cc);
}
tk = iter.next();
}
return function;
}
};
private static final CssTokenTransform BUILDER_ATOMIC = new CssTokenTransform()
{
public CssConstruct build(final CssToken start, final CssTokenIterator iter,
final Predicate limit, final Predicate permitted)
{
CssConstruct.Type type = genericTypeMappings.get(start.type);
CssConstruct cc;
switch (type)
{
case KEYWORD:
cc = new CssKeyword(start.getChars(), start.location);
break;
case URI:
cc = new CssURI(start.getChars(), start.location);
break;
case STRING:
cc = new CssString(start.getChars(), start.location);
break;
case URANGE:
cc = new CssUnicodeRange(start.getChars(), start.location);
break;
case HASHNAME:
cc = new CssHashName(start.getChars(), start.location);
break;
case CLASSNAME:
cc = new CssClassName(start.getChars(), start.location);
break;
default:
throw new IllegalStateException("CssTokenTransform BUILDER_ATOMIC");
}
return permitted.apply(cc) ? cc : null;
}
};
private static final CssTokenTransform BUILDER_CHAR = new CssTokenTransform()
{
public CssConstruct build(CssToken start, CssTokenIterator iter,
Predicate limit, Predicate permitted)
{
CssConstruct ret;
char chr = start.getChar();
if (chr == '{' || chr == '}' || chr == ';')
{
return null;
}
else if (chr == '(' || chr == '[')
{
ret = BUILDER_SCOPEDGROUP.build(start, iter, limit, permitted);
}
else
{
ret = new CssSymbol(start.chars, start.location);
}
if (ret == null)
{
return null;
}
return permitted.apply(ret) ? ret : null;
}
};
/**
* Builder for general scoped groups aka (...) and [...]
*/
static final CssTokenTransform BUILDER_SCOPEDGROUP = new CssTokenTransform()
{
public CssConstruct build(CssToken start, CssTokenIterator iter,
Predicate limit, Predicate permitted)
{
CssScopedGroup.Type type;
Predicate end;
if (MATCH_OPENPAREN.apply(start))
{
type = CssScopedGroup.Type.PAREN;
end = MATCH_CLOSEPAREN;
}
else if (MATCH_OPENSQUAREBRACKET.apply(start))
{
type = CssScopedGroup.Type.BRACKET;
end = MATCH_CLOSESQUAREBRACKET;
}
else
{
throw new IllegalStateException();
}
CssScopedGroup group = new CssScopedGroup(type, start.location);
CssToken tk = iter.next();
while (!end.apply(tk))
{
if (limit.apply(tk))
{
return null;
}
CssConstruct cc = CssConstructFactory.create(tk, iter, limit,
ContextRestrictions.FUNCTION);
if (cc == null || !permitted.apply(cc))
{
return null;
}
else
{
group.components.add(cc);
}
tk = iter.next();
}
return group;
}
};
private static final CssTokenTransform BUILDER_QNTY = new CssTokenTransform()
{
public CssConstruct build(final CssToken start, final CssTokenIterator iter,
final Predicate limit, final Predicate permitted)
{
CssQuantity cq = new CssQuantity(start.chars, quantityMappings.get(start.type),
start.location);
return permitted.apply(cq) ? cq : null;
}
};
private static final Map transformerMappings
= new ImmutableMap.Builder()
.put(CssToken.Type.FUNCTION, BUILDER_FUNCTION)
.put(CssToken.Type.CHAR, BUILDER_CHAR)
.put(CssToken.Type.IDENT, BUILDER_ATOMIC)
.put(CssToken.Type.URI, BUILDER_ATOMIC)
.put(CssToken.Type.STRING, BUILDER_ATOMIC)
.put(CssToken.Type.AND, BUILDER_ATOMIC)
.put(CssToken.Type.NOT, BUILDER_ATOMIC)
.put(CssToken.Type.ONLY, BUILDER_ATOMIC)
.put(CssToken.Type.URANGE, BUILDER_ATOMIC)
.put(CssToken.Type.HASHNAME, BUILDER_ATOMIC)
.put(CssToken.Type.CLASSNAME, BUILDER_ATOMIC)
.put(CssToken.Type.QNTY_ANGLE, BUILDER_QNTY)
.put(CssToken.Type.QNTY_DIMEN, BUILDER_QNTY)
.put(CssToken.Type.QNTY_REMS, BUILDER_QNTY)
.put(CssToken.Type.QNTY_EMS, BUILDER_QNTY)
.put(CssToken.Type.QNTY_EXS, BUILDER_QNTY)
.put(CssToken.Type.QNTY_FREQ, BUILDER_QNTY)
.put(CssToken.Type.QNTY_LENGTH, BUILDER_QNTY)
.put(CssToken.Type.QNTY_PERCENTAGE, BUILDER_QNTY)
.put(CssToken.Type.QNTY_RESOLUTION, BUILDER_QNTY)
.put(CssToken.Type.QNTY_TIME, BUILDER_QNTY)
.put(CssToken.Type.NUMBER, BUILDER_QNTY)
.put(CssToken.Type.INTEGER, BUILDER_QNTY)
.build();
/* Map used by BUILDER_GENERIC to get type */
private static final Map genericTypeMappings
= new ImmutableMap.Builder()
.put(CssToken.Type.IDENT, CssConstruct.Type.KEYWORD)
.put(CssToken.Type.URI, CssConstruct.Type.URI)
.put(CssToken.Type.STRING, CssConstruct.Type.STRING)
.put(CssToken.Type.AND, CssConstruct.Type.KEYWORD)
.put(CssToken.Type.NOT, CssConstruct.Type.KEYWORD)
.put(CssToken.Type.ONLY, CssConstruct.Type.KEYWORD)
.put(CssToken.Type.URANGE, CssConstruct.Type.URANGE)
.put(CssToken.Type.HASHNAME, CssConstruct.Type.HASHNAME)
.put(CssToken.Type.CLASSNAME, CssConstruct.Type.CLASSNAME)
.build();
/* Map used by BUILDER_QNTY to get subtype */
private static final Map quantityMappings
= new ImmutableMap.Builder()
.put(CssToken.Type.QNTY_ANGLE, CssQuantity.Unit.ANGLE)
.put(CssToken.Type.QNTY_DIMEN, CssQuantity.Unit.DIMEN)
.put(CssToken.Type.QNTY_REMS, CssQuantity.Unit.REMS)
.put(CssToken.Type.QNTY_EMS, CssQuantity.Unit.EMS)
.put(CssToken.Type.QNTY_EXS, CssQuantity.Unit.EXS)
.put(CssToken.Type.QNTY_FREQ, CssQuantity.Unit.FREQ)
.put(CssToken.Type.QNTY_LENGTH, CssQuantity.Unit.LENGTH)
.put(CssToken.Type.QNTY_PERCENTAGE, CssQuantity.Unit.PERCENTAGE)
.put(CssToken.Type.QNTY_RESOLUTION, CssQuantity.Unit.RESOLUTION)
.put(CssToken.Type.QNTY_TIME, CssQuantity.Unit.TIME)
.put(CssToken.Type.NUMBER, CssQuantity.Unit.NUMBER)
.put(CssToken.Type.INTEGER, CssQuantity.Unit.INTEGER)
.build();
interface CssTokenTransform
{
CssConstruct build(CssToken start, CssTokenIterator iter, Predicate limit,
Predicate permitted);
}
}
}