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