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