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