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