       1 /*
       2  * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
       3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       4  *
       5  * This code is free software; you can redistribute it and/or modify it
       6  * under the terms of the GNU General Public License version 2 only, as
       7  * published by the Free Software Foundation.  Oracle designates this
       8  * particular file as subject to the "Classpath" exception as provided
       9  * by Oracle in the LICENSE file that accompanied this code.
      10  *
      11  * This code is distributed in the hope that it will be useful, but WITHOUT
      12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
      13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
      14  * version 2 for more details (a copy is included in the LICENSE file that
      15  * accompanied this code).
      16  *
      17  * You should have received a copy of the GNU General Public License version
      18  * 2 along with this work; if not, write to the Free Software Foundation,
      19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
      20  *
      21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
      22  * or visit www.oracle.com if you need additional information or have any
      23  * questions.
      24  */
      25
      26 package com.sun.javafx.css.parser;
      27
      28 import java.io.BufferedReader;
      29 import java.io.CharArrayReader;
      30 import java.io.IOException;
      31 import java.io.InputStreamReader;
      32 import java.io.Reader;
      33 import java.net.URI;
      34 import java.net.URISyntaxException;
      35 import java.net.URL;
      36 import java.text.MessageFormat;
      37 import java.util.ArrayList;
      38 import java.util.Collections;
      39 import java.util.HashMap;
      40 import java.util.List;
      41 import java.util.Locale;
      42 import java.util.Map;
      43 import javafx.geometry.Insets;
      44 import javafx.scene.effect.BlurType;
      45 import javafx.scene.effect.Effect;
      46 import javafx.scene.layout.BackgroundPosition;
      47 import javafx.scene.layout.BackgroundRepeat;
      48 import javafx.scene.layout.BackgroundSize;
      49 import javafx.scene.layout.BorderStrokeStyle;
      50 import javafx.scene.layout.BorderWidths;
      51 import javafx.scene.paint.Color;
      52 import javafx.scene.paint.CycleMethod;
      53 import javafx.scene.paint.Paint;
      54 import javafx.scene.paint.Stop;
      55 import javafx.scene.shape.StrokeLineCap;
      56 import javafx.scene.shape.StrokeLineJoin;
      57 import javafx.scene.shape.StrokeType;
      58 import javafx.scene.text.Font;
      59 import javafx.scene.text.FontPosture;
      60 import javafx.scene.text.FontWeight;
      61 import com.sun.javafx.Utils;
      62 import com.sun.javafx.css.Combinator;
      63 import com.sun.javafx.css.CompoundSelector;
      64 import com.sun.javafx.css.CssError;
      65 import com.sun.javafx.css.Declaration;
      66 import com.sun.javafx.css.FontFace;
      67 import javafx.css.ParsedValue;
      68 import com.sun.javafx.css.ParsedValueImpl;
      69 import com.sun.javafx.css.Rule;
      70 import com.sun.javafx.css.Selector;
      71 import com.sun.javafx.css.SimpleSelector;
      72 import com.sun.javafx.css.Size;
      73 import com.sun.javafx.css.SizeUnits;
      74 import com.sun.javafx.css.StyleManager;
      75 import javafx.css.Styleable;
      76 import com.sun.javafx.css.Stylesheet;
      77 import com.sun.javafx.css.converters.BooleanConverter;
      78 import com.sun.javafx.css.converters.EffectConverter;
      79 import com.sun.javafx.css.converters.EnumConverter;
      80 import com.sun.javafx.css.converters.FontConverter;
      81 import com.sun.javafx.css.converters.InsetsConverter;
      82 import com.sun.javafx.css.converters.PaintConverter;
      83 import com.sun.javafx.css.converters.SizeConverter;
      84 import com.sun.javafx.css.converters.SizeConverter.SequenceConverter;
      85 import com.sun.javafx.css.converters.StringConverter;
      86 import com.sun.javafx.css.converters.URLConverter;
      87 import sun.util.logging.PlatformLogger;
      88 import sun.util.logging.PlatformLogger.Level;
      89 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
      90 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
      91 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
      92 import com.sun.javafx.scene.layout.region.BorderImageSlices;
      93 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
      94 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
      95 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
      96 import com.sun.javafx.scene.layout.region.BorderStyleConverter;
      97 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
      98 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
      99 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
     100 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
     101 import com.sun.javafx.scene.layout.region.Margins;
     102 import com.sun.javafx.scene.layout.region.RepeatStruct;
     103 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
     104 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
     105 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
     106 import java.net.MalformedURLException;
     107
     108 final public class CSSParser {
     109
     110     /* Lazy instantiation */
     111     private static class InstanceHolder {
     112         final static CSSParser INSTANCE = new CSSParser();
     113     }
     114
     115     public static CSSParser getInstance() {
     116         return InstanceHolder.INSTANCE;
     117     }
     118
     119     private CSSParser() {
     120         properties = new HashMap<String,String>();
     121     }
     122
     123     // stylesheet as a string from parse method. This will be null if the
     124     // stylesheet is being parsed from a file; otherwise, the parser is parsing
     125     // a string and this is that string.
     126     private String     stylesheetAsText;
     127
     128     // the url of the stylesheet file, or the docbase of an applet. This will
     129     // be null if the source is not a file or from an applet.
     130     private String        sourceOfStylesheet;
     131
     132     // the Styleable from the node with an in-line style. This will be null
     133     // unless the source of the styles is a Node's styleProperty. In this case,
     134     // the stylesheetString will also be set.
     135     private Styleable  sourceOfInlineStyle;
     136
     137     // source is a file
     138 //    private void setInputSource(String url) {
     139 //        setInputSource(url, null);
     140 //    }
     141
     142     // source is a file
     143     private void setInputSource(String url, String str) {
     144         stylesheetAsText = str;
     145         sourceOfStylesheet = url;
     146         sourceOfInlineStyle = null;
     147     }
     148
     149     // source as string only
     150     private void setInputSource(String str) {
     151         stylesheetAsText = str;
     152         sourceOfStylesheet = null;
     153         sourceOfInlineStyle = null;
     154     }
     155
     156     // source is in-line style
     157     private void setInputSource(Styleable styleable) {
     158         stylesheetAsText = styleable != null ? styleable.getStyle() : null;
     159         sourceOfStylesheet = null;
     160         sourceOfInlineStyle = styleable;
     161     }
     162
     163     private static final PlatformLogger LOGGER;
     164     static {
     165         LOGGER = com.sun.javafx.Logging.getCSSLogger();
     166         final Level level = LOGGER.level();
     167         if (level == null || (
     168             level.compareTo(Level.WARNING) > 0 &&
     169             level != Level.OFF)) {
     170             LOGGER.setLevel(Level.WARNING);
     171         }
     172     }
     173
     174     private static final class ParseException extends Exception {
     175         ParseException(String message) {
     176             this(message,null,null);
     177         }
     178         ParseException(String message, Token tok, CSSParser parser) {
     179             super(message);
     180             this.tok = tok;
     181             if (parser.sourceOfStylesheet != null) {
     182                 source = parser.sourceOfStylesheet;
     183             } else if (parser.sourceOfInlineStyle != null) {
     184                 source = parser.sourceOfInlineStyle.toString();
     185             } else if (parser.stylesheetAsText != null) {
     186                 source = parser.stylesheetAsText;
     187             } else {
     188                 source = "?";
     189             }
     190         }
     191         @Override public String toString() {
     192             StringBuilder builder = new StringBuilder(super.getMessage());
     193             builder.append(source);
     194             if (tok != null) builder.append(": ").append(tok.toString());
     195             return builder.toString();
     196         }
     197         private final Token tok;
     198         private final String source;
     199     }
     200
     201     /**
     202      * Creates a stylesheet from a CSS document string.
     203      *
     204      *@param stylesheetText the CSS document to parse
     205      *@return the Stylesheet
     206      */
     207     public Stylesheet parse(final String stylesheetText) {
     208         final Stylesheet stylesheet = new Stylesheet();
     209         if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
     210             setInputSource(stylesheetText);
     211             Reader reader = new CharArrayReader(stylesheetText.toCharArray());
     212             parse(stylesheet, reader);
     213         }
     214         return stylesheet;
     215     }
     216
     217     /**
     218      * Creates a stylesheet from a CSS document string using docbase as
     219      * the base URL for resolving references within stylesheet.
     220      *
     221      *@param stylesheetText the CSS document to parse
     222      *@return the Stylesheet
     223      */
     224     public Stylesheet parse(final String docbase, final String stylesheetText) throws IOException {
     225         final Stylesheet stylesheet = new Stylesheet(docbase);
     226         if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
     227             setInputSource(docbase, stylesheetText);
     228             Reader reader = new CharArrayReader(stylesheetText.toCharArray());
     229             parse(stylesheet, reader);
     230         }
     231         return stylesheet;
     232     }
     233
     234     /**
     235      * Updates the given stylesheet by reading a CSS document from a URL,
     236      * assuming UTF-8 encoding.
     237      *
     238      *@param  url URL of the stylesheet to parse
     239      *@return the stylesheet
     240      *@throws IOException
     241      */
     242     public Stylesheet parse(final URL url) throws IOException {
     243
     244         final String path = url != null ? url.toExternalForm() : null;
     245         final Stylesheet stylesheet = new Stylesheet(path);
     246         if (url != null) {
     247             setInputSource(path);
     248             Reader reader = new BufferedReader(new InputStreamReader(url.openStream()));
     249             parse(stylesheet, reader);
     250         }
     251         return stylesheet;
     252     }
     253
     254     /* All of the other function calls should wind up here */
     255     private void parse(final Stylesheet stylesheet, final Reader reader) {
     256
     257         CSSLexer lex = CSSLexer.getInstance();
     258         lex.setReader(reader);
     259
     260         try {
     261             this.parse(stylesheet, lex);
     262             reader.close();
     263         } catch (IOException ioe) {
     264         } catch (Exception ex) {
     265             // Sometimes bad syntax causes an exception. The code should be
     266             // fixed to handle the bad syntax, but the fallback is
     267             // to handle the exception here. Uncaught, the exception can cause
     268             // problems like RT-20311
     269             reportException(ex);
     270         }
     271
     272     }
     273
     274     /** Parse an in-line style from a Node */
     275     public Stylesheet parseInlineStyle(final Styleable node) {
     276
     277         Stylesheet stylesheet = new Stylesheet();
     278
     279         final String stylesheetText = (node != null) ? node.getStyle() : null;
     280         if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
     281             setInputSource(node);
     282             final List<Rule> rules = new ArrayList<Rule>();
     283             final Reader reader = new CharArrayReader(stylesheetText.toCharArray());
     284             final CSSLexer lexer = CSSLexer.getInstance();
     285             lexer.setReader(reader);
     286             try {
     287                 currentToken = nextToken(lexer);
     288                 final List<Declaration> declarations = declarations(lexer);
     289                 if (declarations != null && !declarations.isEmpty()) {
     290                     final Selector selector = Selector.getUniversalSelector();
     291                     final Rule rule = new Rule(
     292                         Collections.singletonList(selector),
     293                         declarations
     294                     );
     295                     rules.add(rule);
     296                 }
     297                 reader.close();
     298             } catch (IOException ioe) {
     299             } catch (Exception ex) {
     300                 // Sometimes bad syntax causes an exception. The code should be
     301                 // fixed to handle the bad syntax, but the fallback is
     302                 // to handle the exception here. Uncaught, the exception can cause
     303                 // problems like RT-20311
     304                 reportException(ex);
     305             }
     306             stylesheet.getRules().addAll(rules);
     307         }
     308
     309         // don't retain reference to the styleable
     310         setInputSource((Styleable) null);
     311
     312         return stylesheet;
     313     }
     314
     315     /** convenience method for unit tests */
     316     public ParsedValueImpl parseExpr(String property, String expr) {
     317         ParsedValueImpl value = null;
     318         try {
     319             setInputSource(null, property + ": " + expr);
     320             char buf[] = new char[expr.length() + 1];
     321             System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
     322             buf[buf.length-1] = ';';
     323
     324             Reader reader = new CharArrayReader(buf);
     325             CSSLexer lex = CSSLexer.getInstance();
     326             lex.setReader(reader);
     327
     328             currentToken = nextToken(lex);
     329             CSSParser.Term term = this.expr(lex);
     330             value = valueFor(property, term);
     331
     332             reader.close();
     333         } catch (IOException ioe) {
     334         } catch (ParseException e) {
     335             if (LOGGER.isLoggable(Level.WARNING)) {
     336                 LOGGER.warning("\"" +property + ": " + expr  + "\" " + e.toString());
     337             }
     338         } catch (Exception ex) {
     339             // Sometimes bad syntax causes an exception. The code should be
     340             // fixed to handle the bad syntax, but the fallback is
     341             // to handle the exception here. Uncaught, the exception can cause
     342             // problems like RT-20311
     343             reportException(ex);
     344         }
     345         return value;
     346     }
     347     /*
     348      * Map of property names found while parsing. If a value matches a
     349      * property name, then the value is a lookup.
     350      */
     351     private final Map<String,String> properties;
     352
     353     /*
     354      * While parsing a declaration, tokens from parsing value (that is,
     355      * the expr rule) are held in this tree structure which is then passed
     356      * to methods which convert the tree into a ParsedValueImpl.
     357      *
     358      * Each term in expr is a Term. For simple terms, like HASH, the
     359      * Term is just the Token. If the term is a function, then the
     360      * Term is a linked-list of Term, the first being the function
     361      * name and each nextArg being the arguments.
     362      *
     363      * If there is more than one term in the expr (insets, for example),
     364      * then the terms are linked on nextInSequence. If there is more than one
     365      * layer (sequence of terms), then each layer becomes the nextLayer
     366      * to the last root in the previous sequence.
     367      *
     368      * The easiest way to think of it is that a comma starts a nextLayer (except
     369      * when a function arg).
     370      *
     371      * The expr part of the declaration "-fx-padding 1 2, 3 4;" would look
     372      * like this:
     373      * [1 | nextLayer | nextInSeries]-->[2 | nextLayer | nextInSeries]-->null
     374      *            |                            |
     375      *          null                           |
     376      *       .---------------------------------'
     377      *       '-->[3 | nextLayer | nextInSeries]-->[4 | nextLayer | nextInSeries]-->null
     378      *                    |                              |
     379      *                   null                           null
     380      *
     381      * The first argument in a function needs to be distinct from the
     382      * remaining args so that the args of a function in the middle of
     383      * a function will not be omitted. Consider 'f0(a, f1(b, c), d)'
     384      * If we relied only on nextArg, then the next arg of f0 would be a but
     385      * the nextArg of f1 would be d. With firstArg, the firstArg of f0 is a,
     386      * the nextArg of a is f1, the firstArg of f1 is b and the nextArg of f1 is d.
     387      *
     388      * TODO: now that the parser is the parser and not an adjunct to an ANTLR
     389      * parser, this Term stuff shouldn't be needed.
     390      */
     391     static class Term {
     392         final Token token;
     393         Term nextInSeries;
     394         Term nextLayer;
     395         Term firstArg;
     396         Term nextArg;
     397         Term(Token token) {
     398             this.token = token;
     399             this.nextLayer = null;
     400             this.nextInSeries = null;
     401             this.firstArg = null;
     402             this.nextArg = null;
     403         }
     404         Term() {
     405             this(null);
     406         }
     407
     408         @Override public String toString() {
     409             StringBuilder buf = new StringBuilder();
     410             if (token != null) buf.append(String.valueOf(token.getText()));
     411             if (nextInSeries != null) {
     412                 buf.append("<nextInSeries>");
     413                 buf.append(nextInSeries.toString());
     414                 buf.append("</nextInSeries>\n");
     415             }
     416             if (nextLayer != null) {
     417                 buf.append("<nextLayer>");
     418                 buf.append(nextLayer.toString());
     419                 buf.append("</nextLayer>\n");
     420             }
     421             if (firstArg != null) {
     422                 buf.append("<args>");
     423                 buf.append(firstArg.toString());
     424                 if (nextArg != null) {
     425                     buf.append(nextArg.toString());
     426                 }
     427                 buf.append("</args>");
     428             }
     429
     430             return buf.toString();
     431         }
     432
     433     }
     434
     435     private CssError createError(String msg) {
     436
     437         CssError error = null;
     438         if (sourceOfStylesheet != null) {
     439             error = new CssError.StylesheetParsingError(sourceOfStylesheet, msg);
     440         } else if (sourceOfInlineStyle != null) {
     441             error = new CssError.InlineStyleParsingError(sourceOfInlineStyle, msg);
     442         } else {
     443             error = new CssError.StringParsingError(stylesheetAsText, msg);
     444         }
     445         return error;
     446     }
     447
     448     private void reportError(CssError error) {
     449         List<CssError> errors = null;
     450         if ((errors = StyleManager.getErrors()) != null) {
     451             errors.add(error);
     452         }
     453     }
     454
     455     private void error(final Term root, final String msg) throws ParseException {
     456
     457         final Token token = root != null ? root.token : null;
     458         final ParseException pe = new ParseException(msg,token,this);
     459         reportError(createError(pe.toString()));
     460         throw pe;
     461     }
     462
     463     private void reportException(Exception exception) {
     464
     465         if (LOGGER.isLoggable(Level.WARNING)) {
     466             final StackTraceElement[] stea = exception.getStackTrace();
     467             if (stea.length > 0) {
     468                 final StringBuilder buf =
     469                     new StringBuilder("Please report ");
     470                 buf.append(exception.getClass().getName())
     471                    .append(" at:");
     472                 int end = 0;
     473                 while(end < stea.length) {
     474                     // only report parser part of the stack trace.
     475                     if (!getClass().getName().equals(stea[end].getClassName())) {
     476                         break;
     477                     }
     478                     buf.append("\n\t")
     479                     .append(stea[end++].toString());
     480                 }
     481                 LOGGER.warning(buf.toString());
     482             }
     483         }
     484     }
     485
     486     private String formatDeprecatedMessage(final Term root, final String syntax) {
     487         final StringBuilder buf =
     488             new StringBuilder("Using deprecated syntax for ");
     489         buf.append(syntax);
     490         if (sourceOfStylesheet != null){
     491             buf.append(" at ")
     492                .append(sourceOfStylesheet)
     493                .append("[")
     494                .append(root.token.getLine())
     495                .append(',')
     496                .append(root.token.getOffset())
     497                .append("]");
     498         }
     499         buf.append(". Refer to the CSS Reference Guide.");
     500         return buf.toString();
     501     }
     502
     503     // Assumes string is not a lookup!
     504     private ParsedValueImpl<Color,Color> colorValueOfString(String str) {
     505
     506         if(str.startsWith("#") || str.startsWith("0x")) {
     507
     508             double a = 1.0f;
     509             String c = str;
     510             final int prefixLength = (str.startsWith("#")) ? 1 : 2;
     511
     512             final int len = c.length();
     513             // rgba or rrggbbaa - trim off the alpha
     514             if ( (len-prefixLength) == 4) {
     515                 a = Integer.parseInt(c.substring(len-1), 16) / 15.0f;
     516                 c = c.substring(0,len-1);
     517             } else if ((len-prefixLength) == 8) {
     518                 a = Integer.parseInt(c.substring(len-2), 16) / 255.0f;
     519                 c = c.substring(0,len-2);
     520             }
     521             // else color was rgb or rrggbb (no alpha)
     522             return new ParsedValueImpl<Color,Color>(Color.web(c,a), null);
     523         }
     524
     525         try {
     526             return new ParsedValueImpl<Color,Color>(Color.web(str), null);
     527         } catch (final IllegalArgumentException e) {
     528         } catch (final NullPointerException e) {
     529         }
     530
     531         // not a color
     532         return null;
     533     }
     534
     535     private String stripQuotes(String string) {
     536         return com.sun.javafx.Utils.stripQuotes(string);
     537     }
     538
     539     private double clamp(double min, double val, double max) {
     540         if (val < min) return min;
     541         if (max < val) return max;
     542         return val;
     543     }
     544
     545     // Return true if the token is a size type or an identifier
     546     // (which would indicate a lookup).
     547     private boolean isSize(Token token) {
     548         final int ttype = token.getType();
     549         switch (ttype) {
     550         case CSSLexer.NUMBER:
     551         case CSSLexer.PERCENTAGE:
     552         case CSSLexer.EMS:
     553         case CSSLexer.EXS:
     554         case CSSLexer.PX:
     555         case CSSLexer.CM:
     556         case CSSLexer.MM:
     557         case CSSLexer.IN:
     558         case CSSLexer.PT:
     559         case CSSLexer.PC:
     560         case CSSLexer.DEG:
     561         case CSSLexer.GRAD:
     562         case CSSLexer.RAD:
     563         case CSSLexer.TURN:
     564             return true;
     565         default:
     566             return token.getType() == CSSLexer.IDENT;
     567         }
     568     }
     569
     570     private Size size(final Token token) throws ParseException {
     571         SizeUnits units = SizeUnits.PX;
     572         // Amount to trim off the suffix, if any. Most are 2 chars.
     573         int trim = 2;
     574         final String sval = token.getText().trim();
     575         final int len = sval.length();
     576         final int ttype = token.getType();
     577         switch (ttype) {
     578         case CSSLexer.NUMBER:
     579             units = SizeUnits.PX;
     580             trim = 0;
     581             break;
     582         case CSSLexer.PERCENTAGE:
     583             units = SizeUnits.PERCENT;
     584             trim = 1;
     585             break;
     586         case CSSLexer.EMS:
     587             units = SizeUnits.EM;
     588             break;
     589         case CSSLexer.EXS:
     590             units = SizeUnits.EX;
     591             break;
     592         case CSSLexer.PX:
     593             units = SizeUnits.PX;
     594             break;
     595         case CSSLexer.CM:
     596             units = SizeUnits.CM;
     597             break;
     598         case CSSLexer.MM:
     599             units = SizeUnits.MM;
     600             break;
     601         case CSSLexer.IN:
     602             units = SizeUnits.IN;
     603             break;
     604         case CSSLexer.PT:
     605             units = SizeUnits.PT;
     606             break;
     607         case CSSLexer.PC:
     608             units = SizeUnits.PC;
     609             break;
     610         case CSSLexer.DEG:
     611             units = SizeUnits.DEG;
     612             trim = 3;
     613             break;
     614         case CSSLexer.GRAD:
     615             units = SizeUnits.GRAD;
     616             trim = 4;
     617             break;
     618         case CSSLexer.RAD:
     619             units = SizeUnits.RAD;
     620             trim = 3;
     621             break;
     622         case CSSLexer.TURN:
     623             units = SizeUnits.TURN;
     624             trim = 5;
     625             break;
     626         default:
     627             if (LOGGER.isLoggable(Level.FINEST)) {
     628                 LOGGER.finest("Expected \'<number>\'");
     629             }
     630             ParseException re = new ParseException("Expected \'<number>\'",token, this);
     631             reportError(createError(re.toString()));
     632             throw re;
     633         }
     634         // TODO: Handle NumberFormatException
     635         return new Size(
     636             Double.parseDouble(sval.substring(0,len-trim)),
     637             units
     638         );
     639     }
     640
     641     // Count the number of terms in a series
     642     private int numberOfTerms(final Term root) {
     643         if (root == null) return 0;
     644
     645         int nTerms = 0;
     646         Term term = root;
     647         do {
     648             nTerms += 1;
     649             term = term.nextInSeries;
     650         } while (term != null);
     651         return nTerms;
     652     }
     653
     654     // Count the number of series of terms
     655     private int numberOfLayers(final Term root) {
     656         if (root == null) return 0;
     657
     658         int nLayers = 0;
     659         Term term = root;
     660         do {
     661             nLayers += 1;
     662             while (term.nextInSeries != null) {
     663                 term = term.nextInSeries;
     664             }
     665             term = term.nextLayer;
     666         } while (term != null);
     667         return nLayers;
     668     }
     669
     670     // Count the number of args of terms. root is the function.
     671     private int numberOfArgs(final Term root) {
     672         if (root == null) return 0;
     673
     674         int nArgs = 0;
     675         Term term = root.firstArg;
     676         while (term != null) {
     677             nArgs += 1;
     678             term = term.nextArg;
     679         }
     680         return nArgs;
     681     }
     682
     683     // Get the next layer following this term, which may be null
     684     private Term nextLayer(final Term root) {
     685         if (root == null) return null;
     686
     687         Term term = root;
     688         while (term.nextInSeries != null) {
     689             term = term.nextInSeries;
     690         }
     691         return term.nextLayer;
     692     }
     693
     694     ////////////////////////////////////////////////////////////////////////////
     695     //
     696     // Parsing routines
     697     //
     698     ////////////////////////////////////////////////////////////////////////////
     699
     700     ParsedValueImpl valueFor(String property, Term root) throws ParseException {
     701         final String prop = property.toLowerCase(Locale.ROOT);
     702         properties.put(prop, prop);
     703         if (root == null || root.token == null) {
     704             error(root, "Expected value for property \'" + prop + "\'");
     705         }
     706
     707         if (root.token.getType() == CSSLexer.IDENT) {
     708             if ("inherit".equalsIgnoreCase(root.token.getText())) {
     709                 return new ParsedValueImpl<String,String>("inherit", null);
     710             } else if ("null".equalsIgnoreCase(root.token.getText())) {
     711                 return new ParsedValueImpl<String,String>("null", null);
     712             }
     713         }
     714
     715         if ("-fx-background-color".equals(property)) {
     716             return parsePaintLayers(root);
     717         } else if ("-fx-background-image".equals(prop)) {
     718             return parseURILayers(root);
     719         } else if ("-fx-background-insets".equals(prop)) {
     720              return parseInsetsLayers(root);
     721         } else if ("-fx-opaque-insets".equals(prop)) {
     722             return parseInsetsLayer(root);
     723         } else if ("-fx-background-position".equals(prop)) {
     724              return parseBackgroundPositionLayers(root);
     725         } else if ("-fx-background-radius".equals(prop)) {
     726              return parseInsetsLayers(root);
     727         } else if ("-fx-background-repeat".equals(prop)) {
     728              return parseBackgroundRepeatStyleLayers(root);
     729         } else if ("-fx-background-size".equals(prop)) {
     730              return parseBackgroundSizeLayers(root);
     731         } else if ("-fx-border-color".equals(prop)) {
     732              return parseBorderPaintLayers(root);
     733         } else if ("-fx-border-insets".equals(prop)) {
     734              return parseInsetsLayers(root);
     735         } else if ("-fx-border-radius".equals(prop)) {
     736              return parseMarginsLayers(root);
     737         } else if ("-fx-border-style".equals(prop)) {
     738              return parseBorderStyleLayers(root);
     739         } else if ("-fx-border-width".equals(prop)) {
     740              return parseMarginsLayers(root);
     741         } else if ("-fx-border-image-insets".equals(prop)) {
     742              return parseInsetsLayers(root);
     743         } else if ("-fx-border-image-repeat".equals(prop)) {
     744              return parseBorderImageRepeatStyleLayers(root);
     745         } else if ("-fx-border-image-slice".equals(prop)) {
     746              return parseBorderImageSliceLayers(root);
     747         } else if ("-fx-border-image-source".equals(prop)) {
     748              return parseURILayers(root);
     749         } else if ("-fx-border-image-width".equals(prop)) {
     750              return parseBorderImageWidthLayers(root);
     751         } else if ("-fx-padding".equals(prop)) {
     752             ParsedValueImpl<?,Size>[] sides = parseSizeSeries(root);
     753             return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance());
     754         } else if ("-fx-label-padding".equals(prop)) {
     755             ParsedValueImpl<?,Size>[] sides = parseSizeSeries(root);
     756             return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance());
     757         } else if (prop.endsWith("font-family")) {
     758             return parseFontFamily(root);
     759         } else if (prop.endsWith("font-size")) {
     760             ParsedValueImpl fsize = parseFontSize(root);
     761             if (fsize == null) error(root, "Expected \'<font-size>\'");
     762             return fsize;
     763         } else if (prop.endsWith("font-style")) {
     764             ParsedValueImpl fstyle = parseFontStyle(root);
     765             if (fstyle == null) error(root, "Expected \'<font-style>\'");
     766             return fstyle;
     767         } else if (prop.endsWith("font-weight")) {
     768             ParsedValueImpl fweight = parseFontWeight(root);
     769             if (fweight == null) error(root, "Expected \'<font-style>\'");
     770             return fweight;
     771         } else if (prop.endsWith("font")) {
     772             return parseFont(root);
     773         } else if ("-fx-stroke-dash-array".equals(prop)) {
     774             // TODO: Figure out a way that these properties don't need to be
     775             // special cased.
     776             Term term = root;
     777             int nArgs = numberOfTerms(term);
     778             ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs];
     779             int segment = 0;
     780             while(term != null) {
     781                 segments[segment++] = parseSize(term);
     782                 term = term.nextInSeries;
     783             }
     784
     785             return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SequenceConverter.getInstance());
     786
     787         } else if ("-fx-stroke-line-join".equals(prop)) {
     788             // TODO: Figure out a way that these properties don't need to be
     789             // special cased.
     790             ParsedValueImpl[] values = parseStrokeLineJoin(root);
     791             if (values == null) error(root, "Expected \'miter', \'bevel\' or \'round\'");
     792             return values[0];
     793         } else if ("-fx-stroke-line-cap".equals(prop)) {
     794             // TODO: Figure out a way that these properties don't need to be
     795             // special cased.
     796             ParsedValueImpl value = parseStrokeLineCap(root);
     797             if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
     798             return value;
     799         } else if ("-fx-stroke-type".equals(prop)) {
     800             // TODO: Figure out a way that these properties don't need to be
     801             // special cased.
     802             ParsedValueImpl value = parseStrokeType(root);
     803             if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
     804             return value;
     805         } else if ("-fx-font-smoothing-type".equals(prop)) {
     806             // TODO: Figure out a way that these properties don't need to be
     807             // special cased.
     808             String str = null;
     809             int ttype = -1;
     810             final Token token = root.token;
     811
     812             if (root.token == null
     813                     || ((ttype = root.token.getType()) != CSSLexer.STRING
     814                          && ttype != CSSLexer.IDENT)
     815                     || (str = root.token.getText()) == null
     816                     || str.isEmpty()) {
     817                 error(root,  "Expected STRING or IDENT");
     818             }
     819             return new ParsedValueImpl<String, String>(stripQuotes(str), null, false);
     820         }
     821         return parse(root);
     822     }
     823
     824     private ParsedValueImpl parse(Term root) throws ParseException {
     825
     826         if (root.token == null) error(root, "Parse error");
     827         final Token token = root.token;
     828         ParsedValueImpl value = null; // value to return;
     829
     830         final int ttype = token.getType();
     831         switch (ttype) {
     832         case CSSLexer.NUMBER:
     833         case CSSLexer.PERCENTAGE:
     834         case CSSLexer.EMS:
     835         case CSSLexer.EXS:
     836         case CSSLexer.PX:
     837         case CSSLexer.CM:
     838         case CSSLexer.MM:
     839         case CSSLexer.IN:
     840         case CSSLexer.PT:
     841         case CSSLexer.PC:
     842         case CSSLexer.DEG:
     843         case CSSLexer.GRAD:
     844         case CSSLexer.RAD:
     845         case CSSLexer.TURN:
     846             ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null);
     847             value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance());
     848             break;
     849         case CSSLexer.STRING:
     850         case CSSLexer.IDENT:
     851             boolean isIdent = ttype == CSSLexer.IDENT;
     852             final String str = stripQuotes(token.getText());
     853             final String text = str.toLowerCase(Locale.ROOT);
     854             if ("ladder".equals(text)) {
     855                 value = ladder(root);
     856             } else if ("linear".equals(text)) {
     857                 value = linearGradient(root);
     858             } else if ("radial".equals(text)) {
     859                 value = radialGradient(root);
     860             } else if ("true".equals(text)) {
     861                 // TODO: handling of boolean is really bogus
     862                 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance());
     863             } else if ("false".equals(text)) {
     864                 // TODO: handling of boolean is really bogus
     865                 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance());
     866             } else {
     867                 // if the property value is another property, then it needs to be looked up.
     868                 boolean needsLookup = isIdent && properties.containsKey(str);
     869                 if (needsLookup || ((value = colorValueOfString(str)) == null )) {
     870                     value = new ParsedValueImpl<String,String>(str, null, isIdent || needsLookup);
     871                 }
     872             }
     873             break;
     874         case CSSLexer.HASH:
     875             final String clr = token.getText();
     876             try {
     877                 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null);
     878             } catch (final IllegalArgumentException e) {
     879                 error(root, e.getMessage());
     880             }
     881             break;
     882         case CSSLexer.FUNCTION:
     883             return  parseFunction(root);
     884         default:
     885             final String msg = "Unknown token type: \'" + ttype + "\'";
     886             error(root, msg);
     887         }
     888         return value;
     889
     890     }
     891
     892     /* Parse size.
     893      * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
     894      */
     895     private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException {
     896
     897         if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'");
     898
     899         ParsedValueImpl<?,Size> value = null;
     900
     901         if (root.token.getType() != CSSLexer.IDENT) {
     902
     903             Size size = size(root.token);
     904             value = new ParsedValueImpl<Size,Size>(size, null);
     905
     906         } else {
     907
     908             String key = root.token.getText();
     909             value = new ParsedValueImpl<String,Size>(key, null, true);
     910
     911         }
     912
     913         return value;
     914     }
     915
     916     private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException {
     917
     918         ParsedValueImpl<?,Color> color = null;
     919         if (root.token != null &&
     920             (root.token.getType() == CSSLexer.IDENT ||
     921              root.token.getType() == CSSLexer.HASH ||
     922              root.token.getType() == CSSLexer.FUNCTION)) {
     923
     924             color = parse(root);
     925
     926         } else {
     927             error(root,  "Expected \'<color>\'");
     928         }
     929         return color;
     930     }
     931
     932     // rgb(NUMBER, NUMBER, NUMBER)
     933     // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
     934     // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
     935     // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
     936     private ParsedValueImpl rgb(Term root) throws ParseException {
     937
     938         // first term in the chain is the function name...
     939         final String fn = (root.token != null) ? root.token.getText() : null;
     940         if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
     941             final String msg = "Expected \'rgb\' or \'rgba\'";
     942             error(root, msg);
     943         }
     944
     945         Term arg = root;
     946         Token rtok, gtok, btok, atok;
     947
     948         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
     949         if ((rtok = arg.token) == null ||
     950             (rtok.getType() != CSSLexer.NUMBER &&
     951              rtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
     952
     953         root = arg;
     954
     955         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
     956         if ((gtok = arg.token) == null ||
     957             (gtok.getType() != CSSLexer.NUMBER &&
     958              gtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
     959
     960         root = arg;
     961
     962         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
     963         if ((btok = arg.token) == null ||
     964             (btok.getType() != CSSLexer.NUMBER &&
     965              btok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
     966
     967         root = arg;
     968
     969         if ((arg = arg.nextArg) != null) {
     970             if ((atok = arg.token) == null ||
     971                  atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
     972         } else {
     973             atok = null;
     974         }
     975
     976         int argType = rtok.getType();
     977         if (argType != gtok.getType() || argType != btok.getType() ||
     978             (argType != CSSLexer.NUMBER && argType != CSSLexer.PERCENTAGE)) {
     979             error(root, "Argument type mistmatch");
     980         }
     981
     982         final String rtext = rtok.getText();
     983         final String gtext = gtok.getText();
     984         final String btext = btok.getText();
     985
     986         double rval = 0;
     987         double gval = 0;
     988         double bval = 0;
     989         if (argType == CSSLexer.NUMBER) {
     990             rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
     991             gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
     992             bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
     993         } else {
     994             rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
     995             gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
     996             bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
     997         }
     998
     999         final String atext = (atok != null) ? atok.getText() : null;
    1000         final double aval =  (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;
    1001
    1002         return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null);
    1003
    1004     }
    1005
    1006     // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
    1007     // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
    1008     private ParsedValueImpl hsb(Term root) throws ParseException {
    1009
    1010         // first term in the chain is the function name...
    1011         final String fn = (root.token != null) ? root.token.getText() : null;
    1012         if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
    1013             final String msg = "Expected \'hsb\' or \'hsba\'";
    1014             error(root, msg);
    1015         }
    1016
    1017         Term arg = root;
    1018         Token htok, stok, btok, atok;
    1019
    1020         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'");
    1021         if ((htok = arg.token) == null || htok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
    1022
    1023         root = arg;
    1024
    1025         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
    1026         if ((stok = arg.token) == null || stok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
    1027
    1028         root = arg;
    1029
    1030         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
    1031         if ((btok = arg.token) == null || btok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
    1032
    1033         root = arg;
    1034
    1035         if ((arg = arg.nextArg) != null) {
    1036             if ((atok = arg.token) == null || atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
    1037         } else {
    1038             atok = null;
    1039         }
    1040
    1041         final Size hval = size(htok);
    1042         final Size sval = size(stok);
    1043         final Size bval = size(btok);
    1044
    1045         final double hue = hval.pixels(); // no clamp - hue can be negative
    1046         final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
    1047         final double brightness = clamp(0.0f, bval.pixels(), 1.0f);
    1048
    1049         final Size aval = (atok != null) ? size(atok) : null;
    1050         final double opacity =  (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;
    1051
    1052         return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null);
    1053     }
    1054
    1055     // derive(color, pct)
    1056     private ParsedValueImpl<ParsedValue[],Color> derive(final Term root)
    1057             throws ParseException {
    1058
    1059         // first term in the chain is the function name...
    1060         final String fn = (root.token != null) ? root.token.getText() : null;
    1061         if (fn == null || !"derive".regionMatches(true, 0, fn, 0, 6)) {
    1062             final String msg = "Expected \'derive\'";
    1063             error(root, msg);
    1064         }
    1065
    1066         Term arg = root;
    1067         if ((arg = arg.firstArg) == null) error(root, "Expected \'<color>\'");
    1068
    1069         final ParsedValueImpl<?,Color> color = parseColor(arg);
    1070
    1071         final Term prev = arg;
    1072         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<percent\'");
    1073
    1074         final ParsedValueImpl<?,Size> brightness = parseSize(arg);
    1075
    1076         ParsedValueImpl[] values = new ParsedValueImpl[] { color, brightness };
    1077         return new ParsedValueImpl<ParsedValue[],Color>(values, DeriveColorConverter.getInstance());
    1078     }
    1079
    1080     // 'ladder' color 'stops' stop+
    1081     private ParsedValueImpl<ParsedValue[],Color> ladder(final Term root) throws ParseException {
    1082
    1083         // first term in the chain is the function name...
    1084         final String fn = (root.token != null) ? root.token.getText() : null;
    1085         if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
    1086             final String msg = "Expected \'ladder\'";
    1087             error(root, msg);
    1088         }
    1089
    1090         if (LOGGER.isLoggable(Level.WARNING)) {
    1091             LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
    1092         }
    1093
    1094         Term term = root;
    1095
    1096         if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'");
    1097         final ParsedValueImpl<?,Color> color = parse(term);
    1098
    1099         Term prev = term;
    1100
    1101         if ((term = term.nextInSeries) == null) error(prev,  "Expected \'stops\'");
    1102         if (term.token == null ||
    1103             term.token.getType() != CSSLexer.IDENT ||
    1104             !"stops".equalsIgnoreCase(term.token.getText())) error(term,  "Expected \'stops\'");
    1105
    1106         prev = term;
    1107
    1108         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'");
    1109
    1110         int nStops = 0;
    1111         Term temp = term;
    1112         do {
    1113             nStops += 1;
    1114             // if next token type is IDENT, then we have CycleMethod
    1115         } while (((temp = temp.nextInSeries) != null) &&
    1116                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
    1117
    1118         ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
    1119         values[0] = color;
    1120         int stopIndex = 1;
    1121         do {
    1122             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
    1123             if (stop != null) values[stopIndex++] = stop;
    1124             prev = term;
    1125         } while(((term = term.nextInSeries) != null) &&
    1126                  (term.token.getType() == CSSLexer.LPAREN));
    1127
    1128         // if term is not null and the last term was not an lparen,
    1129         // then term starts a new series of Paint. Point
    1130         // root.nextInSeries to term so the next loop skips over the
    1131         // already parsed ladder bits.
    1132         if (term != null) {
    1133             root.nextInSeries = term;
    1134         }
    1135
    1136         // if term is null, then we are at the end of a series.
    1137         // root points to 'ladder', now we want the next term after root
    1138         // to be the term after the last stop, which may be another layer
    1139         else {
    1140             root.nextInSeries = null;
    1141             root.nextLayer = prev.nextLayer;
    1142         }
    1143
    1144         return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
    1145     }
    1146
    1147     // <ladder> = ladder(<color>, <color-stop>[, <color-stop>]+ )
    1148     private ParsedValueImpl<ParsedValue[],Color> parseLadder(final Term root) throws ParseException {
    1149
    1150         // first term in the chain is the function name...
    1151         final String fn = (root.token != null) ? root.token.getText() : null;
    1152         if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
    1153             final String msg = "Expected \'ladder\'";
    1154             error(root, msg);
    1155         }
    1156
    1157         Term term = root;
    1158
    1159         if ((term = term.firstArg) == null) error(root, "Expected \'<color>\'");
    1160         final ParsedValueImpl<?,Color> color = parse(term);
    1161
    1162         Term prev = term;
    1163
    1164         if ((term = term.nextArg) == null)
    1165             error(prev,  "Expected \'<color-stop>[, <color-stop>]+\'");
    1166
    1167         ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(term);
    1168
    1169         ParsedValueImpl[] values = new ParsedValueImpl[stops.length+1];
    1170         values[0] = color;
    1171         System.arraycopy(stops, 0, values, 1, stops.length);
    1172         return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
    1173     }
    1174
    1175     // parse (<number>, <color>)+
    1176     // root.token should be a size
    1177     // root.token.next should be a color
    1178     private ParsedValueImpl<ParsedValue[], Stop> stop(final Term root)
    1179             throws ParseException {
    1180
    1181         // first term in the chain is the function name...
    1182         final String fn = (root.token != null) ? root.token.getText() : null;
    1183         if (fn == null || !"(".equals(fn)) {
    1184             final String msg = "Expected \'(\'";
    1185             error(root, msg);
    1186         }
    1187
    1188         Term arg = null;
    1189
    1190         if ((arg = root.firstArg) == null) error(root,  "Expected \'<number>\'");
    1191
    1192         ParsedValueImpl<?,Size> size = parseSize(arg);
    1193
    1194         Term prev = arg;
    1195         if ((arg = arg.nextArg) == null) error(prev,  "Expected \'<color>\'");
    1196
    1197         ParsedValueImpl<?,Color> color = parseColor(arg);
    1198
    1199         ParsedValueImpl[] values = new ParsedValueImpl[] { size, color };
    1200         return new ParsedValueImpl<ParsedValue[],Stop>(values, StopConverter.getInstance());
    1201
    1202     }
    1203
    1204     // http://dev.w3.org/csswg/css3-images/#color-stop-syntax
    1205     // <color-stop> = <color> [ <percentage> | <length> ]?
    1206     private ParsedValueImpl<ParsedValue[], Stop>[] parseColorStops(final Term root)
    1207             throws ParseException {
    1208
    1209         int nArgs = 1;
    1210         Term temp = root;
    1211         while(temp != null) {
    1212             if (temp.nextArg != null) {
    1213                 nArgs += 1;
    1214                 temp = temp.nextArg;
    1215             } else if (temp.nextInSeries != null) {
    1216                 temp = temp.nextInSeries;
    1217             } else {
    1218                 break;
    1219             }
    1220         }
    1221
    1222         if (nArgs < 2) {
    1223             error(root, "Expected \'<color-stop>\'");
    1224         }
    1225
    1226         ParsedValueImpl<?,Color>[] colors = new ParsedValueImpl[nArgs];
    1227         Size[] positions = new Size[nArgs];
    1228         java.util.Arrays.fill(positions, null);
    1229
    1230         Term stop = root;
    1231         Term prev = root;
    1232         SizeUnits units = null;
    1233         for (int n = 0; n<nArgs; n++) {
    1234
    1235             colors[n] = parseColor(stop);
    1236
    1237             prev = stop;
    1238             Term term = stop.nextInSeries;
    1239             if (term != null) {
    1240                 if (isSize(term.token)) {
    1241                     positions[n] = size(term.token);
    1242                     if (units != null) {
    1243                         if (units != positions[n].getUnits()) {
    1244                             error(term, "Parser unable to handle mixed \'<percent>\' and \'<length>\'");
    1245                         }
    1246                     }
    1247                 } else {
    1248                     error(prev, "Expected \'<percent>\' or \'<length>\'");
    1249                 }
    1250                 prev = term;
    1251                 stop = term.nextArg;
    1252             } else {
    1253                 prev = stop;
    1254                 stop = stop.nextArg;
    1255             }
    1256
    1257         }
    1258
    1259         //
    1260         // normalize positions according to
    1261         // http://dev.w3.org/csswg/css3-images/#color-stop-syntax
    1262         //
    1263         // If the first color-stop does not have a position, set its
    1264         // position to 0%. If the last color-stop does not have a position,
    1265         // set its position to 100%.
    1266         if (positions[0] == null) positions[0] = new Size(0, SizeUnits.PERCENT);
    1267         if (positions[nArgs-1] == null) positions[nArgs-1] = new Size(100, SizeUnits.PERCENT);
    1268
    1269         // If a color-stop has a position that is less than the specified
    1270         // position of any color-stop before it in the list, set its
    1271         // position to be equal to the largest specified position of any
    1272         // color-stop before it.
    1273         Size max = null;
    1274         for (int n = 1 ; n<nArgs; n++) {
    1275             Size pos0 = positions[n-1];
    1276             if (pos0 == null) continue;
    1277             if (max == null || max.getValue() < pos0.getValue()) {
    1278                 // TODO: this doesn't work with mixed length and percent
    1279                 max = pos0;
    1280             }
    1281             Size pos1 = positions[n];
    1282             if (pos1 == null) continue;
    1283
    1284             if (pos1.getValue() < max.getValue()) positions[n] = max;
    1285         }
    1286
    1287         // If any color-stop still does not have a position, then,
    1288         // for each run of adjacent color-stops without positions, set
    1289         // their positions so that they are evenly spaced between the
    1290         // preceding and following color-stops with positions.
    1291         Size preceding = null;
    1292         int withoutIndex = -1;
    1293         for (int n = 0 ; n<nArgs; n++) {
    1294             Size pos = positions[n];
    1295             if (pos == null) {
    1296                 if (withoutIndex == -1) withoutIndex = n;
    1297                 continue;
    1298             } else {
    1299                 if (withoutIndex > -1) {
    1300
    1301                     int nWithout = n - withoutIndex;
    1302                     double precedingValue = preceding.getValue();
    1303                     double delta =
    1304                         (pos.getValue() - precedingValue) / (nWithout + 1);
    1305
    1306                     while(withoutIndex < n) {
    1307                         precedingValue += delta;
    1308                         positions[withoutIndex++] =
    1309                             new Size(precedingValue, pos.getUnits());
    1310                     }
    1311                     withoutIndex = -1;
    1312                     preceding = pos;
    1313                 } else {
    1314                     preceding = pos;
    1315                 }
    1316             }
    1317         }
    1318
    1319         ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs];
    1320         for (int n=0; n<nArgs; n++) {
    1321             stops[n] = new ParsedValueImpl<ParsedValue[],Stop>(
    1322                 new ParsedValueImpl[] {
    1323                     new ParsedValueImpl<Size,Size>(positions[n], null),
    1324                     colors[n]
    1325                 },
    1326                 StopConverter.getInstance()
    1327             );
    1328         }
    1329
    1330         return stops;
    1331
    1332     }
    1333
    1334     // parse (<number>, <number>)
    1335     private ParsedValueImpl[] point(final Term root) throws ParseException {
    1336
    1337         if (root.token == null ||
    1338             root.token.getType() != CSSLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'");
    1339
    1340         final String fn = root.token.getText();
    1341         if (fn == null || !"(".equalsIgnoreCase(fn)) {
    1342             final String msg = "Expected \'(\'";
    1343             error(root, msg);
    1344         }
    1345
    1346         Term arg = null;
    1347
    1348         // no <number>
    1349         if ((arg = root.firstArg) == null)  error(root, "Expected \'<number>\'");
    1350
    1351         final ParsedValueImpl<?,Size> ptX = parseSize(arg);
    1352
    1353         final Term prev = arg;
    1354
    1355         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1356
    1357         final ParsedValueImpl<?,Size> ptY = parseSize(arg);
    1358
    1359         return new ParsedValueImpl[] { ptX, ptY };
    1360     }
    1361
    1362     private ParsedValueImpl parseFunction(final Term root) throws ParseException {
    1363
    1364         // Text from parser is function name plus the lparen, e.g., 'derive('
    1365         final String fcn = (root.token != null) ? root.token.getText() : null;
    1366         if (fcn == null) {
    1367             error(root, "Expected function name");
    1368         } else if ("rgb".regionMatches(true, 0, fcn, 0, 3)) {
    1369             return rgb(root);
    1370         } else if ("hsb".regionMatches(true, 0, fcn, 0, 3)) {
    1371             return hsb(root);
    1372         } else if ("derive".regionMatches(true, 0, fcn, 0, 6)) {
    1373             return derive(root);
    1374         } else if ("innershadow".regionMatches(true, 0, fcn, 0, 11)) {
    1375             return innershadow(root);
    1376         } else if ("dropshadow".regionMatches(true, 0, fcn, 0, 10)) {
    1377             return dropshadow(root);
    1378         } else if ("linear-gradient".regionMatches(true, 0, fcn, 0, 15)) {
    1379             return parseLinearGradient(root);
    1380         } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
    1381             return parseRadialGradient(root);
    1382         } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
    1383             return parseImagePattern(root);
    1384         } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
    1385             return parseRepeatingImagePattern(root);
    1386         } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
    1387             return parseLadder(root);
    1388         } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
    1389             return parseRegion(root);
    1390         } else if ("url".regionMatches(true, 0, fcn, 0, 3)) {
    1391             return parseURI(root);
    1392         } else {
    1393             error(root, "Unexpected function \'" + fcn + "\'");
    1394         }
    1395         return null;
    1396     }
    1397
    1398     private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException {
    1399
    1400         if (root == null) return null;
    1401         if (root.token == null ||
    1402             root.token.getType() != CSSLexer.IDENT ||
    1403             root.token.getText() == null ||
    1404             root.token.getText().isEmpty()) {
    1405             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
    1406             error(root, msg);
    1407         }
    1408         final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
    1409         BlurType blurType = BlurType.THREE_PASS_BOX;
    1410         if ("gaussian".equals(blurStr)) {
    1411             blurType = BlurType.GAUSSIAN;
    1412         } else if ("one-pass-box".equals(blurStr)) {
    1413             blurType = BlurType.ONE_PASS_BOX;
    1414         } else if ("two-pass-box".equals(blurStr)) {
    1415             blurType = BlurType.TWO_PASS_BOX;
    1416         } else if ("three-pass-box".equals(blurStr)) {
    1417             blurType = BlurType.THREE_PASS_BOX;
    1418         } else {
    1419             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
    1420             error(root, msg);
    1421         }
    1422         return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class));
    1423     }
    1424
    1425     // innershadow <blur-type> <color> <radius> <choke> <offset-x> <offset-y>
    1426     private ParsedValueImpl innershadow(final Term root) throws ParseException {
    1427
    1428         // first term in the chain is the function name...
    1429         final String fn = (root.token != null) ? root.token.getText() : null;
    1430         if (!"innershadow".regionMatches(true, 0, fn, 0, 11)) {
    1431             final String msg = "Expected \'innershadow\'";
    1432             error(root, msg);
    1433         }
    1434
    1435         Term arg;
    1436
    1437         if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'");
    1438         ParsedValueImpl<String,BlurType> blurVal = blurType(arg);
    1439
    1440         Term prev = arg;
    1441         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'");
    1442
    1443         ParsedValueImpl<?,Color> colorVal = parseColor(arg);
    1444
    1445         prev = arg;
    1446         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1447
    1448         ParsedValueImpl<?,Size> radiusVal = parseSize(arg);
    1449
    1450         prev = arg;
    1451         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1452
    1453         ParsedValueImpl<?,Size> chokeVal = parseSize(arg);
    1454
    1455         prev = arg;
    1456         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1457
    1458         ParsedValueImpl<?,Size> offsetXVal = parseSize(arg);
    1459
    1460         prev = arg;
    1461         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1462
    1463         ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
    1464
    1465         ParsedValueImpl[] values = new ParsedValueImpl[] {
    1466             blurVal,
    1467             colorVal,
    1468             radiusVal,
    1469             chokeVal,
    1470             offsetXVal,
    1471             offsetYVal
    1472         };
    1473         return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.InnerShadowConverter.getInstance());
    1474     }
    1475
    1476     // dropshadow <blur-type> <color> <radius> <spread> <offset-x> <offset-y>
    1477     private ParsedValueImpl dropshadow(final Term root) throws ParseException {
    1478
    1479         // first term in the chain is the function name...
    1480         final String fn = (root.token != null) ? root.token.getText() : null;
    1481         if (!"dropshadow".regionMatches(true, 0, fn, 0, 10)) {
    1482             final String msg = "Expected \'dropshadow\'";
    1483             error(root, msg);
    1484         }
    1485
    1486         Term arg;
    1487
    1488         if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'");
    1489         ParsedValueImpl<String,BlurType> blurVal = blurType(arg);
    1490
    1491         Term prev = arg;
    1492         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'");
    1493
    1494         ParsedValueImpl<?,Color> colorVal = parseColor(arg);
    1495
    1496         prev = arg;
    1497         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1498
    1499         ParsedValueImpl<?,Size> radiusVal = parseSize(arg);
    1500
    1501         prev = arg;
    1502         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1503
    1504         ParsedValueImpl<?,Size> spreadVal = parseSize(arg);
    1505
    1506         prev = arg;
    1507         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1508
    1509         ParsedValueImpl<?,Size> offsetXVal = parseSize(arg);
    1510
    1511         prev = arg;
    1512         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
    1513
    1514         ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
    1515
    1516         ParsedValueImpl[] values = new ParsedValueImpl[] {
    1517             blurVal,
    1518             colorVal,
    1519             radiusVal,
    1520             spreadVal,
    1521             offsetXVal,
    1522             offsetYVal
    1523         };
    1524         return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance());
    1525     }
    1526
    1527     // returns null if the Term is null or is not a cycle method.
    1528     private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) {
    1529         CycleMethod cycleMethod = null;
    1530         if (root != null && root.token.getType() == CSSLexer.IDENT) {
    1531
    1532             final String text = root.token.getText().toLowerCase(Locale.ROOT);
    1533             if ("repeat".equals(text)) {
    1534                 cycleMethod = CycleMethod.REPEAT;
    1535             } else if ("reflect".equals(text)) {
    1536                 cycleMethod = CycleMethod.REFLECT;
    1537             } else if ("no-cycle".equals(text)) {
    1538                 cycleMethod = CycleMethod.NO_CYCLE;
    1539             }
    1540         }
    1541         if (cycleMethod != null)
    1542             return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
    1543         else
    1544             return null;
    1545     }
    1546
    1547     // linear <point> TO <point> STOPS <stop>+ cycleMethod?
    1548     private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException {
    1549
    1550         final String fn = (root.token != null) ? root.token.getText() : null;
    1551         if (fn == null || !"linear".equalsIgnoreCase(fn)) {
    1552             final String msg = "Expected \'linear\'";
    1553             error(root, msg);
    1554         }
    1555
    1556         if (LOGGER.isLoggable(Level.WARNING)) {
    1557             LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
    1558         }
    1559
    1560         Term term = root;
    1561
    1562         if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'");
    1563
    1564         final ParsedValueImpl<?,Size>[] startPt = point(term);
    1565
    1566         Term prev = term;
    1567         if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
    1568         if (term.token == null ||
    1569             term.token.getType() != CSSLexer.IDENT ||
    1570             !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");
    1571
    1572         prev = term;
    1573         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
    1574
    1575         final ParsedValueImpl<?,Size>[] endPt = point(term);
    1576
    1577         prev = term;
    1578         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
    1579         if (term.token == null ||
    1580             term.token.getType() != CSSLexer.IDENT ||
    1581             !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
    1582
    1583         prev = term;
    1584         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
    1585
    1586         int nStops = 0;
    1587         Term temp = term;
    1588         do {
    1589             nStops += 1;
    1590             // if next token type is IDENT, then we have CycleMethod
    1591         } while (((temp = temp.nextInSeries) != null) &&
    1592                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
    1593
    1594         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
    1595         int stopIndex = 0;
    1596         do {
    1597             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
    1598             if (stop != null) stops[stopIndex++] = stop;
    1599             prev = term;
    1600         } while(((term = term.nextInSeries) != null) &&
    1601                 (term.token.getType() == CSSLexer.LPAREN));
    1602
    1603         // term is either null or is a cycle method, or the start of another Paint.
    1604         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
    1605
    1606         if (cycleMethod == null) {
    1607
    1608             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
    1609
    1610             // if term is not null and the last term was not a cycle method,
    1611             // then term starts a new series or layer of Paint
    1612             if (term != null) {
    1613                 root.nextInSeries = term;
    1614             }
    1615
    1616             // if term is null, then we are at the end of a series.
    1617             // root points to 'linear', now we want the next term after root
    1618             // to be the term after the last stop, which may be another layer
    1619             else {
    1620                 root.nextInSeries = null;
    1621                 root.nextLayer = prev.nextLayer;
    1622             }
    1623
    1624
    1625         } else {
    1626             // last term was a CycleMethod, so term is not null.
    1627             // root points at 'linear', now we want the next term after root
    1628             // to be the term after cyclemethod, which may be another series
    1629             // of paint or another layer.
    1630             //
    1631             root.nextInSeries = term.nextInSeries;
    1632             root.nextLayer = term.nextLayer;
    1633         }
    1634
    1635         ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length];
    1636         int index = 0;
    1637         values[index++] = (startPt != null) ? startPt[0] : null;
    1638         values[index++] = (startPt != null) ? startPt[1] : null;
    1639         values[index++] = (endPt != null) ? endPt[0] : null;
    1640         values[index++] = (endPt != null) ? endPt[1] : null;
    1641         values[index++] = cycleMethod;
    1642         for (int n=0; n<stops.length; n++) values[index++] = stops[n];
    1643         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance());
    1644     }
    1645
    1646     // Based off http://dev.w3.org/csswg/css3-images/#linear-gradients
    1647     //
    1648     // <linear-gradient> = linear-gradient(
    1649     //        [ [from <point> to <point>] | [ to <side-or-corner> ] ] ,]? [ [ repeat | reflect ] ,]?
    1650     //        <color-stop>[, <color-stop>]+
    1651     // )
    1652     //
    1653     //
    1654     // <point> = <percentage> <percentage> | <length> <length>
    1655     // <side-or-corner> = [left | right] || [top | bottom]
    1656     //
    1657     // If neither repeat nor reflect are given, then the CycleMethod defaults "NO_CYCLE".
    1658     // If neither [from <point> to <point>] nor [ to <side-or-corner> ] are given,
    1659     // then the gradient direction defaults to 'to bottom'.
    1660     // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax.
    1661     private ParsedValueImpl parseLinearGradient(final Term root) throws ParseException {
    1662
    1663         // first term in the chain is the function name...
    1664         final String fn = (root.token != null) ? root.token.getText() : null;
    1665         if (!"linear-gradient".regionMatches(true, 0, fn, 0, 15)) {
    1666             final String msg = "Expected \'linear-gradient\'";
    1667             error(root, msg);
    1668         }
    1669
    1670         Term arg;
    1671
    1672         if ((arg = root.firstArg) == null ||
    1673              arg.token == null ||
    1674              arg.token.getText().isEmpty()) {
    1675             error(root,
    1676                 "Expected \'from <point> to <point>\' or \'to <side-or-corner>\' " +
    1677                 "or \'<cycle-method>\' or \'<color-stop>\'");
    1678         }
    1679
    1680         Term prev = arg;
    1681 //        ParsedValueImpl<Size,Size> angleVal = null;
    1682         ParsedValueImpl<?,Size>[] startPt = null;
    1683         ParsedValueImpl<?,Size>[] endPt = null;
    1684
    1685         if ("from".equalsIgnoreCase(arg.token.getText())) {
    1686
    1687             prev = arg;
    1688             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    1689
    1690             ParsedValueImpl<?,Size> ptX = parseSize(arg);
    1691
    1692             prev = arg;
    1693             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    1694
    1695             ParsedValueImpl<?,Size> ptY = parseSize(arg);
    1696
    1697             startPt = new ParsedValueImpl[] { ptX, ptY };
    1698
    1699             prev = arg;
    1700             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
    1701             if (arg.token == null ||
    1702                 arg.token.getType() != CSSLexer.IDENT ||
    1703                 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");
    1704
    1705             prev = arg;
    1706             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    1707
    1708             ptX = parseSize(arg);
    1709
    1710             prev = arg;
    1711             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    1712
    1713             ptY = parseSize(arg);
    1714
    1715             endPt = new ParsedValueImpl[] { ptX, ptY };
    1716
    1717             prev = arg;
    1718             arg = arg.nextArg;
    1719
    1720         } else if("to".equalsIgnoreCase(arg.token.getText())) {
    1721
    1722             prev = arg;
    1723             if ((arg = arg.nextInSeries) == null ||
    1724                 arg.token == null ||
    1725                 arg.token.getType() != CSSLexer.IDENT ||
    1726                 arg.token.getText().isEmpty()) {
    1727                 error (prev, "Expected \'<side-or-corner>\'");
    1728             }
    1729
    1730
    1731             int startX = 0;
    1732             int startY = 0;
    1733             int endX = 0;
    1734             int endY = 0;
    1735
    1736             String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
    1737             // The keywords denote the direction.
    1738             if ("top".equals(sideOrCorner1)) {
    1739                 // going toward the top, then start at the bottom
    1740                 startY = 100;
    1741                 endY = 0;
    1742
    1743             } else if ("bottom".equals(sideOrCorner1)) {
    1744                 // going toward the bottom, then start at the top
    1745                 startY = 0;
    1746                 endY = 100;
    1747
    1748             } else if ("right".equals(sideOrCorner1)) {
    1749                 // going toward the right, then start at the left
    1750                 startX = 0;
    1751                 endX = 100;
    1752
    1753             } else if ("left".equals(sideOrCorner1)) {
    1754                 // going toward the left, then start at the right
    1755                 startX = 100;
    1756                 endX = 0;
    1757
    1758             } else {
    1759                 error(arg, "Invalid \'<side-or-corner>\'");
    1760             }
    1761
    1762             prev = arg;
    1763             if (arg.nextInSeries != null) {
    1764                 arg = arg.nextInSeries;
    1765                 if (arg.token != null &&
    1766                     arg.token.getType() == CSSLexer.IDENT &&
    1767                     !arg.token.getText().isEmpty()) {
    1768
    1769                     String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);
    1770
    1771                     // if right or left has already been given,
    1772                     // then either startX or endX will not be zero.
    1773                     if ("right".equals(sideOrCorner2) &&
    1774                         startX == 0 && endX == 0) {
    1775                         // start left, end right
    1776                         startX = 0;
    1777                         endX = 100;
    1778                     } else if ("left".equals(sideOrCorner2) &&
    1779                         startX == 0 && endX == 0) {
    1780                         // start right, end left
    1781                         startX = 100;
    1782                         endX = 0;
    1783
    1784                     // if top or bottom has already been given,
    1785                     // then either startY or endY will not be zero.
    1786                     } else if("top".equals(sideOrCorner2) &&
    1787                         startY == 0 && endY == 0) {
    1788                         // start bottom, end top
    1789                         startY = 100;
    1790                         endY = 0;
    1791                     } else if ("bottom".equals(sideOrCorner2) &&
    1792                         startY == 0 && endY == 0) {
    1793                         // start top, end bottom
    1794                         startY = 0;
    1795                         endY = 100;
    1796
    1797                     } else {
    1798                         error(arg, "Invalid \'<side-or-corner>\'");
    1799                     }
    1800
    1801                 } else {
    1802                     error (prev, "Expected \'<side-or-corner>\'");
    1803                 }
    1804             }
    1805
    1806
    1807             startPt = new ParsedValueImpl[] {
    1808                 new ParsedValueImpl<Size,Size>(new Size(startX, SizeUnits.PERCENT), null),
    1809                 new ParsedValueImpl<Size,Size>(new Size(startY, SizeUnits.PERCENT), null)
    1810             };
    1811
    1812             endPt = new ParsedValueImpl[] {
    1813                 new ParsedValueImpl<Size,Size>(new Size(endX, SizeUnits.PERCENT), null),
    1814                 new ParsedValueImpl<Size,Size>(new Size(endY, SizeUnits.PERCENT), null)
    1815             };
    1816
    1817             prev = arg;
    1818             arg = arg.nextArg;
    1819         }
    1820
    1821         if (startPt == null && endPt == null) {
    1822             // spec says defaults to bottom
    1823             startPt = new ParsedValueImpl[] {
    1824                 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null),
    1825                 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null)
    1826             };
    1827
    1828             endPt = new ParsedValueImpl[] {
    1829                 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null),
    1830                 new ParsedValueImpl<Size,Size>(new Size(100, SizeUnits.PERCENT), null)
    1831             };
    1832         }
    1833
    1834         if (arg == null ||
    1835             arg.token == null ||
    1836             arg.token.getText().isEmpty()) {
    1837             error(prev, "Expected \'<cycle-method>\' or \'<color-stop>\'");
    1838         }
    1839
    1840         CycleMethod cycleMethod = CycleMethod.NO_CYCLE;
    1841         if ("reflect".equalsIgnoreCase(arg.token.getText())) {
    1842             cycleMethod = CycleMethod.REFLECT;
    1843             prev = arg;
    1844             arg = arg.nextArg;
    1845         } else if ("repeat".equalsIgnoreCase(arg.token.getText())) {
    1846             cycleMethod = CycleMethod.REFLECT;
    1847             prev = arg;
    1848             arg = arg.nextArg;
    1849         }
    1850
    1851         if (arg == null  ||
    1852             arg.token == null ||
    1853             arg.token.getText().isEmpty()) {
    1854             error(prev, "Expected \'<color-stop>\'");
    1855         }
    1856
    1857         ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg);
    1858
    1859         ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length];
    1860         int index = 0;
    1861         values[index++] = (startPt != null) ? startPt[0] : null;
    1862         values[index++] = (startPt != null) ? startPt[1] : null;
    1863         values[index++] = (endPt != null) ? endPt[0] : null;
    1864         values[index++] = (endPt != null) ? endPt[1] : null;
    1865         values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
    1866         for (int n=0; n<stops.length; n++) values[index++] = stops[n];
    1867         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance());
    1868
    1869     }
    1870
    1871     // radial [focus-angle <number | percent>]? [focus-distance <size>]?
    1872     // [center (<size>,<size>)]? <size>
    1873     // stops [ ( <number> , <color> ) ]+ [ repeat | reflect ]?
    1874     private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException {
    1875
    1876         final String fn = (root.token != null) ? root.token.getText() : null;
    1877         if (fn == null || !"radial".equalsIgnoreCase(fn)) {
    1878             final String msg = "Expected \'radial\'";
    1879             error(root, msg);
    1880         }
    1881
    1882         if (LOGGER.isLoggable(Level.WARNING)) {
    1883             LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
    1884         }
    1885
    1886         Term term = root;
    1887         Term prev = root;
    1888
    1889         if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
    1890         if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
    1891
    1892
    1893         ParsedValueImpl<?,Size> focusAngle = null;
    1894         if (term.token.getType() == CSSLexer.IDENT) {
    1895             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
    1896             if ("focus-angle".equals(keyword)) {
    1897
    1898                 prev = term;
    1899                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
    1900                 if (term.token == null) error(prev, "Expected \'<number>\'");
    1901
    1902                 focusAngle = parseSize(term);
    1903
    1904                 prev = term;
    1905                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
    1906                 if (term.token == null) error(term,  "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
    1907             }
    1908         }
    1909
    1910         ParsedValueImpl<?,Size> focusDistance = null;
    1911         if (term.token.getType() == CSSLexer.IDENT) {
    1912             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
    1913             if ("focus-distance".equals(keyword)) {
    1914
    1915                 prev = term;
    1916                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
    1917                 if (term.token == null) error(prev, "Expected \'<number>\'");
    1918
    1919                 focusDistance = parseSize(term);
    1920
    1921                 prev = term;
    1922                 if ((term = term.nextInSeries) == null) error(prev, "Expected  \'center (<number>,<number>)\' or \'<size>\'");
    1923                 if (term.token == null) error(term,  "Expected  \'center (<number>,<number>)\' or \'<size>\'");
    1924             }
    1925         }
    1926
    1927         ParsedValueImpl<?,Size>[] centerPoint = null;
    1928         if (term.token.getType() == CSSLexer.IDENT) {
    1929             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
    1930             if ("center".equals(keyword)) {
    1931
    1932                 prev = term;
    1933                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'");
    1934                 if (term.token == null ||
    1935                     term.token.getType() != CSSLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'");
    1936
    1937                 centerPoint = point(term);
    1938
    1939                 prev = term;
    1940                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'");
    1941                 if (term.token == null) error(term,  "Expected \'<size>\'");
    1942             }
    1943         }
    1944
    1945         ParsedValueImpl<?,Size> radius = parseSize(term);
    1946
    1947         prev = term;
    1948         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
    1949         if (term.token == null ||
    1950             term.token.getType() != CSSLexer.IDENT) error(term, "Expected \'stops\' keyword");
    1951
    1952         if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
    1953
    1954         prev = term;
    1955         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
    1956
    1957         int nStops = 0;
    1958         Term temp = term;
    1959         do {
    1960             nStops += 1;
    1961             // if next token type is IDENT, then we have CycleMethod
    1962         } while (((temp = temp.nextInSeries) != null) &&
    1963                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
    1964
    1965         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
    1966         int stopIndex = 0;
    1967         do {
    1968             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
    1969             if (stop != null) stops[stopIndex++] = stop;
    1970             prev = term;
    1971         } while(((term = term.nextInSeries) != null) &&
    1972                 (term.token.getType() == CSSLexer.LPAREN));
    1973
    1974         // term is either null or is a cycle method, or the start of another Paint.
    1975         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
    1976
    1977         if (cycleMethod == null) {
    1978
    1979             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
    1980
    1981             // if term is not null and the last term was not a cycle method,
    1982             // then term starts a new series or layer of Paint
    1983             if (term != null) {
    1984                 root.nextInSeries = term;
    1985             }
    1986
    1987             // if term is null, then we are at the end of a series.
    1988             // root points to 'linear', now we want the next term after root
    1989             // to be the term after the last stop, which may be another layer
    1990             else {
    1991                 root.nextInSeries = null;
    1992                 root.nextLayer = prev.nextLayer;
    1993             }
    1994
    1995
    1996         } else {
    1997             // last term was a CycleMethod, so term is not null.
    1998             // root points at 'linear', now we want the next term after root
    1999             // to be the term after cyclemethod, which may be another series
    2000             // of paint or another layer.
    2001             //
    2002             root.nextInSeries = term.nextInSeries;
    2003             root.nextLayer = term.nextLayer;
    2004         }
    2005
    2006         ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length];
    2007         int index = 0;
    2008         values[index++] = focusAngle;
    2009         values[index++] = focusDistance;
    2010         values[index++] = (centerPoint != null) ? centerPoint[0] : null;
    2011         values[index++] = (centerPoint != null) ? centerPoint[1] : null;
    2012         values[index++] = radius;
    2013         values[index++] = cycleMethod;
    2014         for (int n=0; n<stops.length; n++) values[index++] = stops[n];
    2015         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance());
    2016     }
    2017
    2018     // Based off http://dev.w3.org/csswg/css3-images/#radial-gradients
    2019     //
    2020     // <radial-gradient> = radial-gradient(
    2021     //        [ focus-angle <angle>, ]?
    2022     //        [ focus-distance <percentage>, ]?
    2023     //        [ center <point>, ]?
    2024     //        radius <length>,
    2025     //        [ [ repeat | reflect ] ,]?
    2026     //        <color-stop>[, <color-stop>]+ )
    2027     //
    2028     // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax.
    2029     private ParsedValueImpl parseRadialGradient(final Term root) throws ParseException {
    2030
    2031         // first term in the chain is the function name...
    2032         final String fn = (root.token != null) ? root.token.getText() : null;
    2033         if (!"radial-gradient".regionMatches(true, 0, fn, 0, 15)) {
    2034             final String msg = "Expected \'radial-gradient\'";
    2035             error(root, msg);
    2036         }
    2037
    2038         Term arg;
    2039
    2040         if ((arg = root.firstArg) == null ||
    2041              arg.token == null ||
    2042              arg.token.getText().isEmpty()) {
    2043             error(root,
    2044                 "Expected \'focus-angle <angle>\' " +
    2045                 "or \'focus-distance <percentage>\' " +
    2046                 "or \'center <point>\' " +
    2047                 "or \'radius [<length> | <percentage>]\'");
    2048         }
    2049
    2050         Term prev = arg;
    2051         ParsedValueImpl<?,Size>focusAngle = null;
    2052         ParsedValueImpl<?,Size>focusDistance = null;
    2053         ParsedValueImpl<?,Size>[] centerPoint = null;
    2054         ParsedValueImpl<?,Size>radius = null;
    2055
    2056         if ("focus-angle".equalsIgnoreCase(arg.token.getText())) {
    2057
    2058             prev = arg;
    2059             if ((arg = arg.nextInSeries) == null ||
    2060                     !isSize(arg.token)) error(prev, "Expected \'<angle>\'");
    2061
    2062             Size angle = size(arg.token);
    2063             switch(angle.getUnits()) {
    2064                 case DEG:
    2065                 case RAD:
    2066                 case GRAD:
    2067                 case TURN:
    2068                 case PX:
    2069                     break;
    2070                 default:
    2071                     error(arg, "Expected [deg | rad | grad | turn ]");
    2072             }
    2073             focusAngle = new ParsedValueImpl<Size,Size>(angle, null);
    2074
    2075             prev = arg;
    2076             if ((arg = arg.nextArg) == null)
    2077                 error(prev, "Expected \'focus-distance <percentage>\' " +
    2078                             "or \'center <point>\' " +
    2079                             "or \'radius [<length> | <percentage>]\'");
    2080
    2081         }
    2082
    2083         if ("focus-distance".equalsIgnoreCase(arg.token.getText())) {
    2084
    2085             prev = arg;
    2086             if ((arg = arg.nextInSeries) == null ||
    2087                     !isSize(arg.token)) error(prev, "Expected \'<percentage>\'");
    2088
    2089             Size distance = size(arg.token);
    2090
    2091             // "The focus point is always specified relative to the center
    2092             // point by an angle and a distance relative to the radius."
    2093             switch(distance.getUnits()) {
    2094                 case PERCENT:
    2095                     break;
    2096                 default:
    2097                     error(arg, "Expected \'%\'");
    2098             }
    2099             focusDistance = new ParsedValueImpl<Size,Size>(distance, null);
    2100
    2101             prev = arg;
    2102             if ((arg = arg.nextArg) == null)
    2103                 error(prev, "Expected \'center <center>\' " +
    2104                             "or \'radius <length>\'");
    2105
    2106         }
    2107
    2108         if ("center".equalsIgnoreCase(arg.token.getText())) {
    2109
    2110             prev = arg;
    2111             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    2112
    2113             ParsedValueImpl<?,Size> ptX = parseSize(arg);
    2114
    2115             prev = arg;
    2116             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
    2117
    2118             ParsedValueImpl<?,Size> ptY = parseSize(arg);
    2119
    2120             centerPoint = new ParsedValueImpl[] { ptX, ptY };
    2121
    2122             prev = arg;
    2123             if ((arg = arg.nextArg) == null)
    2124                 error(prev, "Expected \'radius [<length> | <percentage>]\'");
    2125         }
    2126
    2127         if ("radius".equalsIgnoreCase(arg.token.getText())) {
    2128
    2129             prev = arg;
    2130             if ((arg = arg.nextInSeries) == null ||
    2131                 !isSize(arg.token)) error(prev, "Expected \'[<length> | <percentage>]\'");
    2132
    2133             radius = parseSize(arg);
    2134
    2135             prev = arg;
    2136             if ((arg = arg.nextArg) == null)
    2137                 error(prev, "Expected \'radius [<length> | <percentage>]\'");
    2138         }
    2139
    2140         CycleMethod cycleMethod = CycleMethod.NO_CYCLE;
    2141         if ("reflect".equalsIgnoreCase(arg.token.getText())) {
    2142             cycleMethod = CycleMethod.REFLECT;
    2143             prev = arg;
    2144             arg = arg.nextArg;
    2145         } else if ("repeat".equalsIgnoreCase(arg.token.getText())) {
    2146             cycleMethod = CycleMethod.REFLECT;
    2147             prev = arg;
    2148             arg = arg.nextArg;
    2149         }
    2150
    2151         if (arg == null  ||
    2152             arg.token == null ||
    2153             arg.token.getText().isEmpty()) {
    2154             error(prev, "Expected \'<color-stop>\'");
    2155         }
    2156
    2157         ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg);
    2158
    2159         ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length];
    2160         int index = 0;
    2161         values[index++] = focusAngle;
    2162         values[index++] = focusDistance;
    2163         values[index++] = (centerPoint != null) ? centerPoint[0] : null;
    2164         values[index++] = (centerPoint != null) ? centerPoint[1] : null;
    2165         values[index++] = radius;
    2166         values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
    2167         for (int n=0; n<stops.length; n++) values[index++] = stops[n];
    2168         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance());
    2169
    2170     }
    2171
    2172     // Based off ImagePattern constructor
    2173     //
    2174     // image-pattern(<uri-string>[,<size>,<size>,<size>,<size>[,<boolean>]?]?)
    2175     //
    2176     private ParsedValueImpl<ParsedValue[], Paint> parseImagePattern(final Term root) throws ParseException {
    2177
    2178         // first term in the chain is the function name...
    2179         final String fn = (root.token != null) ? root.token.getText() : null;
    2180         // NOTE: We should put this in an assertion, so as not to do this work ordinarily, because only a
    2181         // bug in the parser can cause this.
    2182         assert !"image-pattern".regionMatches(true, 0, fn, 0, 13) : "Expected \'image-pattern\'";
    2183
    2184         Term arg;
    2185         if ((arg = root.firstArg) == null ||
    2186              arg.token == null ||
    2187              arg.token.getText().isEmpty()) {
    2188             error(root,
    2189                 "Expected \'<uri-string>\'");
    2190         }
    2191
    2192         Term prev = arg;
    2193
    2194         final String uri = arg.token.getText();
    2195         ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
    2196             new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
    2197             null // placeholder for Stylesheet URL
    2198         };
    2199         ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
    2200
    2201         // If nextArg is null, then there are no remaining arguments, so we are done.
    2202         if (arg.nextArg == null) {
    2203             ParsedValueImpl[] values = new ParsedValueImpl[1];
    2204             values[0] = parsedURI;
    2205             return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance());
    2206         }
    2207
    2208         // There must now be 4 sizes in a row.
    2209         Token token;
    2210         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'");
    2211         ParsedValueImpl<?, Size> x = parseSize(arg);
    2212
    2213         prev = arg;
    2214         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'");
    2215         ParsedValueImpl<?, Size> y = parseSize(arg);
    2216
    2217         prev = arg;
    2218         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'");
    2219         ParsedValueImpl<?, Size> w = parseSize(arg);
    2220
    2221         prev = arg;
    2222         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'");
    2223         ParsedValueImpl<?, Size> h = parseSize(arg);
    2224
    2225         // If there are no more args, then we are done.
    2226         if (arg.nextArg == null) {
    2227             ParsedValueImpl[] values = new ParsedValueImpl[5];
    2228             values[0] = parsedURI;
    2229             values[1] = x;
    2230             values[2] = y;
    2231             values[3] = w;
    2232             values[4] = h;
    2233             return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance());
    2234         }
    2235
    2236         prev = arg;
    2237         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<boolean>\'");
    2238         if ((token = arg.token) == null || token.getText() == null) error(arg, "Expected \'<boolean>\'");
    2239
    2240         ParsedValueImpl[] values = new ParsedValueImpl[6];
    2241         values[0] = parsedURI;
    2242         values[1] = x;
    2243         values[2] = y;
    2244         values[3] = w;
    2245         values[4] = h;
    2246         values[5] = new ParsedValueImpl<Boolean, Boolean>(Boolean.parseBoolean(token.getText()), null);
    2247         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance());
    2248     }
    2249
    2250     // For tiling ImagePatterns easily.
    2251     //
    2252     // repeating-image-pattern(<uri-string>)
    2253     //
    2254     private ParsedValueImpl<ParsedValue[], Paint> parseRepeatingImagePattern(final Term root) throws ParseException {
    2255         // first term in the chain is the function name...
    2256         final String fn = (root.token != null) ? root.token.getText() : null;
    2257         // NOTE: We should put this in an assertion, so as not to do this work ordinarily, because only a
    2258         // bug in the parser can cause this.
    2259         assert !"repeating-image-pattern".regionMatches(true, 0, fn, 0, 23) : "Expected \'repeating-image-pattern\'";
    2260
    2261         Term arg;
    2262         if ((arg = root.firstArg) == null ||
    2263              arg.token == null ||
    2264              arg.token.getText().isEmpty()) {
    2265             error(root,
    2266                 "Expected \'<uri-string>\'");
    2267         }
    2268
    2269         final String uri = arg.token.getText();
    2270         ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
    2271             new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
    2272             null // placeholder for Stylesheet URL
    2273         };
    2274         ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
    2275         ParsedValueImpl[] values = new ParsedValueImpl[1];
    2276         values[0] = parsedURI;
    2277         return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RepeatingImagePatternConverter.getInstance());
    2278     }
    2279
    2280     // parse a series of paint values separated by commas.
    2281     // i.e., <paint> [, <paint>]*
    2282     private ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]> parsePaintLayers(Term root)
    2283             throws ParseException {
    2284
    2285         // how many paints in the series?
    2286         int nPaints = numberOfLayers(root);
    2287
    2288         ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[nPaints];
    2289
    2290         Term temp = root;
    2291         int paint = 0;
    2292
    2293         do {
    2294             if (temp.token == null ||
    2295                 temp.token.getText() == null ||
    2296                 temp.token.getText().isEmpty()) error(temp, "Expected \'<paint>\'");
    2297
    2298             paints[paint++] = (ParsedValueImpl<?,Paint>)parse(temp);
    2299
    2300             temp = nextLayer(temp);
    2301         } while (temp != null);
    2302
    2303         return new ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>(paints, PaintConverter.SequenceConverter.getInstance());
    2304
    2305     }
    2306
    2307     // An size or a series of four size values
    2308     // <size> | <size> <size> <size> <size>
    2309     private ParsedValueImpl<?,Size>[] parseSizeSeries(final Term root)
    2310             throws ParseException {
    2311
    2312         Term temp = root;
    2313         ParsedValueImpl<?,Size>[] sides = new ParsedValueImpl[4];
    2314         int side = 0;
    2315
    2316         while (side < 4 && temp != null) {
    2317             sides[side++] = parseSize(temp);
    2318             temp = temp.nextInSeries;
    2319         }
    2320
    2321         if (side < 2) sides[1] = sides[0]; // right = top
    2322         if (side < 3) sides[2] = sides[0]; // bottom = top
    2323         if (side < 4) sides[3] = sides[1]; // left = right
    2324
    2325         return sides;
    2326     }
    2327
    2328     // A series of inset or sets of four inset values
    2329     // <size> | <size> <size> <size> <size> [ , [ <size> | <size> <size> <size> <size>] ]*
    2330     private ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]> parseInsetsLayers(Term root)
    2331             throws ParseException {
    2332
    2333         int nLayers = numberOfLayers(root);
    2334
    2335         Term temp = root;
    2336         int layer = 0;
    2337         ParsedValueImpl<ParsedValue[],Insets>[] layers = new ParsedValueImpl[nLayers];
    2338
    2339         while(temp != null) {
    2340             ParsedValueImpl<?,Size>[] sides = parseSizeSeries(temp);
    2341             layers[layer++] = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance());
    2342             while(temp.nextInSeries != null) {
    2343                 temp = temp.nextInSeries;
    2344             }
    2345             temp = nextLayer(temp);
    2346         }
    2347
    2348         return new ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]>(layers, InsetsConverter.SequenceConverter.getInstance());
    2349     }
    2350
    2351     // A single inset (1, 2, 3, or 4 digits)
    2352     // <size> | <size> <size> <size> <size>
    2353     private ParsedValueImpl<ParsedValue[],Insets> parseInsetsLayer(Term root)
    2354             throws ParseException {
    2355
    2356         Term temp = root;
    2357         ParsedValueImpl<ParsedValue[],Insets> layer = null;
    2358
    2359         while(temp != null) {
    2360             ParsedValueImpl<?,Size>[] sides = parseSizeSeries(temp);
    2361             layer = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance());
    2362             while(temp.nextInSeries != null) {
    2363                 temp = temp.nextInSeries;
    2364             }
    2365             temp = nextLayer(temp);
    2366         }
    2367         return layer;
    2368     }
    2369
    2370     // <size> | <size> <size> <size> <size>
    2371     private ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]> parseMarginsLayers(Term root)
    2372             throws ParseException {
    2373
    2374         int nLayers = numberOfLayers(root);
    2375
    2376         Term temp = root;
    2377         int layer = 0;
    2378         ParsedValueImpl<ParsedValue[],Margins>[] layers = new ParsedValueImpl[nLayers];
    2379
    2380         while(temp != null) {
    2381             ParsedValueImpl<?,Size>[] sides = parseSizeSeries(temp);
    2382             layers[layer++] = new ParsedValueImpl<ParsedValue[],Margins>(sides, Margins.Converter.getInstance());
    2383             while(temp.nextInSeries != null) {
    2384                 temp = temp.nextInSeries;
    2385             }
    2386             temp = nextLayer(temp);
    2387         }
    2388
    2389         return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance());
    2390     }
    2391
    2392     /* Constant for background position */
    2393     private final static ParsedValueImpl<Size,Size> ZERO_PERCENT =
    2394             new ParsedValueImpl<Size,Size>(new Size(0f, SizeUnits.PERCENT), null);
    2395     /* Constant for background position */
    2396     private final static ParsedValueImpl<Size,Size> FIFTY_PERCENT =
    2397             new ParsedValueImpl<Size,Size>(new Size(50f, SizeUnits.PERCENT), null);
    2398     /* Constant for background position */
    2399     private final static ParsedValueImpl<Size,Size> ONE_HUNDRED_PERCENT =
    2400             new ParsedValueImpl<Size,Size>(new Size(100f, SizeUnits.PERCENT), null);
    2401
    2402     private static boolean isPositionKeyWord(String value) {
    2403         return "center".equalsIgnoreCase(value) || "top".equalsIgnoreCase(value) || "bottom".equalsIgnoreCase(value) || "left".equalsIgnoreCase(value) || "right".equalsIgnoreCase(value);
    2404     }
    2405
    2406     /*
    2407      * http://www.w3.org/TR/css3-background/#the-background-position
    2408      *
    2409      * <bg-position> = [
    2410      *   [ top | bottom ]
    2411      * |
    2412      *   [ <percentage> | <length> | left | center | right ]
    2413      *   [ <percentage> | <length> | top  | center | bottom ]?
    2414      * |
    2415      *   [ center | [ left | right  ] [ <percentage> | <length> ]? ] &&
    2416      *   [ center | [ top  | bottom ] [ <percentage> | <length> ]? ]
    2417      * ]
    2418      *
    2419      * From the W3 spec:
    2420      *
    2421      * returned ParsedValueImpl is [size, size, size, size] with the semantics
    2422      * [top offset, right offset, bottom offset left offset]
    2423      */
    2424     private ParsedValueImpl<ParsedValue[], BackgroundPosition> parseBackgroundPosition(Term term)
    2425         throws ParseException {
    2426
    2427         if (term.token == null ||
    2428             term.token.getText() == null ||
    2429             term.token.getText().isEmpty()) error(term, "Expected \'<bg-position>\'");
    2430
    2431         Term  termOne = term;
    2432         Token valueOne = term.token;
    2433
    2434         Term  termTwo = termOne.nextInSeries;
    2435         Token valueTwo = (termTwo != null) ? termTwo.token : null;
    2436
    2437         Term termThree = (termTwo != null) ? termTwo.nextInSeries : null;
    2438         Token valueThree = (termThree != null) ? termThree.token : null;
    2439
    2440         Term termFour = (termThree != null) ? termThree.nextInSeries : null;
    2441         Token valueFour = (termFour != null) ? termFour.token : null;
    2442
    2443         // are the horizontal and vertical exchanged
    2444         if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) {
    2445             // 2 values filled
    2446             String v1 = valueOne.getText();
    2447             String v2 = valueTwo.getText();
    2448             if( ("top".equals(v1) || "bottom".equals(v1))
    2449                     && ("left".equals(v2) || "right".equals(v2) || "center".equals(v2)) ) {
    2450                 {
    2451                     Token tmp = valueTwo;
    2452                     valueTwo = valueOne;
    2453                     valueOne = tmp;
    2454                 }
    2455
    2456                 {
    2457                     Term tmp = termTwo;
    2458                     termTwo = termOne;
    2459                     termOne = tmp;
    2460                 }
    2461             }
    2462         } else if( valueOne != null && valueTwo != null && valueThree != null ) {
    2463             Term[] termArray = null;
    2464             Token[] tokeArray = null;
    2465             // 4 values filled
    2466             if( valueFour != null ) {
    2467                 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText()))
    2468                         && ("left".equals(valueThree.getText()) || "right".equals(valueThree.getText())) ) {
    2469                     // e.g. top 50 left 20
    2470                     termArray = new Term[] { termThree, termFour, termOne, termTwo };
    2471                     tokeArray = new Token[] { valueThree, valueFour, valueOne, valueTwo };
    2472                 }
    2473             } else {
    2474                 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) ) {
    2475                     if( ("left".equals(valueTwo.getText()) || "right".equals(valueTwo.getText())) ) {
    2476                         // e.g. top left 50
    2477                         termArray = new Term[] { termTwo, termThree, termOne, null };
    2478                         tokeArray = new Token[] { valueTwo, valueThree, valueOne, null };
    2479                     } else {
    2480                         // e.g. top 50 left
    2481                         termArray = new Term[] { termThree, termOne, termTwo, null };
    2482                         tokeArray = new Token[] { valueThree, valueOne, valueTwo, null };
    2483                     }
    2484                 }
    2485             }
    2486
    2487             if( termArray != null ) {
    2488                 termOne = termArray[0];
    2489                 termTwo = termArray[1];
    2490                 termThree = termArray[2];
    2491                 termFour = termArray[3];
    2492
    2493                 valueOne = tokeArray[0];
    2494                 valueTwo = tokeArray[1];
    2495                 valueThree = tokeArray[2];
    2496                 valueFour = tokeArray[3];
    2497             }
    2498         }
    2499
    2500
    2501         ParsedValueImpl<?,Size> top, right, bottom, left;
    2502         top = right = bottom = left = ZERO_PERCENT;
    2503         {
    2504             if(valueOne == null && valueTwo == null && valueThree == null && valueFour == null) {
    2505                 error(term, "No value found for background-position");
    2506             } else if( valueOne != null && valueTwo == null && valueThree == null && valueFour == null ) {
    2507                 // Only one value
    2508                 String v1 = valueOne.getText();
    2509
    2510                 if( "center".equals(v1) ) {
    2511                     left = FIFTY_PERCENT;
    2512                     right = ZERO_PERCENT;
    2513
    2514                     top = FIFTY_PERCENT;
    2515                     bottom = ZERO_PERCENT;
    2516
    2517                 } else if("left".equals(v1)) {
    2518                     left = ZERO_PERCENT;
    2519                     right = ZERO_PERCENT;
    2520
    2521                     top = FIFTY_PERCENT;
    2522                     bottom = ZERO_PERCENT;
    2523
    2524                 } else if( "right".equals(v1) ) {
    2525                     left = ONE_HUNDRED_PERCENT;
    2526                     right = ZERO_PERCENT;
    2527
    2528                     top = FIFTY_PERCENT;
    2529                     bottom = ZERO_PERCENT;
    2530
    2531                 } else if( "top".equals(v1) ) {
    2532                     left = FIFTY_PERCENT;
    2533                     right = ZERO_PERCENT;
    2534
    2535                     top = ZERO_PERCENT;
    2536                     bottom = ZERO_PERCENT;
    2537
    2538                 } else if( "bottom".equals(v1) ) {
    2539                     left = FIFTY_PERCENT;
    2540                     right = ZERO_PERCENT;
    2541
    2542                     top = ONE_HUNDRED_PERCENT;
    2543                     bottom = ZERO_PERCENT;
    2544                 } else {
    2545                     left = parseSize(termOne);
    2546                     right = ZERO_PERCENT;
    2547                     top = ZERO_PERCENT;
    2548                     bottom = ZERO_PERCENT;
    2549                 }
    2550             } else if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) {
    2551                 // 2 values
    2552                 String v1 = valueOne.getText().toLowerCase(Locale.ROOT);
    2553                 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT);
    2554
    2555                 if( ! isPositionKeyWord(v1) ) {
    2556                     left = parseSize(termOne);
    2557                     right = ZERO_PERCENT;
    2558
    2559                     if( "top".equals(v2) ) {
    2560                         top = ZERO_PERCENT;
    2561                         bottom = ZERO_PERCENT;
    2562                     } else if( "bottom".equals(v2) ) {
    2563                         top = ONE_HUNDRED_PERCENT;
    2564                         bottom = ZERO_PERCENT;
    2565                     } else if( "center".equals(v2) ) {
    2566                         top = FIFTY_PERCENT;
    2567                         bottom = ZERO_PERCENT;
    2568                     } else if( !isPositionKeyWord(v2) ) {
    2569                         top = parseSize(termTwo);
    2570                         bottom = ZERO_PERCENT;
    2571                     } else {
    2572                         error(termTwo,"Expected 'top', 'bottom', 'center' or <size>");
    2573                     }
    2574                 } else if( v1.equals("left") || v1.equals("right") ) {
    2575                     left = v1.equals("right") ? ONE_HUNDRED_PERCENT : ZERO_PERCENT;
    2576                     right = ZERO_PERCENT;
    2577
    2578                     if( ! isPositionKeyWord(v2) ) {
    2579                         top = parseSize(termTwo);
    2580                         bottom = ZERO_PERCENT;
    2581                     } else if( v2.equals("top") || v2.equals("bottom") || v2.equals("center") ) {
    2582                         if( v2.equals("top") ) {
    2583                             top = ZERO_PERCENT;
    2584                             bottom = ZERO_PERCENT;
    2585                         } else if(v2.equals("center")) {
    2586                             top = FIFTY_PERCENT;
    2587                             bottom = ZERO_PERCENT;
    2588                         } else {
    2589                             top = ONE_HUNDRED_PERCENT;
    2590                             bottom = ZERO_PERCENT;
    2591                         }
    2592                     } else {
    2593                         error(termTwo,"Expected 'top', 'bottom', 'center' or <size>");
    2594                     }
    2595                 } else if( v1.equals("center") ) {
    2596                     left = FIFTY_PERCENT;
    2597                     right = ZERO_PERCENT;
    2598
    2599                     if( v2.equals("top") ) {
    2600                         top = ZERO_PERCENT;
    2601                         bottom = ZERO_PERCENT;
    2602                     } else if( v2.equals("bottom") ) {
    2603                         top = ONE_HUNDRED_PERCENT;
    2604                         bottom = ZERO_PERCENT;
    2605                     } else if( v2.equals("center") ) {
    2606                         top = FIFTY_PERCENT;
    2607                         bottom = ZERO_PERCENT;
    2608                     } else if( ! isPositionKeyWord(v2) ) {
    2609                         top = parseSize(termTwo);
    2610                         bottom = ZERO_PERCENT;
    2611                     } else {
    2612                         error(termTwo,"Expected 'top', 'bottom', 'center' or <size>");
    2613                     }
    2614                 }
    2615             } else if( valueOne != null && valueTwo != null && valueThree != null && valueFour == null ) {
    2616                 String v1 = valueOne.getText().toLowerCase(Locale.ROOT);
    2617                 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT);
    2618                 String v3 = valueThree.getText().toLowerCase(Locale.ROOT);
    2619
    2620                 if( ! isPositionKeyWord(v1) || "center".equals(v1) ) {
    2621                     // 1 is horizontal
    2622                     // means 2 & 3 are vertical
    2623                     if( "center".equals(v1) ) {
    2624                         left = FIFTY_PERCENT;
    2625                     } else {
    2626                         left = parseSize(termOne);
    2627                     }
    2628                     right = ZERO_PERCENT;
    2629
    2630                     if( !isPositionKeyWord(v3) ) {
    2631                         if( "top".equals(v2) ) {
    2632                             top = parseSize(termThree);
    2633                             bottom = ZERO_PERCENT;
    2634                         } else if( "bottom".equals(v2) ) {
    2635                             top = ZERO_PERCENT;
    2636                             bottom = parseSize(termThree);
    2637                         } else {
    2638                             error(termTwo,"Expected 'top' or 'bottom'");
    2639                         }
    2640                     } else {
    2641                         error(termThree,"Expected <size>");
    2642                     }
    2643                 } else if( "left".equals(v1) || "right".equals(v1)  ) {
    2644                     if( ! isPositionKeyWord(v2) ) {
    2645                         // 1 & 2 are horizontal
    2646                         // 3 is vertical
    2647                         if( "left".equals(v1) ) {
    2648                             left = parseSize(termTwo);
    2649                             right = ZERO_PERCENT;
    2650                         } else {
    2651                             left = ZERO_PERCENT;
    2652                             right = parseSize(termTwo);
    2653                         }
    2654
    2655                         if( "top".equals(v3) ) {
    2656                             top = ZERO_PERCENT;
    2657                             bottom = ZERO_PERCENT;
    2658                         } else if( "bottom".equals(v3) ) {
    2659                             top = ONE_HUNDRED_PERCENT;
    2660                             bottom = ZERO_PERCENT;
    2661                         } else if( "center".equals(v3) ) {
    2662                             top = FIFTY_PERCENT;
    2663                             bottom = ZERO_PERCENT;
    2664                         } else {
    2665                             error(termThree,"Expected 'top', 'bottom' or 'center'");
    2666                         }
    2667                     } else {
    2668                         // 1 is horizontal
    2669                         // 2 & 3 are vertical
    2670                         if( "left".equals(v1) ) {
    2671                             left = ZERO_PERCENT;
    2672                             right = ZERO_PERCENT;
    2673                         } else {
    2674                             left = ONE_HUNDRED_PERCENT;
    2675                             right = ZERO_PERCENT;
    2676                         }
    2677
    2678                         if( ! isPositionKeyWord(v3) ) {
    2679                             if( "top".equals(v2) ) {
    2680                                 top = parseSize(termThree);
    2681                                 bottom = ZERO_PERCENT;
    2682                             } else if( "bottom".equals(v2) ) {
    2683                                 top = ZERO_PERCENT;
    2684                                 bottom = parseSize(termThree);
    2685                             } else {
    2686                                 error(termTwo,"Expected 'top' or 'bottom'");
    2687                             }
    2688                         } else {
    2689                             error(termThree,"Expected <size>");
    2690                         }
    2691                     }
    2692                 }
    2693             } else {
    2694                 String v1 = valueOne.getText().toLowerCase(Locale.ROOT);
    2695                 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT);
    2696                 String v3 = valueThree.getText().toLowerCase(Locale.ROOT);
    2697                 String v4 = valueFour.getText().toLowerCase(Locale.ROOT);
    2698
    2699                 if( (v1.equals("left") || v1.equals("right")) && (v3.equals("top") || v3.equals("bottom") ) && ! isPositionKeyWord(v2) && ! isPositionKeyWord(v4) ) {
    2700                     if( v1.equals("left") ) {
    2701                         left = parseSize(termTwo);
    2702                         right = ZERO_PERCENT;
    2703                     } else {
    2704                         left = ZERO_PERCENT;
    2705                         right = parseSize(termTwo);
    2706                     }
    2707
    2708                     if( v3.equals("top") ) {
    2709                         top = parseSize(termFour);
    2710                         bottom = ZERO_PERCENT;
    2711                     } else {
    2712                         top = ZERO_PERCENT;
    2713                         bottom = parseSize(termFour);
    2714                     }
    2715
    2716                 } else {
    2717                     error(term,"Expected 'left' or 'right' followed by <size> followed by 'top' or 'bottom' followed by <size>");
    2718                 }
    2719             }
    2720         }
    2721
    2722         ParsedValueImpl<?,Size>[] values = new ParsedValueImpl[] {top, right, bottom, left};
    2723         return new ParsedValueImpl<ParsedValue[], BackgroundPosition>(values, BackgroundPositionConverter.getInstance());
    2724     }
    2725
    2726     private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>
    2727             parseBackgroundPositionLayers(final Term root) throws ParseException {
    2728
    2729         int nLayers = numberOfLayers(root);
    2730         ParsedValueImpl<ParsedValue[], BackgroundPosition>[] layers = new ParsedValueImpl[nLayers];
    2731         int layer = 0;
    2732         Term term = root;
    2733         while (term != null) {
    2734             layers[layer++] = parseBackgroundPosition(term);
    2735             term = nextLayer(term);
    2736         }
    2737         return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance());
    2738     }
    2739
    2740     /*
    2741     http://www.w3.org/TR/css3-background/#the-background-repeat
    2742     <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
    2743     */
    2744     private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root)
    2745             throws ParseException {
    2746
    2747         BackgroundRepeat xAxis, yAxis;
    2748         xAxis = yAxis = BackgroundRepeat.NO_REPEAT;
    2749
    2750         Term term = root;
    2751
    2752         if (term.token == null ||
    2753             term.token.getType() != CSSLexer.IDENT ||
    2754             term.token.getText() == null ||
    2755             term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'");
    2756
    2757         String text = term.token.getText().toLowerCase(Locale.ROOT);
    2758         if ("repeat-x".equals(text)) {
    2759             xAxis = BackgroundRepeat.REPEAT;
    2760             yAxis = BackgroundRepeat.NO_REPEAT;
    2761         } else if ("repeat-y".equals(text)) {
    2762             xAxis = BackgroundRepeat.NO_REPEAT;
    2763             yAxis = BackgroundRepeat.REPEAT;
    2764         } else if ("repeat".equals(text)) {
    2765             xAxis = BackgroundRepeat.REPEAT;
    2766             yAxis = BackgroundRepeat.REPEAT;
    2767         } else if ("space".equals(text)) {
    2768             xAxis = BackgroundRepeat.SPACE;
    2769             yAxis = BackgroundRepeat.SPACE;
    2770         } else if ("round".equals(text)) {
    2771             xAxis = BackgroundRepeat.ROUND;
    2772             yAxis = BackgroundRepeat.ROUND;
    2773         } else if ("no-repeat".equals(text)) {
    2774             xAxis = BackgroundRepeat.NO_REPEAT;
    2775             yAxis = BackgroundRepeat.NO_REPEAT;
    2776         } else if ("stretch".equals(text)) {
    2777             xAxis = BackgroundRepeat.NO_REPEAT;
    2778             yAxis = BackgroundRepeat.NO_REPEAT;
    2779         } else {
    2780             error(term, "Expected  \'<repeat-style>\' " + text);
    2781         }
    2782
    2783         if ((term = term.nextInSeries) != null &&
    2784              term.token != null &&
    2785              term.token.getType() == CSSLexer.IDENT &&
    2786              term.token.getText() != null &&
    2787              !term.token.getText().isEmpty()) {
    2788
    2789             text = term.token.getText().toLowerCase(Locale.ROOT);
    2790             if ("repeat-x".equals(text)) {
    2791                 error(term, "Unexpected \'repeat-x\'");
    2792             } else if ("repeat-y".equals(text)) {
    2793                 error(term, "Unexpected \'repeat-y\'");
    2794             } else if ("repeat".equals(text)) {
    2795                 yAxis = BackgroundRepeat.REPEAT;
    2796             } else if ("space".equals(text)) {
    2797                 yAxis = BackgroundRepeat.SPACE;
    2798             } else if ("round".equals(text)) {
    2799                 yAxis = BackgroundRepeat.ROUND;
    2800             } else if ("no-repeat".equals(text)) {
    2801                 yAxis = BackgroundRepeat.NO_REPEAT;
    2802             } else if ("stretch".equals(text)) {
    2803                 yAxis = BackgroundRepeat.NO_REPEAT;
    2804             } else {
    2805                 error(term, "Expected  \'<repeat-style>\'");
    2806             }
    2807         }
    2808
    2809         return new ParsedValueImpl[] {
    2810             new ParsedValueImpl<String,BackgroundRepeat>(xAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class)),
    2811             new ParsedValueImpl<String,BackgroundRepeat>(yAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class))
    2812         };
    2813     }
    2814
    2815     private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]>
    2816             parseBorderImageRepeatStyleLayers(final Term root) throws ParseException {
    2817
    2818         int nLayers = numberOfLayers(root);
    2819         ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][];
    2820         int layer = 0;
    2821         Term term = root;
    2822         while (term != null) {
    2823             layers[layer++] = parseRepeatStyle(term);
    2824             term = nextLayer(term);
    2825         }
    2826         return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
    2827     }
    2828
    2829
    2830     private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>
    2831             parseBackgroundRepeatStyleLayers(final Term root) throws ParseException {
    2832
    2833         int nLayers = numberOfLayers(root);
    2834         ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][];
    2835         int layer = 0;
    2836         Term term = root;
    2837         while (term != null) {
    2838             layers[layer++] = parseRepeatStyle(term);
    2839             term = nextLayer(term);
    2840         }
    2841         return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
    2842     }
    2843
    2844     /*
    2845     http://www.w3.org/TR/css3-background/#the-background-size
    2846     <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
    2847     */
    2848     private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root)
    2849         throws ParseException {
    2850
    2851         ParsedValueImpl<?,Size> height = null, width = null;
    2852         boolean cover = false, contain = false;
    2853
    2854         Term term = root;
    2855         if (term.token == null) error(term, "Expected \'<bg-size>\'");
    2856
    2857         if (term.token.getType() == CSSLexer.IDENT) {
    2858             final String text =
    2859                 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
    2860
    2861             if ("auto".equals(text)) {
    2862                 // We don't do anything because width / height are already initialized
    2863             } else if ("cover".equals(text)) {
    2864                 cover = true;
    2865             } else if ("contain".equals(text)) {
    2866                 contain = true;
    2867             } else if ("stretch".equals(text)) {
    2868                 width = ONE_HUNDRED_PERCENT;
    2869                 height = ONE_HUNDRED_PERCENT;
    2870             } else {
    2871                 error(term, "Expected \'auto\', \'cover\', \'contain\', or  \'stretch\'");
    2872             }
    2873         } else if (isSize(term.token)) {
    2874             width = parseSize(term);
    2875             height = null;
    2876         } else {
    2877             error(term, "Expected \'<bg-size>\'");
    2878         }
    2879
    2880         if ((term = term.nextInSeries) != null) {
    2881             if (cover || contain) error(term, "Unexpected \'<bg-size>\'");
    2882
    2883             if (term.token.getType() == CSSLexer.IDENT) {
    2884                 final String text =
    2885                     (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
    2886
    2887                 if ("auto".equals(text)) {
    2888                     height = null;
    2889                 } else if ("cover".equals(text)) {
    2890                     error(term, "Unexpected \'cover\'");
    2891                 } else if ("contain".equals(text)) {
    2892                     error(term, "Unexpected \'contain\'");
    2893                 } else if ("stretch".equals(text)) {
    2894                     height = ONE_HUNDRED_PERCENT;
    2895                 } else {
    2896                     error(term, "Expected \'auto\' or \'stretch\'");
    2897                 }
    2898             } else if (isSize(term.token)) {
    2899                 height = parseSize(term);
    2900             } else {
    2901                 error(term, "Expected \'<bg-size>\'");
    2902             }
    2903
    2904         }
    2905
    2906         ParsedValueImpl[] values = new ParsedValueImpl[] {
    2907             width,
    2908             height,
    2909             // TODO: handling of booleans is really bogus
    2910             new ParsedValueImpl<String,Boolean>((cover ? "true" : "false"), BooleanConverter.getInstance()),
    2911             new ParsedValueImpl<String,Boolean>((contain ? "true" : "false"), BooleanConverter.getInstance())
    2912         };
    2913         return new ParsedValueImpl<ParsedValue[], BackgroundSize>(values, BackgroundSizeConverter.getInstance());
    2914     }
    2915
    2916     private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[],  BackgroundSize[]>
    2917             parseBackgroundSizeLayers(final Term root) throws ParseException {
    2918
    2919         int nLayers = numberOfLayers(root);
    2920         ParsedValueImpl<ParsedValue[], BackgroundSize>[] layers = new ParsedValueImpl[nLayers];
    2921         int layer = 0;
    2922         Term term = root;
    2923         while (term != null) {
    2924             layers[layer++] = parseBackgroundSize(term);
    2925             term = nextLayer(term);
    2926         }
    2927         return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[], BackgroundSize[]>(layers, LayeredBackgroundSizeConverter.getInstance());
    2928     }
    2929
    2930     private ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]> parseBorderPaint(final Term root)
    2931         throws ParseException {
    2932
    2933         Term term = root;
    2934         ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[4];
    2935         int paint = 0;
    2936
    2937         while(term != null) {
    2938             if (term.token == null) error(term, "Expected \'<paint>\'");
    2939             paints[paint++] = parse(term);
    2940             term = term.nextInSeries;
    2941         }
    2942
    2943         if (paint < 2) paints[1] = paints[0]; // right = top
    2944         if (paint < 3) paints[2] = paints[0]; // bottom = top
    2945         if (paint < 4) paints[3] = paints[1]; // left = right
    2946
    2947         return new ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]>(paints, StrokeBorderPaintConverter.getInstance());
    2948     }
    2949
    2950     private ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]> parseBorderPaintLayers(final Term root)
    2951         throws ParseException {
    2952
    2953         int nLayers = numberOfLayers(root);
    2954         ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>[] layers = new ParsedValueImpl[nLayers];
    2955         int layer = 0;
    2956         Term term = root;
    2957         while (term != null) {
    2958             layers[layer++] = parseBorderPaint(term);
    2959             term = nextLayer(term);
    2960         }
    2961         return new ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]>(layers, LayeredBorderPaintConverter.getInstance());
    2962     }
    2963
    2964     // borderStyle (borderStyle (borderStyle borderStyle?)?)?
    2965     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]> parseBorderStyleSeries(final Term root)
    2966             throws ParseException {
    2967
    2968         Term term = root;
    2969         ParsedValueImpl<ParsedValue[],BorderStrokeStyle>[] borders = new ParsedValueImpl[4];
    2970         int border = 0;
    2971         while (term != null) {
    2972             borders[border++] = parseBorderStyle(term);
    2973             term = term.nextInSeries;
    2974         }
    2975
    2976         if (border < 2) borders[1] = borders[0]; // right = top
    2977         if (border < 3) borders[2] = borders[0]; // bottom = top
    2978         if (border < 4) borders[3] = borders[1]; // left = right
    2979
    2980         return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>(borders, BorderStrokeStyleSequenceConverter.getInstance());
    2981     }
    2982
    2983
    2984     private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>
    2985             parseBorderStyleLayers(final Term root) throws ParseException {
    2986
    2987         int nLayers = numberOfLayers(root);
    2988         ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers];
    2989         int layer = 0;
    2990         Term term = root;
    2991         while (term != null) {
    2992             layers[layer++] = parseBorderStyleSeries(term);
    2993             term = nextLayer(term);
    2994         }
    2995         return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance());
    2996     }
    2997
    2998     // Only meant to be used from parseBorderStyle, but might be useful elsewhere
    2999     private String getKeyword(final Term term) {
    3000         if (term != null &&
    3001              term.token != null &&
    3002              term.token.getType() == CSSLexer.IDENT &&
    3003              term.token.getText() != null &&
    3004              !term.token.getText().isEmpty()) {
    3005
    3006             return term.token.getText().toLowerCase(Locale.ROOT);
    3007         }
    3008         return null;
    3009     }
    3010
    3011     //<border-style> [ , <border-style> ]*
    3012     // where <border-style> =
    3013     //      <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]?
    3014     // where <dash-style> =
    3015     //      [ none | solid | dotted | dashed ]
    3016     private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root)
    3017             throws ParseException {
    3018
    3019
    3020         ParsedValue<ParsedValue[],Number[]> dashStyle = null;
    3021         ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null;
    3022         ParsedValue<String,StrokeType> strokeType = null;
    3023         ParsedValue<String,StrokeLineJoin> strokeLineJoin = null;
    3024         ParsedValue<ParsedValue<?,Size>,Number> strokeMiterLimit = null;
    3025         ParsedValue<String,StrokeLineCap> strokeLineCap = null;
    3026
    3027         Term term = root;
    3028
    3029         dashStyle = dashStyle(term);
    3030
    3031         Term prev = term;
    3032         term = term.nextInSeries;
    3033         String keyword = getKeyword(term);
    3034
    3035         // dash-style might be followed by 'phase <size>'
    3036         if ("phase".equals(keyword)) {
    3037
    3038             prev = term;
    3039             if ((term = term.nextInSeries) == null ||
    3040                  term.token == null ||
    3041                  !isSize(term.token)) error(term, "Expected \'<size>\'");
    3042
    3043             ParsedValueImpl<?,Size> sizeVal = parseSize(term);
    3044             dashPhase = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance());
    3045
    3046             prev = term;
    3047             term = term.nextInSeries;
    3048         }
    3049
    3050         // stroke type might be next
    3051         strokeType = parseStrokeType(term);
    3052         if (strokeType != null) {
    3053             prev = term;
    3054             term = term.nextInSeries;
    3055         }
    3056
    3057         keyword = getKeyword(term);
    3058
    3059         if ("line-join".equals(keyword)) {
    3060
    3061             prev = term;
    3062             term = term.nextInSeries;
    3063
    3064             ParsedValueImpl[] lineJoinValues = parseStrokeLineJoin(term);
    3065             if (lineJoinValues != null) {
    3066                 strokeLineJoin = lineJoinValues[0];
    3067                 strokeMiterLimit = lineJoinValues[1];
    3068             } else {
    3069                 error(term, "Expected \'miter <size>?\', \'bevel\' or \'round\'");
    3070             }
    3071             prev = term;
    3072             term = term.nextInSeries;
    3073             keyword = getKeyword(term);
    3074         }
    3075
    3076         if ("line-cap".equals(keyword)) {
    3077
    3078             prev = term;
    3079             term = term.nextInSeries;
    3080
    3081             strokeLineCap = parseStrokeLineCap(term);
    3082             if (strokeLineCap == null) {
    3083                 error(term, "Expected \'square\', \'butt\' or \'round\'");
    3084             }
    3085
    3086             prev = term;
    3087             term = term.nextInSeries;
    3088         }
    3089
    3090         if (term != null) {
    3091             // if term is not null, then we have gotten to the end of
    3092             // one border style and term is start of another border style
    3093             root.nextInSeries = term;
    3094         } else {
    3095             // If term is null, then we have gotten to the end of
    3096             // a border style with no more to follow in this series.
    3097             // But there may be another layer.
    3098             root.nextInSeries = null;
    3099             root.nextLayer = prev.nextLayer;
    3100         }
    3101
    3102         final ParsedValue[] values = new ParsedValue[]{
    3103             dashStyle,
    3104             dashPhase,
    3105             strokeType,
    3106             strokeLineJoin,
    3107             strokeMiterLimit,
    3108             strokeLineCap
    3109         };
    3110
    3111         return new ParsedValueImpl(values, BorderStyleConverter.getInstance());
    3112     }
    3113
    3114     //
    3115     // segments(<size> [, <size>]+) | <border-style>
    3116     //
    3117     private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException {
    3118
    3119         if (root.token == null) error(root, "Expected \'<dash-style>\'");
    3120
    3121         final int ttype = root.token.getType();
    3122
    3123         ParsedValue<ParsedValue[],Number[]>  segments = null;
    3124         if (ttype == CSSLexer.IDENT) {
    3125             segments = borderStyle(root);
    3126         } else if (ttype == CSSLexer.FUNCTION) {
    3127             segments = segments(root);
    3128         } else {
    3129             error(root, "Expected \'<dash-style>\'");
    3130         }
    3131
    3132         return segments;
    3133     }
    3134
    3135     /*
    3136     <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
    3137     */
    3138     private ParsedValue<ParsedValue[],Number[]>  borderStyle(Term root)
    3139             throws ParseException {
    3140
    3141         if (root.token == null ||
    3142             root.token.getType() != CSSLexer.IDENT ||
    3143             root.token.getText() == null ||
    3144             root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'");
    3145
    3146         final String text = root.token.getText().toLowerCase(Locale.ROOT);
    3147
    3148         if ("none".equals(text)) {
    3149             return BorderStyleConverter.NONE;
    3150         } else if ("hidden".equals(text)) {
    3151             // The "hidden" mode doesn't make sense for FX, because it is the
    3152             // same as "none" except for border-collapsed CSS tables
    3153             return BorderStyleConverter.NONE;
    3154         } else if ("dotted".equals(text)) {
    3155             return BorderStyleConverter.DOTTED;
    3156         } else if ("dashed".equals(text)) {
    3157             return BorderStyleConverter.DASHED;
    3158         } else if ("solid".equals(text)) {
    3159             return BorderStyleConverter.SOLID;
    3160         } else if ("double".equals(text)) {
    3161             error(root, "Unsupported <border-style> \'double\'");
    3162         } else if ("groove".equals(text)) {
    3163             error(root, "Unsupported <border-style> \'groove\'");
    3164         } else if ("ridge".equals(text)) {
    3165             error(root, "Unsupported <border-style> \'ridge\'");
    3166         } else if ("inset".equals(text)) {
    3167             error(root, "Unsupported <border-style> \'inset\'");
    3168         } else if ("outset".equals(text)) {
    3169             error(root, "Unsupported <border-style> \'outset\'");
    3170         } else {
    3171             error(root, "Unsupported <border-style> \'" + text + "\'");
    3172         }
    3173         // error throws so we should never get here.
    3174         // but the compiler wants a return, so here it is.
    3175         return BorderStyleConverter.SOLID;
    3176     }
    3177
    3178     private ParsedValueImpl<ParsedValue[],Number[]> segments(Term root)
    3179             throws ParseException {
    3180
    3181         // first term in the chain is the function name...
    3182         final String fn = (root.token != null) ? root.token.getText() : null;
    3183         if (!"segments".regionMatches(true, 0, fn, 0, 8)) {
    3184             error(root,"Expected \'segments\'");
    3185         }
    3186
    3187         Term arg = root.firstArg;
    3188         if (arg == null) error(null, "Expected \'<size>\'");
    3189
    3190         int nArgs = numberOfArgs(root);
    3191         ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs];
    3192         int segment = 0;
    3193         while(arg != null) {
    3194             segments[segment++] = parseSize(arg);
    3195             arg = arg.nextArg;
    3196         }
    3197
    3198         return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SizeConverter.SequenceConverter.getInstance());
    3199
    3200     }
    3201
    3202     private ParsedValueImpl<String,StrokeType> parseStrokeType(final Term root)
    3203         throws ParseException {
    3204
    3205         final String keyword = getKeyword(root);
    3206
    3207
    3208         if ("centered".equals(keyword) ||
    3209             "inside".equals(keyword) ||
    3210             "outside".equals(keyword)) {
    3211
    3212             return new ParsedValueImpl<String,StrokeType>(keyword, new EnumConverter(StrokeType.class));
    3213
    3214         }
    3215         return null;
    3216     }
    3217
    3218     // Root term is the term just after the line-join keyword
    3219     // Returns an array of two Values or null.
    3220     // ParsedValueImpl[0] is ParsedValueImpl<StrokeLineJoin,StrokeLineJoin>
    3221     // ParsedValueImpl[1] is ParsedValueImpl<Value<?,Size>,Number> if miter limit is given, null otherwise.
    3222     // If the token is not a StrokeLineJoin, then null is returned.
    3223     private ParsedValueImpl[] parseStrokeLineJoin(final Term root)
    3224         throws ParseException {
    3225
    3226         final String keyword = getKeyword(root);
    3227
    3228         if ("miter".equals(keyword) ||
    3229             "bevel".equals(keyword) ||
    3230             "round".equals(keyword)) {
    3231
    3232             ParsedValueImpl<String,StrokeLineJoin> strokeLineJoin =
    3233                     new ParsedValueImpl<String,StrokeLineJoin>(keyword, new EnumConverter(StrokeLineJoin.class));
    3234
    3235             ParsedValueImpl<ParsedValue<?,Size>,Number> strokeMiterLimit = null;
    3236             if ("miter".equals(keyword)) {
    3237
    3238                 Term next = root.nextInSeries;
    3239                 if (next != null &&
    3240                     next.token != null &&
    3241                     isSize(next.token)) {
    3242
    3243                     root.nextInSeries = next.nextInSeries;
    3244                     ParsedValueImpl<?,Size> sizeVal = parseSize(next);
    3245                     strokeMiterLimit = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance());
    3246                 }
    3247
    3248             }
    3249
    3250             return new ParsedValueImpl[] { strokeLineJoin, strokeMiterLimit };
    3251         }
    3252         return null;
    3253     }
    3254
    3255     // Root term is the term just after the line-cap keyword
    3256     // If the token is not a StrokeLineCap, then null is returned.
    3257     private ParsedValueImpl<String,StrokeLineCap> parseStrokeLineCap(final Term root)
    3258         throws ParseException {
    3259
    3260         final String keyword = getKeyword(root);
    3261
    3262         if ("square".equals(keyword) ||
    3263             "butt".equals(keyword) ||
    3264             "round".equals(keyword)) {
    3265
    3266             return new ParsedValueImpl<String,StrokeLineCap>(keyword, new EnumConverter(StrokeLineCap.class));
    3267         }
    3268         return null;
    3269     }
    3270
    3271     /*
    3272      * http://www.w3.org/TR/css3-background/#the-border-image-slice
    3273      * [<number> | <percentage>]{1,4} && fill?
    3274      */
    3275     private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root)
    3276         throws ParseException {
    3277
    3278         Term term = root;
    3279         if (term.token == null || !isSize(term.token))
    3280                 error(term, "Expected \'<size>\'");
    3281
    3282         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
    3283         Boolean fill = Boolean.FALSE;
    3284
    3285         int inset = 0;
    3286         while (inset < 4 && term != null) {
    3287             insets[inset++] = parseSize(term);
    3288
    3289             if ((term = term.nextInSeries) != null &&
    3290                  term.token != null &&
    3291                  term.token.getType() == CSSLexer.IDENT) {
    3292
    3293                 if("fill".equalsIgnoreCase(term.token.getText())) {
    3294                     fill = Boolean.TRUE;
    3295                     break;
    3296                 }
    3297             }
    3298         }
    3299
    3300         if (inset < 2) insets[1] = insets[0]; // right = top
    3301         if (inset < 3) insets[2] = insets[0]; // bottom = top
    3302         if (inset < 4) insets[3] = insets[1]; // left = right
    3303
    3304         ParsedValueImpl[] values = new ParsedValueImpl[] {
    3305                 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()),
    3306                 new ParsedValueImpl<Boolean,Boolean>(fill, null)
    3307         };
    3308         return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance());
    3309     }
    3310
    3311     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]>
    3312             parseBorderImageSliceLayers(final Term root) throws ParseException {
    3313
    3314         int nLayers = numberOfLayers(root);
    3315         ParsedValueImpl<ParsedValue[], BorderImageSlices>[] layers = new ParsedValueImpl[nLayers];
    3316         int layer = 0;
    3317         Term term = root;
    3318         while (term != null) {
    3319             layers[layer++] = parseBorderImageSlice(term);
    3320             term = nextLayer(term);
    3321         }
    3322         return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]> (layers, SliceSequenceConverter.getInstance());
    3323     }
    3324
    3325     /*
    3326      * http://www.w3.org/TR/css3-background/#border-image-width
    3327      * [ <length> | <percentage> | <number> | auto ]{1,4}
    3328      */
    3329     private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root)
    3330             throws ParseException {
    3331
    3332         Term term = root;
    3333         if (term.token == null || !isSize(term.token))
    3334             error(term, "Expected \'<size>\'");
    3335
    3336         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
    3337
    3338         int inset = 0;
    3339         while (inset < 4 && term != null) {
    3340             insets[inset++] = parseSize(term);
    3341
    3342             if ((term = term.nextInSeries) != null &&
    3343                     term.token != null &&
    3344                     term.token.getType() == CSSLexer.IDENT) {
    3345             }
    3346         }
    3347
    3348         if (inset < 2) insets[1] = insets[0]; // right = top
    3349         if (inset < 3) insets[2] = insets[0]; // bottom = top
    3350         if (inset < 4) insets[3] = insets[1]; // left = right
    3351
    3352         return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance());
    3353     }
    3354
    3355     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]>
    3356         parseBorderImageWidthLayers(final Term root) throws ParseException {
    3357
    3358         int nLayers = numberOfLayers(root);
    3359         ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers];
    3360         int layer = 0;
    3361         Term term = root;
    3362         while (term != null) {
    3363             layers[layer++] = parseBorderImageWidth(term);
    3364             term = nextLayer(term);
    3365         }
    3366         return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance());
    3367     }
    3368
    3369     // parse a Region value
    3370     // i.e., region(".styleClassForRegion") or region("#idForRegion")
    3371     public static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:";
    3372     private ParsedValueImpl<String,String> parseRegion(Term root)
    3373             throws ParseException {
    3374         // first term in the chain is the function name...
    3375         final String fn = (root.token != null) ? root.token.getText() : null;
    3376         if (!"region".regionMatches(true, 0, fn, 0, 6)) {
    3377             error(root,"Expected \'region\'");
    3378         }
    3379
    3380         Term arg = root.firstArg;
    3381         if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
    3382
    3383         if (arg.token == null ||
    3384                 arg.token.getType() != CSSLexer.STRING ||
    3385                 arg.token.getText() == null ||
    3386                 arg.token.getText().isEmpty())  error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
    3387
    3388         final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText());
    3389         return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance());
    3390     }
    3391
    3392     // parse a URI value
    3393     // i.e., url("<uri>")
    3394     private ParsedValueImpl<ParsedValue[],String> parseURI(Term root)
    3395             throws ParseException {
    3396
    3397         // first term in the chain is the function name...
    3398         final String fn = (root.token != null) ? root.token.getText() : null;
    3399         if (!"url".regionMatches(true, 0, fn, 0, 3)) {
    3400             error(root,"Expected \'url\'");
    3401         }
    3402
    3403         Term arg = root.firstArg;
    3404         if (arg == null) error(root, "Expected \'url(\"<uri-string>\")\'");
    3405
    3406         if (arg.token == null ||
    3407             arg.token.getType() != CSSLexer.STRING ||
    3408             arg.token.getText() == null ||
    3409             arg.token.getText().isEmpty()) error(arg, "Expected \'url(\"<uri-string>\")\'");
    3410
    3411         final String uri = arg.token.getText();
    3412         ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
    3413             new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
    3414             null // placeholder for Stylesheet URL
    3415         };
    3416         return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
    3417     }
    3418
    3419     // parse a series of URI values separated by commas.
    3420     // i.e., <uri> [, <uri>]*
    3421     private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root)
    3422             throws ParseException {
    3423
    3424         int nLayers = numberOfLayers(root);
    3425
    3426         Term temp = root;
    3427         int layer = 0;
    3428         ParsedValueImpl<ParsedValue[],String>[] layers = new ParsedValueImpl[nLayers];
    3429
    3430         while(temp != null) {
    3431             layers[layer++] = parseURI(temp);
    3432             temp = nextLayer(temp);
    3433         }
    3434
    3435         return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance());
    3436     }
    3437
    3438     ////////////////////////////////////////////////////////////////////////////
    3439     //
    3440     // http://www.w3.org/TR/css3-fonts
    3441     //
    3442     ////////////////////////////////////////////////////////////////////////////
    3443
    3444     /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */
    3445     private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException {
    3446
    3447         if (root == null) return null;
    3448         final Token token = root.token;
    3449         if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'");
    3450
    3451         Size size = null;
    3452         if (token.getType() == CSSLexer.IDENT) {
    3453             final String ident = token.getText().toLowerCase(Locale.ROOT);
    3454             double value = -1;
    3455             if ("inherit".equals(ident)) {
    3456                 value = 100;
    3457             } else if ("xx-small".equals(ident)) {
    3458                 value = 60;
    3459             } else if ("x-small".equals(ident)) {
    3460                 value = 75;
    3461             } else if ("small".equals(ident)) {
    3462                 value = 80;
    3463             } else if ("medium".equals(ident)) {
    3464                 value = 100;
    3465             } else if ("large".equals(ident)) {
    3466                 value = 120;
    3467             } else if ("x-large".equals(ident)) {
    3468                 value = 150;
    3469             } else if ("xx-large".equals(ident)) {
    3470                 value = 200;
    3471             } else if ("smaller".equals(ident)) {
    3472                 value = 80;
    3473             } else if ("larger".equals(ident)) {
    3474                 value = 120;
    3475             }
    3476
    3477             if (value > -1) {
    3478                 size = new Size(value, SizeUnits.PERCENT);
    3479             }
    3480         }
    3481
    3482         // if size is null, then size is not one of the keywords above.
    3483         if (size == null) {
    3484             size = size(token);
    3485         }
    3486
    3487         ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null);
    3488         return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance());
    3489     }
    3490
    3491     /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */
    3492     private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException {
    3493
    3494         if (root == null) return null;
    3495         final Token token = root.token;
    3496         if (token == null ||
    3497             token.getType() != CSSLexer.IDENT ||
    3498             token.getText() == null ||
    3499             token.getText().isEmpty()) error(root, "Expected \'<font-style>\'");
    3500
    3501         final String ident = token.getText().toLowerCase(Locale.ROOT);
    3502         String posture = FontPosture.REGULAR.name();
    3503
    3504         if ("normal".equals(ident)) {
    3505             posture = FontPosture.REGULAR.name();
    3506         } else if ("italic".equals(ident)) {
    3507             posture = FontPosture.ITALIC.name();
    3508         } else if ("oblique".equals(ident)) {
    3509             posture = FontPosture.ITALIC.name();
    3510         } else if ("inherit".equals(ident)) {
    3511             posture = "inherit";
    3512         } else {
    3513             return null;
    3514         }
    3515
    3516         return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance());
    3517     }
    3518
    3519     /* http://www.w3.org/TR/css3-fonts/#font-weight-the-font-weight-property */
    3520     private ParsedValueImpl<String, FontWeight> parseFontWeight(Term root) throws ParseException {
    3521
    3522         if (root == null) return null;
    3523         final Token token = root.token;
    3524         if (token == null ||
    3525             token.getText() == null ||
    3526             token.getText().isEmpty()) error(root, "Expected \'<font-weight>\'");
    3527
    3528         final String ident = token.getText().toLowerCase(Locale.ROOT);
    3529         String weight = FontWeight.NORMAL.name();
    3530
    3531         if ("inherit".equals(ident)) {
    3532             weight = FontWeight.NORMAL.name();
    3533         } else if ("normal".equals(ident)) {
    3534             weight = FontWeight.NORMAL.name();
    3535         } else if ("bold".equals(ident)) {
    3536             weight = FontWeight.BOLD.name();
    3537         } else if ("bolder".equals(ident)) {
    3538             weight = FontWeight.BOLD.name();
    3539         } else if ("lighter".equals(ident)) {
    3540             weight = FontWeight.LIGHT.name();
    3541         } else if ("100".equals(ident)) {
    3542             weight = FontWeight.findByWeight(100).name();
    3543         } else if ("200".equals(ident)) {
    3544             weight = FontWeight.findByWeight(200).name();
    3545         } else if ("300".equals(ident)) {
    3546             weight = FontWeight.findByWeight(300).name();
    3547         } else if ("400".equals(ident)) {
    3548             weight = FontWeight.findByWeight(400).name();
    3549         } else if ("500".equals(ident)) {
    3550             weight = FontWeight.findByWeight(500).name();
    3551         } else if ("600".equals(ident)) {
    3552             weight = FontWeight.findByWeight(600).name();
    3553         } else if ("700".equals(ident)) {
    3554             weight = FontWeight.findByWeight(700).name();
    3555         } else if ("800".equals(ident)) {
    3556             weight = FontWeight.findByWeight(800).name();
    3557         } else if ("900".equals(ident)) {
    3558             weight = FontWeight.findByWeight(900).name();
    3559         } else {
    3560             error(root, "Expected \'<font-weight>\'");
    3561         }
    3562         return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance());
    3563     }
    3564
    3565     private ParsedValueImpl<String,String>  parseFontFamily(Term root) throws ParseException {
    3566
    3567         if (root == null) return null;
    3568         final Token token = root.token;
    3569         String text = null;
    3570         if (token == null ||
    3571             (token.getType() != CSSLexer.IDENT &&
    3572              token.getType() != CSSLexer.STRING) ||
    3573             (text = token.getText()) == null ||
    3574             text.isEmpty()) error(root, "Expected \'<font-family>\'");
    3575
    3576         final String fam = stripQuotes(text.toLowerCase(Locale.ROOT));
    3577         if ("inherit".equals(fam)) {
    3578             return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance());
    3579         } else if ("serif".equals(fam) ||
    3580             "sans-serif".equals(fam) ||
    3581             "cursive".equals(fam) ||
    3582             "fantasy".equals(fam) ||
    3583             "monospace".equals(fam)) {
    3584             return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance());
    3585         } else {
    3586             return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance());
    3587         }
    3588     }
    3589
    3590     // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily
    3591     private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException {
    3592
    3593         // Because style, variant, weight, size and family can inherit
    3594         // AND style, variant and weight are optional, parsing this backwards
    3595         // is easier.
    3596         Term next = root.nextInSeries;
    3597         root.nextInSeries = null;
    3598         while (next != null) {
    3599             Term temp = next.nextInSeries;
    3600             next.nextInSeries = root;
    3601             root = next;
    3602             next = temp;
    3603         }
    3604
    3605         // Now, root should point to fontFamily
    3606         Token token = root.token;
    3607         int ttype = token.getType();
    3608         if (ttype != CSSLexer.IDENT &&
    3609             ttype != CSSLexer.STRING) error(root, "Expected \'<font-family>\'");
    3610         ParsedValueImpl<String,String> ffamily = parseFontFamily(root);
    3611
    3612         Term term = root;
    3613         if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'");
    3614         if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
    3615
    3616         // Now, term could be the font size or it could be the line-height.
    3617         // If the next term is a forward slash, then it's line-height.
    3618         Term temp;
    3619         if (((temp = term.nextInSeries) != null) &&
    3620             (temp.token != null && temp.token.getType() == CSSLexer.SOLIDUS)) {
    3621
    3622             root = temp;
    3623
    3624             if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'");
    3625             if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
    3626
    3627             token = term.token;
    3628         }
    3629
    3630         ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term);
    3631         if (fsize == null) error(root, "Expected \'<size>\'");
    3632
    3633         ParsedValueImpl<String,FontPosture> fstyle = null;
    3634         ParsedValueImpl<String,FontWeight> fweight = null;
    3635         String fvariant = null;
    3636
    3637         while ((term = term.nextInSeries) != null) {
    3638
    3639             if (term.token == null ||
    3640                 term.token.getType() != CSSLexer.IDENT ||
    3641                 term.token.getText() == null ||
    3642                 term.token.getText().isEmpty())
    3643                 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'");
    3644
    3645             if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) {
    3646                 ;
    3647             } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) {
    3648                 fvariant = term.token.getText();
    3649             } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) {
    3650                 ;
    3651             }
    3652         }
    3653
    3654         ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle };
    3655         return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance());
    3656     }
    3657
    3658     //
    3659     // Parser state machine
    3660     //
    3661     Token currentToken = null;
    3662
    3663     // return the next token that is not whitespace.
    3664     private Token nextToken(CSSLexer lexer) {
    3665
    3666         Token token = null;
    3667
    3668         do {
    3669             token = lexer.nextToken();
    3670         } while ((token != null) &&
    3671                 (token.getType() == CSSLexer.WS) ||
    3672                 (token.getType() == CSSLexer.NL));
    3673
    3674         if (LOGGER.isLoggable(Level.FINEST)) {
    3675             LOGGER.finest(token.toString());
    3676         }
    3677
    3678         return token;
    3679
    3680     }
    3681
    3682     private void parse(Stylesheet stylesheet, CSSLexer lexer) {
    3683
    3684         // need to read the first token
    3685         currentToken = nextToken(lexer);
    3686
    3687         while ((currentToken != null) &&
    3688                (currentToken.getType() != Token.EOF)) {
    3689
    3690             if (currentToken.getType() == CSSLexer.FONT_FACE) {
    3691                 FontFace fontFace = fontFace(lexer);
    3692                 if (fontFace != null) stylesheet.getFontFaces().add(fontFace);
    3693                 currentToken = nextToken(lexer);
    3694                 continue;
    3695             }
    3696
    3697             List<Selector> selectors = selectors(lexer);
    3698             if (selectors == null) return;
    3699
    3700             if ((currentToken == null) ||
    3701                 (currentToken.getType() != CSSLexer.LBRACE)) {
    3702                     final int line = currentToken != null ? currentToken.getLine() : -1;
    3703                     final int pos = currentToken != null ? currentToken.getOffset() : -1;
    3704                     final String msg =
    3705                         MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]",
    3706                         line,pos);
    3707                     CssError error = createError(msg);
    3708                     if (LOGGER.isLoggable(Level.WARNING)) {
    3709                         LOGGER.warning(error.toString());
    3710                     }
    3711                     reportError(error);
    3712                 currentToken = null;
    3713                 return;
    3714             }
    3715
    3716             // get past the LBRACE
    3717             currentToken = nextToken(lexer);
    3718
    3719             List<Declaration> declarations = declarations(lexer);
    3720             if (declarations == null) return;
    3721
    3722             if ((currentToken != null) &&
    3723                 (currentToken.getType() != CSSLexer.RBRACE)) {
    3724                     final int line = currentToken.getLine();
    3725                     final int pos = currentToken.getOffset();
    3726                     final String msg =
    3727                         MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]",
    3728                         line,pos);
    3729                     CssError error = createError(msg);
    3730                     if (LOGGER.isLoggable(Level.WARNING)) {
    3731                         LOGGER.warning(error.toString());
    3732                     }
    3733                     reportError(error);
    3734                 currentToken = null;
    3735                 return;
    3736             }
    3737
    3738             stylesheet.getRules().add(new Rule(selectors, declarations));
    3739
    3740             currentToken = nextToken(lexer);
    3741
    3742         }
    3743         currentToken = null;
    3744     }
    3745
    3746     private FontFace fontFace(CSSLexer lexer) {
    3747         final Map<String,String> descriptors = new HashMap<String,String>();
    3748         final List<FontFace.FontFaceSrc> sources = new ArrayList<FontFace.FontFaceSrc>();
    3749         while(true) {
    3750             currentToken = nextToken(lexer);
    3751             if (currentToken.getType() == CSSLexer.IDENT) {
    3752                 String key = currentToken.getText();
    3753                 // ignore the colon that follows
    3754                 currentToken = nextToken(lexer);
    3755                 // get the next token after colon
    3756                 currentToken = nextToken(lexer);
    3757                 // ignore all but "src"
    3758                 if ("src".equalsIgnoreCase(key)) {
    3759                     while(true) {
    3760                         if((currentToken != null) &&
    3761                                 (currentToken.getType() != CSSLexer.SEMI) &&
    3762                                 (currentToken.getType() != CSSLexer.RBRACE) &&
    3763                                 (currentToken.getType() != Token.EOF)) {
    3764
    3765                             if (currentToken.getType() == CSSLexer.IDENT) {
    3766                                 // simple reference to other font-family
    3767                                 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.REFERENCE,currentToken.getText()));
    3768                             } else if (currentToken.getType() == CSSLexer.FUNCTION) {
    3769                                 if ("url(".equalsIgnoreCase(currentToken.getText())) {
    3770                                     // consume the function token
    3771                                     currentToken = nextToken(lexer);
    3772                                     // parse function contents
    3773                                     final StringBuilder urlSb = new StringBuilder();
    3774                                     while(true) {
    3775                                         if((currentToken != null) && (currentToken.getType() != CSSLexer.RPAREN) &&
    3776                                                 (currentToken.getType() != Token.EOF)) {
    3777                                             urlSb.append(currentToken.getText());
    3778                                         } else {
    3779                                             break;
    3780                                         }
    3781                                         currentToken = nextToken(lexer);
    3782                                     }
    3783                                     int start = 0, end = urlSb.length();
    3784                                     if (urlSb.charAt(start) == '\'' || urlSb.charAt(start) == '\"') start ++;
    3785                                     if (urlSb.charAt(start) == '/' || urlSb.charAt(start) == '\\') start ++;
    3786                                     if (urlSb.charAt(end-1) == '\'' || urlSb.charAt(end-1) == '\"') end --;
    3787                                     final String urlStr = urlSb.substring(start,end);
    3788
    3789                                     URL url = null;
    3790                                     Exception exception = null;
    3791                                     try {
    3792                                         URI stylesheetUri = new URI(sourceOfStylesheet);
    3793                                         URI fontUri = stylesheetUri.resolve(urlStr);
    3794                                         url = fontUri.toURL();
    3795                                     } catch (URISyntaxException |  MalformedURLException malf) {
    3796
    3797                                         final int line = currentToken.getLine();
    3798                                         final int pos = currentToken.getOffset();
    3799                                         final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]",line,pos,urlStr);
    3800                                         CssError error = createError(msg);
    3801                                         if (LOGGER.isLoggable(Level.WARNING)) {
    3802                                             LOGGER.warning(error.toString());
    3803                                         }
    3804                                         reportError(error);
    3805
    3806                                         // skip the rest.
    3807                                         while(currentToken != null) {
    3808                                             int ttype = currentToken.getType();
    3809                                             if (ttype == CSSLexer.RPAREN ||
    3810                                                 ttype == Token.EOF) {
    3811                                                 return null;
    3812                                             }
    3813                                             currentToken = nextToken(lexer);
    3814                                         }
    3815                                     }
    3816                                     String format = null;
    3817                                     while(true) {
    3818                                         currentToken = nextToken(lexer);
    3819                                         final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF;
    3820                                         if (ttype == CSSLexer.FUNCTION) {
    3821                                             if ("format(".equalsIgnoreCase(currentToken.getText())) {
    3822                                                 continue;
    3823                                             } else {
    3824                                                 break;
    3825                                             }
    3826                                         } else if (ttype == CSSLexer.IDENT ||
    3827                                                 ttype == CSSLexer.STRING) {
    3828
    3829                                             format = Utils.stripQuotes(currentToken.getText());
    3830                                         } else if (ttype == CSSLexer.RPAREN) {
    3831                                             continue;
    3832                                         } else {
    3833                                             break;
    3834                                         }
    3835                                     }
    3836
    3837                                     sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.URL,url.toExternalForm(), format));
    3838                                 } else if ("local(".equalsIgnoreCase(currentToken.getText())) {
    3839                                     // consume the function token
    3840                                     currentToken = nextToken(lexer);
    3841                                     // parse function contents
    3842                                     final StringBuilder localSb = new StringBuilder();
    3843                                     while(true) {
    3844                                         if((currentToken != null) && (currentToken.getType() != CSSLexer.RPAREN) &&
    3845                                                 (currentToken.getType() != Token.EOF)) {
    3846                                             localSb.append(currentToken.getText());
    3847                                         } else {
    3848                                             break;
    3849                                         }
    3850                                         currentToken = nextToken(lexer);
    3851                                     }
    3852                                     int start = 0, end = localSb.length();
    3853                                     if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++;
    3854                                     if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --;
    3855                                     final String local = localSb.substring(start,end);
    3856                                     sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.LOCAL,local));
    3857                                 } else {
    3858                                     // error unknown fontface src type
    3859                                     final int line = currentToken.getLine();
    3860                                     final int pos = currentToken.getOffset();
    3861                                     final String msg = MessageFormat.format("Unknown @font-face src type ["+currentToken.getText()+")] at [{0,number,#},{1,number,#}]",line,pos);
    3862                                     CssError error = createError(msg);
    3863                                     if (LOGGER.isLoggable(Level.WARNING)) {
    3864                                         LOGGER.warning(error.toString());
    3865                                     }
    3866                                     reportError(error);
    3867
    3868                                 }
    3869                             } else  if (currentToken.getType() == CSSLexer.COMMA) {
    3870                                 // ignore
    3871                             } else {
    3872                                 // error unexpected token
    3873                                 final int line = currentToken.getLine();
    3874                                 final int pos = currentToken.getOffset();
    3875                                 final String msg = MessageFormat.format("Unexpected TOKEN ["+currentToken.getText()+"] at [{0,number,#},{1,number,#}]",line,pos);
    3876                                 CssError error = createError(msg);
    3877                                 if (LOGGER.isLoggable(Level.WARNING)) {
    3878                                     LOGGER.warning(error.toString());
    3879                                 }
    3880                                 reportError(error);
    3881                             }
    3882                         } else {
    3883                             break;
    3884                         }
    3885                         currentToken = nextToken(lexer);
    3886                     }
    3887                 } else {
    3888                     StringBuilder descriptorVal = new StringBuilder();
    3889                     while(true) {
    3890                         if((currentToken != null) && (currentToken.getType() != CSSLexer.SEMI) &&
    3891                             (currentToken.getType() != Token.EOF)) {
    3892                             descriptorVal.append(currentToken.getText());
    3893                         } else {
    3894                             break;
    3895                         }
    3896                         currentToken = nextToken(lexer);
    3897                     }
    3898                     descriptors.put(key,descriptorVal.toString());
    3899                 }
    3900 //                continue;
    3901             }
    3902
    3903             if ((currentToken == null) ||
    3904                 (currentToken.getType() == CSSLexer.RBRACE) ||
    3905                 (currentToken.getType() == Token.EOF)) {
    3906                 break;
    3907             }
    3908
    3909         }
    3910         return new FontFace(descriptors, sources);
    3911     }
    3912
    3913
    3914     private List<Selector> selectors(CSSLexer lexer) {
    3915
    3916         List<Selector> selectors = new ArrayList<Selector>();
    3917
    3918         while(true) {
    3919             Selector selector = selector(lexer);
    3920             if (selector == null) {
    3921                 // some error happened, skip the rule...
    3922                 while ((currentToken != null) &&
    3923                        (currentToken.getType() != CSSLexer.RBRACE) &&
    3924                        (currentToken.getType() != Token.EOF)) {
    3925                     currentToken = nextToken(lexer);
    3926                 }
    3927
    3928                 // current token is either RBRACE or EOF. Calling
    3929                 // currentToken will get the next token or EOF.
    3930                 currentToken = nextToken(lexer);
    3931
    3932                 // skipped the last rule?
    3933                 if (currentToken == null || currentToken.getType() == Token.EOF) {
    3934                     currentToken = null;
    3935                     return null;
    3936                 }
    3937
    3938                 continue;
    3939             }
    3940             selectors.add(selector);
    3941
    3942             if ((currentToken != null) &&
    3943                 (currentToken.getType() == CSSLexer.COMMA)) {
    3944                 // get past the comma
    3945                 currentToken = nextToken(lexer);
    3946                 continue;
    3947             }
    3948
    3949             // currentToken was either null or not a comma
    3950             // so we are done with selectors.
    3951             break;
    3952         }
    3953
    3954         return selectors;
    3955     }
    3956
    3957     private Selector selector(CSSLexer lexer) {
    3958
    3959         List<Combinator> combinators = null;
    3960         List<SimpleSelector> sels = null;
    3961
    3962         SimpleSelector ancestor = simpleSelector(lexer);
    3963         if (ancestor == null) return null;
    3964
    3965         while (true) {
    3966             Combinator comb = combinator(lexer);
    3967             if (comb != null) {
    3968                 if (combinators == null) {
    3969                     combinators = new ArrayList<Combinator>();
    3970                 }
    3971                 combinators.add(comb);
    3972                 SimpleSelector descendant = simpleSelector(lexer);
    3973                 if (descendant == null) return null;
    3974                 if (sels == null) {
    3975                     sels = new ArrayList<SimpleSelector>();
    3976                     sels.add(ancestor);
    3977                 }
    3978                 sels.add(descendant);
    3979             } else {
    3980                 break;
    3981             }
    3982         }
    3983
    3984         // RT-15473
    3985         // We might return from selector with a NL token instead of an
    3986         // LBRACE, so skip past the NL here.
    3987         if (currentToken != null && currentToken.getType() == CSSLexer.NL) {
    3988             currentToken = nextToken(lexer);
    3989         }
    3990
    3991
    3992         if (sels == null) {
    3993             return ancestor;
    3994         } else {
    3995             return new CompoundSelector(sels,combinators);
    3996         }
    3997
    3998     }
    3999
    4000     private SimpleSelector simpleSelector(CSSLexer lexer) {
    4001
    4002         String esel = "*"; // element selector. default to universal
    4003         String isel = ""; // id selector
    4004         List<String>  csels = null; // class selector
    4005         List<String> pclasses = null; // pseudoclasses
    4006
    4007         while (true) {
    4008
    4009             final int ttype =
    4010                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
    4011
    4012             switch(ttype) {
    4013                 // element selector
    4014                 case CSSLexer.STAR:
    4015                 case CSSLexer.IDENT:
    4016                     esel = currentToken.getText();
    4017                     break;
    4018
    4019                 // class selector
    4020                 case CSSLexer.DOT:
    4021                     currentToken = nextToken(lexer);
    4022                     if (currentToken != null &&
    4023                         currentToken.getType() == CSSLexer.IDENT) {
    4024                         if (csels == null) {
    4025                             csels = new ArrayList<String>();
    4026                         }
    4027                         csels.add(currentToken.getText());
    4028                     } else {
    4029                         currentToken = Token.INVALID_TOKEN;
    4030                         return null;
    4031                     }
    4032                     break;
    4033
    4034                 // id selector
    4035                 case CSSLexer.HASH:
    4036                     isel = currentToken.getText().substring(1);
    4037                     break;
    4038
    4039                 case CSSLexer.COLON:
    4040                     currentToken = nextToken(lexer);
    4041                     if (currentToken != null && pclasses == null) {
    4042                         pclasses = new ArrayList<String>();
    4043                     }
    4044
    4045                     if (currentToken.getType() == CSSLexer.IDENT) {
    4046                         pclasses.add(currentToken.getText());
    4047                     } else if (currentToken.getType() == CSSLexer.FUNCTION){
    4048                         String pclass = functionalPseudo(lexer);
    4049                         pclasses.add(pclass);
    4050                     } else {
    4051                         currentToken = Token.INVALID_TOKEN;
    4052                     }
    4053
    4054                     if (currentToken.getType() == Token.INVALID) {
    4055                         return null;
    4056                     }
    4057                     break;
    4058
    4059                 case CSSLexer.NL:
    4060                 case CSSLexer.WS:
    4061                 case CSSLexer.COMMA:
    4062                 case CSSLexer.GREATER:
    4063                 case CSSLexer.LBRACE:
    4064                 case Token.EOF:
    4065                     return new SimpleSelector(esel, csels, pclasses, isel);
    4066
    4067                 default:
    4068                     return null;
    4069
    4070
    4071             }
    4072
    4073             // get the next token, but don't skip whitespace
    4074             // since it may be a combinator
    4075             currentToken = lexer.nextToken();
    4076             if (LOGGER.isLoggable(Level.FINEST)) {
    4077                 LOGGER.finest(currentToken.toString());
    4078             }
    4079         }
    4080     }
    4081
    4082     // From http://www.w3.org/TR/selectors/#grammar
    4083     //  functional_pseudo
    4084     //      : FUNCTION S* expression ')'
    4085     //      ;
    4086     //  expression
    4087     //      /* In CSS3, the expressions are identifiers, strings, */
    4088     //      /* or of the form "an+b" */
    4089     //      : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
    4090     //      ;
    4091     private String functionalPseudo(CSSLexer lexer) {
    4092
    4093         // TODO: This is not how we should handle functional pseudo-classes in the long-run!
    4094
    4095         StringBuilder pclass = new StringBuilder(currentToken.getText());
    4096
    4097         while(true) {
    4098
    4099             currentToken = nextToken(lexer);
    4100
    4101             switch(currentToken.getType()) {
    4102
    4103                 // TODO: lexer doesn't really scan right and isn't CSS3,
    4104                 // so PLUS, '-', NUMBER, etc are all useless at this point.
    4105                 case CSSLexer.STRING:
    4106                 case CSSLexer.IDENT:
    4107                     pclass.append(currentToken.getText());
    4108                     break;
    4109
    4110                 case CSSLexer.RPAREN:
    4111                     pclass.append(')');
    4112                     return pclass.toString();
    4113
    4114                 default:
    4115                     currentToken = Token.INVALID_TOKEN;
    4116                     return null;
    4117             }
    4118         }
    4119
    4120     }
    4121
    4122     private Combinator combinator(CSSLexer lexer) {
    4123
    4124         Combinator combinator = null;
    4125
    4126         while (true) {
    4127
    4128             final int ttype =
    4129                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
    4130
    4131             switch(ttype) {
    4132
    4133                 case CSSLexer.WS:
    4134                     // need to check if combinator is null since child token
    4135                     // might be surrounded by whitespace.
    4136                     if (combinator == null && " ".equals(currentToken.getText())) {
    4137                         combinator = Combinator.DESCENDANT;
    4138                     }
    4139                     break;
    4140
    4141                 case CSSLexer.GREATER:
    4142                     // no need to check if combinator is null here
    4143                     combinator = Combinator.CHILD;
    4144                     break;
    4145
    4146                 case CSSLexer.STAR:
    4147                 case CSSLexer.IDENT:
    4148                 case CSSLexer.DOT:
    4149                 case CSSLexer.HASH:
    4150                 case CSSLexer.COLON:
    4151                     return combinator;
    4152
    4153                 default:
    4154                     // only selector is expected
    4155                     return null;
    4156
    4157             }
    4158
    4159             // get the next token, but don't skip whitespace
    4160             currentToken = lexer.nextToken();
    4161             if (LOGGER.isLoggable(Level.FINEST)) {
    4162                 LOGGER.finest(currentToken.toString());
    4163             }
    4164         }
    4165     }
    4166
    4167     private List<Declaration> declarations(CSSLexer lexer) {
    4168
    4169         List<Declaration> declarations = new ArrayList<Declaration>();
    4170
    4171         while (true) {
    4172
    4173             Declaration decl = declaration(lexer);
    4174             if (decl != null) {
    4175                 declarations.add(decl);
    4176             } else {
    4177                 // some error happened, skip the decl...
    4178                 while ((currentToken != null) &&
    4179                        (currentToken.getType() != CSSLexer.SEMI) &&
    4180                        (currentToken.getType() != CSSLexer.RBRACE) &&
    4181                        (currentToken.getType() != Token.EOF)) {
    4182                     currentToken = nextToken(lexer);
    4183                 }
    4184
    4185                 // current token is either SEMI, RBRACE or EOF.
    4186                 if (currentToken != null &&
    4187                     currentToken.getType() != CSSLexer.SEMI)
    4188                     return declarations;
    4189             }
    4190
    4191             // declaration; declaration; ???
    4192             // RT-17830 - allow declaration;;
    4193             while ((currentToken != null) &&
    4194                     (currentToken.getType() == CSSLexer.SEMI)) {
    4195                 currentToken = nextToken(lexer);
    4196             }
    4197
    4198             // if it is delcaration; declaration, then the
    4199             // next token should be an IDENT.
    4200             if ((currentToken != null) &&
    4201                 (currentToken.getType() == CSSLexer.IDENT)) {
    4202                 continue;
    4203             }
    4204
    4205             break;
    4206         }
    4207
    4208         return declarations;
    4209     }
    4210
    4211     private Declaration declaration(CSSLexer lexer) {
    4212
    4213         final int ttype =
    4214             (currentToken != null) ? currentToken.getType() : Token.INVALID;
    4215
    4216         if ((currentToken == null) ||
    4217             (currentToken.getType() != CSSLexer.IDENT)) {
    4218 //
    4219 //            RT-16547: this warning was misleading because an empty rule
    4220 //            not invalid. Some people put in empty rules just as placeholders.
    4221 //
    4222 //            if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
    4223 //                final int line = currentToken != null ? currentToken.getLine() : -1;
    4224 //                final int pos = currentToken != null ? currentToken.getOffset() : -1;
    4225 //                final String url =
    4226 //                    (stylesheet != null && stylesheet.getUrl() != null) ?
    4227 //                        stylesheet.getUrl().toExternalForm() : "?";
    4228 //                LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]",
    4229 //                    url,line,pos);
    4230 //            }
    4231             return null;
    4232         }
    4233
    4234         String property = currentToken.getText();
    4235
    4236         currentToken = nextToken(lexer);
    4237
    4238         if ((currentToken == null) ||
    4239             (currentToken.getType() != CSSLexer.COLON)) {
    4240                 final int line = currentToken.getLine();
    4241                 final int pos = currentToken.getOffset();
    4242                 final String msg =
    4243                         MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]",
    4244                     line,pos);
    4245                 CssError error = createError(msg);
    4246                 if (LOGGER.isLoggable(Level.WARNING)) {
    4247                     LOGGER.warning(error.toString());
    4248                 }
    4249                 reportError(error);
    4250             return null;
    4251         }
    4252
    4253         currentToken = nextToken(lexer);
    4254
    4255         Term root = expr(lexer);
    4256         ParsedValueImpl value = null;
    4257         try {
    4258             value = (root != null) ? valueFor(property, root) : null;
    4259         } catch (ParseException re) {
    4260                 Token badToken = re.tok;
    4261                 final int line = badToken != null ? badToken.getLine() : -1;
    4262                 final int pos = badToken != null ? badToken.getOffset() : -1;
    4263                 final String msg =
    4264                         MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]",
    4265                     line,pos,re.getMessage(),property);
    4266                 CssError error = createError(msg);
    4267                 if (LOGGER.isLoggable(Level.WARNING)) {
    4268                     LOGGER.warning(error.toString());
    4269                 }
    4270                 reportError(error);
    4271             return null;
    4272         }
    4273
    4274         boolean important = currentToken.getType() == CSSLexer.IMPORTANT_SYM;
    4275         if (important) currentToken = nextToken(lexer);
    4276
    4277         Declaration decl = (value != null)
    4278                 ? new Declaration(property, value, important) : null;
    4279         return decl;
    4280     }
    4281
    4282     private Term expr(CSSLexer lexer) {
    4283
    4284         final Term expr = term(lexer);
    4285         Term current = expr;
    4286
    4287         while(true) {
    4288
    4289             // if current is null, then term returned null
    4290             final int ttype =
    4291                 (current != null && currentToken != null)
    4292                     ? currentToken.getType() : Token.INVALID;
    4293
    4294             if (ttype == Token.INVALID) {
    4295                 skipExpr(lexer);
    4296                 return null;
    4297             } else if (ttype == CSSLexer.SEMI ||
    4298                 ttype == CSSLexer.IMPORTANT_SYM ||
    4299                 ttype == CSSLexer.RBRACE ||
    4300                 ttype == Token.EOF) {
    4301                 return expr;
    4302             } else if (ttype == CSSLexer.COMMA) {
    4303             // comma breaks up sequences of terms.
    4304                 // next series of terms chains off the last term in
    4305                 // the current series.
    4306                 currentToken = nextToken(lexer);
    4307                 current = current.nextLayer = term(lexer);
    4308             } else {
    4309                 current = current.nextInSeries = term(lexer);
    4310             }
    4311
    4312         }
    4313     }
    4314
    4315     private void skipExpr(CSSLexer lexer) {
    4316
    4317         while(true) {
    4318
    4319             currentToken = nextToken(lexer);
    4320
    4321             final int ttype =
    4322                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
    4323
    4324             if (ttype == CSSLexer.SEMI ||
    4325                 ttype == CSSLexer.RBRACE ||
    4326                 ttype == Token.EOF) {
    4327                 return;
    4328             }
    4329         }
    4330     }
    4331
    4332     private Term term(CSSLexer lexer) {
    4333
    4334         final int ttype =
    4335             (currentToken != null) ? currentToken.getType() : Token.INVALID;
    4336
    4337         switch (ttype) {
    4338
    4339             case CSSLexer.NUMBER:
    4340             case CSSLexer.CM:
    4341             case CSSLexer.EMS:
    4342             case CSSLexer.EXS:
    4343             case CSSLexer.IN:
    4344             case CSSLexer.MM:
    4345             case CSSLexer.PC:
    4346             case CSSLexer.PT:
    4347             case CSSLexer.PX:
    4348             case CSSLexer.DEG:
    4349             case CSSLexer.GRAD:
    4350             case CSSLexer.RAD:
    4351             case CSSLexer.TURN:
    4352             case CSSLexer.PERCENTAGE:
    4353                 break;
    4354
    4355             case CSSLexer.STRING:
    4356                 break;
    4357             case CSSLexer.IDENT:
    4358                 break;
    4359
    4360             case CSSLexer.HASH:
    4361                 break;
    4362
    4363             case CSSLexer.FUNCTION:
    4364             case CSSLexer.LPAREN:
    4365
    4366                 Term function = new Term(currentToken);
    4367                 currentToken = nextToken(lexer);
    4368
    4369                 Term arg = term(lexer);
    4370                 function.firstArg = arg;
    4371
    4372                 while(true) {
    4373
    4374                     final int operator =
    4375                         currentToken != null ? currentToken.getType() : Token.INVALID;
    4376
    4377                     if (operator == CSSLexer.RPAREN) {
    4378                         currentToken = nextToken(lexer);
    4379                         return function;
    4380                     } else if (operator == CSSLexer.COMMA) {
    4381                         // comma breaks up sequences of terms.
    4382                         // next series of terms chains off the last term in
    4383                         // the current series.
    4384                         currentToken = nextToken(lexer);
    4385                         arg = arg.nextArg = term(lexer);
    4386
    4387                     } else {
    4388                         arg = arg.nextInSeries = term(lexer);
    4389                     }
    4390
    4391                 }
    4392
    4393             default:
    4394                 final int line = currentToken != null ? currentToken.getLine() : -1;
    4395                 final int pos = currentToken != null ? currentToken.getOffset() : -1;
    4396                 final String text = currentToken != null ? currentToken.getText() : "";
    4397                 final String msg =
    4398                     MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]",
    4399                     "\'",text,line,pos);
    4400                 CssError error = createError(msg);
    4401                 if (LOGGER.isLoggable(Level.WARNING)) {
    4402                     LOGGER.warning(error.toString());
    4403                 }
    4404                 reportError(error);
    4405                 return null;
    4406 //                currentToken = nextToken(lexer);
    4407 //
    4408 //                return new Term(Token.INVALID_TOKEN);
    4409         }
    4410
    4411         Term term = new Term(currentToken);
    4412         currentToken = nextToken(lexer);
    4413         return term;
    4414     }
    4415 }