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