modules/graphics/src/main/java/javafx/css/CssParser.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization


   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.css.parser;
  27 
  28 import com.sun.javafx.util.Utils;
  29 import com.sun.javafx.css.Combinator;
  30 import com.sun.javafx.css.CompoundSelector;
  31 import com.sun.javafx.css.CssError;
  32 import com.sun.javafx.css.Declaration;
  33 import com.sun.javafx.css.FontFace;
  34 import com.sun.javafx.css.ParsedValueImpl;
  35 import com.sun.javafx.css.Rule;
  36 import com.sun.javafx.css.Selector;
  37 import com.sun.javafx.css.SimpleSelector;
  38 import com.sun.javafx.css.Size;
  39 import com.sun.javafx.css.SizeUnits;
  40 import com.sun.javafx.css.StyleManager;
  41 import com.sun.javafx.css.Stylesheet;
  42 import com.sun.javafx.css.converters.BooleanConverter;
  43 import com.sun.javafx.css.converters.DurationConverter;
  44 import com.sun.javafx.css.converters.EffectConverter;
  45 import com.sun.javafx.css.converters.EnumConverter;
  46 import com.sun.javafx.css.converters.FontConverter;
  47 import com.sun.javafx.css.converters.InsetsConverter;
  48 import com.sun.javafx.css.converters.PaintConverter;
  49 import com.sun.javafx.css.converters.SizeConverter;
  50 import com.sun.javafx.css.converters.SizeConverter.SequenceConverter;
  51 import com.sun.javafx.css.converters.StringConverter;
  52 import com.sun.javafx.css.converters.URLConverter;




  53 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
  54 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
  55 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
  56 import com.sun.javafx.scene.layout.region.BorderImageSlices;
  57 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
  58 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
  59 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
  60 import com.sun.javafx.scene.layout.region.BorderStyleConverter;

  61 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
  62 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
  63 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
  64 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
  65 import com.sun.javafx.scene.layout.region.Margins;
  66 import com.sun.javafx.scene.layout.region.RepeatStruct;
  67 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
  68 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
  69 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
  70 import javafx.css.ParsedValue;
  71 import javafx.css.StyleConverter;
  72 import javafx.css.Styleable;
  73 import javafx.geometry.Insets;
  74 import javafx.scene.effect.BlurType;
  75 import javafx.scene.effect.Effect;
  76 import javafx.scene.layout.BackgroundPosition;
  77 import javafx.scene.layout.BackgroundRepeat;
  78 import javafx.scene.layout.BackgroundSize;
  79 import javafx.scene.layout.BorderStrokeStyle;
  80 import javafx.scene.layout.BorderWidths;
  81 import javafx.scene.layout.CornerRadii;
  82 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
  83 import javafx.scene.paint.Color;
  84 import javafx.scene.paint.CycleMethod;
  85 import javafx.scene.paint.Paint;
  86 import javafx.scene.paint.Stop;
  87 import javafx.scene.shape.StrokeLineCap;
  88 import javafx.scene.shape.StrokeLineJoin;
  89 import javafx.scene.shape.StrokeType;
  90 import javafx.scene.text.Font;
  91 import javafx.scene.text.FontPosture;
  92 import javafx.scene.text.FontWeight;
  93 import javafx.util.Duration;
  94 import sun.util.logging.PlatformLogger;
  95 import sun.util.logging.PlatformLogger.Level;
  96 
  97 import java.io.BufferedReader;
  98 import java.io.CharArrayReader;
  99 import java.io.IOException;
 100 import java.io.InputStreamReader;
 101 import java.io.Reader;
 102 import java.net.MalformedURLException;
 103 import java.net.URI;
 104 import java.net.URISyntaxException;
 105 import java.net.URL;
 106 import java.text.MessageFormat;
 107 import java.util.ArrayList;
 108 import java.util.Collections;
 109 import java.util.HashMap;
 110 import java.util.List;
 111 import java.util.Locale;
 112 import java.util.Map;
 113 import java.util.Stack;
 114 
 115 final public class CSSParser {
 116 
 117     /**
 118      * @deprecated As of 8u40, use new CSSParser() instead.
 119      */
 120     @Deprecated
 121     public static CSSParser getInstance() {
 122         return new CSSParser();
 123     }
 124 
 125     public CSSParser() {
 126         properties = new HashMap<String,String>();
 127     }
 128 
 129     // stylesheet as a string from parse method. This will be null if the
 130     // stylesheet is being parsed from a file; otherwise, the parser is parsing
 131     // a string and this is that string.
 132     private String     stylesheetAsText;
 133 
 134     // the url of the stylesheet file, or the docbase of an applet. This will
 135     // be null if the source is not a file or from an applet.
 136     private String        sourceOfStylesheet;
 137 
 138     // the Styleable from the node with an in-line style. This will be null
 139     // unless the source of the styles is a Node's styleProperty. In this case,
 140     // the stylesheetString will also be set.
 141     private Styleable sourceOfInlineStyle;
 142 
 143     // source is a file
 144     private void setInputSource(String url, String str) {
 145         stylesheetAsText = str;


 150     // source as string only
 151     private void setInputSource(String str) {
 152         stylesheetAsText = str;
 153         sourceOfStylesheet = null;
 154         sourceOfInlineStyle = null;
 155     }
 156 
 157     // source is in-line style
 158     private void setInputSource(Styleable styleable) {
 159         stylesheetAsText = styleable != null ? styleable.getStyle() : null;
 160         sourceOfStylesheet = null;
 161         sourceOfInlineStyle = styleable;
 162     }
 163 
 164     private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
 165 
 166     private static final class ParseException extends Exception {
 167         ParseException(String message) {
 168             this(message,null,null);
 169         }
 170         ParseException(String message, Token tok, CSSParser parser) {
 171             super(message);
 172             this.tok = tok;
 173             if (parser.sourceOfStylesheet != null) {
 174                 source = parser.sourceOfStylesheet;
 175             } else if (parser.sourceOfInlineStyle != null) {
 176                 source = parser.sourceOfInlineStyle.toString();
 177             } else if (parser.stylesheetAsText != null) {
 178                 source = parser.stylesheetAsText;
 179             } else {
 180                 source = "?";
 181             }
 182         }
 183         @Override public String toString() {
 184             StringBuilder builder = new StringBuilder(super.getMessage());
 185             builder.append(source);
 186             if (tok != null) builder.append(": ").append(tok.toString());
 187             return builder.toString();
 188         }
 189         private final Token tok;
 190         private final String source;


 233      *
 234      *@param  url URL of the stylesheet to parse
 235      *@return the stylesheet
 236      *@throws IOException
 237      */
 238     public Stylesheet parse(final URL url) throws IOException {
 239 
 240         final String path = url != null ? url.toExternalForm() : null;
 241         final Stylesheet stylesheet = new Stylesheet(path);
 242         if (url != null) {
 243             setInputSource(path, null);
 244             try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
 245                 parse(stylesheet, reader);
 246             }
 247         }
 248         return stylesheet;
 249     }
 250 
 251     /* All of the other function calls should wind up here */
 252     private void parse(final Stylesheet stylesheet, final Reader reader) {
 253 
 254 //        CSSLexer lex = CSSLexer.getInstance();
 255         CSSLexer lex = new CSSLexer();
 256         lex.setReader(reader);
 257 
 258         try {
 259             this.parse(stylesheet, lex);
 260         } catch (Exception ex) {
 261             // Sometimes bad syntax causes an exception. The code should be
 262             // fixed to handle the bad syntax, but the fallback is
 263             // to handle the exception here. Uncaught, the exception can cause
 264             // problems like RT-20311
 265             reportException(ex);
 266         }
 267 
 268     }
 269 
 270     /** Parse an in-line style from a Node */
 271     public Stylesheet parseInlineStyle(final Styleable node) {
 272 
 273         Stylesheet stylesheet = new Stylesheet();
 274 
 275         final String stylesheetText = (node != null) ? node.getStyle() : null;
 276         if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
 277             setInputSource(node);
 278             final List<Rule> rules = new ArrayList<Rule>();
 279             try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
 280                 final CSSLexer lexer = CSSLexer.getInstance();
 281                 lexer.setReader(reader);
 282                 currentToken = nextToken(lexer);
 283                 final List<Declaration> declarations = declarations(lexer);
 284                 if (declarations != null && !declarations.isEmpty()) {
 285                     final Selector selector = Selector.getUniversalSelector();
 286                     final Rule rule = new Rule(
 287                         Collections.singletonList(selector),
 288                         declarations
 289                     );
 290                     rules.add(rule);
 291                 }
 292             } catch (IOException ioe) {
 293             } catch (Exception ex) {
 294                 // Sometimes bad syntax causes an exception. The code should be
 295                 // fixed to handle the bad syntax, but the fallback is
 296                 // to handle the exception here. Uncaught, the exception can cause
 297                 // problems like RT-20311
 298                 reportException(ex);
 299             }
 300             stylesheet.getRules().addAll(rules);
 301         }
 302 
 303         // don't retain reference to the styleable
 304         setInputSource((Styleable) null);
 305 
 306         return stylesheet;
 307     }
 308 
 309     /** convenience method for unit tests */
 310     public ParsedValueImpl parseExpr(String property, String expr) {
 311         if (property == null || expr == null) return null;
 312 
 313         ParsedValueImpl value = null;
 314         setInputSource(null, property + ": " + expr);
 315         char buf[] = new char[expr.length() + 1];
 316         System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
 317         buf[buf.length-1] = ';';
 318 
 319         try (Reader reader = new CharArrayReader(buf)) {
 320             CSSLexer lex = CSSLexer.getInstance();
 321             lex.setReader(reader);
 322 
 323             currentToken = nextToken(lex);
 324             CSSParser.Term term = this.expr(lex);
 325             value = valueFor(property, term, lex);
 326         } catch (IOException ioe) {
 327         } catch (ParseException e) {
 328             if (LOGGER.isLoggable(Level.WARNING)) {
 329                 LOGGER.warning("\"" +property + ": " + expr  + "\" " + e.toString());
 330             }
 331         } catch (Exception ex) {
 332             // Sometimes bad syntax causes an exception. The code should be
 333             // fixed to handle the bad syntax, but the fallback is
 334             // to handle the exception here. Uncaught, the exception can cause
 335             // problems like RT-20311
 336             reportException(ex);
 337         }
 338         return value;
 339     }
 340     /*
 341      * Map of property names found while parsing. If a value matches a
 342      * property name, then the value is a lookup.
 343      */
 344     private final Map<String,String> properties;


 408             }
 409             if (nextLayer != null) {
 410                 buf.append("<nextLayer>");
 411                 buf.append(nextLayer.toString());
 412                 buf.append("</nextLayer>\n");
 413             }
 414             if (firstArg != null) {
 415                 buf.append("<args>");
 416                 buf.append(firstArg.toString());
 417                 if (nextArg != null) {
 418                     buf.append(nextArg.toString());
 419                 }
 420                 buf.append("</args>");
 421             }
 422 
 423             return buf.toString();
 424         }
 425 
 426     }
 427 
 428     private CssError createError(String msg) {
 429 
 430         CssError error = null;
 431         if (sourceOfStylesheet != null) {
 432             error = new CssError.StylesheetParsingError(sourceOfStylesheet, msg);
 433         } else if (sourceOfInlineStyle != null) {
 434             error = new CssError.InlineStyleParsingError(sourceOfInlineStyle, msg);
 435         } else {
 436             error = new CssError.StringParsingError(stylesheetAsText, msg);
 437         }
 438         return error;
 439     }
 440 
 441     private void reportError(CssError error) {
 442         List<CssError> errors = null;
 443         if ((errors = StyleManager.getErrors()) != null) {
 444             errors.add(error);
 445         }
 446     }
 447 
 448     private void error(final Term root, final String msg) throws ParseException {
 449 
 450         final Token token = root != null ? root.token : null;
 451         final ParseException pe = new ParseException(msg,token,this);
 452         reportError(createError(pe.toString()));
 453         throw pe;
 454     }
 455 
 456     private void reportException(Exception exception) {
 457 
 458         if (LOGGER.isLoggable(Level.WARNING)) {
 459             final StackTraceElement[] stea = exception.getStackTrace();
 460             if (stea.length > 0) {
 461                 final StringBuilder buf =
 462                     new StringBuilder("Please report ");


 523 
 524         // not a color
 525         return null;
 526     }
 527 
 528     private String stripQuotes(String string) {
 529         return com.sun.javafx.util.Utils.stripQuotes(string);
 530     }
 531 
 532     private double clamp(double min, double val, double max) {
 533         if (val < min) return min;
 534         if (max < val) return max;
 535         return val;
 536     }
 537 
 538     // Return true if the token is a size type or an identifier
 539     // (which would indicate a lookup).
 540     private boolean isSize(Token token) {
 541         final int ttype = token.getType();
 542         switch (ttype) {
 543         case CSSLexer.NUMBER:
 544         case CSSLexer.PERCENTAGE:
 545         case CSSLexer.EMS:
 546         case CSSLexer.EXS:
 547         case CSSLexer.PX:
 548         case CSSLexer.CM:
 549         case CSSLexer.MM:
 550         case CSSLexer.IN:
 551         case CSSLexer.PT:
 552         case CSSLexer.PC:
 553         case CSSLexer.DEG:
 554         case CSSLexer.GRAD:
 555         case CSSLexer.RAD:
 556         case CSSLexer.TURN:
 557             return true;
 558         default:
 559             return token.getType() == CSSLexer.IDENT;
 560         }
 561     }
 562 
 563     private Size size(final Token token) throws ParseException {
 564         SizeUnits units = SizeUnits.PX;
 565         // Amount to trim off the suffix, if any. Most are 2 chars.
 566         int trim = 2;
 567         final String sval = token.getText().trim();
 568         final int len = sval.length();
 569         final int ttype = token.getType();
 570         switch (ttype) {
 571         case CSSLexer.NUMBER:
 572             units = SizeUnits.PX;
 573             trim = 0;
 574             break;
 575         case CSSLexer.PERCENTAGE:
 576             units = SizeUnits.PERCENT;
 577             trim = 1;
 578             break;
 579         case CSSLexer.EMS:
 580             units = SizeUnits.EM;
 581             break;
 582         case CSSLexer.EXS:
 583             units = SizeUnits.EX;
 584             break;
 585         case CSSLexer.PX:
 586             units = SizeUnits.PX;
 587             break;
 588         case CSSLexer.CM:
 589             units = SizeUnits.CM;
 590             break;
 591         case CSSLexer.MM:
 592             units = SizeUnits.MM;
 593             break;
 594         case CSSLexer.IN:
 595             units = SizeUnits.IN;
 596             break;
 597         case CSSLexer.PT:
 598             units = SizeUnits.PT;
 599             break;
 600         case CSSLexer.PC:
 601             units = SizeUnits.PC;
 602             break;
 603         case CSSLexer.DEG:
 604             units = SizeUnits.DEG;
 605             trim = 3;
 606             break;
 607         case CSSLexer.GRAD:
 608             units = SizeUnits.GRAD;
 609             trim = 4;
 610             break;
 611         case CSSLexer.RAD:
 612             units = SizeUnits.RAD;
 613             trim = 3;
 614             break;
 615         case CSSLexer.TURN:
 616             units = SizeUnits.TURN;
 617             trim = 4;
 618             break;
 619         case CSSLexer.SECONDS:
 620             units = SizeUnits.S;
 621             trim = 1;
 622             break;
 623         case CSSLexer.MS:
 624             units = SizeUnits.MS;
 625             break;
 626         default:
 627             if (LOGGER.isLoggable(Level.FINEST)) {
 628                 LOGGER.finest("Expected \'<number>\'");
 629             }
 630             ParseException re = new ParseException("Expected \'<number>\'",token, this);
 631             reportError(createError(re.toString()));
 632             throw re;
 633         }
 634         // TODO: Handle NumberFormatException
 635         return new Size(
 636             Double.parseDouble(sval.substring(0,len-trim)),
 637             units
 638         );
 639     }
 640 
 641     // Count the number of terms in a series
 642     private int numberOfTerms(final Term root) {
 643         if (root == null) return 0;


 680         return nArgs;
 681     }
 682 
 683     // Get the next layer following this term, which may be null
 684     private Term nextLayer(final Term root) {
 685         if (root == null) return null;
 686 
 687         Term term = root;
 688         while (term.nextInSeries != null) {
 689             term = term.nextInSeries;
 690         }
 691         return term.nextLayer;
 692     }
 693 
 694     ////////////////////////////////////////////////////////////////////////////
 695     //
 696     // Parsing routines
 697     //
 698     ////////////////////////////////////////////////////////////////////////////
 699 
 700     ParsedValueImpl valueFor(String property, Term root, CSSLexer lexer) throws ParseException {
 701         final String prop = property.toLowerCase(Locale.ROOT);
 702         properties.put(prop, prop);
 703         if (root == null || root.token == null) {
 704             error(root, "Expected value for property \'" + prop + "\'");
 705         }
 706 
 707         if (root.token.getType() == CSSLexer.IDENT) {
 708             final String txt = root.token.getText();
 709             if ("inherit".equalsIgnoreCase(txt)) {
 710                 return new ParsedValueImpl<String,String>("inherit", null);
 711             } else if ("null".equalsIgnoreCase(txt)
 712                     || "none".equalsIgnoreCase(txt)) {
 713                 return new ParsedValueImpl<String,String>("null", null);
 714             }
 715         }
 716         if ("-fx-fill".equals(prop)) {
 717              ParsedValueImpl pv = parse(root);
 718             if (pv.getConverter() == StyleConverter.getUrlConverter()) {
 719                 // ImagePatternConverter expects array of ParsedValue where element 0 is the URL
 720                 // Pending RT-33574
 721                 pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance());
 722             }
 723             return pv;
 724         }
 725         else if ("-fx-background-color".equals(prop)) {
 726             return parsePaintLayers(root);
 727         } else if ("-fx-background-image".equals(prop)) {


 803         } else if ("-fx-stroke-line-cap".equals(prop)) {
 804             // TODO: Figure out a way that these properties don't need to be
 805             // special cased.
 806             ParsedValueImpl value = parseStrokeLineCap(root);
 807             if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
 808             return value;
 809         } else if ("-fx-stroke-type".equals(prop)) {
 810             // TODO: Figure out a way that these properties don't need to be
 811             // special cased.
 812             ParsedValueImpl value = parseStrokeType(root);
 813             if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
 814             return value;
 815         } else if ("-fx-font-smoothing-type".equals(prop)) {
 816             // TODO: Figure out a way that these properties don't need to be
 817             // special cased.
 818             String str = null;
 819             int ttype = -1;
 820             final Token token = root.token;
 821 
 822             if (root.token == null
 823                     || ((ttype = root.token.getType()) != CSSLexer.STRING
 824                          && ttype != CSSLexer.IDENT)
 825                     || (str = root.token.getText()) == null
 826                     || str.isEmpty()) {
 827                 error(root,  "Expected STRING or IDENT");
 828             }
 829             return new ParsedValueImpl<String, String>(stripQuotes(str), null, false);
 830         }
 831         return parse(root);
 832     }
 833 
 834     private ParsedValueImpl parse(Term root) throws ParseException {
 835 
 836         if (root.token == null) error(root, "Parse error");
 837         final Token token = root.token;
 838         ParsedValueImpl value = null; // value to return;
 839 
 840         final int ttype = token.getType();
 841         switch (ttype) {
 842         case CSSLexer.NUMBER:
 843         case CSSLexer.PERCENTAGE:
 844         case CSSLexer.EMS:
 845         case CSSLexer.EXS:
 846         case CSSLexer.PX:
 847         case CSSLexer.CM:
 848         case CSSLexer.MM:
 849         case CSSLexer.IN:
 850         case CSSLexer.PT:
 851         case CSSLexer.PC:
 852         case CSSLexer.DEG:
 853         case CSSLexer.GRAD:
 854         case CSSLexer.RAD:
 855         case CSSLexer.TURN:
 856             if (root.nextInSeries == null) {
 857                 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null);
 858                 value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance());
 859             } else {
 860                 ParsedValueImpl<Size,Size>[] sizeValue = parseSizeSeries(root);
 861                 value = new ParsedValueImpl<ParsedValue[],Number[]>(sizeValue, SizeConverter.SequenceConverter.getInstance());
 862             }
 863             break;
 864         case CSSLexer.SECONDS:
 865         case CSSLexer.MS: {
 866             ParsedValue<Size, Size> sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
 867             value = new ParsedValueImpl<ParsedValue<?, Size>, Duration>(sizeValue, DurationConverter.getInstance());
 868             break;
 869         }
 870         case CSSLexer.STRING:
 871         case CSSLexer.IDENT:
 872             boolean isIdent = ttype == CSSLexer.IDENT;
 873             final String str = stripQuotes(token.getText());
 874             final String text = str.toLowerCase(Locale.ROOT);
 875             if ("ladder".equals(text)) {
 876                 value = ladder(root);
 877             } else if ("linear".equals(text) && (root.nextInSeries) != null) {
 878                 // if nextInSeries is null, then assume this is _not_ an old-style linear gradient
 879                 value = linearGradient(root);
 880             } else if ("radial".equals(text) && (root.nextInSeries) != null) {
 881                 // if nextInSeries is null, then assume this is _not_ an old-style radial gradient
 882                 value = radialGradient(root);
 883             } else if ("infinity".equals(text)) {
 884                 Size size = new Size(Double.MAX_VALUE, SizeUnits.PX);
 885                 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size, null);
 886                 value = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeValue, SizeConverter.getInstance());
 887             } else if ("indefinite".equals(text)) {
 888                 Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX);
 889                 ParsedValueImpl<Size,Size> sizeValue = new ParsedValueImpl<>(size, null);
 890                 value = new ParsedValueImpl<ParsedValue<?,Size>,Duration>(sizeValue, DurationConverter.getInstance());
 891             } else if ("true".equals(text)) {
 892                 // TODO: handling of boolean is really bogus
 893                 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance());
 894             } else if ("false".equals(text)) {
 895                 // TODO: handling of boolean is really bogus
 896                 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance());
 897             } else {
 898                 // if the property value is another property, then it needs to be looked up.
 899                 boolean needsLookup = isIdent && properties.containsKey(text);
 900                 if (needsLookup || ((value = colorValueOfString(str)) == null )) {
 901                     // If the value is a lookup, make sure to use the lower-case text so it matches the property
 902                     // in the Declaration. If the value is not a lookup, then use str since the value might
 903                     // be a string which could have some case sensitive meaning
 904                     //
 905                     // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201
 906                     value = new ParsedValueImpl<String,String>(needsLookup ? text : str, null, isIdent || needsLookup);
 907                 }
 908             }
 909             break;
 910         case CSSLexer.HASH:
 911             final String clr = token.getText();
 912             try {
 913                 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null);
 914             } catch (final IllegalArgumentException e) {
 915                 error(root, e.getMessage());
 916             }
 917             break;
 918         case CSSLexer.FUNCTION:
 919             return  parseFunction(root);
 920         case CSSLexer.URL:
 921             return parseURI(root);
 922         default:
 923             final String msg = "Unknown token type: \'" + ttype + "\'";
 924             error(root, msg);
 925         }
 926         return value;
 927 
 928     }
 929 
 930     /* Parse size.
 931      * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
 932      */
 933     private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException {
 934 
 935         if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'");
 936 
 937         ParsedValueImpl<?,Size> value = null;
 938 
 939         if (root.token.getType() != CSSLexer.IDENT) {
 940 
 941             Size size = size(root.token);
 942             value = new ParsedValueImpl<Size,Size>(size, null);
 943 
 944         } else {
 945 
 946             String key = root.token.getText();
 947             value = new ParsedValueImpl<String,Size>(key, null, true);
 948 
 949         }
 950 
 951         return value;
 952     }
 953 
 954     private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException {
 955 
 956         ParsedValueImpl<?,Color> color = null;
 957         if (root.token != null &&
 958             (root.token.getType() == CSSLexer.IDENT ||
 959              root.token.getType() == CSSLexer.HASH ||
 960              root.token.getType() == CSSLexer.FUNCTION)) {
 961 
 962             color = parse(root);
 963 
 964         } else {
 965             error(root,  "Expected \'<color>\'");
 966         }
 967         return color;
 968     }
 969 
 970     // rgb(NUMBER, NUMBER, NUMBER)
 971     // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
 972     // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
 973     // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
 974     private ParsedValueImpl rgb(Term root) throws ParseException {
 975 
 976         // first term in the chain is the function name...
 977         final String fn = (root.token != null) ? root.token.getText() : null;
 978         if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
 979             final String msg = "Expected \'rgb\' or \'rgba\'";
 980             error(root, msg);
 981         }
 982 
 983         Term arg = root;
 984         Token rtok, gtok, btok, atok;
 985 
 986         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
 987         if ((rtok = arg.token) == null ||
 988             (rtok.getType() != CSSLexer.NUMBER &&
 989              rtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
 990 
 991         root = arg;
 992 
 993         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
 994         if ((gtok = arg.token) == null ||
 995             (gtok.getType() != CSSLexer.NUMBER &&
 996              gtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
 997 
 998         root = arg;
 999 
1000         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
1001         if ((btok = arg.token) == null ||
1002             (btok.getType() != CSSLexer.NUMBER &&
1003              btok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
1004 
1005         root = arg;
1006 
1007         if ((arg = arg.nextArg) != null) {
1008             if ((atok = arg.token) == null ||
1009                  atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1010         } else {
1011             atok = null;
1012         }
1013 
1014         int argType = rtok.getType();
1015         if (argType != gtok.getType() || argType != btok.getType() ||
1016             (argType != CSSLexer.NUMBER && argType != CSSLexer.PERCENTAGE)) {
1017             error(root, "Argument type mistmatch");
1018         }
1019 
1020         final String rtext = rtok.getText();
1021         final String gtext = gtok.getText();
1022         final String btext = btok.getText();
1023 
1024         double rval = 0;
1025         double gval = 0;
1026         double bval = 0;
1027         if (argType == CSSLexer.NUMBER) {
1028             rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
1029             gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
1030             bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
1031         } else {
1032             rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
1033             gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
1034             bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
1035         }
1036 
1037         final String atext = (atok != null) ? atok.getText() : null;
1038         final double aval =  (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;
1039 
1040         return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null);
1041 
1042     }
1043 
1044     // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
1045     // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
1046     private ParsedValueImpl hsb(Term root) throws ParseException {
1047 
1048         // first term in the chain is the function name...
1049         final String fn = (root.token != null) ? root.token.getText() : null;
1050         if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
1051             final String msg = "Expected \'hsb\' or \'hsba\'";
1052             error(root, msg);
1053         }
1054 
1055         Term arg = root;
1056         Token htok, stok, btok, atok;
1057 
1058         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'");
1059         if ((htok = arg.token) == null || htok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1060 
1061         root = arg;
1062 
1063         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1064         if ((stok = arg.token) == null || stok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1065 
1066         root = arg;
1067 
1068         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1069         if ((btok = arg.token) == null || btok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1070 
1071         root = arg;
1072 
1073         if ((arg = arg.nextArg) != null) {
1074             if ((atok = arg.token) == null || atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1075         } else {
1076             atok = null;
1077         }
1078 
1079         final Size hval = size(htok);
1080         final Size sval = size(stok);
1081         final Size bval = size(btok);
1082 
1083         final double hue = hval.pixels(); // no clamp - hue can be negative
1084         final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
1085         final double brightness = clamp(0.0f, bval.pixels(), 1.0f);
1086 
1087         final Size aval = (atok != null) ? size(atok) : null;
1088         final double opacity =  (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;
1089 
1090         return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null);
1091     }
1092 
1093     // derive(color, pct)
1094     private ParsedValueImpl<ParsedValue[],Color> derive(final Term root)


1121         // first term in the chain is the function name...
1122         final String fn = (root.token != null) ? root.token.getText() : null;
1123         if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
1124             final String msg = "Expected \'ladder\'";
1125             error(root, msg);
1126         }
1127 
1128         if (LOGGER.isLoggable(Level.WARNING)) {
1129             LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
1130         }
1131 
1132         Term term = root;
1133 
1134         if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'");
1135         final ParsedValueImpl<?,Color> color = parse(term);
1136 
1137         Term prev = term;
1138 
1139         if ((term = term.nextInSeries) == null) error(prev,  "Expected \'stops\'");
1140         if (term.token == null ||
1141             term.token.getType() != CSSLexer.IDENT ||
1142             !"stops".equalsIgnoreCase(term.token.getText())) error(term,  "Expected \'stops\'");
1143 
1144         prev = term;
1145 
1146         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'");
1147 
1148         int nStops = 0;
1149         Term temp = term;
1150         do {
1151             nStops += 1;
1152             // if next token type is IDENT, then we have CycleMethod
1153         } while (((temp = temp.nextInSeries) != null) &&
1154                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1155 
1156         ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
1157         values[0] = color;
1158         int stopIndex = 1;
1159         do {
1160             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1161             if (stop != null) values[stopIndex++] = stop;
1162             prev = term;
1163         } while(((term = term.nextInSeries) != null) &&
1164                  (term.token.getType() == CSSLexer.LPAREN));
1165 
1166         // if term is not null and the last term was not an lparen,
1167         // then term starts a new series of Paint. Point
1168         // root.nextInSeries to term so the next loop skips over the
1169         // already parsed ladder bits.
1170         if (term != null) {
1171             root.nextInSeries = term;
1172         }
1173 
1174         // if term is null, then we are at the end of a series.
1175         // root points to 'ladder', now we want the next term after root
1176         // to be the term after the last stop, which may be another layer
1177         else {
1178             root.nextInSeries = null;
1179             root.nextLayer = prev.nextLayer;
1180         }
1181 
1182         return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
1183     }
1184 


1355 
1356         ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs];
1357         for (int n=0; n<nArgs; n++) {
1358             stops[n] = new ParsedValueImpl<ParsedValue[],Stop>(
1359                 new ParsedValueImpl[] {
1360                     new ParsedValueImpl<Size,Size>(positions[n], null),
1361                     colors[n]
1362                 },
1363                 StopConverter.getInstance()
1364             );
1365         }
1366 
1367         return stops;
1368 
1369     }
1370 
1371     // parse (<number>, <number>)
1372     private ParsedValueImpl[] point(final Term root) throws ParseException {
1373 
1374         if (root.token == null ||
1375             root.token.getType() != CSSLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'");
1376 
1377         final String fn = root.token.getText();
1378         if (fn == null || !"(".equalsIgnoreCase(fn)) {
1379             final String msg = "Expected \'(\'";
1380             error(root, msg);
1381         }
1382 
1383         Term arg = null;
1384 
1385         // no <number>
1386         if ((arg = root.firstArg) == null)  error(root, "Expected \'<number>\'");
1387 
1388         final ParsedValueImpl<?,Size> ptX = parseSize(arg);
1389 
1390         final Term prev = arg;
1391 
1392         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1393 
1394         final ParsedValueImpl<?,Size> ptY = parseSize(arg);
1395 


1417         } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
1418             return parseRadialGradient(root);
1419         } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
1420             return parseImagePattern(root);
1421         } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
1422             return parseRepeatingImagePattern(root);
1423         } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
1424             return parseLadder(root);
1425         } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
1426             return parseRegion(root);
1427         } else {
1428             error(root, "Unexpected function \'" + fcn + "\'");
1429         }
1430         return null;
1431     }
1432 
1433     private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException {
1434 
1435         if (root == null) return null;
1436         if (root.token == null ||
1437             root.token.getType() != CSSLexer.IDENT ||
1438             root.token.getText() == null ||
1439             root.token.getText().isEmpty()) {
1440             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1441             error(root, msg);
1442         }
1443         final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
1444         BlurType blurType = BlurType.THREE_PASS_BOX;
1445         if ("gaussian".equals(blurStr)) {
1446             blurType = BlurType.GAUSSIAN;
1447         } else if ("one-pass-box".equals(blurStr)) {
1448             blurType = BlurType.ONE_PASS_BOX;
1449         } else if ("two-pass-box".equals(blurStr)) {
1450             blurType = BlurType.TWO_PASS_BOX;
1451         } else if ("three-pass-box".equals(blurStr)) {
1452             blurType = BlurType.THREE_PASS_BOX;
1453         } else {
1454             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1455             error(root, msg);
1456         }
1457         return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class));


1545 
1546         prev = arg;
1547         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1548 
1549         ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
1550 
1551         ParsedValueImpl[] values = new ParsedValueImpl[] {
1552             blurVal,
1553             colorVal,
1554             radiusVal,
1555             spreadVal,
1556             offsetXVal,
1557             offsetYVal
1558         };
1559         return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance());
1560     }
1561 
1562     // returns null if the Term is null or is not a cycle method.
1563     private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) {
1564         CycleMethod cycleMethod = null;
1565         if (root != null && root.token.getType() == CSSLexer.IDENT) {
1566 
1567             final String text = root.token.getText().toLowerCase(Locale.ROOT);
1568             if ("repeat".equals(text)) {
1569                 cycleMethod = CycleMethod.REPEAT;
1570             } else if ("reflect".equals(text)) {
1571                 cycleMethod = CycleMethod.REFLECT;
1572             } else if ("no-cycle".equals(text)) {
1573                 cycleMethod = CycleMethod.NO_CYCLE;
1574             }
1575         }
1576         if (cycleMethod != null)
1577             return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1578         else
1579             return null;
1580     }
1581 
1582     // linear <point> TO <point> STOPS <stop>+ cycleMethod?
1583     private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException {
1584 
1585         final String fn = (root.token != null) ? root.token.getText() : null;
1586         if (fn == null || !"linear".equalsIgnoreCase(fn)) {
1587             final String msg = "Expected \'linear\'";
1588             error(root, msg);
1589         }
1590 
1591         if (LOGGER.isLoggable(Level.WARNING)) {
1592             LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
1593         }
1594 
1595         Term term = root;
1596 
1597         if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'");
1598 
1599         final ParsedValueImpl<?,Size>[] startPt = point(term);
1600 
1601         Term prev = term;
1602         if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
1603         if (term.token == null ||
1604             term.token.getType() != CSSLexer.IDENT ||
1605             !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");
1606 
1607         prev = term;
1608         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1609 
1610         final ParsedValueImpl<?,Size>[] endPt = point(term);
1611 
1612         prev = term;
1613         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1614         if (term.token == null ||
1615             term.token.getType() != CSSLexer.IDENT ||
1616             !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1617 
1618         prev = term;
1619         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1620 
1621         int nStops = 0;
1622         Term temp = term;
1623         do {
1624             nStops += 1;
1625             // if next token type is IDENT, then we have CycleMethod
1626         } while (((temp = temp.nextInSeries) != null) &&
1627                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1628 
1629         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1630         int stopIndex = 0;
1631         do {
1632             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1633             if (stop != null) stops[stopIndex++] = stop;
1634             prev = term;
1635         } while(((term = term.nextInSeries) != null) &&
1636                 (term.token.getType() == CSSLexer.LPAREN));
1637 
1638         // term is either null or is a cycle method, or the start of another Paint.
1639         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1640 
1641         if (cycleMethod == null) {
1642 
1643             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1644 
1645             // if term is not null and the last term was not a cycle method,
1646             // then term starts a new series or layer of Paint
1647             if (term != null) {
1648                 root.nextInSeries = term;
1649             }
1650 
1651             // if term is null, then we are at the end of a series.
1652             // root points to 'linear', now we want the next term after root
1653             // to be the term after the last stop, which may be another layer
1654             else {
1655                 root.nextInSeries = null;
1656                 root.nextLayer = prev.nextLayer;


1717         ParsedValueImpl<?,Size>[] startPt = null;
1718         ParsedValueImpl<?,Size>[] endPt = null;
1719 
1720         if ("from".equalsIgnoreCase(arg.token.getText())) {
1721 
1722             prev = arg;
1723             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1724 
1725             ParsedValueImpl<?,Size> ptX = parseSize(arg);
1726 
1727             prev = arg;
1728             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1729 
1730             ParsedValueImpl<?,Size> ptY = parseSize(arg);
1731 
1732             startPt = new ParsedValueImpl[] { ptX, ptY };
1733 
1734             prev = arg;
1735             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
1736             if (arg.token == null ||
1737                 arg.token.getType() != CSSLexer.IDENT ||
1738                 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");
1739 
1740             prev = arg;
1741             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1742 
1743             ptX = parseSize(arg);
1744 
1745             prev = arg;
1746             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1747 
1748             ptY = parseSize(arg);
1749 
1750             endPt = new ParsedValueImpl[] { ptX, ptY };
1751 
1752             prev = arg;
1753             arg = arg.nextArg;
1754 
1755         } else if("to".equalsIgnoreCase(arg.token.getText())) {
1756 
1757             prev = arg;
1758             if ((arg = arg.nextInSeries) == null ||
1759                 arg.token == null ||
1760                 arg.token.getType() != CSSLexer.IDENT ||
1761                 arg.token.getText().isEmpty()) {
1762                 error (prev, "Expected \'<side-or-corner>\'");
1763             }
1764 
1765 
1766             int startX = 0;
1767             int startY = 0;
1768             int endX = 0;
1769             int endY = 0;
1770 
1771             String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
1772             // The keywords denote the direction.
1773             if ("top".equals(sideOrCorner1)) {
1774                 // going toward the top, then start at the bottom
1775                 startY = 100;
1776                 endY = 0;
1777 
1778             } else if ("bottom".equals(sideOrCorner1)) {
1779                 // going toward the bottom, then start at the top
1780                 startY = 0;
1781                 endY = 100;
1782 
1783             } else if ("right".equals(sideOrCorner1)) {
1784                 // going toward the right, then start at the left
1785                 startX = 0;
1786                 endX = 100;
1787 
1788             } else if ("left".equals(sideOrCorner1)) {
1789                 // going toward the left, then start at the right
1790                 startX = 100;
1791                 endX = 0;
1792 
1793             } else {
1794                 error(arg, "Invalid \'<side-or-corner>\'");
1795             }
1796 
1797             prev = arg;
1798             if (arg.nextInSeries != null) {
1799                 arg = arg.nextInSeries;
1800                 if (arg.token != null &&
1801                     arg.token.getType() == CSSLexer.IDENT &&
1802                     !arg.token.getText().isEmpty()) {
1803 
1804                     String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);
1805 
1806                     // if right or left has already been given,
1807                     // then either startX or endX will not be zero.
1808                     if ("right".equals(sideOrCorner2) &&
1809                         startX == 0 && endX == 0) {
1810                         // start left, end right
1811                         startX = 0;
1812                         endX = 100;
1813                     } else if ("left".equals(sideOrCorner2) &&
1814                         startX == 0 && endX == 0) {
1815                         // start right, end left
1816                         startX = 100;
1817                         endX = 0;
1818 
1819                     // if top or bottom has already been given,
1820                     // then either startY or endY will not be zero.
1821                     } else if("top".equals(sideOrCorner2) &&


1909     private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException {
1910 
1911         final String fn = (root.token != null) ? root.token.getText() : null;
1912         if (fn == null || !"radial".equalsIgnoreCase(fn)) {
1913             final String msg = "Expected \'radial\'";
1914             error(root, msg);
1915         }
1916 
1917         if (LOGGER.isLoggable(Level.WARNING)) {
1918             LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
1919         }
1920 
1921         Term term = root;
1922         Term prev = root;
1923 
1924         if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1925         if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1926 
1927 
1928         ParsedValueImpl<?,Size> focusAngle = null;
1929         if (term.token.getType() == CSSLexer.IDENT) {
1930             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1931             if ("focus-angle".equals(keyword)) {
1932 
1933                 prev = term;
1934                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1935                 if (term.token == null) error(prev, "Expected \'<number>\'");
1936 
1937                 focusAngle = parseSize(term);
1938 
1939                 prev = term;
1940                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1941                 if (term.token == null) error(term,  "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1942             }
1943         }
1944 
1945         ParsedValueImpl<?,Size> focusDistance = null;
1946         if (term.token.getType() == CSSLexer.IDENT) {
1947             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1948             if ("focus-distance".equals(keyword)) {
1949 
1950                 prev = term;
1951                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1952                 if (term.token == null) error(prev, "Expected \'<number>\'");
1953 
1954                 focusDistance = parseSize(term);
1955 
1956                 prev = term;
1957                 if ((term = term.nextInSeries) == null) error(prev, "Expected  \'center (<number>,<number>)\' or \'<size>\'");
1958                 if (term.token == null) error(term,  "Expected  \'center (<number>,<number>)\' or \'<size>\'");
1959             }
1960         }
1961 
1962         ParsedValueImpl<?,Size>[] centerPoint = null;
1963         if (term.token.getType() == CSSLexer.IDENT) {
1964             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1965             if ("center".equals(keyword)) {
1966 
1967                 prev = term;
1968                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'");
1969                 if (term.token == null ||
1970                     term.token.getType() != CSSLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'");
1971 
1972                 centerPoint = point(term);
1973 
1974                 prev = term;
1975                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'");
1976                 if (term.token == null) error(term,  "Expected \'<size>\'");
1977             }
1978         }
1979 
1980         ParsedValueImpl<?,Size> radius = parseSize(term);
1981 
1982         prev = term;
1983         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
1984         if (term.token == null ||
1985             term.token.getType() != CSSLexer.IDENT) error(term, "Expected \'stops\' keyword");
1986 
1987         if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1988 
1989         prev = term;
1990         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1991 
1992         int nStops = 0;
1993         Term temp = term;
1994         do {
1995             nStops += 1;
1996             // if next token type is IDENT, then we have CycleMethod
1997         } while (((temp = temp.nextInSeries) != null) &&
1998                  ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1999 
2000         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
2001         int stopIndex = 0;
2002         do {
2003             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
2004             if (stop != null) stops[stopIndex++] = stop;
2005             prev = term;
2006         } while(((term = term.nextInSeries) != null) &&
2007                 (term.token.getType() == CSSLexer.LPAREN));
2008 
2009         // term is either null or is a cycle method, or the start of another Paint.
2010         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
2011 
2012         if (cycleMethod == null) {
2013 
2014             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
2015 
2016             // if term is not null and the last term was not a cycle method,
2017             // then term starts a new series or layer of Paint
2018             if (term != null) {
2019                 root.nextInSeries = term;
2020             }
2021 
2022             // if term is null, then we are at the end of a series.
2023             // root points to 'linear', now we want the next term after root
2024             // to be the term after the last stop, which may be another layer
2025             else {
2026                 root.nextInSeries = null;
2027                 root.nextLayer = prev.nextLayer;


2422             }
2423             temp = nextLayer(temp);
2424         }
2425 
2426         return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance());
2427     }
2428 
2429     // <size> | <size> <size> <size> <size>
2430     private ParsedValueImpl<Size, Size>[] parseSizeSeries(Term root)
2431             throws ParseException {
2432 
2433         if (root.token == null) error(root, "Parse error");
2434 
2435         List<ParsedValueImpl<Size,Size>> sizes = new ArrayList<>();
2436 
2437         Term term = root;
2438         while(term != null) {
2439             Token token = term.token;
2440             final int ttype = token.getType();
2441             switch (ttype) {
2442                 case CSSLexer.NUMBER:
2443                 case CSSLexer.PERCENTAGE:
2444                 case CSSLexer.EMS:
2445                 case CSSLexer.EXS:
2446                 case CSSLexer.PX:
2447                 case CSSLexer.CM:
2448                 case CSSLexer.MM:
2449                 case CSSLexer.IN:
2450                 case CSSLexer.PT:
2451                 case CSSLexer.PC:
2452                 case CSSLexer.DEG:
2453                 case CSSLexer.GRAD:
2454                 case CSSLexer.RAD:
2455                 case CSSLexer.TURN:
2456                     ParsedValueImpl sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
2457                     sizes.add(sizeValue);
2458                     break;
2459                 default:
2460                     error (root, "expected series of <size>");
2461             }
2462             term = term.nextInSeries;
2463         }
2464         return sizes.toArray(new ParsedValueImpl[sizes.size()]);
2465 
2466     }
2467 
2468     // http://www.w3.org/TR/css3-background/#the-border-radius
2469     // <size>{1,4} [ '/' <size>{1,4}]? [',' <size>{1,4} [ '/' <size>{1,4}]?]?
2470     private ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root)
2471             throws ParseException {
2472 
2473 
2474         int nLayers = numberOfLayers(root);
2475 
2476         Term term = root;
2477         int layer = 0;
2478         ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers];
2479 
2480         while(term != null) {
2481 
2482             int nHorizontalTerms = 0;
2483             Term temp = term;
2484             while (temp != null) {
2485                 if (temp.token.getType() == CSSLexer.SOLIDUS) {
2486                     temp = temp.nextInSeries;
2487                     break;
2488                 }
2489                 nHorizontalTerms += 1;
2490                 temp = temp.nextInSeries;
2491             };
2492 
2493             int nVerticalTerms = 0;
2494             while (temp != null) {
2495                 if (temp.token.getType() == CSSLexer.SOLIDUS) {
2496                     error(temp, "unexpected SOLIDUS");
2497                     break;
2498                 }
2499                 nVerticalTerms += 1;
2500                 temp = temp.nextInSeries;
2501             }
2502 
2503             if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) {
2504                 error(root, "expected [<length>|<percentage>]{1,4} [/ [<length>|<percentage>]{1,4}]?");
2505             }
2506 
2507             // used as index into margins[]. horizontal = 0, vertical = 1
2508             int orientation = 0;
2509 
2510             // at most, there should be four radii in the horizontal orientation and four in the vertical.
2511             ParsedValueImpl<?,Size>[][] radii = new ParsedValueImpl[2][4];
2512 
2513             ParsedValueImpl<?,Size> zero = new ParsedValueImpl<Size,Size>(new Size(0,SizeUnits.PX), null);
2514             for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; }
2515 
2516             int hr = 0;
2517             int vr = 0;
2518 
2519             Term lastTerm = term;
2520             while ((hr <= 4) && (vr <= 4) && (term != null)) {
2521 
2522                 if (term.token.getType() == CSSLexer.SOLIDUS) {
2523                     orientation += 1;
2524                 } else  {
2525                     ParsedValueImpl<?,Size> parsedValue = parseSize(term);
2526                     if (orientation == 0) {
2527                         radii[orientation][hr++] = parsedValue;
2528                     } else {
2529                         radii[orientation][vr++] = parsedValue;
2530                     }
2531                 }
2532                 lastTerm = term;
2533                 term = term.nextInSeries;
2534             }
2535 
2536             //
2537             // http://www.w3.org/TR/css3-background/#the-border-radius
2538             // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left.
2539             // If bottom-left is omitted it is the same as top-right.
2540             // If bottom-right is omitted it is the same as top-left.
2541             // If top-right is omitted it is the same as top-left.
2542             //


2923         while (term != null) {
2924             layers[layer++] = parseBackgroundPosition(term);
2925             term = nextLayer(term);
2926         }
2927         return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance());
2928     }
2929 
2930     /*
2931     http://www.w3.org/TR/css3-background/#the-background-repeat
2932     <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
2933     */
2934     private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root)
2935             throws ParseException {
2936 
2937         BackgroundRepeat xAxis, yAxis;
2938         xAxis = yAxis = BackgroundRepeat.NO_REPEAT;
2939 
2940         Term term = root;
2941 
2942         if (term.token == null ||
2943             term.token.getType() != CSSLexer.IDENT ||
2944             term.token.getText() == null ||
2945             term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'");
2946 
2947         String text = term.token.getText().toLowerCase(Locale.ROOT);
2948         if ("repeat-x".equals(text)) {
2949             xAxis = BackgroundRepeat.REPEAT;
2950             yAxis = BackgroundRepeat.NO_REPEAT;
2951         } else if ("repeat-y".equals(text)) {
2952             xAxis = BackgroundRepeat.NO_REPEAT;
2953             yAxis = BackgroundRepeat.REPEAT;
2954         } else if ("repeat".equals(text)) {
2955             xAxis = BackgroundRepeat.REPEAT;
2956             yAxis = BackgroundRepeat.REPEAT;
2957         } else if ("space".equals(text)) {
2958             xAxis = BackgroundRepeat.SPACE;
2959             yAxis = BackgroundRepeat.SPACE;
2960         } else if ("round".equals(text)) {
2961             xAxis = BackgroundRepeat.ROUND;
2962             yAxis = BackgroundRepeat.ROUND;
2963         } else if ("no-repeat".equals(text)) {
2964             xAxis = BackgroundRepeat.NO_REPEAT;
2965             yAxis = BackgroundRepeat.NO_REPEAT;
2966         } else if ("stretch".equals(text)) {
2967             xAxis = BackgroundRepeat.NO_REPEAT;
2968             yAxis = BackgroundRepeat.NO_REPEAT;
2969         } else {
2970             error(term, "Expected  \'<repeat-style>\' " + text);
2971         }
2972 
2973         if ((term = term.nextInSeries) != null &&
2974              term.token != null &&
2975              term.token.getType() == CSSLexer.IDENT &&
2976              term.token.getText() != null &&
2977              !term.token.getText().isEmpty()) {
2978 
2979             text = term.token.getText().toLowerCase(Locale.ROOT);
2980             if ("repeat-x".equals(text)) {
2981                 error(term, "Unexpected \'repeat-x\'");
2982             } else if ("repeat-y".equals(text)) {
2983                 error(term, "Unexpected \'repeat-y\'");
2984             } else if ("repeat".equals(text)) {
2985                 yAxis = BackgroundRepeat.REPEAT;
2986             } else if ("space".equals(text)) {
2987                 yAxis = BackgroundRepeat.SPACE;
2988             } else if ("round".equals(text)) {
2989                 yAxis = BackgroundRepeat.ROUND;
2990             } else if ("no-repeat".equals(text)) {
2991                 yAxis = BackgroundRepeat.NO_REPEAT;
2992             } else if ("stretch".equals(text)) {
2993                 yAxis = BackgroundRepeat.NO_REPEAT;
2994             } else {
2995                 error(term, "Expected  \'<repeat-style>\'");


3027         while (term != null) {
3028             layers[layer++] = parseRepeatStyle(term);
3029             term = nextLayer(term);
3030         }
3031         return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
3032     }
3033 
3034     /*
3035     http://www.w3.org/TR/css3-background/#the-background-size
3036     <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
3037     */
3038     private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root)
3039         throws ParseException {
3040 
3041         ParsedValueImpl<?,Size> height = null, width = null;
3042         boolean cover = false, contain = false;
3043 
3044         Term term = root;
3045         if (term.token == null) error(term, "Expected \'<bg-size>\'");
3046 
3047         if (term.token.getType() == CSSLexer.IDENT) {
3048             final String text =
3049                 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3050 
3051             if ("auto".equals(text)) {
3052                 // We don't do anything because width / height are already initialized
3053             } else if ("cover".equals(text)) {
3054                 cover = true;
3055             } else if ("contain".equals(text)) {
3056                 contain = true;
3057             } else if ("stretch".equals(text)) {
3058                 width = ONE_HUNDRED_PERCENT;
3059                 height = ONE_HUNDRED_PERCENT;
3060             } else {
3061                 error(term, "Expected \'auto\', \'cover\', \'contain\', or  \'stretch\'");
3062             }
3063         } else if (isSize(term.token)) {
3064             width = parseSize(term);
3065             height = null;
3066         } else {
3067             error(term, "Expected \'<bg-size>\'");
3068         }
3069 
3070         if ((term = term.nextInSeries) != null) {
3071             if (cover || contain) error(term, "Unexpected \'<bg-size>\'");
3072 
3073             if (term.token.getType() == CSSLexer.IDENT) {
3074                 final String text =
3075                     (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3076 
3077                 if ("auto".equals(text)) {
3078                     height = null;
3079                 } else if ("cover".equals(text)) {
3080                     error(term, "Unexpected \'cover\'");
3081                 } else if ("contain".equals(text)) {
3082                     error(term, "Unexpected \'contain\'");
3083                 } else if ("stretch".equals(text)) {
3084                     height = ONE_HUNDRED_PERCENT;
3085                 } else {
3086                     error(term, "Expected \'auto\' or \'stretch\'");
3087                 }
3088             } else if (isSize(term.token)) {
3089                 height = parseSize(term);
3090             } else {
3091                 error(term, "Expected \'<bg-size>\'");
3092             }
3093 


3172 
3173 
3174     private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>
3175             parseBorderStyleLayers(final Term root) throws ParseException {
3176 
3177         int nLayers = numberOfLayers(root);
3178         ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers];
3179         int layer = 0;
3180         Term term = root;
3181         while (term != null) {
3182             layers[layer++] = parseBorderStyleSeries(term);
3183             term = nextLayer(term);
3184         }
3185         return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance());
3186     }
3187 
3188     // Only meant to be used from parseBorderStyle, but might be useful elsewhere
3189     private String getKeyword(final Term term) {
3190         if (term != null &&
3191              term.token != null &&
3192              term.token.getType() == CSSLexer.IDENT &&
3193              term.token.getText() != null &&
3194              !term.token.getText().isEmpty()) {
3195 
3196             return term.token.getText().toLowerCase(Locale.ROOT);
3197         }
3198         return null;
3199     }
3200 
3201     //<border-style> [ , <border-style> ]*
3202     // where <border-style> =
3203     //      <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]?
3204     // where <dash-style> =
3205     //      [ none | solid | dotted | dashed ]
3206     private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root)
3207             throws ParseException {
3208 
3209 
3210         ParsedValue<ParsedValue[],Number[]> dashStyle = null;
3211         ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null;
3212         ParsedValue<String,StrokeType> strokeType = null;


3294             dashPhase,
3295             strokeType,
3296             strokeLineJoin,
3297             strokeMiterLimit,
3298             strokeLineCap
3299         };
3300 
3301         return new ParsedValueImpl(values, BorderStyleConverter.getInstance());
3302     }
3303 
3304     //
3305     // segments(<size> [, <size>]+) | <border-style>
3306     //
3307     private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException {
3308 
3309         if (root.token == null) error(root, "Expected \'<dash-style>\'");
3310 
3311         final int ttype = root.token.getType();
3312 
3313         ParsedValue<ParsedValue[],Number[]>  segments = null;
3314         if (ttype == CSSLexer.IDENT) {
3315             segments = borderStyle(root);
3316         } else if (ttype == CSSLexer.FUNCTION) {
3317             segments = segments(root);
3318         } else {
3319             error(root, "Expected \'<dash-style>\'");
3320         }
3321 
3322         return segments;
3323     }
3324 
3325     /*
3326     <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
3327     */
3328     private ParsedValue<ParsedValue[],Number[]>  borderStyle(Term root)
3329             throws ParseException {
3330 
3331         if (root.token == null ||
3332             root.token.getType() != CSSLexer.IDENT ||
3333             root.token.getText() == null ||
3334             root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'");
3335 
3336         final String text = root.token.getText().toLowerCase(Locale.ROOT);
3337 
3338         if ("none".equals(text)) {
3339             return BorderStyleConverter.NONE;
3340         } else if ("hidden".equals(text)) {
3341             // The "hidden" mode doesn't make sense for FX, because it is the
3342             // same as "none" except for border-collapsed CSS tables
3343             return BorderStyleConverter.NONE;
3344         } else if ("dotted".equals(text)) {
3345             return BorderStyleConverter.DOTTED;
3346         } else if ("dashed".equals(text)) {
3347             return BorderStyleConverter.DASHED;
3348         } else if ("solid".equals(text)) {
3349             return BorderStyleConverter.SOLID;
3350         } else if ("double".equals(text)) {
3351             error(root, "Unsupported <border-style> \'double\'");
3352         } else if ("groove".equals(text)) {


3461     /*
3462      * http://www.w3.org/TR/css3-background/#the-border-image-slice
3463      * [<number> | <percentage>]{1,4} && fill?
3464      */
3465     private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root)
3466         throws ParseException {
3467 
3468         Term term = root;
3469         if (term.token == null || !isSize(term.token))
3470                 error(term, "Expected \'<size>\'");
3471 
3472         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3473         Boolean fill = Boolean.FALSE;
3474 
3475         int inset = 0;
3476         while (inset < 4 && term != null) {
3477             insets[inset++] = parseSize(term);
3478 
3479             if ((term = term.nextInSeries) != null &&
3480                  term.token != null &&
3481                  term.token.getType() == CSSLexer.IDENT) {
3482 
3483                 if("fill".equalsIgnoreCase(term.token.getText())) {
3484                     fill = Boolean.TRUE;
3485                     break;
3486                 }
3487             }
3488         }
3489 
3490         if (inset < 2) insets[1] = insets[0]; // right = top
3491         if (inset < 3) insets[2] = insets[0]; // bottom = top
3492         if (inset < 4) insets[3] = insets[1]; // left = right
3493 
3494         ParsedValueImpl[] values = new ParsedValueImpl[] {
3495                 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()),
3496                 new ParsedValueImpl<Boolean,Boolean>(fill, null)
3497         };
3498         return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance());
3499     }
3500 
3501     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]>


3514 
3515     /*
3516      * http://www.w3.org/TR/css3-background/#border-image-width
3517      * [ <length> | <percentage> | <number> | auto ]{1,4}
3518      */
3519     private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root)
3520             throws ParseException {
3521 
3522         Term term = root;
3523         if (term.token == null || !isSize(term.token))
3524             error(term, "Expected \'<size>\'");
3525 
3526         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3527 
3528         int inset = 0;
3529         while (inset < 4 && term != null) {
3530             insets[inset++] = parseSize(term);
3531 
3532             if ((term = term.nextInSeries) != null &&
3533                     term.token != null &&
3534                     term.token.getType() == CSSLexer.IDENT) {
3535             }
3536         }
3537 
3538         if (inset < 2) insets[1] = insets[0]; // right = top
3539         if (inset < 3) insets[2] = insets[0]; // bottom = top
3540         if (inset < 4) insets[3] = insets[1]; // left = right
3541 
3542         return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance());
3543     }
3544 
3545     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]>
3546         parseBorderImageWidthLayers(final Term root) throws ParseException {
3547 
3548         int nLayers = numberOfLayers(root);
3549         ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers];
3550         int layer = 0;
3551         Term term = root;
3552         while (term != null) {
3553             layers[layer++] = parseBorderImageWidth(term);
3554             term = nextLayer(term);
3555         }
3556         return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance());
3557     }
3558 
3559     // parse a Region value
3560     // i.e., region(".styleClassForRegion") or region("#idForRegion")
3561     public static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:";
3562     private ParsedValueImpl<String,String> parseRegion(Term root)
3563             throws ParseException {
3564         // first term in the chain is the function name...
3565         final String fn = (root.token != null) ? root.token.getText() : null;
3566         if (!"region".regionMatches(true, 0, fn, 0, 6)) {
3567             error(root,"Expected \'region\'");
3568         }
3569 
3570         Term arg = root.firstArg;
3571         if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3572 
3573         if (arg.token == null ||
3574                 arg.token.getType() != CSSLexer.STRING ||
3575                 arg.token.getText() == null ||
3576                 arg.token.getText().isEmpty())  error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3577 
3578         final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText());
3579         return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance());
3580     }
3581 
3582     // url("<uri>") is tokenized by the lexer, so the root arg should be a URL token.
3583     private ParsedValueImpl<ParsedValue[],String> parseURI(Term root)
3584             throws ParseException {
3585 
3586         if (root == null) error(root, "Expected \'url(\"<uri-string>\")\'");
3587 
3588         if (root.token == null ||
3589             root.token.getType() != CSSLexer.URL ||
3590             root.token.getText() == null ||
3591             root.token.getText().isEmpty()) error(root, "Expected \'url(\"<uri-string>\")\'");
3592 
3593         final String uri = root.token.getText();
3594         ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
3595             new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
3596             null // placeholder for Stylesheet URL
3597         };
3598         return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
3599     }
3600 
3601     // parse a series of URI values separated by commas.
3602     // i.e., <uri> [, <uri>]*
3603     private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root)
3604             throws ParseException {
3605 
3606         int nLayers = numberOfLayers(root);
3607 
3608         Term temp = root;
3609         int layer = 0;


3614             temp = nextLayer(temp);
3615         }
3616 
3617         return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance());
3618     }
3619 
3620     ////////////////////////////////////////////////////////////////////////////
3621     //
3622     // http://www.w3.org/TR/css3-fonts
3623     //
3624     ////////////////////////////////////////////////////////////////////////////
3625 
3626     /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */
3627     private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException {
3628 
3629         if (root == null) return null;
3630         final Token token = root.token;
3631         if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'");
3632 
3633         Size size = null;
3634         if (token.getType() == CSSLexer.IDENT) {
3635             final String ident = token.getText().toLowerCase(Locale.ROOT);
3636             double value = -1;
3637             if ("inherit".equals(ident)) {
3638                 value = 100;
3639             } else if ("xx-small".equals(ident)) {
3640                 value = 60;
3641             } else if ("x-small".equals(ident)) {
3642                 value = 75;
3643             } else if ("small".equals(ident)) {
3644                 value = 80;
3645             } else if ("medium".equals(ident)) {
3646                 value = 100;
3647             } else if ("large".equals(ident)) {
3648                 value = 120;
3649             } else if ("x-large".equals(ident)) {
3650                 value = 150;
3651             } else if ("xx-large".equals(ident)) {
3652                 value = 200;
3653             } else if ("smaller".equals(ident)) {
3654                 value = 80;


3659             if (value > -1) {
3660                 size = new Size(value, SizeUnits.PERCENT);
3661             }
3662         }
3663 
3664         // if size is null, then size is not one of the keywords above.
3665         if (size == null) {
3666             size = size(token);
3667         }
3668 
3669         ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null);
3670         return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance());
3671     }
3672 
3673     /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */
3674     private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException {
3675 
3676         if (root == null) return null;
3677         final Token token = root.token;
3678         if (token == null ||
3679             token.getType() != CSSLexer.IDENT ||
3680             token.getText() == null ||
3681             token.getText().isEmpty()) error(root, "Expected \'<font-style>\'");
3682 
3683         final String ident = token.getText().toLowerCase(Locale.ROOT);
3684         String posture = FontPosture.REGULAR.name();
3685 
3686         if ("normal".equals(ident)) {
3687             posture = FontPosture.REGULAR.name();
3688         } else if ("italic".equals(ident)) {
3689             posture = FontPosture.ITALIC.name();
3690         } else if ("oblique".equals(ident)) {
3691             posture = FontPosture.ITALIC.name();
3692         } else if ("inherit".equals(ident)) {
3693             posture = "inherit";
3694         } else {
3695             return null;
3696         }
3697 
3698         return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance());
3699     }


3733         } else if ("600".equals(ident)) {
3734             weight = FontWeight.findByWeight(600).name();
3735         } else if ("700".equals(ident)) {
3736             weight = FontWeight.findByWeight(700).name();
3737         } else if ("800".equals(ident)) {
3738             weight = FontWeight.findByWeight(800).name();
3739         } else if ("900".equals(ident)) {
3740             weight = FontWeight.findByWeight(900).name();
3741         } else {
3742             error(root, "Expected \'<font-weight>\'");
3743         }
3744         return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance());
3745     }
3746 
3747     private ParsedValueImpl<String,String>  parseFontFamily(Term root) throws ParseException {
3748 
3749         if (root == null) return null;
3750         final Token token = root.token;
3751         String text = null;
3752         if (token == null ||
3753             (token.getType() != CSSLexer.IDENT &&
3754              token.getType() != CSSLexer.STRING) ||
3755             (text = token.getText()) == null ||
3756             text.isEmpty()) error(root, "Expected \'<font-family>\'");
3757 
3758         final String fam = stripQuotes(text.toLowerCase(Locale.ROOT));
3759         if ("inherit".equals(fam)) {
3760             return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance());
3761         } else if ("serif".equals(fam) ||
3762             "sans-serif".equals(fam) ||
3763             "cursive".equals(fam) ||
3764             "fantasy".equals(fam) ||
3765             "monospace".equals(fam)) {
3766             return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance());
3767         } else {
3768             return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance());
3769         }
3770     }
3771 
3772     // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily
3773     private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException {
3774 
3775         // Because style, variant, weight, size and family can inherit
3776         // AND style, variant and weight are optional, parsing this backwards
3777         // is easier.
3778         Term next = root.nextInSeries;
3779         root.nextInSeries = null;
3780         while (next != null) {
3781             Term temp = next.nextInSeries;
3782             next.nextInSeries = root;
3783             root = next;
3784             next = temp;
3785         }
3786 
3787         // Now, root should point to fontFamily
3788         Token token = root.token;
3789         int ttype = token.getType();
3790         if (ttype != CSSLexer.IDENT &&
3791             ttype != CSSLexer.STRING) error(root, "Expected \'<font-family>\'");
3792         ParsedValueImpl<String,String> ffamily = parseFontFamily(root);
3793 
3794         Term term = root;
3795         if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'");
3796         if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3797 
3798         // Now, term could be the font size or it could be the line-height.
3799         // If the next term is a forward slash, then it's line-height.
3800         Term temp;
3801         if (((temp = term.nextInSeries) != null) &&
3802             (temp.token != null && temp.token.getType() == CSSLexer.SOLIDUS)) {
3803 
3804             root = temp;
3805 
3806             if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'");
3807             if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3808 
3809             token = term.token;
3810         }
3811 
3812         ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term);
3813         if (fsize == null) error(root, "Expected \'<size>\'");
3814 
3815         ParsedValueImpl<String,FontPosture> fstyle = null;
3816         ParsedValueImpl<String,FontWeight> fweight = null;
3817         String fvariant = null;
3818 
3819         while ((term = term.nextInSeries) != null) {
3820 
3821             if (term.token == null ||
3822                 term.token.getType() != CSSLexer.IDENT ||
3823                 term.token.getText() == null ||
3824                 term.token.getText().isEmpty())
3825                 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'");
3826 
3827             if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) {
3828                 ;
3829             } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) {
3830                 fvariant = term.token.getText();
3831             } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) {
3832                 ;
3833             }
3834         }
3835 
3836         ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle };
3837         return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance());
3838     }
3839 
3840     //
3841     // Parser state machine
3842     //
3843     Token currentToken = null;
3844 
3845     // return the next token that is not whitespace.
3846     private Token nextToken(CSSLexer lexer) {
3847 
3848         Token token = null;
3849 
3850         do {
3851             token = lexer.nextToken();
3852         } while ((token != null) &&
3853                 (token.getType() == CSSLexer.WS) ||
3854                 (token.getType() == CSSLexer.NL));
3855 
3856         if (LOGGER.isLoggable(Level.FINEST)) {
3857             LOGGER.finest(token.toString());
3858         }
3859 
3860         return token;
3861 
3862     }
3863 
3864     // keep track of what is in process of being parsed to avoid import loops
3865     private static Stack<String> imports;
3866 
3867     private void parse(Stylesheet stylesheet, CSSLexer lexer) {
3868 
3869         // need to read the first token
3870         currentToken = nextToken(lexer);
3871 
3872         while((currentToken != null) &&
3873                 (currentToken.getType() == CSSLexer.AT_KEYWORD)) {
3874 
3875             currentToken = nextToken(lexer);
3876 
3877             if (currentToken == null || currentToken.getType() != CSSLexer.IDENT) {
3878 
3879                 // just using ParseException for a nice error message, not for throwing the exception.
3880                 ParseException parseException = new ParseException("Expected IDENT", currentToken, this);
3881                 final String msg = parseException.toString();
3882                 CssError error = createError(msg);
3883                 if (LOGGER.isLoggable(Level.WARNING)) {
3884                     LOGGER.warning(error.toString());
3885                 }
3886                 reportError(error);
3887 
3888                 // get past EOL or SEMI
3889                 do {
3890                     currentToken = lexer.nextToken();
3891                 } while ((currentToken != null) &&
3892                         (currentToken.getType() == CSSLexer.SEMI) ||
3893                         (currentToken.getType() == CSSLexer.WS) ||
3894                         (currentToken.getType() == CSSLexer.NL));
3895                 continue;
3896             }
3897 
3898             String keyword = currentToken.getText().toLowerCase(Locale.ROOT);
3899             if ("font-face".equals(keyword)) {
3900                 FontFace fontFace = fontFace(lexer);
3901                 if (fontFace != null) stylesheet.getFontFaces().add(fontFace);
3902                 currentToken = nextToken(lexer);
3903                 continue;
3904 
3905             } else if ("import".equals(keyword)) {
3906 
3907                 if (CSSParser.imports == null) {
3908                     CSSParser.imports = new Stack<>();
3909                 }
3910 
3911                 if (!imports.contains(sourceOfStylesheet)) {
3912 
3913                     imports.push(sourceOfStylesheet);
3914 
3915                     Stylesheet importedStylesheet = handleImport(lexer);
3916 
3917                     if (importedStylesheet != null) {
3918                         stylesheet.importStylesheet(importedStylesheet);
3919                     }
3920 
3921                     imports.pop();
3922 
3923                     if (CSSParser.imports.isEmpty()) {
3924                         CSSParser.imports = null;
3925                     }
3926 
3927                 } else {
3928 // Import imports import!
3929                     final int line = currentToken.getLine();
3930                     final int pos = currentToken.getOffset();
3931                     final String msg =
3932                             MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]",
3933                                     line, pos, imports.peek());
3934                     CssError error = createError(msg);
3935                     if (LOGGER.isLoggable(Level.WARNING)) {
3936                         LOGGER.warning(error.toString());
3937                     }
3938                     reportError(error);
3939                 }
3940 
3941                 // get past EOL or SEMI
3942                 do {
3943                     currentToken = lexer.nextToken();
3944                 } while ((currentToken != null) &&
3945                         (currentToken.getType() == CSSLexer.SEMI) ||
3946                         (currentToken.getType() == CSSLexer.WS) ||
3947                         (currentToken.getType() == CSSLexer.NL));
3948 
3949                 continue;
3950 
3951             }
3952         }
3953 
3954         while ((currentToken != null) &&
3955                (currentToken.getType() != Token.EOF)) {
3956 
3957             List<Selector> selectors = selectors(lexer);
3958             if (selectors == null) return;
3959 
3960             if ((currentToken == null) ||
3961                 (currentToken.getType() != CSSLexer.LBRACE)) {
3962                     final int line = currentToken != null ? currentToken.getLine() : -1;
3963                     final int pos = currentToken != null ? currentToken.getOffset() : -1;
3964                     final String msg =
3965                         MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]",
3966                                 line, pos);
3967                     CssError error = createError(msg);
3968                     if (LOGGER.isLoggable(Level.WARNING)) {
3969                         LOGGER.warning(error.toString());
3970                     }
3971                     reportError(error);
3972                 currentToken = null;
3973                 return;
3974             }
3975 
3976             // get past the LBRACE
3977             currentToken = nextToken(lexer);
3978 
3979             List<Declaration> declarations = declarations(lexer);
3980             if (declarations == null) return;
3981 
3982             if ((currentToken != null) &&
3983                 (currentToken.getType() != CSSLexer.RBRACE)) {
3984                     final int line = currentToken.getLine();
3985                     final int pos = currentToken.getOffset();
3986                     final String msg =
3987                         MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]",
3988                         line,pos);
3989                     CssError error = createError(msg);
3990                     if (LOGGER.isLoggable(Level.WARNING)) {
3991                         LOGGER.warning(error.toString());
3992                     }
3993                     reportError(error);
3994                 currentToken = null;
3995                 return;
3996             }
3997 
3998             stylesheet.getRules().add(new Rule(selectors, declarations));
3999 
4000             currentToken = nextToken(lexer);
4001 
4002         }
4003         currentToken = null;
4004     }
4005 
4006     private FontFace fontFace(CSSLexer lexer) {
4007         final Map<String,String> descriptors = new HashMap<String,String>();
4008         final List<FontFace.FontFaceSrc> sources = new ArrayList<FontFace.FontFaceSrc>();
4009         while(true) {
4010             currentToken = nextToken(lexer);
4011             if (currentToken.getType() == CSSLexer.IDENT) {
4012                 String key = currentToken.getText();
4013                 // ignore the colon that follows
4014                 currentToken = nextToken(lexer);
4015                 // get the next token after colon
4016                 currentToken = nextToken(lexer);
4017                 // ignore all but "src"
4018                 if ("src".equalsIgnoreCase(key)) {
4019                     while(true) {
4020                         if((currentToken != null) &&
4021                                 (currentToken.getType() != CSSLexer.SEMI) &&
4022                                 (currentToken.getType() != CSSLexer.RBRACE) &&
4023                                 (currentToken.getType() != Token.EOF)) {
4024 
4025                             if (currentToken.getType() == CSSLexer.IDENT) {
4026                                 // simple reference to other font-family
4027                                 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.REFERENCE,currentToken.getText()));
4028 
4029                             } else if (currentToken.getType() == CSSLexer.URL) {
4030 
4031                                 // let URLConverter do the conversion
4032                                 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4033                                         new ParsedValueImpl<String,String>(currentToken.getText(), StringConverter.getInstance()),
4034                                         new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4035                                 };
4036                                 ParsedValue<ParsedValue[], String> parsedValue =
4037                                         new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4038                                 String urlStr = parsedValue.convert(null);
4039 
4040                                 URL url = null;
4041                                 try {
4042                                     URI fontUri = new URI(urlStr);
4043                                     url = fontUri.toURL();
4044                                 } catch (URISyntaxException |  MalformedURLException malf) {
4045 
4046                                     final int line = currentToken.getLine();
4047                                     final int pos = currentToken.getOffset();
4048                                     final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]",line,pos,urlStr);
4049                                     CssError error = createError(msg);
4050                                     if (LOGGER.isLoggable(Level.WARNING)) {
4051                                         LOGGER.warning(error.toString());
4052                                     }
4053                                     reportError(error);
4054 
4055                                     // skip the rest.
4056                                     while(currentToken != null) {
4057                                         int ttype = currentToken.getType();
4058                                         if (ttype == CSSLexer.RBRACE ||
4059                                                 ttype == Token.EOF) {
4060                                             return null;
4061                                         }
4062                                         currentToken = nextToken(lexer);
4063                                     }
4064                                 }
4065 
4066                                 String format = null;
4067                                 while(true) {
4068                                     currentToken = nextToken(lexer);
4069                                     final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF;
4070                                     if (ttype == CSSLexer.FUNCTION) {
4071                                         if ("format(".equalsIgnoreCase(currentToken.getText())) {
4072                                             continue;
4073                                         } else {
4074                                             break;
4075                                         }
4076                                     } else if (ttype == CSSLexer.IDENT ||
4077                                             ttype == CSSLexer.STRING) {
4078 
4079                                         format = Utils.stripQuotes(currentToken.getText());
4080                                     } else if (ttype == CSSLexer.RPAREN) {
4081                                         continue;
4082                                     } else {
4083                                         break;
4084                                     }
4085                                 }
4086                                 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.URL,url.toExternalForm(), format));
4087 
4088                             } else if (currentToken.getType() == CSSLexer.FUNCTION) {
4089                                 if ("local(".equalsIgnoreCase(currentToken.getText())) {
4090                                     // consume the function token
4091                                     currentToken = nextToken(lexer);
4092                                     // parse function contents
4093                                     final StringBuilder localSb = new StringBuilder();
4094                                     while(true) {
4095                                         if((currentToken != null) && (currentToken.getType() != CSSLexer.RPAREN) &&
4096                                                 (currentToken.getType() != Token.EOF)) {
4097                                             localSb.append(currentToken.getText());
4098                                         } else {
4099                                             break;
4100                                         }
4101                                         currentToken = nextToken(lexer);
4102                                     }
4103                                     int start = 0, end = localSb.length();
4104                                     if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++;
4105                                     if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --;
4106                                     final String local = localSb.substring(start,end);
4107                                     sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.LOCAL,local));
4108                                 } else {
4109                                     // error unknown fontface src type
4110                                     final int line = currentToken.getLine();
4111                                     final int pos = currentToken.getOffset();
4112                                     final String msg = MessageFormat.format("Unknown @font-face src type ["+currentToken.getText()+")] at [{0,number,#},{1,number,#}]",line,pos);
4113                                     CssError error = createError(msg);
4114                                     if (LOGGER.isLoggable(Level.WARNING)) {
4115                                         LOGGER.warning(error.toString());
4116                                     }
4117                                     reportError(error);
4118 
4119                                 }
4120                             } else  if (currentToken.getType() == CSSLexer.COMMA) {
4121                                 // ignore
4122                             } else {
4123                                 // error unexpected token
4124                                 final int line = currentToken.getLine();
4125                                 final int pos = currentToken.getOffset();
4126                                 final String msg = MessageFormat.format("Unexpected TOKEN ["+currentToken.getText()+"] at [{0,number,#},{1,number,#}]",line,pos);
4127                                 CssError error = createError(msg);
4128                                 if (LOGGER.isLoggable(Level.WARNING)) {
4129                                     LOGGER.warning(error.toString());
4130                                 }
4131                                 reportError(error);
4132                             }
4133                         } else {
4134                             break;
4135                         }
4136                         currentToken = nextToken(lexer);
4137                     }
4138                 } else {
4139                     StringBuilder descriptorVal = new StringBuilder();
4140                     while(true) {
4141                         if((currentToken != null) && (currentToken.getType() != CSSLexer.SEMI) &&
4142                             (currentToken.getType() != Token.EOF)) {
4143                             descriptorVal.append(currentToken.getText());
4144                         } else {
4145                             break;
4146                         }
4147                         currentToken = nextToken(lexer);
4148                     }
4149                     descriptors.put(key,descriptorVal.toString());
4150                 }
4151 //                continue;
4152             }
4153 
4154             if ((currentToken == null) ||
4155                 (currentToken.getType() == CSSLexer.RBRACE) ||
4156                 (currentToken.getType() == Token.EOF)) {
4157                 break;
4158             }
4159 
4160         }
4161         return new FontFace(descriptors, sources);
4162     }
4163 
4164     private Stylesheet handleImport(CSSLexer lexer) {
4165         currentToken = nextToken(lexer);
4166 
4167         if (currentToken == null || currentToken.getType() == Token.EOF) {
4168             return null;
4169         }
4170 
4171         int ttype = currentToken.getType();
4172 
4173         String fname = null;
4174         if (ttype == CSSLexer.STRING || ttype == CSSLexer.URL) {
4175             fname = currentToken.getText();
4176         }
4177 
4178         Stylesheet importedStylesheet = null;
4179         final String _sourceOfStylesheet = sourceOfStylesheet;
4180 
4181         if (fname != null) {
4182             // let URLConverter do the conversion
4183             ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4184                     new ParsedValueImpl<String,String>(fname, StringConverter.getInstance()),
4185                     new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4186             };
4187             ParsedValue<ParsedValue[], String> parsedValue =
4188                     new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4189 
4190             String urlString = parsedValue.convert(null);
4191             importedStylesheet = StyleManager.loadStylesheet(urlString);
4192 
4193             // When we load an imported stylesheet, the sourceOfStylesheet field
4194             // gets set to the new stylesheet. Once it is done loading we must reset
4195             // this field back to the previous value, otherwise we will potentially
4196             // run into problems (for example, see RT-40346).
4197             sourceOfStylesheet = _sourceOfStylesheet;
4198         }
4199         if (importedStylesheet == null) {
4200             final String msg =
4201                     MessageFormat.format("Could not import {0}", fname);
4202             CssError error = createError(msg);
4203             if (LOGGER.isLoggable(Level.WARNING)) {
4204                 LOGGER.warning(error.toString());
4205             }
4206             reportError(error);
4207         }
4208         return importedStylesheet;
4209     }
4210 
4211     private List<Selector> selectors(CSSLexer lexer) {
4212 
4213         List<Selector> selectors = new ArrayList<Selector>();
4214 
4215         while(true) {
4216             Selector selector = selector(lexer);
4217             if (selector == null) {
4218                 // some error happened, skip the rule...
4219                 while ((currentToken != null) &&
4220                        (currentToken.getType() != CSSLexer.RBRACE) &&
4221                        (currentToken.getType() != Token.EOF)) {
4222                     currentToken = nextToken(lexer);
4223                 }
4224 
4225                 // current token is either RBRACE or EOF. Calling
4226                 // currentToken will get the next token or EOF.
4227                 currentToken = nextToken(lexer);
4228 
4229                 // skipped the last rule?
4230                 if (currentToken == null || currentToken.getType() == Token.EOF) {
4231                     currentToken = null;
4232                     return null;
4233                 }
4234 
4235                 continue;
4236             }
4237             selectors.add(selector);
4238 
4239             if ((currentToken != null) &&
4240                 (currentToken.getType() == CSSLexer.COMMA)) {
4241                 // get past the comma
4242                 currentToken = nextToken(lexer);
4243                 continue;
4244             }
4245 
4246             // currentToken was either null or not a comma
4247             // so we are done with selectors.
4248             break;
4249         }
4250 
4251         return selectors;
4252     }
4253 
4254     private Selector selector(CSSLexer lexer) {
4255 
4256         List<Combinator> combinators = null;
4257         List<SimpleSelector> sels = null;
4258 
4259         SimpleSelector ancestor = simpleSelector(lexer);
4260         if (ancestor == null) return null;
4261 
4262         while (true) {
4263             Combinator comb = combinator(lexer);
4264             if (comb != null) {
4265                 if (combinators == null) {
4266                     combinators = new ArrayList<Combinator>();
4267                 }
4268                 combinators.add(comb);
4269                 SimpleSelector descendant = simpleSelector(lexer);
4270                 if (descendant == null) return null;
4271                 if (sels == null) {
4272                     sels = new ArrayList<SimpleSelector>();
4273                     sels.add(ancestor);
4274                 }
4275                 sels.add(descendant);
4276             } else {
4277                 break;
4278             }
4279         }
4280 
4281         // RT-15473
4282         // We might return from selector with a NL token instead of an
4283         // LBRACE, so skip past the NL here.
4284         if (currentToken != null && currentToken.getType() == CSSLexer.NL) {
4285             currentToken = nextToken(lexer);
4286         }
4287 
4288 
4289         if (sels == null) {
4290             return ancestor;
4291         } else {
4292             return new CompoundSelector(sels,combinators);
4293         }
4294 
4295     }
4296 
4297     private SimpleSelector simpleSelector(CSSLexer lexer) {
4298 
4299         String esel = "*"; // element selector. default to universal
4300         String isel = ""; // id selector
4301         List<String> csels = null; // class selector
4302         List<String> pclasses = null; // pseudoclasses
4303 
4304         while (true) {
4305 
4306             final int ttype =
4307                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4308 
4309             switch(ttype) {
4310                 // element selector
4311                 case CSSLexer.STAR:
4312                 case CSSLexer.IDENT:
4313                     esel = currentToken.getText();
4314                     break;
4315 
4316                 // class selector
4317                 case CSSLexer.DOT:
4318                     currentToken = nextToken(lexer);
4319                     if (currentToken != null &&
4320                         currentToken.getType() == CSSLexer.IDENT) {
4321                         if (csels == null) {
4322                             csels = new ArrayList<String>();
4323                         }
4324                         csels.add(currentToken.getText());
4325                     } else {
4326                         currentToken = Token.INVALID_TOKEN;
4327                         return null;
4328                     }
4329                     break;
4330 
4331                 // id selector
4332                 case CSSLexer.HASH:
4333                     isel = currentToken.getText().substring(1);
4334                     break;
4335 
4336                 case CSSLexer.COLON:
4337                     currentToken = nextToken(lexer);
4338                     if (currentToken != null && pclasses == null) {
4339                         pclasses = new ArrayList<String>();
4340                     }
4341 
4342                     if (currentToken.getType() == CSSLexer.IDENT) {
4343                         pclasses.add(currentToken.getText());
4344                     } else if (currentToken.getType() == CSSLexer.FUNCTION){
4345                         String pclass = functionalPseudo(lexer);
4346                         pclasses.add(pclass);
4347                     } else {
4348                         currentToken = Token.INVALID_TOKEN;
4349                     }
4350 
4351                     if (currentToken.getType() == Token.INVALID) {
4352                         return null;
4353                     }
4354                     break;
4355 
4356                 case CSSLexer.NL:
4357                 case CSSLexer.WS:
4358                 case CSSLexer.COMMA:
4359                 case CSSLexer.GREATER:
4360                 case CSSLexer.LBRACE:
4361                 case Token.EOF:
4362                     return new SimpleSelector(esel, csels, pclasses, isel);
4363 
4364                 default:
4365                     return null;
4366 
4367 
4368             }
4369 
4370             // get the next token, but don't skip whitespace
4371             // since it may be a combinator
4372             currentToken = lexer.nextToken();
4373             if (LOGGER.isLoggable(Level.FINEST)) {
4374                 LOGGER.finest(currentToken.toString());
4375             }
4376         }
4377     }
4378 
4379     // From http://www.w3.org/TR/selectors/#grammar
4380     //  functional_pseudo
4381     //      : FUNCTION S* expression ')'
4382     //      ;
4383     //  expression
4384     //      /* In CSS3, the expressions are identifiers, strings, */
4385     //      /* or of the form "an+b" */
4386     //      : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
4387     //      ;
4388     private String functionalPseudo(CSSLexer lexer) {
4389 
4390         // TODO: This is not how we should handle functional pseudo-classes in the long-run!
4391 
4392         StringBuilder pclass = new StringBuilder(currentToken.getText());
4393 
4394         while(true) {
4395 
4396             currentToken = nextToken(lexer);
4397 
4398             switch(currentToken.getType()) {
4399 
4400                 // TODO: lexer doesn't really scan right and isn't CSS3,
4401                 // so PLUS, '-', NUMBER, etc are all useless at this point.
4402                 case CSSLexer.STRING:
4403                 case CSSLexer.IDENT:
4404                     pclass.append(currentToken.getText());
4405                     break;
4406 
4407                 case CSSLexer.RPAREN:
4408                     pclass.append(')');
4409                     return pclass.toString();
4410 
4411                 default:
4412                     currentToken = Token.INVALID_TOKEN;
4413                     return null;
4414             }
4415         }
4416 
4417     }
4418 
4419     private Combinator combinator(CSSLexer lexer) {
4420 
4421         Combinator combinator = null;
4422 
4423         while (true) {
4424 
4425             final int ttype =
4426                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4427 
4428             switch(ttype) {
4429 
4430                 case CSSLexer.WS:
4431                     // need to check if combinator is null since child token
4432                     // might be surrounded by whitespace.
4433                     if (combinator == null && " ".equals(currentToken.getText())) {
4434                         combinator = Combinator.DESCENDANT;
4435                     }
4436                     break;
4437 
4438                 case CSSLexer.GREATER:
4439                     // no need to check if combinator is null here
4440                     combinator = Combinator.CHILD;
4441                     break;
4442 
4443                 case CSSLexer.STAR:
4444                 case CSSLexer.IDENT:
4445                 case CSSLexer.DOT:
4446                 case CSSLexer.HASH:
4447                 case CSSLexer.COLON:
4448                     return combinator;
4449 
4450                 default:
4451                     // only selector is expected
4452                     return null;
4453 
4454             }
4455 
4456             // get the next token, but don't skip whitespace
4457             currentToken = lexer.nextToken();
4458             if (LOGGER.isLoggable(Level.FINEST)) {
4459                 LOGGER.finest(currentToken.toString());
4460             }
4461         }
4462     }
4463 
4464     private List<Declaration> declarations(CSSLexer lexer) {
4465 
4466         List<Declaration> declarations = new ArrayList<Declaration>();
4467 
4468         while (true) {
4469 
4470             Declaration decl = declaration(lexer);
4471             if (decl != null) {
4472                 declarations.add(decl);
4473             } else {
4474                 // some error happened, skip the decl...
4475                 while ((currentToken != null) &&
4476                        (currentToken.getType() != CSSLexer.SEMI) &&
4477                        (currentToken.getType() != CSSLexer.RBRACE) &&
4478                        (currentToken.getType() != Token.EOF)) {
4479                     currentToken = nextToken(lexer);
4480                 }
4481 
4482                 // current token is either SEMI, RBRACE or EOF.
4483                 if (currentToken != null &&
4484                     currentToken.getType() != CSSLexer.SEMI)
4485                     return declarations;
4486             }
4487 
4488             // declaration; declaration; ???
4489             // RT-17830 - allow declaration;;
4490             while ((currentToken != null) &&
4491                     (currentToken.getType() == CSSLexer.SEMI)) {
4492                 currentToken = nextToken(lexer);
4493             }
4494 
4495             // if it is delcaration; declaration, then the
4496             // next token should be an IDENT.
4497             if ((currentToken != null) &&
4498                 (currentToken.getType() == CSSLexer.IDENT)) {
4499                 continue;
4500             }
4501 
4502             break;
4503         }
4504 
4505         return declarations;
4506     }
4507 
4508     private Declaration declaration(CSSLexer lexer) {
4509 
4510         final int ttype =
4511             (currentToken != null) ? currentToken.getType() : Token.INVALID;
4512 
4513         if ((currentToken == null) ||
4514             (currentToken.getType() != CSSLexer.IDENT)) {
4515 //
4516 //            RT-16547: this warning was misleading because an empty rule
4517 //            not invalid. Some people put in empty rules just as placeholders.
4518 //
4519 //            if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
4520 //                final int line = currentToken != null ? currentToken.getLine() : -1;
4521 //                final int pos = currentToken != null ? currentToken.getOffset() : -1;
4522 //                final String url =
4523 //                    (stylesheet != null && stylesheet.getUrl() != null) ?
4524 //                        stylesheet.getUrl().toExternalForm() : "?";
4525 //                LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]",
4526 //                    url,line,pos);
4527 //            }
4528             return null;
4529         }
4530 
4531         String property = currentToken.getText();
4532 
4533         currentToken = nextToken(lexer);
4534 
4535         if ((currentToken == null) ||
4536             (currentToken.getType() != CSSLexer.COLON)) {
4537                 final int line = currentToken.getLine();
4538                 final int pos = currentToken.getOffset();
4539                 final String msg =
4540                         MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]",
4541                     line,pos);
4542                 CssError error = createError(msg);
4543                 if (LOGGER.isLoggable(Level.WARNING)) {
4544                     LOGGER.warning(error.toString());
4545                 }
4546                 reportError(error);
4547             return null;
4548         }
4549 
4550         currentToken = nextToken(lexer);
4551 
4552         Term root = expr(lexer);
4553         ParsedValueImpl value = null;
4554         try {
4555             value = (root != null) ? valueFor(property, root, lexer) : null;
4556         } catch (ParseException re) {
4557                 Token badToken = re.tok;
4558                 final int line = badToken != null ? badToken.getLine() : -1;
4559                 final int pos = badToken != null ? badToken.getOffset() : -1;
4560                 final String msg =
4561                         MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]",
4562                     line,pos,re.getMessage(),property);
4563                 CssError error = createError(msg);
4564                 if (LOGGER.isLoggable(Level.WARNING)) {
4565                     LOGGER.warning(error.toString());
4566                 }
4567                 reportError(error);
4568             return null;
4569         }
4570 
4571         boolean important = currentToken.getType() == CSSLexer.IMPORTANT_SYM;
4572         if (important) currentToken = nextToken(lexer);
4573 
4574         Declaration decl = (value != null)
4575                 ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null;
4576         return decl;
4577     }
4578 
4579     private Term expr(CSSLexer lexer) {
4580 
4581         final Term expr = term(lexer);
4582         Term current = expr;
4583 
4584         while(true) {
4585 
4586             // if current is null, then term returned null
4587             final int ttype =
4588                 (current != null && currentToken != null)
4589                     ? currentToken.getType() : Token.INVALID;
4590 
4591             if (ttype == Token.INVALID) {
4592                 skipExpr(lexer);
4593                 return null;
4594             } else if (ttype == CSSLexer.SEMI ||
4595                 ttype == CSSLexer.IMPORTANT_SYM ||
4596                 ttype == CSSLexer.RBRACE ||
4597                 ttype == Token.EOF) {
4598                 return expr;
4599             } else if (ttype == CSSLexer.COMMA) {
4600             // comma breaks up sequences of terms.
4601                 // next series of terms chains off the last term in
4602                 // the current series.
4603                 currentToken = nextToken(lexer);
4604                 current = current.nextLayer = term(lexer);
4605             } else {
4606                 current = current.nextInSeries = term(lexer);
4607             }
4608 
4609         }
4610     }
4611 
4612     private void skipExpr(CSSLexer lexer) {
4613 
4614         while(true) {
4615 
4616             currentToken = nextToken(lexer);
4617 
4618             final int ttype =
4619                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4620 
4621             if (ttype == CSSLexer.SEMI ||
4622                 ttype == CSSLexer.RBRACE ||
4623                 ttype == Token.EOF) {
4624                 return;
4625             }
4626         }
4627     }
4628 
4629     private Term term(CSSLexer lexer) {
4630 
4631         final int ttype =
4632             (currentToken != null) ? currentToken.getType() : Token.INVALID;
4633 
4634         switch (ttype) {
4635 
4636             case CSSLexer.NUMBER:
4637             case CSSLexer.CM:
4638             case CSSLexer.EMS:
4639             case CSSLexer.EXS:
4640             case CSSLexer.IN:
4641             case CSSLexer.MM:
4642             case CSSLexer.PC:
4643             case CSSLexer.PT:
4644             case CSSLexer.PX:
4645             case CSSLexer.DEG:
4646             case CSSLexer.GRAD:
4647             case CSSLexer.RAD:
4648             case CSSLexer.TURN:
4649             case CSSLexer.PERCENTAGE:
4650             case CSSLexer.SECONDS:
4651             case CSSLexer.MS:
4652                 break;
4653 
4654             case CSSLexer.STRING:
4655                 break;
4656             case CSSLexer.IDENT:
4657                 break;
4658 
4659             case CSSLexer.HASH:
4660                 break;
4661 
4662             case CSSLexer.FUNCTION:
4663             case CSSLexer.LPAREN:
4664 
4665                 Term function = new Term(currentToken);
4666                 currentToken = nextToken(lexer);
4667 
4668                 Term arg = term(lexer);
4669                 function.firstArg = arg;
4670 
4671                 while(true) {
4672 
4673                     final int operator =
4674                         currentToken != null ? currentToken.getType() : Token.INVALID;
4675 
4676                     if (operator == CSSLexer.RPAREN) {
4677                         currentToken = nextToken(lexer);
4678                         return function;
4679                     } else if (operator == CSSLexer.COMMA) {
4680                         // comma breaks up sequences of terms.
4681                         // next series of terms chains off the last term in
4682                         // the current series.
4683                         currentToken = nextToken(lexer);
4684                         arg = arg.nextArg = term(lexer);
4685 
4686                     } else {
4687                         arg = arg.nextInSeries = term(lexer);
4688                     }
4689 
4690                 }
4691 
4692             case CSSLexer.URL:
4693                 break;
4694 
4695             case CSSLexer.SOLIDUS:
4696                 break;
4697 
4698             default:
4699                 final int line = currentToken != null ? currentToken.getLine() : -1;
4700                 final int pos = currentToken != null ? currentToken.getOffset() : -1;
4701                 final String text = currentToken != null ? currentToken.getText() : "";
4702                 final String msg =
4703                     MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]",
4704                     "\'",text,line,pos);
4705                 CssError error = createError(msg);
4706                 if (LOGGER.isLoggable(Level.WARNING)) {
4707                     LOGGER.warning(error.toString());
4708                 }
4709                 reportError(error);
4710                 return null;
4711 //                currentToken = nextToken(lexer);
4712 //
4713 //                return new Term(Token.INVALID_TOKEN);
4714         }
4715 
4716         Term term = new Term(currentToken);
4717         currentToken = nextToken(lexer);
4718         return term;


























































































































4719     }
4720 }


   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.css;
  27 

  28 import com.sun.javafx.css.Combinator;
  29 import com.sun.javafx.css.FontFaceImpl;



  30 import com.sun.javafx.css.ParsedValueImpl;





  31 import com.sun.javafx.css.StyleManager;
  32 import com.sun.javafx.util.Utils;
  33 import javafx.css.converter.BooleanConverter;
  34 import javafx.css.converter.DurationConverter;
  35 import javafx.css.converter.EffectConverter;
  36 import javafx.css.converter.EnumConverter;
  37 import javafx.css.converter.FontConverter;
  38 import javafx.css.converter.InsetsConverter;
  39 import javafx.css.converter.PaintConverter;
  40 import javafx.css.converter.SizeConverter;
  41 import javafx.css.converter.SizeConverter.SequenceConverter;
  42 import javafx.css.converter.StringConverter;
  43 import javafx.css.converter.URLConverter;
  44 import javafx.css.converter.DeriveColorConverter;
  45 import javafx.css.converter.LadderConverter;
  46 import javafx.css.converter.StopConverter;
  47 import com.sun.javafx.css.parser.Token;
  48 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
  49 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
  50 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
  51 import com.sun.javafx.scene.layout.region.BorderImageSlices;
  52 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
  53 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
  54 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
  55 import com.sun.javafx.scene.layout.region.BorderStyleConverter;
  56 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
  57 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
  58 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
  59 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
  60 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
  61 import com.sun.javafx.scene.layout.region.Margins;
  62 import com.sun.javafx.scene.layout.region.RepeatStruct;
  63 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
  64 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
  65 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
  66 import javafx.collections.ObservableList;


  67 import javafx.geometry.Insets;
  68 import javafx.scene.effect.BlurType;
  69 import javafx.scene.effect.Effect;
  70 import javafx.scene.layout.BackgroundPosition;
  71 import javafx.scene.layout.BackgroundRepeat;
  72 import javafx.scene.layout.BackgroundSize;
  73 import javafx.scene.layout.BorderStrokeStyle;
  74 import javafx.scene.layout.BorderWidths;
  75 import javafx.scene.layout.CornerRadii;

  76 import javafx.scene.paint.Color;
  77 import javafx.scene.paint.CycleMethod;
  78 import javafx.scene.paint.Paint;
  79 import javafx.scene.paint.Stop;
  80 import javafx.scene.shape.StrokeLineCap;
  81 import javafx.scene.shape.StrokeLineJoin;
  82 import javafx.scene.shape.StrokeType;
  83 import javafx.scene.text.Font;
  84 import javafx.scene.text.FontPosture;
  85 import javafx.scene.text.FontWeight;
  86 import javafx.util.Duration;
  87 import sun.util.logging.PlatformLogger;
  88 import sun.util.logging.PlatformLogger.Level;
  89 
  90 import java.io.BufferedReader;
  91 import java.io.CharArrayReader;
  92 import java.io.IOException;
  93 import java.io.InputStreamReader;
  94 import java.io.Reader;
  95 import java.net.MalformedURLException;
  96 import java.net.URI;
  97 import java.net.URISyntaxException;
  98 import java.net.URL;
  99 import java.text.MessageFormat;
 100 import java.util.ArrayList;
 101 import java.util.Collections;
 102 import java.util.HashMap;
 103 import java.util.List;
 104 import java.util.Locale;
 105 import java.util.Map;
 106 import java.util.Stack;
 107 
 108 /**
 109  * @since 9


 110  */
 111 final public class CssParser {



 112 
 113     public CssParser() {
 114         properties = new HashMap<String,String>();
 115     }
 116 
 117     // stylesheet as a string from parse method. This will be null if the
 118     // stylesheet is being parsed from a file; otherwise, the parser is parsing
 119     // a string and this is that string.
 120     private String     stylesheetAsText;
 121 
 122     // the url of the stylesheet file, or the docbase of an applet. This will
 123     // be null if the source is not a file or from an applet.
 124     private String        sourceOfStylesheet;
 125 
 126     // the Styleable from the node with an in-line style. This will be null
 127     // unless the source of the styles is a Node's styleProperty. In this case,
 128     // the stylesheetString will also be set.
 129     private Styleable sourceOfInlineStyle;
 130 
 131     // source is a file
 132     private void setInputSource(String url, String str) {
 133         stylesheetAsText = str;


 138     // source as string only
 139     private void setInputSource(String str) {
 140         stylesheetAsText = str;
 141         sourceOfStylesheet = null;
 142         sourceOfInlineStyle = null;
 143     }
 144 
 145     // source is in-line style
 146     private void setInputSource(Styleable styleable) {
 147         stylesheetAsText = styleable != null ? styleable.getStyle() : null;
 148         sourceOfStylesheet = null;
 149         sourceOfInlineStyle = styleable;
 150     }
 151 
 152     private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
 153 
 154     private static final class ParseException extends Exception {
 155         ParseException(String message) {
 156             this(message,null,null);
 157         }
 158         ParseException(String message, Token tok, CssParser parser) {
 159             super(message);
 160             this.tok = tok;
 161             if (parser.sourceOfStylesheet != null) {
 162                 source = parser.sourceOfStylesheet;
 163             } else if (parser.sourceOfInlineStyle != null) {
 164                 source = parser.sourceOfInlineStyle.toString();
 165             } else if (parser.stylesheetAsText != null) {
 166                 source = parser.stylesheetAsText;
 167             } else {
 168                 source = "?";
 169             }
 170         }
 171         @Override public String toString() {
 172             StringBuilder builder = new StringBuilder(super.getMessage());
 173             builder.append(source);
 174             if (tok != null) builder.append(": ").append(tok.toString());
 175             return builder.toString();
 176         }
 177         private final Token tok;
 178         private final String source;


 221      *
 222      *@param  url URL of the stylesheet to parse
 223      *@return the stylesheet
 224      *@throws IOException
 225      */
 226     public Stylesheet parse(final URL url) throws IOException {
 227 
 228         final String path = url != null ? url.toExternalForm() : null;
 229         final Stylesheet stylesheet = new Stylesheet(path);
 230         if (url != null) {
 231             setInputSource(path, null);
 232             try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
 233                 parse(stylesheet, reader);
 234             }
 235         }
 236         return stylesheet;
 237     }
 238 
 239     /* All of the other function calls should wind up here */
 240     private void parse(final Stylesheet stylesheet, final Reader reader) {
 241         CssLexer lex = new CssLexer();


 242         lex.setReader(reader);
 243 
 244         try {
 245             this.parse(stylesheet, lex);
 246         } catch (Exception ex) {
 247             // Sometimes bad syntax causes an exception. The code should be
 248             // fixed to handle the bad syntax, but the fallback is
 249             // to handle the exception here. Uncaught, the exception can cause
 250             // problems like RT-20311
 251             reportException(ex);
 252         }
 253 
 254     }
 255 
 256     /** Parse an in-line style from a Node */
 257     public Stylesheet parseInlineStyle(final Styleable node) {
 258 
 259         Stylesheet stylesheet = new Stylesheet();
 260 
 261         final String stylesheetText = (node != null) ? node.getStyle() : null;
 262         if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
 263             setInputSource(node);
 264             final List<Rule> rules = new ArrayList<Rule>();
 265             try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
 266                 final CssLexer lexer = new CssLexer();
 267                 lexer.setReader(reader);
 268                 currentToken = nextToken(lexer);
 269                 final List<Declaration> declarations = declarations(lexer);
 270                 if (declarations != null && !declarations.isEmpty()) {
 271                     final Selector selector = Selector.getUniversalSelector();
 272                     final Rule rule = new Rule(
 273                         Collections.singletonList(selector),
 274                         declarations
 275                     );
 276                     rules.add(rule);
 277                 }
 278             } catch (IOException ioe) {
 279             } catch (Exception ex) {
 280                 // Sometimes bad syntax causes an exception. The code should be
 281                 // fixed to handle the bad syntax, but the fallback is
 282                 // to handle the exception here. Uncaught, the exception can cause
 283                 // problems like RT-20311
 284                 reportException(ex);
 285             }
 286             stylesheet.getRules().addAll(rules);
 287         }
 288 
 289         // don't retain reference to the styleable
 290         setInputSource((Styleable) null);
 291 
 292         return stylesheet;
 293     }
 294 
 295     /** convenience method for unit tests */
 296     public ParsedValue parseExpr(String property, String expr) {
 297         if (property == null || expr == null) return null;
 298 
 299         ParsedValueImpl value = null;
 300         setInputSource(null, property + ": " + expr);
 301         char buf[] = new char[expr.length() + 1];
 302         System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
 303         buf[buf.length-1] = ';';
 304 
 305         try (Reader reader = new CharArrayReader(buf)) {
 306             CssLexer lex = new CssLexer();
 307             lex.setReader(reader);
 308 
 309             currentToken = nextToken(lex);
 310             CssParser.Term term = this.expr(lex);
 311             value = valueFor(property, term, lex);
 312         } catch (IOException ioe) {
 313         } catch (ParseException e) {
 314             if (LOGGER.isLoggable(Level.WARNING)) {
 315                 LOGGER.warning("\"" +property + ": " + expr  + "\" " + e.toString());
 316             }
 317         } catch (Exception ex) {
 318             // Sometimes bad syntax causes an exception. The code should be
 319             // fixed to handle the bad syntax, but the fallback is
 320             // to handle the exception here. Uncaught, the exception can cause
 321             // problems like RT-20311
 322             reportException(ex);
 323         }
 324         return value;
 325     }
 326     /*
 327      * Map of property names found while parsing. If a value matches a
 328      * property name, then the value is a lookup.
 329      */
 330     private final Map<String,String> properties;


 394             }
 395             if (nextLayer != null) {
 396                 buf.append("<nextLayer>");
 397                 buf.append(nextLayer.toString());
 398                 buf.append("</nextLayer>\n");
 399             }
 400             if (firstArg != null) {
 401                 buf.append("<args>");
 402                 buf.append(firstArg.toString());
 403                 if (nextArg != null) {
 404                     buf.append(nextArg.toString());
 405                 }
 406                 buf.append("</args>");
 407             }
 408 
 409             return buf.toString();
 410         }
 411 
 412     }
 413 
 414     private ParseError createError(String msg) {
 415 
 416         ParseError error = null;
 417         if (sourceOfStylesheet != null) {
 418             error = new ParseError.StylesheetParsingError(sourceOfStylesheet, msg);
 419         } else if (sourceOfInlineStyle != null) {
 420             error = new ParseError.InlineStyleParsingError(sourceOfInlineStyle, msg);
 421         } else {
 422             error = new ParseError.StringParsingError(stylesheetAsText, msg);
 423         }
 424         return error;
 425     }
 426 
 427     private void reportError(ParseError error) {
 428         List<ParseError> errors = null;
 429         if ((errors = StyleManager.getErrors()) != null) {
 430             errors.add(error);
 431         }
 432     }
 433 
 434     private void error(final Term root, final String msg) throws ParseException {
 435 
 436         final Token token = root != null ? root.token : null;
 437         final ParseException pe = new ParseException(msg,token,this);
 438         reportError(createError(pe.toString()));
 439         throw pe;
 440     }
 441 
 442     private void reportException(Exception exception) {
 443 
 444         if (LOGGER.isLoggable(Level.WARNING)) {
 445             final StackTraceElement[] stea = exception.getStackTrace();
 446             if (stea.length > 0) {
 447                 final StringBuilder buf =
 448                     new StringBuilder("Please report ");


 509 
 510         // not a color
 511         return null;
 512     }
 513 
 514     private String stripQuotes(String string) {
 515         return com.sun.javafx.util.Utils.stripQuotes(string);
 516     }
 517 
 518     private double clamp(double min, double val, double max) {
 519         if (val < min) return min;
 520         if (max < val) return max;
 521         return val;
 522     }
 523 
 524     // Return true if the token is a size type or an identifier
 525     // (which would indicate a lookup).
 526     private boolean isSize(Token token) {
 527         final int ttype = token.getType();
 528         switch (ttype) {
 529         case CssLexer.NUMBER:
 530         case CssLexer.PERCENTAGE:
 531         case CssLexer.EMS:
 532         case CssLexer.EXS:
 533         case CssLexer.PX:
 534         case CssLexer.CM:
 535         case CssLexer.MM:
 536         case CssLexer.IN:
 537         case CssLexer.PT:
 538         case CssLexer.PC:
 539         case CssLexer.DEG:
 540         case CssLexer.GRAD:
 541         case CssLexer.RAD:
 542         case CssLexer.TURN:
 543             return true;
 544         default:
 545             return token.getType() == CssLexer.IDENT;
 546         }
 547     }
 548 
 549     private Size size(final Token token) throws ParseException {
 550         SizeUnits units = SizeUnits.PX;
 551         // Amount to trim off the suffix, if any. Most are 2 chars.
 552         int trim = 2;
 553         final String sval = token.getText().trim();
 554         final int len = sval.length();
 555         final int ttype = token.getType();
 556         switch (ttype) {
 557         case CssLexer.NUMBER:
 558             units = SizeUnits.PX;
 559             trim = 0;
 560             break;
 561         case CssLexer.PERCENTAGE:
 562             units = SizeUnits.PERCENT;
 563             trim = 1;
 564             break;
 565         case CssLexer.EMS:
 566             units = SizeUnits.EM;
 567             break;
 568         case CssLexer.EXS:
 569             units = SizeUnits.EX;
 570             break;
 571         case CssLexer.PX:
 572             units = SizeUnits.PX;
 573             break;
 574         case CssLexer.CM:
 575             units = SizeUnits.CM;
 576             break;
 577         case CssLexer.MM:
 578             units = SizeUnits.MM;
 579             break;
 580         case CssLexer.IN:
 581             units = SizeUnits.IN;
 582             break;
 583         case CssLexer.PT:
 584             units = SizeUnits.PT;
 585             break;
 586         case CssLexer.PC:
 587             units = SizeUnits.PC;
 588             break;
 589         case CssLexer.DEG:
 590             units = SizeUnits.DEG;
 591             trim = 3;
 592             break;
 593         case CssLexer.GRAD:
 594             units = SizeUnits.GRAD;
 595             trim = 4;
 596             break;
 597         case CssLexer.RAD:
 598             units = SizeUnits.RAD;
 599             trim = 3;
 600             break;
 601         case CssLexer.TURN:
 602             units = SizeUnits.TURN;
 603             trim = 4;
 604             break;
 605         case CssLexer.SECONDS:
 606             units = SizeUnits.S;
 607             trim = 1;
 608             break;
 609         case CssLexer.MS:
 610             units = SizeUnits.MS;
 611             break;
 612         default:
 613             if (LOGGER.isLoggable(Level.FINEST)) {
 614                 LOGGER.finest("Expected \'<number>\'");
 615             }
 616             ParseException re = new ParseException("Expected \'<number>\'",token, this);
 617             reportError(createError(re.toString()));
 618             throw re;
 619         }
 620         // TODO: Handle NumberFormatException
 621         return new Size(
 622             Double.parseDouble(sval.substring(0,len-trim)),
 623             units
 624         );
 625     }
 626 
 627     // Count the number of terms in a series
 628     private int numberOfTerms(final Term root) {
 629         if (root == null) return 0;


 666         return nArgs;
 667     }
 668 
 669     // Get the next layer following this term, which may be null
 670     private Term nextLayer(final Term root) {
 671         if (root == null) return null;
 672 
 673         Term term = root;
 674         while (term.nextInSeries != null) {
 675             term = term.nextInSeries;
 676         }
 677         return term.nextLayer;
 678     }
 679 
 680     ////////////////////////////////////////////////////////////////////////////
 681     //
 682     // Parsing routines
 683     //
 684     ////////////////////////////////////////////////////////////////////////////
 685 
 686     ParsedValueImpl valueFor(String property, Term root, CssLexer lexer) throws ParseException {
 687         final String prop = property.toLowerCase(Locale.ROOT);
 688         properties.put(prop, prop);
 689         if (root == null || root.token == null) {
 690             error(root, "Expected value for property \'" + prop + "\'");
 691         }
 692 
 693         if (root.token.getType() == CssLexer.IDENT) {
 694             final String txt = root.token.getText();
 695             if ("inherit".equalsIgnoreCase(txt)) {
 696                 return new ParsedValueImpl<String,String>("inherit", null);
 697             } else if ("null".equalsIgnoreCase(txt)
 698                     || "none".equalsIgnoreCase(txt)) {
 699                 return new ParsedValueImpl<String,String>("null", null);
 700             }
 701         }
 702         if ("-fx-fill".equals(prop)) {
 703              ParsedValueImpl pv = parse(root);
 704             if (pv.getConverter() == StyleConverter.getUrlConverter()) {
 705                 // ImagePatternConverter expects array of ParsedValue where element 0 is the URL
 706                 // Pending RT-33574
 707                 pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance());
 708             }
 709             return pv;
 710         }
 711         else if ("-fx-background-color".equals(prop)) {
 712             return parsePaintLayers(root);
 713         } else if ("-fx-background-image".equals(prop)) {


 789         } else if ("-fx-stroke-line-cap".equals(prop)) {
 790             // TODO: Figure out a way that these properties don't need to be
 791             // special cased.
 792             ParsedValueImpl value = parseStrokeLineCap(root);
 793             if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
 794             return value;
 795         } else if ("-fx-stroke-type".equals(prop)) {
 796             // TODO: Figure out a way that these properties don't need to be
 797             // special cased.
 798             ParsedValueImpl value = parseStrokeType(root);
 799             if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
 800             return value;
 801         } else if ("-fx-font-smoothing-type".equals(prop)) {
 802             // TODO: Figure out a way that these properties don't need to be
 803             // special cased.
 804             String str = null;
 805             int ttype = -1;
 806             final Token token = root.token;
 807 
 808             if (root.token == null
 809                     || ((ttype = root.token.getType()) != CssLexer.STRING
 810                          && ttype != CssLexer.IDENT)
 811                     || (str = root.token.getText()) == null
 812                     || str.isEmpty()) {
 813                 error(root,  "Expected STRING or IDENT");
 814             }
 815             return new ParsedValueImpl<String, String>(stripQuotes(str), null, false);
 816         }
 817         return parse(root);
 818     }
 819 
 820     private ParsedValueImpl parse(Term root) throws ParseException {
 821 
 822         if (root.token == null) error(root, "Parse error");
 823         final Token token = root.token;
 824         ParsedValueImpl value = null; // value to return;
 825 
 826         final int ttype = token.getType();
 827         switch (ttype) {
 828         case CssLexer.NUMBER:
 829         case CssLexer.PERCENTAGE:
 830         case CssLexer.EMS:
 831         case CssLexer.EXS:
 832         case CssLexer.PX:
 833         case CssLexer.CM:
 834         case CssLexer.MM:
 835         case CssLexer.IN:
 836         case CssLexer.PT:
 837         case CssLexer.PC:
 838         case CssLexer.DEG:
 839         case CssLexer.GRAD:
 840         case CssLexer.RAD:
 841         case CssLexer.TURN:
 842             if (root.nextInSeries == null) {
 843                 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null);
 844                 value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance());
 845             } else {
 846                 ParsedValueImpl<Size,Size>[] sizeValue = parseSizeSeries(root);
 847                 value = new ParsedValueImpl<ParsedValue[],Number[]>(sizeValue, SizeConverter.SequenceConverter.getInstance());
 848             }
 849             break;
 850         case CssLexer.SECONDS:
 851         case CssLexer.MS: {
 852             ParsedValue<Size, Size> sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
 853             value = new ParsedValueImpl<ParsedValue<?, Size>, Duration>(sizeValue, DurationConverter.getInstance());
 854             break;
 855         }
 856         case CssLexer.STRING:
 857         case CssLexer.IDENT:
 858             boolean isIdent = ttype == CssLexer.IDENT;
 859             final String str = stripQuotes(token.getText());
 860             final String text = str.toLowerCase(Locale.ROOT);
 861             if ("ladder".equals(text)) {
 862                 value = ladder(root);
 863             } else if ("linear".equals(text) && (root.nextInSeries) != null) {
 864                 // if nextInSeries is null, then assume this is _not_ an old-style linear gradient
 865                 value = linearGradient(root);
 866             } else if ("radial".equals(text) && (root.nextInSeries) != null) {
 867                 // if nextInSeries is null, then assume this is _not_ an old-style radial gradient
 868                 value = radialGradient(root);
 869             } else if ("infinity".equals(text)) {
 870                 Size size = new Size(Double.MAX_VALUE, SizeUnits.PX);
 871                 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size, null);
 872                 value = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeValue, SizeConverter.getInstance());
 873             } else if ("indefinite".equals(text)) {
 874                 Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX);
 875                 ParsedValueImpl<Size,Size> sizeValue = new ParsedValueImpl<>(size, null);
 876                 value = new ParsedValueImpl<ParsedValue<?,Size>,Duration>(sizeValue, DurationConverter.getInstance());
 877             } else if ("true".equals(text)) {
 878                 // TODO: handling of boolean is really bogus
 879                 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance());
 880             } else if ("false".equals(text)) {
 881                 // TODO: handling of boolean is really bogus
 882                 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance());
 883             } else {
 884                 // if the property value is another property, then it needs to be looked up.
 885                 boolean needsLookup = isIdent && properties.containsKey(text);
 886                 if (needsLookup || ((value = colorValueOfString(str)) == null )) {
 887                     // If the value is a lookup, make sure to use the lower-case text so it matches the property
 888                     // in the Declaration. If the value is not a lookup, then use str since the value might
 889                     // be a string which could have some case sensitive meaning
 890                     //
 891                     // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201
 892                     value = new ParsedValueImpl<String,String>(needsLookup ? text : str, null, isIdent || needsLookup);
 893                 }
 894             }
 895             break;
 896         case CssLexer.HASH:
 897             final String clr = token.getText();
 898             try {
 899                 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null);
 900             } catch (final IllegalArgumentException e) {
 901                 error(root, e.getMessage());
 902             }
 903             break;
 904         case CssLexer.FUNCTION:
 905             return  parseFunction(root);
 906         case CssLexer.URL:
 907             return parseURI(root);
 908         default:
 909             final String msg = "Unknown token type: \'" + ttype + "\'";
 910             error(root, msg);
 911         }
 912         return value;
 913 
 914     }
 915 
 916     /* Parse size.
 917      * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
 918      */
 919     private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException {
 920 
 921         if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'");
 922 
 923         ParsedValueImpl<?,Size> value = null;
 924 
 925         if (root.token.getType() != CssLexer.IDENT) {
 926 
 927             Size size = size(root.token);
 928             value = new ParsedValueImpl<Size,Size>(size, null);
 929 
 930         } else {
 931 
 932             String key = root.token.getText();
 933             value = new ParsedValueImpl<String,Size>(key, null, true);
 934 
 935         }
 936 
 937         return value;
 938     }
 939 
 940     private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException {
 941 
 942         ParsedValueImpl<?,Color> color = null;
 943         if (root.token != null &&
 944             (root.token.getType() == CssLexer.IDENT ||
 945              root.token.getType() == CssLexer.HASH ||
 946              root.token.getType() == CssLexer.FUNCTION)) {
 947 
 948             color = parse(root);
 949 
 950         } else {
 951             error(root,  "Expected \'<color>\'");
 952         }
 953         return color;
 954     }
 955 
 956     // rgb(NUMBER, NUMBER, NUMBER)
 957     // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
 958     // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
 959     // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
 960     private ParsedValueImpl rgb(Term root) throws ParseException {
 961 
 962         // first term in the chain is the function name...
 963         final String fn = (root.token != null) ? root.token.getText() : null;
 964         if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
 965             final String msg = "Expected \'rgb\' or \'rgba\'";
 966             error(root, msg);
 967         }
 968 
 969         Term arg = root;
 970         Token rtok, gtok, btok, atok;
 971 
 972         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
 973         if ((rtok = arg.token) == null ||
 974             (rtok.getType() != CssLexer.NUMBER &&
 975              rtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
 976 
 977         root = arg;
 978 
 979         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
 980         if ((gtok = arg.token) == null ||
 981             (gtok.getType() != CssLexer.NUMBER &&
 982              gtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
 983 
 984         root = arg;
 985 
 986         if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
 987         if ((btok = arg.token) == null ||
 988             (btok.getType() != CssLexer.NUMBER &&
 989              btok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
 990 
 991         root = arg;
 992 
 993         if ((arg = arg.nextArg) != null) {
 994             if ((atok = arg.token) == null ||
 995                  atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
 996         } else {
 997             atok = null;
 998         }
 999 
1000         int argType = rtok.getType();
1001         if (argType != gtok.getType() || argType != btok.getType() ||
1002             (argType != CssLexer.NUMBER && argType != CssLexer.PERCENTAGE)) {
1003             error(root, "Argument type mistmatch");
1004         }
1005 
1006         final String rtext = rtok.getText();
1007         final String gtext = gtok.getText();
1008         final String btext = btok.getText();
1009 
1010         double rval = 0;
1011         double gval = 0;
1012         double bval = 0;
1013         if (argType == CssLexer.NUMBER) {
1014             rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
1015             gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
1016             bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
1017         } else {
1018             rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
1019             gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
1020             bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
1021         }
1022 
1023         final String atext = (atok != null) ? atok.getText() : null;
1024         final double aval =  (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;
1025 
1026         return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null);
1027 
1028     }
1029 
1030     // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
1031     // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
1032     private ParsedValueImpl hsb(Term root) throws ParseException {
1033 
1034         // first term in the chain is the function name...
1035         final String fn = (root.token != null) ? root.token.getText() : null;
1036         if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
1037             final String msg = "Expected \'hsb\' or \'hsba\'";
1038             error(root, msg);
1039         }
1040 
1041         Term arg = root;
1042         Token htok, stok, btok, atok;
1043 
1044         if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'");
1045         if ((htok = arg.token) == null || htok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
1046 
1047         root = arg;
1048 
1049         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1050         if ((stok = arg.token) == null || stok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1051 
1052         root = arg;
1053 
1054         if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1055         if ((btok = arg.token) == null || btok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1056 
1057         root = arg;
1058 
1059         if ((arg = arg.nextArg) != null) {
1060             if ((atok = arg.token) == null || atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
1061         } else {
1062             atok = null;
1063         }
1064 
1065         final Size hval = size(htok);
1066         final Size sval = size(stok);
1067         final Size bval = size(btok);
1068 
1069         final double hue = hval.pixels(); // no clamp - hue can be negative
1070         final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
1071         final double brightness = clamp(0.0f, bval.pixels(), 1.0f);
1072 
1073         final Size aval = (atok != null) ? size(atok) : null;
1074         final double opacity =  (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;
1075 
1076         return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null);
1077     }
1078 
1079     // derive(color, pct)
1080     private ParsedValueImpl<ParsedValue[],Color> derive(final Term root)


1107         // first term in the chain is the function name...
1108         final String fn = (root.token != null) ? root.token.getText() : null;
1109         if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
1110             final String msg = "Expected \'ladder\'";
1111             error(root, msg);
1112         }
1113 
1114         if (LOGGER.isLoggable(Level.WARNING)) {
1115             LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
1116         }
1117 
1118         Term term = root;
1119 
1120         if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'");
1121         final ParsedValueImpl<?,Color> color = parse(term);
1122 
1123         Term prev = term;
1124 
1125         if ((term = term.nextInSeries) == null) error(prev,  "Expected \'stops\'");
1126         if (term.token == null ||
1127             term.token.getType() != CssLexer.IDENT ||
1128             !"stops".equalsIgnoreCase(term.token.getText())) error(term,  "Expected \'stops\'");
1129 
1130         prev = term;
1131 
1132         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'");
1133 
1134         int nStops = 0;
1135         Term temp = term;
1136         do {
1137             nStops += 1;
1138             // if next token type is IDENT, then we have CycleMethod
1139         } while (((temp = temp.nextInSeries) != null) &&
1140                  ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1141 
1142         ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
1143         values[0] = color;
1144         int stopIndex = 1;
1145         do {
1146             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1147             if (stop != null) values[stopIndex++] = stop;
1148             prev = term;
1149         } while(((term = term.nextInSeries) != null) &&
1150                  (term.token.getType() == CssLexer.LPAREN));
1151 
1152         // if term is not null and the last term was not an lparen,
1153         // then term starts a new series of Paint. Point
1154         // root.nextInSeries to term so the next loop skips over the
1155         // already parsed ladder bits.
1156         if (term != null) {
1157             root.nextInSeries = term;
1158         }
1159 
1160         // if term is null, then we are at the end of a series.
1161         // root points to 'ladder', now we want the next term after root
1162         // to be the term after the last stop, which may be another layer
1163         else {
1164             root.nextInSeries = null;
1165             root.nextLayer = prev.nextLayer;
1166         }
1167 
1168         return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
1169     }
1170 


1341 
1342         ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs];
1343         for (int n=0; n<nArgs; n++) {
1344             stops[n] = new ParsedValueImpl<ParsedValue[],Stop>(
1345                 new ParsedValueImpl[] {
1346                     new ParsedValueImpl<Size,Size>(positions[n], null),
1347                     colors[n]
1348                 },
1349                 StopConverter.getInstance()
1350             );
1351         }
1352 
1353         return stops;
1354 
1355     }
1356 
1357     // parse (<number>, <number>)
1358     private ParsedValueImpl[] point(final Term root) throws ParseException {
1359 
1360         if (root.token == null ||
1361             root.token.getType() != CssLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'");
1362 
1363         final String fn = root.token.getText();
1364         if (fn == null || !"(".equalsIgnoreCase(fn)) {
1365             final String msg = "Expected \'(\'";
1366             error(root, msg);
1367         }
1368 
1369         Term arg = null;
1370 
1371         // no <number>
1372         if ((arg = root.firstArg) == null)  error(root, "Expected \'<number>\'");
1373 
1374         final ParsedValueImpl<?,Size> ptX = parseSize(arg);
1375 
1376         final Term prev = arg;
1377 
1378         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1379 
1380         final ParsedValueImpl<?,Size> ptY = parseSize(arg);
1381 


1403         } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
1404             return parseRadialGradient(root);
1405         } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
1406             return parseImagePattern(root);
1407         } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
1408             return parseRepeatingImagePattern(root);
1409         } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
1410             return parseLadder(root);
1411         } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
1412             return parseRegion(root);
1413         } else {
1414             error(root, "Unexpected function \'" + fcn + "\'");
1415         }
1416         return null;
1417     }
1418 
1419     private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException {
1420 
1421         if (root == null) return null;
1422         if (root.token == null ||
1423             root.token.getType() != CssLexer.IDENT ||
1424             root.token.getText() == null ||
1425             root.token.getText().isEmpty()) {
1426             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1427             error(root, msg);
1428         }
1429         final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
1430         BlurType blurType = BlurType.THREE_PASS_BOX;
1431         if ("gaussian".equals(blurStr)) {
1432             blurType = BlurType.GAUSSIAN;
1433         } else if ("one-pass-box".equals(blurStr)) {
1434             blurType = BlurType.ONE_PASS_BOX;
1435         } else if ("two-pass-box".equals(blurStr)) {
1436             blurType = BlurType.TWO_PASS_BOX;
1437         } else if ("three-pass-box".equals(blurStr)) {
1438             blurType = BlurType.THREE_PASS_BOX;
1439         } else {
1440             final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1441             error(root, msg);
1442         }
1443         return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class));


1531 
1532         prev = arg;
1533         if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1534 
1535         ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
1536 
1537         ParsedValueImpl[] values = new ParsedValueImpl[] {
1538             blurVal,
1539             colorVal,
1540             radiusVal,
1541             spreadVal,
1542             offsetXVal,
1543             offsetYVal
1544         };
1545         return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance());
1546     }
1547 
1548     // returns null if the Term is null or is not a cycle method.
1549     private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) {
1550         CycleMethod cycleMethod = null;
1551         if (root != null && root.token.getType() == CssLexer.IDENT) {
1552 
1553             final String text = root.token.getText().toLowerCase(Locale.ROOT);
1554             if ("repeat".equals(text)) {
1555                 cycleMethod = CycleMethod.REPEAT;
1556             } else if ("reflect".equals(text)) {
1557                 cycleMethod = CycleMethod.REFLECT;
1558             } else if ("no-cycle".equals(text)) {
1559                 cycleMethod = CycleMethod.NO_CYCLE;
1560             }
1561         }
1562         if (cycleMethod != null)
1563             return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1564         else
1565             return null;
1566     }
1567 
1568     // linear <point> TO <point> STOPS <stop>+ cycleMethod?
1569     private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException {
1570 
1571         final String fn = (root.token != null) ? root.token.getText() : null;
1572         if (fn == null || !"linear".equalsIgnoreCase(fn)) {
1573             final String msg = "Expected \'linear\'";
1574             error(root, msg);
1575         }
1576 
1577         if (LOGGER.isLoggable(Level.WARNING)) {
1578             LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
1579         }
1580 
1581         Term term = root;
1582 
1583         if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'");
1584 
1585         final ParsedValueImpl<?,Size>[] startPt = point(term);
1586 
1587         Term prev = term;
1588         if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
1589         if (term.token == null ||
1590             term.token.getType() != CssLexer.IDENT ||
1591             !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");
1592 
1593         prev = term;
1594         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1595 
1596         final ParsedValueImpl<?,Size>[] endPt = point(term);
1597 
1598         prev = term;
1599         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1600         if (term.token == null ||
1601             term.token.getType() != CssLexer.IDENT ||
1602             !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1603 
1604         prev = term;
1605         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1606 
1607         int nStops = 0;
1608         Term temp = term;
1609         do {
1610             nStops += 1;
1611             // if next token type is IDENT, then we have CycleMethod
1612         } while (((temp = temp.nextInSeries) != null) &&
1613                  ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1614 
1615         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1616         int stopIndex = 0;
1617         do {
1618             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1619             if (stop != null) stops[stopIndex++] = stop;
1620             prev = term;
1621         } while(((term = term.nextInSeries) != null) &&
1622                 (term.token.getType() == CssLexer.LPAREN));
1623 
1624         // term is either null or is a cycle method, or the start of another Paint.
1625         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1626 
1627         if (cycleMethod == null) {
1628 
1629             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1630 
1631             // if term is not null and the last term was not a cycle method,
1632             // then term starts a new series or layer of Paint
1633             if (term != null) {
1634                 root.nextInSeries = term;
1635             }
1636 
1637             // if term is null, then we are at the end of a series.
1638             // root points to 'linear', now we want the next term after root
1639             // to be the term after the last stop, which may be another layer
1640             else {
1641                 root.nextInSeries = null;
1642                 root.nextLayer = prev.nextLayer;


1703         ParsedValueImpl<?,Size>[] startPt = null;
1704         ParsedValueImpl<?,Size>[] endPt = null;
1705 
1706         if ("from".equalsIgnoreCase(arg.token.getText())) {
1707 
1708             prev = arg;
1709             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1710 
1711             ParsedValueImpl<?,Size> ptX = parseSize(arg);
1712 
1713             prev = arg;
1714             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1715 
1716             ParsedValueImpl<?,Size> ptY = parseSize(arg);
1717 
1718             startPt = new ParsedValueImpl[] { ptX, ptY };
1719 
1720             prev = arg;
1721             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
1722             if (arg.token == null ||
1723                 arg.token.getType() != CssLexer.IDENT ||
1724                 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");
1725 
1726             prev = arg;
1727             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1728 
1729             ptX = parseSize(arg);
1730 
1731             prev = arg;
1732             if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1733 
1734             ptY = parseSize(arg);
1735 
1736             endPt = new ParsedValueImpl[] { ptX, ptY };
1737 
1738             prev = arg;
1739             arg = arg.nextArg;
1740 
1741         } else if("to".equalsIgnoreCase(arg.token.getText())) {
1742 
1743             prev = arg;
1744             if ((arg = arg.nextInSeries) == null ||
1745                 arg.token == null ||
1746                 arg.token.getType() != CssLexer.IDENT ||
1747                 arg.token.getText().isEmpty()) {
1748                 error (prev, "Expected \'<side-or-corner>\'");
1749             }
1750 
1751 
1752             int startX = 0;
1753             int startY = 0;
1754             int endX = 0;
1755             int endY = 0;
1756 
1757             String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
1758             // The keywords denote the direction.
1759             if ("top".equals(sideOrCorner1)) {
1760                 // going toward the top, then start at the bottom
1761                 startY = 100;
1762                 endY = 0;
1763 
1764             } else if ("bottom".equals(sideOrCorner1)) {
1765                 // going toward the bottom, then start at the top
1766                 startY = 0;
1767                 endY = 100;
1768 
1769             } else if ("right".equals(sideOrCorner1)) {
1770                 // going toward the right, then start at the left
1771                 startX = 0;
1772                 endX = 100;
1773 
1774             } else if ("left".equals(sideOrCorner1)) {
1775                 // going toward the left, then start at the right
1776                 startX = 100;
1777                 endX = 0;
1778 
1779             } else {
1780                 error(arg, "Invalid \'<side-or-corner>\'");
1781             }
1782 
1783             prev = arg;
1784             if (arg.nextInSeries != null) {
1785                 arg = arg.nextInSeries;
1786                 if (arg.token != null &&
1787                     arg.token.getType() == CssLexer.IDENT &&
1788                     !arg.token.getText().isEmpty()) {
1789 
1790                     String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);
1791 
1792                     // if right or left has already been given,
1793                     // then either startX or endX will not be zero.
1794                     if ("right".equals(sideOrCorner2) &&
1795                         startX == 0 && endX == 0) {
1796                         // start left, end right
1797                         startX = 0;
1798                         endX = 100;
1799                     } else if ("left".equals(sideOrCorner2) &&
1800                         startX == 0 && endX == 0) {
1801                         // start right, end left
1802                         startX = 100;
1803                         endX = 0;
1804 
1805                     // if top or bottom has already been given,
1806                     // then either startY or endY will not be zero.
1807                     } else if("top".equals(sideOrCorner2) &&


1895     private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException {
1896 
1897         final String fn = (root.token != null) ? root.token.getText() : null;
1898         if (fn == null || !"radial".equalsIgnoreCase(fn)) {
1899             final String msg = "Expected \'radial\'";
1900             error(root, msg);
1901         }
1902 
1903         if (LOGGER.isLoggable(Level.WARNING)) {
1904             LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
1905         }
1906 
1907         Term term = root;
1908         Term prev = root;
1909 
1910         if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1911         if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1912 
1913 
1914         ParsedValueImpl<?,Size> focusAngle = null;
1915         if (term.token.getType() == CssLexer.IDENT) {
1916             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1917             if ("focus-angle".equals(keyword)) {
1918 
1919                 prev = term;
1920                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1921                 if (term.token == null) error(prev, "Expected \'<number>\'");
1922 
1923                 focusAngle = parseSize(term);
1924 
1925                 prev = term;
1926                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1927                 if (term.token == null) error(term,  "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1928             }
1929         }
1930 
1931         ParsedValueImpl<?,Size> focusDistance = null;
1932         if (term.token.getType() == CssLexer.IDENT) {
1933             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1934             if ("focus-distance".equals(keyword)) {
1935 
1936                 prev = term;
1937                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1938                 if (term.token == null) error(prev, "Expected \'<number>\'");
1939 
1940                 focusDistance = parseSize(term);
1941 
1942                 prev = term;
1943                 if ((term = term.nextInSeries) == null) error(prev, "Expected  \'center (<number>,<number>)\' or \'<size>\'");
1944                 if (term.token == null) error(term,  "Expected  \'center (<number>,<number>)\' or \'<size>\'");
1945             }
1946         }
1947 
1948         ParsedValueImpl<?,Size>[] centerPoint = null;
1949         if (term.token.getType() == CssLexer.IDENT) {
1950             final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1951             if ("center".equals(keyword)) {
1952 
1953                 prev = term;
1954                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'");
1955                 if (term.token == null ||
1956                     term.token.getType() != CssLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'");
1957 
1958                 centerPoint = point(term);
1959 
1960                 prev = term;
1961                 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'");
1962                 if (term.token == null) error(term,  "Expected \'<size>\'");
1963             }
1964         }
1965 
1966         ParsedValueImpl<?,Size> radius = parseSize(term);
1967 
1968         prev = term;
1969         if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
1970         if (term.token == null ||
1971             term.token.getType() != CssLexer.IDENT) error(term, "Expected \'stops\' keyword");
1972 
1973         if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1974 
1975         prev = term;
1976         if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1977 
1978         int nStops = 0;
1979         Term temp = term;
1980         do {
1981             nStops += 1;
1982             // if next token type is IDENT, then we have CycleMethod
1983         } while (((temp = temp.nextInSeries) != null) &&
1984                  ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1985 
1986         ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1987         int stopIndex = 0;
1988         do {
1989             ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1990             if (stop != null) stops[stopIndex++] = stop;
1991             prev = term;
1992         } while(((term = term.nextInSeries) != null) &&
1993                 (term.token.getType() == CssLexer.LPAREN));
1994 
1995         // term is either null or is a cycle method, or the start of another Paint.
1996         ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1997 
1998         if (cycleMethod == null) {
1999 
2000             cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
2001 
2002             // if term is not null and the last term was not a cycle method,
2003             // then term starts a new series or layer of Paint
2004             if (term != null) {
2005                 root.nextInSeries = term;
2006             }
2007 
2008             // if term is null, then we are at the end of a series.
2009             // root points to 'linear', now we want the next term after root
2010             // to be the term after the last stop, which may be another layer
2011             else {
2012                 root.nextInSeries = null;
2013                 root.nextLayer = prev.nextLayer;


2408             }
2409             temp = nextLayer(temp);
2410         }
2411 
2412         return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance());
2413     }
2414 
2415     // <size> | <size> <size> <size> <size>
2416     private ParsedValueImpl<Size, Size>[] parseSizeSeries(Term root)
2417             throws ParseException {
2418 
2419         if (root.token == null) error(root, "Parse error");
2420 
2421         List<ParsedValueImpl<Size,Size>> sizes = new ArrayList<>();
2422 
2423         Term term = root;
2424         while(term != null) {
2425             Token token = term.token;
2426             final int ttype = token.getType();
2427             switch (ttype) {
2428                 case CssLexer.NUMBER:
2429                 case CssLexer.PERCENTAGE:
2430                 case CssLexer.EMS:
2431                 case CssLexer.EXS:
2432                 case CssLexer.PX:
2433                 case CssLexer.CM:
2434                 case CssLexer.MM:
2435                 case CssLexer.IN:
2436                 case CssLexer.PT:
2437                 case CssLexer.PC:
2438                 case CssLexer.DEG:
2439                 case CssLexer.GRAD:
2440                 case CssLexer.RAD:
2441                 case CssLexer.TURN:
2442                     ParsedValueImpl sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
2443                     sizes.add(sizeValue);
2444                     break;
2445                 default:
2446                     error (root, "expected series of <size>");
2447             }
2448             term = term.nextInSeries;
2449         }
2450         return sizes.toArray(new ParsedValueImpl[sizes.size()]);
2451 
2452     }
2453 
2454     // http://www.w3.org/TR/css3-background/#the-border-radius
2455     // <size>{1,4} [ '/' <size>{1,4}]? [',' <size>{1,4} [ '/' <size>{1,4}]?]?
2456     private ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root)
2457             throws ParseException {
2458 
2459 
2460         int nLayers = numberOfLayers(root);
2461 
2462         Term term = root;
2463         int layer = 0;
2464         ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers];
2465 
2466         while(term != null) {
2467 
2468             int nHorizontalTerms = 0;
2469             Term temp = term;
2470             while (temp != null) {
2471                 if (temp.token.getType() == CssLexer.SOLIDUS) {
2472                     temp = temp.nextInSeries;
2473                     break;
2474                 }
2475                 nHorizontalTerms += 1;
2476                 temp = temp.nextInSeries;
2477             };
2478 
2479             int nVerticalTerms = 0;
2480             while (temp != null) {
2481                 if (temp.token.getType() == CssLexer.SOLIDUS) {
2482                     error(temp, "unexpected SOLIDUS");
2483                     break;
2484                 }
2485                 nVerticalTerms += 1;
2486                 temp = temp.nextInSeries;
2487             }
2488 
2489             if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) {
2490                 error(root, "expected [<length>|<percentage>]{1,4} [/ [<length>|<percentage>]{1,4}]?");
2491             }
2492 
2493             // used as index into margins[]. horizontal = 0, vertical = 1
2494             int orientation = 0;
2495 
2496             // at most, there should be four radii in the horizontal orientation and four in the vertical.
2497             ParsedValueImpl<?,Size>[][] radii = new ParsedValueImpl[2][4];
2498 
2499             ParsedValueImpl<?,Size> zero = new ParsedValueImpl<Size,Size>(new Size(0,SizeUnits.PX), null);
2500             for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; }
2501 
2502             int hr = 0;
2503             int vr = 0;
2504 
2505             Term lastTerm = term;
2506             while ((hr <= 4) && (vr <= 4) && (term != null)) {
2507 
2508                 if (term.token.getType() == CssLexer.SOLIDUS) {
2509                     orientation += 1;
2510                 } else  {
2511                     ParsedValueImpl<?,Size> parsedValue = parseSize(term);
2512                     if (orientation == 0) {
2513                         radii[orientation][hr++] = parsedValue;
2514                     } else {
2515                         radii[orientation][vr++] = parsedValue;
2516                     }
2517                 }
2518                 lastTerm = term;
2519                 term = term.nextInSeries;
2520             }
2521 
2522             //
2523             // http://www.w3.org/TR/css3-background/#the-border-radius
2524             // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left.
2525             // If bottom-left is omitted it is the same as top-right.
2526             // If bottom-right is omitted it is the same as top-left.
2527             // If top-right is omitted it is the same as top-left.
2528             //


2909         while (term != null) {
2910             layers[layer++] = parseBackgroundPosition(term);
2911             term = nextLayer(term);
2912         }
2913         return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance());
2914     }
2915 
2916     /*
2917     http://www.w3.org/TR/css3-background/#the-background-repeat
2918     <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
2919     */
2920     private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root)
2921             throws ParseException {
2922 
2923         BackgroundRepeat xAxis, yAxis;
2924         xAxis = yAxis = BackgroundRepeat.NO_REPEAT;
2925 
2926         Term term = root;
2927 
2928         if (term.token == null ||
2929             term.token.getType() != CssLexer.IDENT ||
2930             term.token.getText() == null ||
2931             term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'");
2932 
2933         String text = term.token.getText().toLowerCase(Locale.ROOT);
2934         if ("repeat-x".equals(text)) {
2935             xAxis = BackgroundRepeat.REPEAT;
2936             yAxis = BackgroundRepeat.NO_REPEAT;
2937         } else if ("repeat-y".equals(text)) {
2938             xAxis = BackgroundRepeat.NO_REPEAT;
2939             yAxis = BackgroundRepeat.REPEAT;
2940         } else if ("repeat".equals(text)) {
2941             xAxis = BackgroundRepeat.REPEAT;
2942             yAxis = BackgroundRepeat.REPEAT;
2943         } else if ("space".equals(text)) {
2944             xAxis = BackgroundRepeat.SPACE;
2945             yAxis = BackgroundRepeat.SPACE;
2946         } else if ("round".equals(text)) {
2947             xAxis = BackgroundRepeat.ROUND;
2948             yAxis = BackgroundRepeat.ROUND;
2949         } else if ("no-repeat".equals(text)) {
2950             xAxis = BackgroundRepeat.NO_REPEAT;
2951             yAxis = BackgroundRepeat.NO_REPEAT;
2952         } else if ("stretch".equals(text)) {
2953             xAxis = BackgroundRepeat.NO_REPEAT;
2954             yAxis = BackgroundRepeat.NO_REPEAT;
2955         } else {
2956             error(term, "Expected  \'<repeat-style>\' " + text);
2957         }
2958 
2959         if ((term = term.nextInSeries) != null &&
2960              term.token != null &&
2961              term.token.getType() == CssLexer.IDENT &&
2962              term.token.getText() != null &&
2963              !term.token.getText().isEmpty()) {
2964 
2965             text = term.token.getText().toLowerCase(Locale.ROOT);
2966             if ("repeat-x".equals(text)) {
2967                 error(term, "Unexpected \'repeat-x\'");
2968             } else if ("repeat-y".equals(text)) {
2969                 error(term, "Unexpected \'repeat-y\'");
2970             } else if ("repeat".equals(text)) {
2971                 yAxis = BackgroundRepeat.REPEAT;
2972             } else if ("space".equals(text)) {
2973                 yAxis = BackgroundRepeat.SPACE;
2974             } else if ("round".equals(text)) {
2975                 yAxis = BackgroundRepeat.ROUND;
2976             } else if ("no-repeat".equals(text)) {
2977                 yAxis = BackgroundRepeat.NO_REPEAT;
2978             } else if ("stretch".equals(text)) {
2979                 yAxis = BackgroundRepeat.NO_REPEAT;
2980             } else {
2981                 error(term, "Expected  \'<repeat-style>\'");


3013         while (term != null) {
3014             layers[layer++] = parseRepeatStyle(term);
3015             term = nextLayer(term);
3016         }
3017         return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
3018     }
3019 
3020     /*
3021     http://www.w3.org/TR/css3-background/#the-background-size
3022     <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
3023     */
3024     private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root)
3025         throws ParseException {
3026 
3027         ParsedValueImpl<?,Size> height = null, width = null;
3028         boolean cover = false, contain = false;
3029 
3030         Term term = root;
3031         if (term.token == null) error(term, "Expected \'<bg-size>\'");
3032 
3033         if (term.token.getType() == CssLexer.IDENT) {
3034             final String text =
3035                 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3036 
3037             if ("auto".equals(text)) {
3038                 // We don't do anything because width / height are already initialized
3039             } else if ("cover".equals(text)) {
3040                 cover = true;
3041             } else if ("contain".equals(text)) {
3042                 contain = true;
3043             } else if ("stretch".equals(text)) {
3044                 width = ONE_HUNDRED_PERCENT;
3045                 height = ONE_HUNDRED_PERCENT;
3046             } else {
3047                 error(term, "Expected \'auto\', \'cover\', \'contain\', or  \'stretch\'");
3048             }
3049         } else if (isSize(term.token)) {
3050             width = parseSize(term);
3051             height = null;
3052         } else {
3053             error(term, "Expected \'<bg-size>\'");
3054         }
3055 
3056         if ((term = term.nextInSeries) != null) {
3057             if (cover || contain) error(term, "Unexpected \'<bg-size>\'");
3058 
3059             if (term.token.getType() == CssLexer.IDENT) {
3060                 final String text =
3061                     (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3062 
3063                 if ("auto".equals(text)) {
3064                     height = null;
3065                 } else if ("cover".equals(text)) {
3066                     error(term, "Unexpected \'cover\'");
3067                 } else if ("contain".equals(text)) {
3068                     error(term, "Unexpected \'contain\'");
3069                 } else if ("stretch".equals(text)) {
3070                     height = ONE_HUNDRED_PERCENT;
3071                 } else {
3072                     error(term, "Expected \'auto\' or \'stretch\'");
3073                 }
3074             } else if (isSize(term.token)) {
3075                 height = parseSize(term);
3076             } else {
3077                 error(term, "Expected \'<bg-size>\'");
3078             }
3079 


3158 
3159 
3160     private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>
3161             parseBorderStyleLayers(final Term root) throws ParseException {
3162 
3163         int nLayers = numberOfLayers(root);
3164         ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers];
3165         int layer = 0;
3166         Term term = root;
3167         while (term != null) {
3168             layers[layer++] = parseBorderStyleSeries(term);
3169             term = nextLayer(term);
3170         }
3171         return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance());
3172     }
3173 
3174     // Only meant to be used from parseBorderStyle, but might be useful elsewhere
3175     private String getKeyword(final Term term) {
3176         if (term != null &&
3177              term.token != null &&
3178              term.token.getType() == CssLexer.IDENT &&
3179              term.token.getText() != null &&
3180              !term.token.getText().isEmpty()) {
3181 
3182             return term.token.getText().toLowerCase(Locale.ROOT);
3183         }
3184         return null;
3185     }
3186 
3187     //<border-style> [ , <border-style> ]*
3188     // where <border-style> =
3189     //      <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]?
3190     // where <dash-style> =
3191     //      [ none | solid | dotted | dashed ]
3192     private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root)
3193             throws ParseException {
3194 
3195 
3196         ParsedValue<ParsedValue[],Number[]> dashStyle = null;
3197         ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null;
3198         ParsedValue<String,StrokeType> strokeType = null;


3280             dashPhase,
3281             strokeType,
3282             strokeLineJoin,
3283             strokeMiterLimit,
3284             strokeLineCap
3285         };
3286 
3287         return new ParsedValueImpl(values, BorderStyleConverter.getInstance());
3288     }
3289 
3290     //
3291     // segments(<size> [, <size>]+) | <border-style>
3292     //
3293     private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException {
3294 
3295         if (root.token == null) error(root, "Expected \'<dash-style>\'");
3296 
3297         final int ttype = root.token.getType();
3298 
3299         ParsedValue<ParsedValue[],Number[]>  segments = null;
3300         if (ttype == CssLexer.IDENT) {
3301             segments = borderStyle(root);
3302         } else if (ttype == CssLexer.FUNCTION) {
3303             segments = segments(root);
3304         } else {
3305             error(root, "Expected \'<dash-style>\'");
3306         }
3307 
3308         return segments;
3309     }
3310 
3311     /*
3312     <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
3313     */
3314     private ParsedValue<ParsedValue[],Number[]>  borderStyle(Term root)
3315             throws ParseException {
3316 
3317         if (root.token == null ||
3318             root.token.getType() != CssLexer.IDENT ||
3319             root.token.getText() == null ||
3320             root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'");
3321 
3322         final String text = root.token.getText().toLowerCase(Locale.ROOT);
3323 
3324         if ("none".equals(text)) {
3325             return BorderStyleConverter.NONE;
3326         } else if ("hidden".equals(text)) {
3327             // The "hidden" mode doesn't make sense for FX, because it is the
3328             // same as "none" except for border-collapsed CSS tables
3329             return BorderStyleConverter.NONE;
3330         } else if ("dotted".equals(text)) {
3331             return BorderStyleConverter.DOTTED;
3332         } else if ("dashed".equals(text)) {
3333             return BorderStyleConverter.DASHED;
3334         } else if ("solid".equals(text)) {
3335             return BorderStyleConverter.SOLID;
3336         } else if ("double".equals(text)) {
3337             error(root, "Unsupported <border-style> \'double\'");
3338         } else if ("groove".equals(text)) {


3447     /*
3448      * http://www.w3.org/TR/css3-background/#the-border-image-slice
3449      * [<number> | <percentage>]{1,4} && fill?
3450      */
3451     private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root)
3452         throws ParseException {
3453 
3454         Term term = root;
3455         if (term.token == null || !isSize(term.token))
3456                 error(term, "Expected \'<size>\'");
3457 
3458         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3459         Boolean fill = Boolean.FALSE;
3460 
3461         int inset = 0;
3462         while (inset < 4 && term != null) {
3463             insets[inset++] = parseSize(term);
3464 
3465             if ((term = term.nextInSeries) != null &&
3466                  term.token != null &&
3467                  term.token.getType() == CssLexer.IDENT) {
3468 
3469                 if("fill".equalsIgnoreCase(term.token.getText())) {
3470                     fill = Boolean.TRUE;
3471                     break;
3472                 }
3473             }
3474         }
3475 
3476         if (inset < 2) insets[1] = insets[0]; // right = top
3477         if (inset < 3) insets[2] = insets[0]; // bottom = top
3478         if (inset < 4) insets[3] = insets[1]; // left = right
3479 
3480         ParsedValueImpl[] values = new ParsedValueImpl[] {
3481                 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()),
3482                 new ParsedValueImpl<Boolean,Boolean>(fill, null)
3483         };
3484         return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance());
3485     }
3486 
3487     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]>


3500 
3501     /*
3502      * http://www.w3.org/TR/css3-background/#border-image-width
3503      * [ <length> | <percentage> | <number> | auto ]{1,4}
3504      */
3505     private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root)
3506             throws ParseException {
3507 
3508         Term term = root;
3509         if (term.token == null || !isSize(term.token))
3510             error(term, "Expected \'<size>\'");
3511 
3512         ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3513 
3514         int inset = 0;
3515         while (inset < 4 && term != null) {
3516             insets[inset++] = parseSize(term);
3517 
3518             if ((term = term.nextInSeries) != null &&
3519                     term.token != null &&
3520                     term.token.getType() == CssLexer.IDENT) {
3521             }
3522         }
3523 
3524         if (inset < 2) insets[1] = insets[0]; // right = top
3525         if (inset < 3) insets[2] = insets[0]; // bottom = top
3526         if (inset < 4) insets[3] = insets[1]; // left = right
3527 
3528         return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance());
3529     }
3530 
3531     private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]>
3532         parseBorderImageWidthLayers(final Term root) throws ParseException {
3533 
3534         int nLayers = numberOfLayers(root);
3535         ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers];
3536         int layer = 0;
3537         Term term = root;
3538         while (term != null) {
3539             layers[layer++] = parseBorderImageWidth(term);
3540             term = nextLayer(term);
3541         }
3542         return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance());
3543     }
3544 
3545     // parse a Region value
3546     // i.e., region(".styleClassForRegion") or region("#idForRegion")
3547     private static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:";
3548     private ParsedValueImpl<String,String> parseRegion(Term root)
3549             throws ParseException {
3550         // first term in the chain is the function name...
3551         final String fn = (root.token != null) ? root.token.getText() : null;
3552         if (!"region".regionMatches(true, 0, fn, 0, 6)) {
3553             error(root,"Expected \'region\'");
3554         }
3555 
3556         Term arg = root.firstArg;
3557         if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3558 
3559         if (arg.token == null ||
3560                 arg.token.getType() != CssLexer.STRING ||
3561                 arg.token.getText() == null ||
3562                 arg.token.getText().isEmpty())  error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3563 
3564         final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText());
3565         return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance());
3566     }
3567 
3568     // url("<uri>") is tokenized by the lexer, so the root arg should be a URL token.
3569     private ParsedValueImpl<ParsedValue[],String> parseURI(Term root)
3570             throws ParseException {
3571 
3572         if (root == null) error(root, "Expected \'url(\"<uri-string>\")\'");
3573 
3574         if (root.token == null ||
3575             root.token.getType() != CssLexer.URL ||
3576             root.token.getText() == null ||
3577             root.token.getText().isEmpty()) error(root, "Expected \'url(\"<uri-string>\")\'");
3578 
3579         final String uri = root.token.getText();
3580         ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
3581             new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
3582             null // placeholder for Stylesheet URL
3583         };
3584         return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
3585     }
3586 
3587     // parse a series of URI values separated by commas.
3588     // i.e., <uri> [, <uri>]*
3589     private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root)
3590             throws ParseException {
3591 
3592         int nLayers = numberOfLayers(root);
3593 
3594         Term temp = root;
3595         int layer = 0;


3600             temp = nextLayer(temp);
3601         }
3602 
3603         return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance());
3604     }
3605 
3606     ////////////////////////////////////////////////////////////////////////////
3607     //
3608     // http://www.w3.org/TR/css3-fonts
3609     //
3610     ////////////////////////////////////////////////////////////////////////////
3611 
3612     /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */
3613     private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException {
3614 
3615         if (root == null) return null;
3616         final Token token = root.token;
3617         if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'");
3618 
3619         Size size = null;
3620         if (token.getType() == CssLexer.IDENT) {
3621             final String ident = token.getText().toLowerCase(Locale.ROOT);
3622             double value = -1;
3623             if ("inherit".equals(ident)) {
3624                 value = 100;
3625             } else if ("xx-small".equals(ident)) {
3626                 value = 60;
3627             } else if ("x-small".equals(ident)) {
3628                 value = 75;
3629             } else if ("small".equals(ident)) {
3630                 value = 80;
3631             } else if ("medium".equals(ident)) {
3632                 value = 100;
3633             } else if ("large".equals(ident)) {
3634                 value = 120;
3635             } else if ("x-large".equals(ident)) {
3636                 value = 150;
3637             } else if ("xx-large".equals(ident)) {
3638                 value = 200;
3639             } else if ("smaller".equals(ident)) {
3640                 value = 80;


3645             if (value > -1) {
3646                 size = new Size(value, SizeUnits.PERCENT);
3647             }
3648         }
3649 
3650         // if size is null, then size is not one of the keywords above.
3651         if (size == null) {
3652             size = size(token);
3653         }
3654 
3655         ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null);
3656         return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance());
3657     }
3658 
3659     /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */
3660     private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException {
3661 
3662         if (root == null) return null;
3663         final Token token = root.token;
3664         if (token == null ||
3665             token.getType() != CssLexer.IDENT ||
3666             token.getText() == null ||
3667             token.getText().isEmpty()) error(root, "Expected \'<font-style>\'");
3668 
3669         final String ident = token.getText().toLowerCase(Locale.ROOT);
3670         String posture = FontPosture.REGULAR.name();
3671 
3672         if ("normal".equals(ident)) {
3673             posture = FontPosture.REGULAR.name();
3674         } else if ("italic".equals(ident)) {
3675             posture = FontPosture.ITALIC.name();
3676         } else if ("oblique".equals(ident)) {
3677             posture = FontPosture.ITALIC.name();
3678         } else if ("inherit".equals(ident)) {
3679             posture = "inherit";
3680         } else {
3681             return null;
3682         }
3683 
3684         return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance());
3685     }


3719         } else if ("600".equals(ident)) {
3720             weight = FontWeight.findByWeight(600).name();
3721         } else if ("700".equals(ident)) {
3722             weight = FontWeight.findByWeight(700).name();
3723         } else if ("800".equals(ident)) {
3724             weight = FontWeight.findByWeight(800).name();
3725         } else if ("900".equals(ident)) {
3726             weight = FontWeight.findByWeight(900).name();
3727         } else {
3728             error(root, "Expected \'<font-weight>\'");
3729         }
3730         return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance());
3731     }
3732 
3733     private ParsedValueImpl<String,String>  parseFontFamily(Term root) throws ParseException {
3734 
3735         if (root == null) return null;
3736         final Token token = root.token;
3737         String text = null;
3738         if (token == null ||
3739             (token.getType() != CssLexer.IDENT &&
3740              token.getType() != CssLexer.STRING) ||
3741             (text = token.getText()) == null ||
3742             text.isEmpty()) error(root, "Expected \'<font-family>\'");
3743 
3744         final String fam = stripQuotes(text.toLowerCase(Locale.ROOT));
3745         if ("inherit".equals(fam)) {
3746             return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance());
3747         } else if ("serif".equals(fam) ||
3748             "sans-serif".equals(fam) ||
3749             "cursive".equals(fam) ||
3750             "fantasy".equals(fam) ||
3751             "monospace".equals(fam)) {
3752             return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance());
3753         } else {
3754             return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance());
3755         }
3756     }
3757 
3758     // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily
3759     private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException {
3760 
3761         // Because style, variant, weight, size and family can inherit
3762         // AND style, variant and weight are optional, parsing this backwards
3763         // is easier.
3764         Term next = root.nextInSeries;
3765         root.nextInSeries = null;
3766         while (next != null) {
3767             Term temp = next.nextInSeries;
3768             next.nextInSeries = root;
3769             root = next;
3770             next = temp;
3771         }
3772 
3773         // Now, root should point to fontFamily
3774         Token token = root.token;
3775         int ttype = token.getType();
3776         if (ttype != CssLexer.IDENT &&
3777             ttype != CssLexer.STRING) error(root, "Expected \'<font-family>\'");
3778         ParsedValueImpl<String,String> ffamily = parseFontFamily(root);
3779 
3780         Term term = root;
3781         if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'");
3782         if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3783 
3784         // Now, term could be the font size or it could be the line-height.
3785         // If the next term is a forward slash, then it's line-height.
3786         Term temp;
3787         if (((temp = term.nextInSeries) != null) &&
3788             (temp.token != null && temp.token.getType() == CssLexer.SOLIDUS)) {
3789 
3790             root = temp;
3791 
3792             if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'");
3793             if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3794 
3795             token = term.token;
3796         }
3797 
3798         ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term);
3799         if (fsize == null) error(root, "Expected \'<size>\'");
3800 
3801         ParsedValueImpl<String,FontPosture> fstyle = null;
3802         ParsedValueImpl<String,FontWeight> fweight = null;
3803         String fvariant = null;
3804 
3805         while ((term = term.nextInSeries) != null) {
3806 
3807             if (term.token == null ||
3808                 term.token.getType() != CssLexer.IDENT ||
3809                 term.token.getText() == null ||
3810                 term.token.getText().isEmpty())
3811                 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'");
3812 
3813             if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) {
3814                 ;
3815             } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) {
3816                 fvariant = term.token.getText();
3817             } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) {
3818                 ;
3819             }
3820         }
3821 
3822         ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle };
3823         return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance());
3824     }
3825 
3826     //
3827     // Parser state machine
3828     //
3829     Token currentToken = null;
3830 
3831     // return the next token that is not whitespace.
3832     private Token nextToken(CssLexer lexer) {
3833 
3834         Token token = null;
3835 
3836         do {
3837             token = lexer.nextToken();
3838         } while ((token != null) &&
3839                 (token.getType() == CssLexer.WS) ||
3840                 (token.getType() == CssLexer.NL));
3841 
3842         if (LOGGER.isLoggable(Level.FINEST)) {
3843             LOGGER.finest(token.toString());
3844         }
3845 
3846         return token;
3847 
3848     }
3849 
3850     // keep track of what is in process of being parsed to avoid import loops
3851     private static Stack<String> imports;
3852 
3853     private void parse(Stylesheet stylesheet, CssLexer lexer) {
3854 
3855         // need to read the first token
3856         currentToken = nextToken(lexer);
3857 
3858         while((currentToken != null) &&
3859                 (currentToken.getType() == CssLexer.AT_KEYWORD)) {
3860 
3861             currentToken = nextToken(lexer);
3862 
3863             if (currentToken == null || currentToken.getType() != CssLexer.IDENT) {
3864 
3865                 // just using ParseException for a nice error message, not for throwing the exception.
3866                 ParseException parseException = new ParseException("Expected IDENT", currentToken, this);
3867                 final String msg = parseException.toString();
3868                 ParseError error = createError(msg);
3869                 if (LOGGER.isLoggable(Level.WARNING)) {
3870                     LOGGER.warning(error.toString());
3871                 }
3872                 reportError(error);
3873 
3874                 // get past EOL or SEMI
3875                 do {
3876                     currentToken = lexer.nextToken();
3877                 } while ((currentToken != null) &&
3878                         (currentToken.getType() == CssLexer.SEMI) ||
3879                         (currentToken.getType() == CssLexer.WS) ||
3880                         (currentToken.getType() == CssLexer.NL));
3881                 continue;
3882             }
3883 
3884             String keyword = currentToken.getText().toLowerCase(Locale.ROOT);
3885             if ("font-face".equals(keyword)) {
3886                 FontFace fontFace = fontFace(lexer);
3887                 if (fontFace != null) stylesheet.getFontFaces().add(fontFace);
3888                 currentToken = nextToken(lexer);
3889                 continue;
3890 
3891             } else if ("import".equals(keyword)) {
3892 
3893                 if (CssParser.imports == null) {
3894                     CssParser.imports = new Stack<>();
3895                 }
3896 
3897                 if (!imports.contains(sourceOfStylesheet)) {
3898 
3899                     imports.push(sourceOfStylesheet);
3900 
3901                     Stylesheet importedStylesheet = handleImport(lexer);
3902 
3903                     if (importedStylesheet != null) {
3904                         stylesheet.importStylesheet(importedStylesheet);
3905                     }
3906 
3907                     imports.pop();
3908 
3909                     if (CssParser.imports.isEmpty()) {
3910                         CssParser.imports = null;
3911                     }
3912 
3913                 } else {
3914 // Import imports import!
3915                     final int line = currentToken.getLine();
3916                     final int pos = currentToken.getOffset();
3917                     final String msg =
3918                             MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]",
3919                                     line, pos, imports.peek());
3920                     ParseError error = createError(msg);
3921                     if (LOGGER.isLoggable(Level.WARNING)) {
3922                         LOGGER.warning(error.toString());
3923                     }
3924                     reportError(error);
3925                 }
3926 
3927                 // get past EOL or SEMI
3928                 do {
3929                     currentToken = lexer.nextToken();
3930                 } while ((currentToken != null) &&
3931                         (currentToken.getType() == CssLexer.SEMI) ||
3932                         (currentToken.getType() == CssLexer.WS) ||
3933                         (currentToken.getType() == CssLexer.NL));
3934 
3935                 continue;
3936 
3937             }
3938         }
3939 
3940         while ((currentToken != null) &&
3941                (currentToken.getType() != Token.EOF)) {
3942 
3943             List<Selector> selectors = selectors(lexer);
3944             if (selectors == null) return;
3945 
3946             if ((currentToken == null) ||
3947                 (currentToken.getType() != CssLexer.LBRACE)) {
3948                     final int line = currentToken != null ? currentToken.getLine() : -1;
3949                     final int pos = currentToken != null ? currentToken.getOffset() : -1;
3950                     final String msg =
3951                         MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]",
3952                                 line, pos);
3953                     ParseError error = createError(msg);
3954                     if (LOGGER.isLoggable(Level.WARNING)) {
3955                         LOGGER.warning(error.toString());
3956                     }
3957                     reportError(error);
3958                 currentToken = null;
3959                 return;
3960             }
3961 
3962             // get past the LBRACE
3963             currentToken = nextToken(lexer);
3964 
3965             List<Declaration> declarations = declarations(lexer);
3966             if (declarations == null) return;
3967 
3968             if ((currentToken != null) &&
3969                 (currentToken.getType() != CssLexer.RBRACE)) {
3970                     final int line = currentToken.getLine();
3971                     final int pos = currentToken.getOffset();
3972                     final String msg =
3973                         MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]",
3974                                 line, pos);
3975                     ParseError error = createError(msg);
3976                     if (LOGGER.isLoggable(Level.WARNING)) {
3977                         LOGGER.warning(error.toString());
3978                     }
3979                     reportError(error);
3980                 currentToken = null;
3981                 return;
3982             }
3983 
3984             stylesheet.getRules().add(new Rule(selectors, declarations));
3985 
3986             currentToken = nextToken(lexer);
3987 
3988         }
3989         currentToken = null;
3990     }
3991 
3992     private FontFace fontFace(CssLexer lexer) {
3993         final Map<String,String> descriptors = new HashMap<String,String>();
3994         final List<FontFaceImpl.FontFaceSrc> sources = new ArrayList<FontFaceImpl.FontFaceSrc>();
3995         while(true) {
3996             currentToken = nextToken(lexer);
3997             if (currentToken.getType() == CssLexer.IDENT) {
3998                 String key = currentToken.getText();
3999                 // ignore the colon that follows
4000                 currentToken = nextToken(lexer);
4001                 // get the next token after colon
4002                 currentToken = nextToken(lexer);
4003                 // ignore all but "src"
4004                 if ("src".equalsIgnoreCase(key)) {
4005                     while(true) {
4006                         if((currentToken != null) &&
4007                                 (currentToken.getType() != CssLexer.SEMI) &&
4008                                 (currentToken.getType() != CssLexer.RBRACE) &&
4009                                 (currentToken.getType() != Token.EOF)) {
4010 
4011                             if (currentToken.getType() == CssLexer.IDENT) {
4012                                 // simple reference to other font-family
4013                                 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.REFERENCE,currentToken.getText()));
4014 
4015                             } else if (currentToken.getType() == CssLexer.URL) {
4016 
4017                                 // let URLConverter do the conversion
4018                                 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4019                                         new ParsedValueImpl<String,String>(currentToken.getText(), StringConverter.getInstance()),
4020                                         new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4021                                 };
4022                                 ParsedValue<ParsedValue[], String> parsedValue =
4023                                         new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4024                                 String urlStr = parsedValue.convert(null);
4025 
4026                                 URL url = null;
4027                                 try {
4028                                     URI fontUri = new URI(urlStr);
4029                                     url = fontUri.toURL();
4030                                 } catch (URISyntaxException |  MalformedURLException malf) {
4031 
4032                                     final int line = currentToken.getLine();
4033                                     final int pos = currentToken.getOffset();
4034                                     final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]", line, pos, urlStr);
4035                                     ParseError error = createError(msg);
4036                                     if (LOGGER.isLoggable(Level.WARNING)) {
4037                                         LOGGER.warning(error.toString());
4038                                     }
4039                                     reportError(error);
4040 
4041                                     // skip the rest.
4042                                     while(currentToken != null) {
4043                                         int ttype = currentToken.getType();
4044                                         if (ttype == CssLexer.RBRACE ||
4045                                                 ttype == Token.EOF) {
4046                                             return null;
4047                                         }
4048                                         currentToken = nextToken(lexer);
4049                                     }
4050                                 }
4051 
4052                                 String format = null;
4053                                 while(true) {
4054                                     currentToken = nextToken(lexer);
4055                                     final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF;
4056                                     if (ttype == CssLexer.FUNCTION) {
4057                                         if ("format(".equalsIgnoreCase(currentToken.getText())) {
4058                                             continue;
4059                                         } else {
4060                                             break;
4061                                         }
4062                                     } else if (ttype == CssLexer.IDENT ||
4063                                             ttype == CssLexer.STRING) {
4064 
4065                                         format = Utils.stripQuotes(currentToken.getText());
4066                                     } else if (ttype == CssLexer.RPAREN) {
4067                                         continue;
4068                                     } else {
4069                                         break;
4070                                     }
4071                                 }
4072                                 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.URL,url.toExternalForm(), format));
4073 
4074                             } else if (currentToken.getType() == CssLexer.FUNCTION) {
4075                                 if ("local(".equalsIgnoreCase(currentToken.getText())) {
4076                                     // consume the function token
4077                                     currentToken = nextToken(lexer);
4078                                     // parse function contents
4079                                     final StringBuilder localSb = new StringBuilder();
4080                                     while(true) {
4081                                         if((currentToken != null) && (currentToken.getType() != CssLexer.RPAREN) &&
4082                                                 (currentToken.getType() != Token.EOF)) {
4083                                             localSb.append(currentToken.getText());
4084                                         } else {
4085                                             break;
4086                                         }
4087                                         currentToken = nextToken(lexer);
4088                                     }
4089                                     int start = 0, end = localSb.length();
4090                                     if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++;
4091                                     if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --;
4092                                     final String local = localSb.substring(start,end);
4093                                     sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.LOCAL,local));
4094                                 } else {
4095                                     // error unknown fontface src type
4096                                     final int line = currentToken.getLine();
4097                                     final int pos = currentToken.getOffset();
4098                                     final String msg = MessageFormat.format("Unknown @font-face src type [" + currentToken.getText() + ")] at [{0,number,#},{1,number,#}]", line, pos);
4099                                     ParseError error = createError(msg);
4100                                     if (LOGGER.isLoggable(Level.WARNING)) {
4101                                         LOGGER.warning(error.toString());
4102                                     }
4103                                     reportError(error);
4104 
4105                                 }
4106                             } else  if (currentToken.getType() == CssLexer.COMMA) {
4107                                 // ignore
4108                             } else {
4109                                 // error unexpected token
4110                                 final int line = currentToken.getLine();
4111                                 final int pos = currentToken.getOffset();
4112                                 final String msg = MessageFormat.format("Unexpected TOKEN [" + currentToken.getText() + "] at [{0,number,#},{1,number,#}]", line, pos);
4113                                 ParseError error = createError(msg);
4114                                 if (LOGGER.isLoggable(Level.WARNING)) {
4115                                     LOGGER.warning(error.toString());
4116                                 }
4117                                 reportError(error);
4118                             }
4119                         } else {
4120                             break;
4121                         }
4122                         currentToken = nextToken(lexer);
4123                     }
4124                 } else {
4125                     StringBuilder descriptorVal = new StringBuilder();
4126                     while(true) {
4127                         if((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) &&
4128                             (currentToken.getType() != Token.EOF)) {
4129                             descriptorVal.append(currentToken.getText());
4130                         } else {
4131                             break;
4132                         }
4133                         currentToken = nextToken(lexer);
4134                     }
4135                     descriptors.put(key,descriptorVal.toString());
4136                 }
4137 //                continue;
4138             }
4139 
4140             if ((currentToken == null) ||
4141                 (currentToken.getType() == CssLexer.RBRACE) ||
4142                 (currentToken.getType() == Token.EOF)) {
4143                 break;
4144             }
4145 
4146         }
4147         return new FontFaceImpl(descriptors, sources);
4148     }
4149 
4150     private Stylesheet handleImport(CssLexer lexer) {
4151         currentToken = nextToken(lexer);
4152 
4153         if (currentToken == null || currentToken.getType() == Token.EOF) {
4154             return null;
4155         }
4156 
4157         int ttype = currentToken.getType();
4158 
4159         String fname = null;
4160         if (ttype == CssLexer.STRING || ttype == CssLexer.URL) {
4161             fname = currentToken.getText();
4162         }
4163 
4164         Stylesheet importedStylesheet = null;
4165         final String _sourceOfStylesheet = sourceOfStylesheet;
4166 
4167         if (fname != null) {
4168             // let URLConverter do the conversion
4169             ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4170                     new ParsedValueImpl<String,String>(fname, StringConverter.getInstance()),
4171                     new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4172             };
4173             ParsedValue<ParsedValue[], String> parsedValue =
4174                     new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4175 
4176             String urlString = parsedValue.convert(null);
4177             importedStylesheet = StyleManager.loadStylesheet(urlString);
4178 
4179             // When we load an imported stylesheet, the sourceOfStylesheet field
4180             // gets set to the new stylesheet. Once it is done loading we must reset
4181             // this field back to the previous value, otherwise we will potentially
4182             // run into problems (for example, see RT-40346).
4183             sourceOfStylesheet = _sourceOfStylesheet;
4184         }
4185         if (importedStylesheet == null) {
4186             final String msg =
4187                     MessageFormat.format("Could not import {0}", fname);
4188             ParseError error = createError(msg);
4189             if (LOGGER.isLoggable(Level.WARNING)) {
4190                 LOGGER.warning(error.toString());
4191             }
4192             reportError(error);
4193         }
4194         return importedStylesheet;
4195     }
4196 
4197     private List<Selector> selectors(CssLexer lexer) {
4198 
4199         List<Selector> selectors = new ArrayList<Selector>();
4200 
4201         while(true) {
4202             Selector selector = selector(lexer);
4203             if (selector == null) {
4204                 // some error happened, skip the rule...
4205                 while ((currentToken != null) &&
4206                        (currentToken.getType() != CssLexer.RBRACE) &&
4207                        (currentToken.getType() != Token.EOF)) {
4208                     currentToken = nextToken(lexer);
4209                 }
4210 
4211                 // current token is either RBRACE or EOF. Calling
4212                 // currentToken will get the next token or EOF.
4213                 currentToken = nextToken(lexer);
4214 
4215                 // skipped the last rule?
4216                 if (currentToken == null || currentToken.getType() == Token.EOF) {
4217                     currentToken = null;
4218                     return null;
4219                 }
4220 
4221                 continue;
4222             }
4223             selectors.add(selector);
4224 
4225             if ((currentToken != null) &&
4226                 (currentToken.getType() == CssLexer.COMMA)) {
4227                 // get past the comma
4228                 currentToken = nextToken(lexer);
4229                 continue;
4230             }
4231 
4232             // currentToken was either null or not a comma
4233             // so we are done with selectors.
4234             break;
4235         }
4236 
4237         return selectors;
4238     }
4239 
4240     private Selector selector(CssLexer lexer) {
4241 
4242         List<Combinator> combinators = null;
4243         List<SimpleSelector> sels = null;
4244 
4245         SimpleSelector ancestor = simpleSelector(lexer);
4246         if (ancestor == null) return null;
4247 
4248         while (true) {
4249             Combinator comb = combinator(lexer);
4250             if (comb != null) {
4251                 if (combinators == null) {
4252                     combinators = new ArrayList<Combinator>();
4253                 }
4254                 combinators.add(comb);
4255                 SimpleSelector descendant = simpleSelector(lexer);
4256                 if (descendant == null) return null;
4257                 if (sels == null) {
4258                     sels = new ArrayList<SimpleSelector>();
4259                     sels.add(ancestor);
4260                 }
4261                 sels.add(descendant);
4262             } else {
4263                 break;
4264             }
4265         }
4266 
4267         // RT-15473
4268         // We might return from selector with a NL token instead of an
4269         // LBRACE, so skip past the NL here.
4270         if (currentToken != null && currentToken.getType() == CssLexer.NL) {
4271             currentToken = nextToken(lexer);
4272         }
4273 
4274 
4275         if (sels == null) {
4276             return ancestor;
4277         } else {
4278             return new CompoundSelector(sels,combinators);
4279         }
4280 
4281     }
4282 
4283     private SimpleSelector simpleSelector(CssLexer lexer) {
4284 
4285         String esel = "*"; // element selector. default to universal
4286         String isel = ""; // id selector
4287         List<String> csels = null; // class selector
4288         List<String> pclasses = null; // pseudoclasses
4289 
4290         while (true) {
4291 
4292             final int ttype =
4293                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4294 
4295             switch(ttype) {
4296                 // element selector
4297                 case CssLexer.STAR:
4298                 case CssLexer.IDENT:
4299                     esel = currentToken.getText();
4300                     break;
4301 
4302                 // class selector
4303                 case CssLexer.DOT:
4304                     currentToken = nextToken(lexer);
4305                     if (currentToken != null &&
4306                         currentToken.getType() == CssLexer.IDENT) {
4307                         if (csels == null) {
4308                             csels = new ArrayList<String>();
4309                         }
4310                         csels.add(currentToken.getText());
4311                     } else {
4312                         currentToken = Token.INVALID_TOKEN;
4313                         return null;
4314                     }
4315                     break;
4316 
4317                 // id selector
4318                 case CssLexer.HASH:
4319                     isel = currentToken.getText().substring(1);
4320                     break;
4321 
4322                 case CssLexer.COLON:
4323                     currentToken = nextToken(lexer);
4324                     if (currentToken != null && pclasses == null) {
4325                         pclasses = new ArrayList<String>();
4326                     }
4327 
4328                     if (currentToken.getType() == CssLexer.IDENT) {
4329                         pclasses.add(currentToken.getText());
4330                     } else if (currentToken.getType() == CssLexer.FUNCTION){
4331                         String pclass = functionalPseudo(lexer);
4332                         pclasses.add(pclass);
4333                     } else {
4334                         currentToken = Token.INVALID_TOKEN;
4335                     }
4336 
4337                     if (currentToken.getType() == Token.INVALID) {
4338                         return null;
4339                     }
4340                     break;
4341 
4342                 case CssLexer.NL:
4343                 case CssLexer.WS:
4344                 case CssLexer.COMMA:
4345                 case CssLexer.GREATER:
4346                 case CssLexer.LBRACE:
4347                 case Token.EOF:
4348                     return new SimpleSelector(esel, csels, pclasses, isel);
4349 
4350                 default:
4351                     return null;
4352 
4353 
4354             }
4355 
4356             // get the next token, but don't skip whitespace
4357             // since it may be a combinator
4358             currentToken = lexer.nextToken();
4359             if (LOGGER.isLoggable(Level.FINEST)) {
4360                 LOGGER.finest(currentToken.toString());
4361             }
4362         }
4363     }
4364 
4365     // From http://www.w3.org/TR/selectors/#grammar
4366     //  functional_pseudo
4367     //      : FUNCTION S* expression ')'
4368     //      ;
4369     //  expression
4370     //      /* In CSS3, the expressions are identifiers, strings, */
4371     //      /* or of the form "an+b" */
4372     //      : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
4373     //      ;
4374     private String functionalPseudo(CssLexer lexer) {
4375 
4376         // TODO: This is not how we should handle functional pseudo-classes in the long-run!
4377 
4378         StringBuilder pclass = new StringBuilder(currentToken.getText());
4379 
4380         while(true) {
4381 
4382             currentToken = nextToken(lexer);
4383 
4384             switch(currentToken.getType()) {
4385 
4386                 // TODO: lexer doesn't really scan right and isn't CSS3,
4387                 // so PLUS, '-', NUMBER, etc are all useless at this point.
4388                 case CssLexer.STRING:
4389                 case CssLexer.IDENT:
4390                     pclass.append(currentToken.getText());
4391                     break;
4392 
4393                 case CssLexer.RPAREN:
4394                     pclass.append(')');
4395                     return pclass.toString();
4396 
4397                 default:
4398                     currentToken = Token.INVALID_TOKEN;
4399                     return null;
4400             }
4401         }
4402 
4403     }
4404 
4405     private Combinator combinator(CssLexer lexer) {
4406 
4407         Combinator combinator = null;
4408 
4409         while (true) {
4410 
4411             final int ttype =
4412                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4413 
4414             switch(ttype) {
4415 
4416                 case CssLexer.WS:
4417                     // need to check if combinator is null since child token
4418                     // might be surrounded by whitespace.
4419                     if (combinator == null && " ".equals(currentToken.getText())) {
4420                         combinator = Combinator.DESCENDANT;
4421                     }
4422                     break;
4423 
4424                 case CssLexer.GREATER:
4425                     // no need to check if combinator is null here
4426                     combinator = Combinator.CHILD;
4427                     break;
4428 
4429                 case CssLexer.STAR:
4430                 case CssLexer.IDENT:
4431                 case CssLexer.DOT:
4432                 case CssLexer.HASH:
4433                 case CssLexer.COLON:
4434                     return combinator;
4435 
4436                 default:
4437                     // only selector is expected
4438                     return null;
4439 
4440             }
4441 
4442             // get the next token, but don't skip whitespace
4443             currentToken = lexer.nextToken();
4444             if (LOGGER.isLoggable(Level.FINEST)) {
4445                 LOGGER.finest(currentToken.toString());
4446             }
4447         }
4448     }
4449 
4450     private List<Declaration> declarations(CssLexer lexer) {
4451 
4452         List<Declaration> declarations = new ArrayList<Declaration>();
4453 
4454         while (true) {
4455 
4456             Declaration decl = declaration(lexer);
4457             if (decl != null) {
4458                 declarations.add(decl);
4459             } else {
4460                 // some error happened, skip the decl...
4461                 while ((currentToken != null) &&
4462                        (currentToken.getType() != CssLexer.SEMI) &&
4463                        (currentToken.getType() != CssLexer.RBRACE) &&
4464                        (currentToken.getType() != Token.EOF)) {
4465                     currentToken = nextToken(lexer);
4466                 }
4467 
4468                 // current token is either SEMI, RBRACE or EOF.
4469                 if (currentToken != null &&
4470                     currentToken.getType() != CssLexer.SEMI)
4471                     return declarations;
4472             }
4473 
4474             // declaration; declaration; ???
4475             // RT-17830 - allow declaration;;
4476             while ((currentToken != null) &&
4477                     (currentToken.getType() == CssLexer.SEMI)) {
4478                 currentToken = nextToken(lexer);
4479             }
4480 
4481             // if it is delcaration; declaration, then the
4482             // next token should be an IDENT.
4483             if ((currentToken != null) &&
4484                 (currentToken.getType() == CssLexer.IDENT)) {
4485                 continue;
4486             }
4487 
4488             break;
4489         }
4490 
4491         return declarations;
4492     }
4493 
4494     private Declaration declaration(CssLexer lexer) {
4495 
4496         final int ttype =
4497             (currentToken != null) ? currentToken.getType() : Token.INVALID;
4498 
4499         if ((currentToken == null) ||
4500             (currentToken.getType() != CssLexer.IDENT)) {
4501 //
4502 //            RT-16547: this warning was misleading because an empty rule
4503 //            not invalid. Some people put in empty rules just as placeholders.
4504 //
4505 //            if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
4506 //                final int line = currentToken != null ? currentToken.getLine() : -1;
4507 //                final int pos = currentToken != null ? currentToken.getOffset() : -1;
4508 //                final String url =
4509 //                    (stylesheet != null && stylesheet.getUrl() != null) ?
4510 //                        stylesheet.getUrl().toExternalForm() : "?";
4511 //                LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]",
4512 //                    url,line,pos);
4513 //            }
4514             return null;
4515         }
4516 
4517         String property = currentToken.getText();
4518 
4519         currentToken = nextToken(lexer);
4520 
4521         if ((currentToken == null) ||
4522             (currentToken.getType() != CssLexer.COLON)) {
4523                 final int line = currentToken.getLine();
4524                 final int pos = currentToken.getOffset();
4525                 final String msg =
4526                         MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]",
4527                                 line, pos);
4528                 ParseError error = createError(msg);
4529                 if (LOGGER.isLoggable(Level.WARNING)) {
4530                     LOGGER.warning(error.toString());
4531                 }
4532                 reportError(error);
4533             return null;
4534         }
4535 
4536         currentToken = nextToken(lexer);
4537 
4538         Term root = expr(lexer);
4539         ParsedValueImpl value = null;
4540         try {
4541             value = (root != null) ? valueFor(property, root, lexer) : null;
4542         } catch (ParseException re) {
4543                 Token badToken = re.tok;
4544                 final int line = badToken != null ? badToken.getLine() : -1;
4545                 final int pos = badToken != null ? badToken.getOffset() : -1;
4546                 final String msg =
4547                         MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]",
4548                     line,pos,re.getMessage(),property);
4549                 ParseError error = createError(msg);
4550                 if (LOGGER.isLoggable(Level.WARNING)) {
4551                     LOGGER.warning(error.toString());
4552                 }
4553                 reportError(error);
4554             return null;
4555         }
4556 
4557         boolean important = currentToken.getType() == CssLexer.IMPORTANT_SYM;
4558         if (important) currentToken = nextToken(lexer);
4559 
4560         Declaration decl = (value != null)
4561                 ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null;
4562         return decl;
4563     }
4564 
4565     private Term expr(CssLexer lexer) {
4566 
4567         final Term expr = term(lexer);
4568         Term current = expr;
4569 
4570         while(true) {
4571 
4572             // if current is null, then term returned null
4573             final int ttype =
4574                 (current != null && currentToken != null)
4575                     ? currentToken.getType() : Token.INVALID;
4576 
4577             if (ttype == Token.INVALID) {
4578                 skipExpr(lexer);
4579                 return null;
4580             } else if (ttype == CssLexer.SEMI ||
4581                 ttype == CssLexer.IMPORTANT_SYM ||
4582                 ttype == CssLexer.RBRACE ||
4583                 ttype == Token.EOF) {
4584                 return expr;
4585             } else if (ttype == CssLexer.COMMA) {
4586             // comma breaks up sequences of terms.
4587                 // next series of terms chains off the last term in
4588                 // the current series.
4589                 currentToken = nextToken(lexer);
4590                 current = current.nextLayer = term(lexer);
4591             } else {
4592                 current = current.nextInSeries = term(lexer);
4593             }
4594 
4595         }
4596     }
4597 
4598     private void skipExpr(CssLexer lexer) {
4599 
4600         while(true) {
4601 
4602             currentToken = nextToken(lexer);
4603 
4604             final int ttype =
4605                 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4606 
4607             if (ttype == CssLexer.SEMI ||
4608                 ttype == CssLexer.RBRACE ||
4609                 ttype == Token.EOF) {
4610                 return;
4611             }
4612         }
4613     }
4614 
4615     private Term term(CssLexer lexer) {
4616 
4617         final int ttype =
4618             (currentToken != null) ? currentToken.getType() : Token.INVALID;
4619 
4620         switch (ttype) {
4621 
4622             case CssLexer.NUMBER:
4623             case CssLexer.CM:
4624             case CssLexer.EMS:
4625             case CssLexer.EXS:
4626             case CssLexer.IN:
4627             case CssLexer.MM:
4628             case CssLexer.PC:
4629             case CssLexer.PT:
4630             case CssLexer.PX:
4631             case CssLexer.DEG:
4632             case CssLexer.GRAD:
4633             case CssLexer.RAD:
4634             case CssLexer.TURN:
4635             case CssLexer.PERCENTAGE:
4636             case CssLexer.SECONDS:
4637             case CssLexer.MS:
4638                 break;
4639 
4640             case CssLexer.STRING:
4641                 break;
4642             case CssLexer.IDENT:
4643                 break;
4644 
4645             case CssLexer.HASH:
4646                 break;
4647 
4648             case CssLexer.FUNCTION:
4649             case CssLexer.LPAREN:
4650 
4651                 Term function = new Term(currentToken);
4652                 currentToken = nextToken(lexer);
4653 
4654                 Term arg = term(lexer);
4655                 function.firstArg = arg;
4656 
4657                 while(true) {
4658 
4659                     final int operator =
4660                         currentToken != null ? currentToken.getType() : Token.INVALID;
4661 
4662                     if (operator == CssLexer.RPAREN) {
4663                         currentToken = nextToken(lexer);
4664                         return function;
4665                     } else if (operator == CssLexer.COMMA) {
4666                         // comma breaks up sequences of terms.
4667                         // next series of terms chains off the last term in
4668                         // the current series.
4669                         currentToken = nextToken(lexer);
4670                         arg = arg.nextArg = term(lexer);
4671 
4672                     } else {
4673                         arg = arg.nextInSeries = term(lexer);
4674                     }
4675 
4676                 }
4677 
4678             case CssLexer.URL:
4679                 break;
4680 
4681             case CssLexer.SOLIDUS:
4682                 break;
4683 
4684             default:
4685                 final int line = currentToken != null ? currentToken.getLine() : -1;
4686                 final int pos = currentToken != null ? currentToken.getOffset() : -1;
4687                 final String text = currentToken != null ? currentToken.getText() : "";
4688                 final String msg =
4689                     MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]",
4690                     "\'",text,line,pos);
4691                 ParseError error = createError(msg);
4692                 if (LOGGER.isLoggable(Level.WARNING)) {
4693                     LOGGER.warning(error.toString());
4694                 }
4695                 reportError(error);
4696                 return null;
4697 //                currentToken = nextToken(lexer);
4698 //
4699 //                return new Term(Token.INVALID_TOKEN);
4700         }
4701 
4702         Term term = new Term(currentToken);
4703         currentToken = nextToken(lexer);
4704         return term;
4705     }
4706 
4707     public static ObservableList<ParseError> errorsProperty() {
4708         return StyleManager.errorsProperty();
4709     }
4710 
4711 
4712 
4713     /**
4714      * Encapsulate information about the source and nature of errors encountered
4715      * while parsing CSS or applying styles to Nodes.
4716      */
4717     public static class ParseError {
4718 
4719         /** @return The error message from the CSS code. */
4720         public final String getMessage() {
4721             return message;
4722         }
4723 
4724         public ParseError(String message) {
4725             this.message = message;
4726         }
4727 
4728         final String message;
4729 
4730         @Override public String toString() {
4731             return "CSS Error: " + message;
4732         }
4733 
4734         /** Encapsulate errors arising from parsing of stylesheet files */
4735         public final static class StylesheetParsingError extends ParseError {
4736 
4737             StylesheetParsingError(String url, String message) {
4738                 super(message);
4739                 this.url = url;
4740             }
4741 
4742             String getURL() {
4743                 return url;
4744             }
4745 
4746             private final String url;
4747 
4748             @Override public String toString() {
4749                 final String path = url != null ? url : "?";
4750                 // TBD: i18n
4751                 return "CSS Error parsing " + path + ": " + message;
4752             }
4753 
4754         }
4755 
4756         /** Encapsulate errors arising from parsing of Node's style property */
4757         public final static class InlineStyleParsingError extends ParseError {
4758 
4759             InlineStyleParsingError(Styleable styleable, String message) {
4760                 super(message);
4761                 this.styleable = styleable;
4762             }
4763 
4764             Styleable getStyleable() {
4765                 return styleable;
4766             }
4767 
4768             private final Styleable styleable;
4769 
4770             @Override public String toString() {
4771                 final String inlineStyle = styleable.getStyle();
4772                 final String source = styleable.toString();
4773                 // TBD: i18n
4774                 return "CSS Error parsing in-line style \'" + inlineStyle +
4775                         "\' from " + source + ": " + message;
4776             }
4777         }
4778 
4779         /**
4780          * Encapsulate errors arising from parsing when the style is not
4781          * an in-line style nor is the style from a stylesheet. Primarily to
4782          * support unit testing.
4783          */
4784         public final static class StringParsingError extends ParseError {
4785             private final String style;
4786 
4787             StringParsingError(String style, String message) {
4788                 super(message);
4789                 this.style = style;
4790             }
4791 
4792             String getStyle() {
4793                 return style;
4794             }
4795 
4796             @Override public String toString() {
4797                 // TBD: i18n
4798                 return "CSS Error parsing \'" + style + ": " + message;
4799             }
4800         }
4801 
4802         /** Encapsulates errors arising from applying a style to a Node. */
4803         public final static class PropertySetError extends ParseError {
4804             private final CssMetaData styleableProperty;
4805             private final Styleable styleable;
4806 
4807             public PropertySetError(CssMetaData styleableProperty,
4808                     Styleable styleable, String message) {
4809                 super(message);
4810                 this.styleableProperty = styleableProperty;
4811                 this.styleable = styleable;
4812             }
4813 
4814             Styleable getStyleable() {
4815                 return styleable;
4816             }
4817 
4818             CssMetaData getProperty() {
4819                 return styleableProperty;
4820             }
4821 
4822             @Override public String toString() {
4823                 // TBD: i18n
4824                 return "CSS Error parsing \'" + styleableProperty + ": " + message;
4825             }
4826         }
4827     }
4828 }