    rev 4839 : RT-2529: Locale specific runtime errors (Turkish)

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