All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.css.CssParser Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.css;

import com.sun.javafx.css.Combinator;
import com.sun.javafx.css.FontFaceImpl;
import com.sun.javafx.css.InterpolatorConverter;
import com.sun.javafx.css.ParsedValueImpl;
import com.sun.javafx.css.StyleManager;
import com.sun.javafx.css.TransitionDefinition;
import com.sun.javafx.css.TransitionDefinitionConverter;
import com.sun.javafx.util.Utils;
import javafx.animation.Interpolator;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.DurationConverter;
import javafx.css.converter.EffectConverter;
import javafx.css.converter.EnumConverter;
import javafx.css.converter.FontConverter;
import javafx.css.converter.InsetsConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.css.converter.SizeConverter.SequenceConverter;
import javafx.css.converter.StringConverter;
import javafx.css.converter.URLConverter;
import javafx.css.converter.DeriveColorConverter;
import javafx.css.converter.LadderConverter;
import javafx.css.converter.StopConverter;
import com.sun.javafx.css.parser.Token;
import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
import com.sun.javafx.scene.layout.region.BorderImageSlices;
import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
import com.sun.javafx.scene.layout.region.BorderStyleConverter;
import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
import com.sun.javafx.scene.layout.region.Margins;
import com.sun.javafx.scene.layout.region.RepeatStruct;
import com.sun.javafx.scene.layout.region.RepeatStructConverter;
import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.effect.BlurType;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.util.Duration;
import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.logging.PlatformLogger.Level;


import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;

/**
 * A parser for a CSS document string.
 * @since 9
 */
final public class CssParser {

    /**
     * Constructs a {@code CssParser}.
     */
    public CssParser() {
        properties = new HashMap<>();
    }

    // stylesheet as a string from parse method. This will be null if the
    // stylesheet is being parsed from a file; otherwise, the parser is parsing
    // a string and this is that string.
    private String     stylesheetAsText;

    // the url of the stylesheet file. This will
    // be null if the source is not a file.
    private String        sourceOfStylesheet;

    // the Styleable from the node with an in-line style. This will be null
    // unless the source of the styles is a Node's styleProperty. In this case,
    // the stylesheetString will also be set.
    private Styleable sourceOfInlineStyle;

    // source is a file
    private void setInputSource(String url, String str) {
        stylesheetAsText = str;
        sourceOfStylesheet = url;
        sourceOfInlineStyle = null;
    }

    // source as string only
    private void setInputSource(String str) {
        stylesheetAsText = str;
        sourceOfStylesheet = null;
        sourceOfInlineStyle = null;
    }

    // source is in-line style
    private void setInputSource(Styleable styleable) {
        stylesheetAsText = styleable != null ? styleable.getStyle() : null;
        sourceOfStylesheet = null;
        sourceOfInlineStyle = styleable;
    }

    private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();

    private static final class ParseException extends Exception {
        ParseException(String message) {
            this(message,null,null);
        }
        ParseException(String message, Token tok, CssParser parser) {
            super(message);
            this.tok = tok;
            if (parser.sourceOfStylesheet != null) {
                source = parser.sourceOfStylesheet;
            } else if (parser.sourceOfInlineStyle != null) {
                source = parser.sourceOfInlineStyle.toString();
            } else if (parser.stylesheetAsText != null) {
                source = parser.stylesheetAsText;
            } else {
                source = "?";
            }
        }
        @Override public String toString() {
            StringBuilder builder = new StringBuilder(super.getMessage());
            builder.append(source);
            if (tok != null) builder.append(": ").append(tok.toString());
            return builder.toString();
        }
        private final Token tok;
        private final String source;
    }

    /**
     * Creates a {@code Stylesheet} from a CSS document string.
     *
     * @param stylesheetText the CSS document to parse
     * @return the {@code Stylesheet}
     */
    public Stylesheet parse(final String stylesheetText) {
        final Stylesheet stylesheet = new Stylesheet();
        if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
            setInputSource(stylesheetText);
            try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
                parse(stylesheet, reader);
            } catch (IOException ioe) {
                // this method doesn't explicitly throw IOException
            }
        }
        return stylesheet;
    }

    /**
     * Creates a {@code Stylesheet} from a CSS document string using docbase as the base
     * URL for resolving references within {@code Stylesheet}.
     *
     * @param docbase the doc base for resolving URL references
     * @param stylesheetText the CSS document to parse
     * @return the Stylesheet
     * @throws java.io.IOException the exception
     */
    public Stylesheet parse(final String docbase, final String stylesheetText) throws IOException {
        final Stylesheet stylesheet = new Stylesheet(docbase);
        if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
            setInputSource(docbase, stylesheetText);
            try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
                parse(stylesheet, reader);
            }
        }
        return stylesheet;
    }

    /**
     * Updates the given stylesheet by reading a CSS document from a URL,
     * assuming UTF-8 encoding.
     *
     *@param  url URL of the stylesheet to parse
     *@return the stylesheet
     *@throws IOException the exception
     */
    public Stylesheet parse(final URL url) throws IOException {

        final String path = url != null ? url.toExternalForm() : null;
        final Stylesheet stylesheet = new Stylesheet(path);
        if (url != null) {
            setInputSource(path, null);
            try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
                parse(stylesheet, reader);
            }
        }
        return stylesheet;
    }

    /* All of the other function calls should wind up here */
    private void parse(final Stylesheet stylesheet, final Reader reader) {
        CssLexer lex = new CssLexer();
        lex.setReader(reader);

        try {
            this.parse(stylesheet, lex);
        } catch (Exception ex) {
            // Sometimes bad syntax causes an exception. The code should be
            // fixed to handle the bad syntax, but the fallback is
            // to handle the exception here. Uncaught, the exception can cause
            // problems like RT-20311
            reportException(ex);
        }

    }

    /**
     * Parse an in-line style from a {@code Node}.
     * @param node the styleable node
     * @return the style sheet
     */
    public Stylesheet parseInlineStyle(final Styleable node) {

        Stylesheet stylesheet = new Stylesheet();

        final String stylesheetText = (node != null) ? node.getStyle() : null;
        if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
            setInputSource(node);
            final List rules = new ArrayList<>();
            try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
                final CssLexer lexer = new CssLexer();
                lexer.setReader(reader);
                currentToken = nextToken(lexer);
                final List declarations = declarations(lexer);
                if (declarations != null && !declarations.isEmpty()) {
                    final Selector selector = Selector.getUniversalSelector();
                    final Rule rule = new Rule(
                        Collections.singletonList(selector),
                        declarations
                    );
                    rules.add(rule);
                }
            } catch (IOException ioe) {
            } catch (Exception ex) {
                // Sometimes bad syntax causes an exception. The code should be
                // fixed to handle the bad syntax, but the fallback is
                // to handle the exception here. Uncaught, the exception can cause
                // problems like RT-20311
                reportException(ex);
            }
            stylesheet.getRules().addAll(rules);
        }

        // don't retain reference to the styleable
        setInputSource((Styleable) null);

        return stylesheet;
    }

    /**
     * Convenience method for unit tests.
     * @param property the property
     * @param expr the expression
     * @return the parsed value
     */
    ParsedValue parseExpr(String property, String expr) {
        if (property == null || expr == null) return null;

        ParsedValueImpl value = null;
        setInputSource(null, property + ": " + expr);
        char buf[] = new char[expr.length() + 1];
        System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
        buf[buf.length-1] = ';';

        try (Reader reader = new CharArrayReader(buf)) {
            CssLexer lex = new CssLexer();
            lex.setReader(reader);

            currentToken = nextToken(lex);
            CssParser.Term term = this.expr(lex);
            value = valueFor(property, term, lex);
        } catch (IOException ioe) {
        } catch (ParseException e) {
            if (LOGGER.isLoggable(Level.WARNING)) {
                LOGGER.warning("\"" +property + ": " + expr  + "\" " + e.toString());
            }
        } catch (Exception ex) {
            // Sometimes bad syntax causes an exception. The code should be
            // fixed to handle the bad syntax, but the fallback is
            // to handle the exception here. Uncaught, the exception can cause
            // problems like RT-20311
            reportException(ex);
        }
        return value;
    }
    /*
     * Map of property names found while parsing. If a value matches a
     * property name, then the value is a lookup.
     */
    private final Map properties;

    /*
     * While parsing a declaration, tokens from parsing value (that is,
     * the expr rule) are held in this tree structure which is then passed
     * to methods which convert the tree into a ParsedValueImpl.
     *
     * Each term in expr is a Term. For simple terms, like HASH, the
     * Term is just the Token. If the term is a function, then the
     * Term is a linked-list of Term, the first being the function
     * name and each nextArg being the arguments.
     *
     * If there is more than one term in the expr (insets, for example),
     * then the terms are linked on nextInSequence. If there is more than one
     * layer (sequence of terms), then each layer becomes the nextLayer
     * to the last root in the previous sequence.
     *
     * The easiest way to think of it is that a comma starts a nextLayer (except
     * when a function arg).
     *
     * The expr part of the declaration "-fx-padding 1 2, 3 4;" would look
     * like this:
     * [1 | nextLayer | nextInSeries]-->[2 | nextLayer | nextInSeries]-->null
     *            |                            |
     *          null                           |
     *       .---------------------------------'
     *       '-->[3 | nextLayer | nextInSeries]-->[4 | nextLayer | nextInSeries]-->null
     *                    |                              |
     *                   null                           null
     *
     * The first argument in a function needs to be distinct from the
     * remaining args so that the args of a function in the middle of
     * a function will not be omitted. Consider 'f0(a, f1(b, c), d)'
     * If we relied only on nextArg, then the next arg of f0 would be a but
     * the nextArg of f1 would be d. With firstArg, the firstArg of f0 is a,
     * the nextArg of a is f1, the firstArg of f1 is b and the nextArg of f1 is d.
     *
     * TODO: now that the parser is the parser and not an adjunct to an ANTLR
     * parser, this Term stuff shouldn't be needed.
     */
    static class Term {
        final Token token;
        Term nextInSeries;
        Term nextLayer;
        Term firstArg;
        Term nextArg;
        Term(Token token) {
            this.token = token;
            this.nextLayer = null;
            this.nextInSeries = null;
            this.firstArg = null;
            this.nextArg = null;
        }
        Term() {
            this(null);
        }

        @Override public String toString() {
            StringBuilder buf = new StringBuilder();
            if (token != null) buf.append(String.valueOf(token.getText()));
            if (nextInSeries != null) {
                buf.append("");
                buf.append(nextInSeries.toString());
                buf.append("\n");
            }
            if (nextLayer != null) {
                buf.append("");
                buf.append(nextLayer.toString());
                buf.append("\n");
            }
            if (firstArg != null) {
                buf.append("");
                buf.append(firstArg.toString());
                if (nextArg != null) {
                    buf.append(nextArg.toString());
                }
                buf.append("");
            }

            return buf.toString();
        }

    }

    private ParseError createError(String msg) {

        ParseError error = null;
        if (sourceOfStylesheet != null) {
            error = new ParseError.StylesheetParsingError(sourceOfStylesheet, msg);
        } else if (sourceOfInlineStyle != null) {
            error = new ParseError.InlineStyleParsingError(sourceOfInlineStyle, msg);
        } else {
            error = new ParseError.StringParsingError(stylesheetAsText, msg);
        }
        return error;
    }

    private void reportError(ParseError error) {
        List errors = null;
        if ((errors = StyleManager.getErrors()) != null) {
            errors.add(error);
        }
    }

    private void error(final Term root, final String msg) throws ParseException {

        final Token token = root != null ? root.token : null;
        final ParseException pe = new ParseException(msg,token,this);
        reportError(createError(pe.toString()));
        throw pe;
    }

    private void reportException(Exception exception) {

        if (LOGGER.isLoggable(Level.WARNING)) {
            final StackTraceElement[] stea = exception.getStackTrace();
            if (stea.length > 0) {
                final StringBuilder buf =
                    new StringBuilder("Please report ");
                buf.append(exception.getClass().getName())
                   .append(" at:");
                int end = 0;
                while(end < stea.length) {
                    // only report parser part of the stack trace.
                    if (!getClass().getName().equals(stea[end].getClassName())) {
                        break;
                    }
                    buf.append("\n\t")
                    .append(stea[end++].toString());
                }
                LOGGER.warning(buf.toString());
            }
        }
    }

    private String formatDeprecatedMessage(final Term root, final String syntax) {
        final StringBuilder buf =
            new StringBuilder("Using deprecated syntax for ");
        buf.append(syntax);
        if (sourceOfStylesheet != null){
            buf.append(" at ")
               .append(sourceOfStylesheet)
               .append("[")
               .append(root.token.getLine())
               .append(',')
               .append(root.token.getOffset())
               .append("]");
        }
        buf.append(". Refer to the CSS Reference Guide.");
        return buf.toString();
    }

    // Assumes string is not a lookup!
    private ParsedValueImpl colorValueOfString(String str) {

        if(str.startsWith("#") || str.startsWith("0x")) {

            double a = 1.0f;
            String c = str;
            final int prefixLength = (str.startsWith("#")) ? 1 : 2;

            final int len = c.length();
            // rgba or rrggbbaa - trim off the alpha
            if ( (len-prefixLength) == 4) {
                a = Integer.parseInt(c.substring(len-1), 16) / 15.0f;
                c = c.substring(0,len-1);
            } else if ((len-prefixLength) == 8) {
                a = Integer.parseInt(c.substring(len-2), 16) / 255.0f;
                c = c.substring(0,len-2);
            }
            // else color was rgb or rrggbb (no alpha)
            return new ParsedValueImpl<>(Color.web(c,a), null);
        }

        try {
            return new ParsedValueImpl<>(Color.web(str), null);
        } catch (final IllegalArgumentException e) {
        } catch (final NullPointerException e) {
        }

        // not a color
        return null;
    }

    private String stripQuotes(String string) {
        return com.sun.javafx.util.Utils.stripQuotes(string);
    }

    private double clamp(double min, double val, double max) {
        if (val < min) return min;
        if (max < val) return max;
        return val;
    }

    // Return true if the token is a size type or an identifier
    // (which would indicate a lookup).
    private boolean isSize(Token token) {
        final int ttype = token.getType();
        switch (ttype) {
        case CssLexer.NUMBER:
        case CssLexer.PERCENTAGE:
        case CssLexer.EMS:
        case CssLexer.EXS:
        case CssLexer.PX:
        case CssLexer.CM:
        case CssLexer.MM:
        case CssLexer.IN:
        case CssLexer.PT:
        case CssLexer.PC:
        case CssLexer.DEG:
        case CssLexer.GRAD:
        case CssLexer.RAD:
        case CssLexer.TURN:
            return true;
        default:
            return token.getType() == CssLexer.IDENT;
        }
    }

    private Size size(final Token token) throws ParseException {
        SizeUnits units = SizeUnits.PX;
        // Amount to trim off the suffix, if any. Most are 2 chars.
        int trim = 2;
        final String sval = token.getText().trim();
        final int len = sval.length();
        final int ttype = token.getType();
        switch (ttype) {
        case CssLexer.NUMBER:
            units = SizeUnits.PX;
            trim = 0;
            break;
        case CssLexer.PERCENTAGE:
            units = SizeUnits.PERCENT;
            trim = 1;
            break;
        case CssLexer.EMS:
            units = SizeUnits.EM;
            break;
        case CssLexer.EXS:
            units = SizeUnits.EX;
            break;
        case CssLexer.PX:
            units = SizeUnits.PX;
            break;
        case CssLexer.CM:
            units = SizeUnits.CM;
            break;
        case CssLexer.MM:
            units = SizeUnits.MM;
            break;
        case CssLexer.IN:
            units = SizeUnits.IN;
            break;
        case CssLexer.PT:
            units = SizeUnits.PT;
            break;
        case CssLexer.PC:
            units = SizeUnits.PC;
            break;
        case CssLexer.DEG:
            units = SizeUnits.DEG;
            trim = 3;
            break;
        case CssLexer.GRAD:
            units = SizeUnits.GRAD;
            trim = 4;
            break;
        case CssLexer.RAD:
            units = SizeUnits.RAD;
            trim = 3;
            break;
        case CssLexer.TURN:
            units = SizeUnits.TURN;
            trim = 4;
            break;
        case CssLexer.SECONDS:
            units = SizeUnits.S;
            trim = 1;
            break;
        case CssLexer.MS:
            units = SizeUnits.MS;
            break;
        default:
            if (LOGGER.isLoggable(Level.FINEST)) {
                LOGGER.finest("Expected \'\'");
            }
            ParseException re = new ParseException("Expected \'\'",token, this);
            reportError(createError(re.toString()));
            throw re;
        }
        // TODO: Handle NumberFormatException
        return new Size(
            Double.parseDouble(sval.substring(0,len-trim)),
            units
        );
    }

    // Return true if the token is a time type or an identifier
    // (which would indicate a lookup).
    private boolean isTime(Token token) {
        switch (token.getType()) {
            case CssLexer.SECONDS:
            case CssLexer.MS:
                return true;
            default:
                return token.getType() == CssLexer.IDENT;
        }
    }

    private Size time(Token token) throws ParseException {
        return switch (token.getType()) {
            case CssLexer.SECONDS -> {
                String sval = token.getText().trim();
                double v = Double.parseDouble(sval.substring(0, sval.length() - 1).trim());
                yield new Size(v, SizeUnits.S);
            }

            case CssLexer.MS -> {
                String sval = token.getText().trim();
                double v = Double.parseDouble(sval.substring(0, sval.length() - 2).trim());
                yield new Size(v, SizeUnits.MS);
            }

            default -> {
                if (LOGGER.isLoggable(Level.FINEST)) {
                    LOGGER.finest("Expected \'\'");
                }
                ParseException re = new ParseException("Expected \'\'", token, this);
                reportError(createError(re.toString()));
                throw re;
            }
        };
    }

    // Count the number of terms in a series
    private int numberOfTerms(final Term root) {
        if (root == null) return 0;

        int nTerms = 0;
        Term term = root;
        do {
            nTerms += 1;
            term = term.nextInSeries;
        } while (term != null);
        return nTerms;
    }

    // Count the number of series of terms
    private int numberOfLayers(final Term root) {
        if (root == null) return 0;

        int nLayers = 0;
        Term term = root;
        do {
            nLayers += 1;
            while (term.nextInSeries != null) {
                term = term.nextInSeries;
            }
            term = term.nextLayer;
        } while (term != null);
        return nLayers;
    }

    // Count the number of args of terms. root is the function.
    private int numberOfArgs(final Term root) {
        if (root == null) return 0;

        int nArgs = 0;
        Term term = root.firstArg;
        while (term != null) {
            nArgs += 1;
            term = term.nextArg;
        }
        return nArgs;
    }

    // Get the next layer following this term, which may be null
    private Term nextLayer(final Term root) {
        if (root == null) return null;

        Term term = root;
        while (term.nextInSeries != null) {
            term = term.nextInSeries;
        }
        return term.nextLayer;
    }

    //--------------------------------------------------------------------------
    //
    // Parsing routines
    //
    //--------------------------------------------------------------------------

    ParsedValueImpl valueFor(String property, Term root, CssLexer lexer) throws ParseException {
        final String prop = property.toLowerCase(Locale.ROOT);
        properties.put(prop, prop);
        if (root == null || root.token == null) {
            error(root, "Expected value for property \'" + prop + "\'");
        }

        if (root.token.getType() == CssLexer.IDENT) {
            final String txt = root.token.getText();
            if ("inherit".equalsIgnoreCase(txt)) {
                return new ParsedValueImpl("inherit", null);
            } else if ("null".equalsIgnoreCase(txt)
                    || "none".equalsIgnoreCase(txt)) {
                return new ParsedValueImpl("null", null);
            }
        }
        if ("-fx-fill".equals(prop)) {
             ParsedValueImpl pv = parse(root);
            if (pv.getConverter() == StyleConverter.getUrlConverter()) {
                // ImagePatternConverter expects array of ParsedValue where element 0 is the URL
                // Pending RT-33574
                pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance());
            }
            return pv;
        }
        else if ("-fx-background-color".equals(prop)) {
            return parsePaintLayers(root);
        } else if ("-fx-background-image".equals(prop)) {
            return parseURILayers(root);
        } else if ("-fx-background-insets".equals(prop)) {
             return parseInsetsLayers(root);
        } else if ("-fx-opaque-insets".equals(prop)) {
            return parseInsetsLayer(root);
        } else if ("-fx-background-position".equals(prop)) {
             return parseBackgroundPositionLayers(root);
        } else if ("-fx-background-radius".equals(prop)) {
            return parseCornerRadius(root);
        } else if ("-fx-background-repeat".equals(prop)) {
             return parseBackgroundRepeatStyleLayers(root);
        } else if ("-fx-background-size".equals(prop)) {
             return parseBackgroundSizeLayers(root);
        } else if ("-fx-border-color".equals(prop)) {
             return parseBorderPaintLayers(root);
        } else if ("-fx-border-insets".equals(prop)) {
             return parseInsetsLayers(root);
        } else if ("-fx-border-radius".equals(prop)) {
             return parseCornerRadius(root);
        } else if ("-fx-border-style".equals(prop)) {
             return parseBorderStyleLayers(root);
        } else if ("-fx-border-width".equals(prop)) {
             return parseMarginsLayers(root);
        } else if ("-fx-border-image-insets".equals(prop)) {
             return parseInsetsLayers(root);
        } else if ("-fx-border-image-repeat".equals(prop)) {
             return parseBorderImageRepeatStyleLayers(root);
        } else if ("-fx-border-image-slice".equals(prop)) {
             return parseBorderImageSliceLayers(root);
        } else if ("-fx-border-image-source".equals(prop)) {
             return parseURILayers(root);
        } else if ("-fx-border-image-width".equals(prop)) {
             return parseBorderImageWidthLayers(root);
        } else if ("-fx-padding".equals(prop)) {
            ParsedValueImpl[] sides = parseSize1to4(root);
            return new ParsedValueImpl<>(sides, InsetsConverter.getInstance());
        } else if ("-fx-label-padding".equals(prop)) {
            ParsedValueImpl[] sides = parseSize1to4(root);
            return new ParsedValueImpl<>(sides, InsetsConverter.getInstance());
        } else if (prop.endsWith("font-family")) {
            return parseFontFamily(root);
        } else if (prop.endsWith("font-size")) {
            ParsedValueImpl fsize = parseFontSize(root);
            if (fsize == null) error(root, "Expected \'\'");
            return fsize;
        } else if (prop.endsWith("font-style")) {
            ParsedValueImpl fstyle = parseFontStyle(root);
            if (fstyle == null) error(root, "Expected \'\'");
            return fstyle;
        } else if (prop.endsWith("font-weight")) {
            ParsedValueImpl fweight = parseFontWeight(root);
            if (fweight == null) error(root, "Expected \'\'");
            return fweight;
        } else if (prop.endsWith("font")) {
            return parseFont(root);
        } else if ("-fx-stroke-dash-array".equals(prop)) {
            // TODO: Figure out a way that these properties don't need to be
            // special cased.
            Term term = root;
            int nArgs = numberOfTerms(term);
            ParsedValueImpl[] segments = new ParsedValueImpl[nArgs];
            int segment = 0;
            while(term != null) {
                segments[segment++] = parseSize(term);
                term = term.nextInSeries;
            }

            return new ParsedValueImpl<>(segments,SequenceConverter.getInstance());

        } else if ("-fx-stroke-line-join".equals(prop)) {
            // TODO: Figure out a way that these properties don't need to be
            // special cased.
            ParsedValueImpl[] values = parseStrokeLineJoin(root);
            if (values == null) error(root, "Expected \'miter', \'bevel\' or \'round\'");
            return values[0];
        } else if ("-fx-stroke-line-cap".equals(prop)) {
            // TODO: Figure out a way that these properties don't need to be
            // special cased.
            ParsedValueImpl value = parseStrokeLineCap(root);
            if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
            return value;
        } else if ("-fx-stroke-type".equals(prop)) {
            // TODO: Figure out a way that these properties don't need to be
            // special cased.
            ParsedValueImpl value = parseStrokeType(root);
            if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
            return value;
        } else if ("-fx-font-smoothing-type".equals(prop) || "-fx-blend-mode".equals(prop)) {
            // TODO: Figure out a way that these properties don't need to be special cased.
            String str = null;
            int ttype = -1;
            final Token token = root.token;

            if (root.token == null
                    || ((ttype = root.token.getType()) != CssLexer.STRING
                        && ttype != CssLexer.IDENT)
                    || (str = root.token.getText()) == null
                    || str.isEmpty()) {
                error(root,  "Expected STRING or IDENT");
            }
            return new ParsedValueImpl(stripQuotes(str), null, false);
        } else if ("transition".equals(prop)) {
            return parseTransitionLayers(root);
        } else if ("transition-duration".equals(prop)) {
            return parseDurationLayers(root, false);
        } else if ("transition-delay".equals(prop)) {
            return parseDurationLayers(root, true);
        } else if ("transition-timing-function".equals(prop)) {
            return parseEasingFunctionLayers(root);
        } else if ("transition-property".equals(prop)) {
            return parseTransitionPropertyLayers(root);
        }
        return parse(root);
    }

    private ParsedValueImpl parse(Term root) throws ParseException {

        if (root.token == null) error(root, "Parse error");
        final Token token = root.token;
        ParsedValueImpl value = null; // value to return;

        final int ttype = token.getType();
        switch (ttype) {
        case CssLexer.NUMBER:
        case CssLexer.PERCENTAGE:
        case CssLexer.EMS:
        case CssLexer.EXS:
        case CssLexer.PX:
        case CssLexer.CM:
        case CssLexer.MM:
        case CssLexer.IN:
        case CssLexer.PT:
        case CssLexer.PC:
        case CssLexer.DEG:
        case CssLexer.GRAD:
        case CssLexer.RAD:
        case CssLexer.TURN:
            if (root.nextInSeries == null) {
                ParsedValueImpl sizeValue = new ParsedValueImpl(size(token), null);
                value = new ParsedValueImpl, Number>(sizeValue, SizeConverter.getInstance());
            } else {
                ParsedValueImpl[] sizeValue = parseSizeSeries(root);
                value = new ParsedValueImpl<>(sizeValue, SizeConverter.SequenceConverter.getInstance());
            }
            break;
        case CssLexer.SECONDS:
        case CssLexer.MS: {
            ParsedValue sizeValue = new ParsedValueImpl<>(size(token), null);
            value = new ParsedValueImpl<>(sizeValue, DurationConverter.getInstance());
            break;
        }
        case CssLexer.STRING:
        case CssLexer.IDENT:
            boolean isIdent = ttype == CssLexer.IDENT;
            final String str = stripQuotes(token.getText());
            final String text = str.toLowerCase(Locale.ROOT);
            if ("ladder".equals(text)) {
                value = ladder(root);
            } else if ("linear".equals(text) && (root.nextInSeries) != null) {
                // if nextInSeries is null, then assume this is _not_ an old-style linear gradient
                value = linearGradient(root);
            } else if ("radial".equals(text) && (root.nextInSeries) != null) {
                // if nextInSeries is null, then assume this is _not_ an old-style radial gradient
                value = radialGradient(root);
            } else if ("infinity".equals(text)) {
                Size size = new Size(Double.MAX_VALUE, SizeUnits.PX);
                ParsedValueImpl sizeValue = new ParsedValueImpl(size, null);
                value = new ParsedValueImpl,Number>(sizeValue, SizeConverter.getInstance());
            } else if ("indefinite".equals(text)) {
                Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX);
                ParsedValueImpl sizeValue = new ParsedValueImpl<>(size, null);
                value = new ParsedValueImpl<>(sizeValue, DurationConverter.getInstance());
            } else if ("true".equals(text)) {
                // TODO: handling of boolean is really bogus
                value = new ParsedValueImpl<>("true",BooleanConverter.getInstance());
            } else if ("false".equals(text)) {
                // TODO: handling of boolean is really bogus
                value = new ParsedValueImpl<>("false",BooleanConverter.getInstance());
            } else {
                // if the property value is another property, then it needs to be looked up.
                boolean needsLookup = isIdent && properties.containsKey(text);
                if (needsLookup || ((value = colorValueOfString(str)) == null )) {
                    // If the value is a lookup, make sure to use the lower-case text so it matches the property
                    // in the Declaration. If the value is not a lookup, then use str since the value might
                    // be a string which could have some case sensitive meaning
                    //
                    // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201
                    value = new ParsedValueImpl(needsLookup ? text : str, null, isIdent || needsLookup);
                }
            }
            break;
        case CssLexer.HASH:
            final String clr = token.getText();
            try {
                value = new ParsedValueImpl(Color.web(clr), null);
            } catch (final IllegalArgumentException e) {
                error(root, e.getMessage());
            }
            break;
        case CssLexer.FUNCTION:
            return  parseFunction(root);
        case CssLexer.URL:
            return parseURI(root);
        default:
            final String msg = "Unknown token type: \'" + ttype + "\'";
            error(root, msg);
        }
        return value;

    }

    /* Parse size.
     * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
     */
    private ParsedValueImpl parseSize(final Term root) throws ParseException {

        if (root.token == null || !isSize(root.token)) error(root, "Expected \'\'");

        ParsedValueImpl value = null;

        if (root.token.getType() != CssLexer.IDENT) {

            Size size = size(root.token);
            value = new ParsedValueImpl<>(size, null);

        } else {

            String key = root.token.getText();
            value = new ParsedValueImpl<>(key, null, true);

        }

        return value;
    }

    private ParsedValueImpl parseTime(final Term root) throws ParseException {
        if (root.token == null || !isTime(root.token)) {
            error(root, "Expected \'\'");
        }

        if (root.token.getType() != CssLexer.IDENT) {
            Size time = time(root.token);
            return new ParsedValueImpl<>(time, null);
        }

        String key = root.token.getText();
        return switch (key) {
            case "initial", "inherit" -> new ParsedValueImpl<>(new Size(0, SizeUnits.S), null);
            case "indefinite" -> new ParsedValueImpl<>(new Size(Double.POSITIVE_INFINITY, SizeUnits.S), null);
            default -> new ParsedValueImpl<>(key, null, true);
        };
    }

    private ParsedValueImpl, Duration> parseDuration(
            Term term, boolean allowNegative) throws ParseException {
        ParsedValue time = parseTime(term);
        if (!allowNegative && time.getValue() instanceof Size size && size.getValue() < 0) {
            error(term, "Invalid \'\'");
        }

        return new ParsedValueImpl<>(time, DurationConverter.getInstance());
    }

    private ParsedValueImpl, Duration>[], Duration[]>
            parseDurationLayers(Term term, boolean allowNegative) throws ParseException {
        int nLayers = numberOfLayers(term);
        ParsedValue, Duration>[] layers = new ParsedValueImpl[nLayers];

        for (int i = 0; i < nLayers; ++i) {
            layers[i] = parseDuration(term, allowNegative);
            term = nextLayer(term);
        }

        return new ParsedValueImpl, Duration>[], Duration[]>(
                layers, DurationConverter.SequenceConverter.getInstance());
    }

    private ParsedValueImpl parseColor(final Term root) throws ParseException {

        ParsedValueImpl color = null;
        if (root.token != null &&
            (root.token.getType() == CssLexer.IDENT ||
             root.token.getType() == CssLexer.HASH ||
             root.token.getType() == CssLexer.FUNCTION)) {

            color = parse(root);

        } else {
            error(root,  "Expected \'\'");
        }
        return color;
    }

    // rgb(NUMBER, NUMBER, NUMBER)
    // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
    // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
    // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
    private ParsedValueImpl rgb(Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
            final String msg = "Expected \'rgb\' or \'rgba\'";
            error(root, msg);
        }

        Term arg = root;
        Token rtok, gtok, btok, atok;

        if ((arg = arg.firstArg) == null) error(root, "Expected \'\' or \'\'");
        if ((rtok = arg.token) == null ||
            (rtok.getType() != CssLexer.NUMBER &&
             rtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'\' or \'\'");

        root = arg;

        if ((arg = arg.nextArg) == null) error(root, "Expected \'\' or \'\'");
        if ((gtok = arg.token) == null ||
            (gtok.getType() != CssLexer.NUMBER &&
             gtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'\' or \'\'");

        root = arg;

        if ((arg = arg.nextArg) == null) error(root, "Expected \'\' or \'\'");
        if ((btok = arg.token) == null ||
            (btok.getType() != CssLexer.NUMBER &&
             btok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'\' or \'\'");

        root = arg;

        if ((arg = arg.nextArg) != null) {
            if ((atok = arg.token) == null ||
                 atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'\'");
        } else {
            atok = null;
        }

        int argType = rtok.getType();
        if (argType != gtok.getType() || argType != btok.getType() ||
            (argType != CssLexer.NUMBER && argType != CssLexer.PERCENTAGE)) {
            error(root, "Argument type mistmatch");
        }

        final String rtext = rtok.getText();
        final String gtext = gtok.getText();
        final String btext = btok.getText();

        double rval = 0;
        double gval = 0;
        double bval = 0;
        if (argType == CssLexer.NUMBER) {
            rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
            gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
            bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
        } else {
            rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
            gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
            bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
        }

        final String atext = (atok != null) ? atok.getText() : null;
        final double aval =  (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;

        return new ParsedValueImpl(Color.color(rval,gval,bval,aval), null);

    }

    // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
    // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
    private ParsedValueImpl hsb(Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
            final String msg = "Expected \'hsb\' or \'hsba\'";
            error(root, msg);
        }

        Term arg = root;
        Token htok, stok, btok, atok;

        if ((arg = arg.firstArg) == null) error(root, "Expected \'\'");
        if ((htok = arg.token) == null || htok.getType() != CssLexer.NUMBER) error(arg, "Expected \'\'");

        root = arg;

        if ((arg = arg.nextArg) == null) error(root, "Expected \'\'");
        if ((stok = arg.token) == null || stok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'\'");

        root = arg;

        if ((arg = arg.nextArg) == null) error(root, "Expected \'\'");
        if ((btok = arg.token) == null || btok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'\'");

        root = arg;

        if ((arg = arg.nextArg) != null) {
            if ((atok = arg.token) == null || atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'\'");
        } else {
            atok = null;
        }

        final Size hval = size(htok);
        final Size sval = size(stok);
        final Size bval = size(btok);

        final double hue = hval.pixels(); // no clamp - hue can be negative
        final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
        final double brightness = clamp(0.0f, bval.pixels(), 1.0f);

        final Size aval = (atok != null) ? size(atok) : null;
        final double opacity =  (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;

        return new ParsedValueImpl(Color.hsb(hue, saturation, brightness, opacity), null);
    }

    // derive(color, pct)
    private ParsedValueImpl derive(final Term root)
            throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"derive".regionMatches(true, 0, fn, 0, 6)) {
            final String msg = "Expected \'derive\'";
            error(root, msg);
        }

        Term arg = root;
        if ((arg = arg.firstArg) == null) error(root, "Expected \'\'");

        final ParsedValueImpl color = parseColor(arg);

        final Term prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \' brightness = parseSize(arg);

        ParsedValueImpl[] values = new ParsedValueImpl[] { color, brightness };
        return new ParsedValueImpl<>(values, DeriveColorConverter.getInstance());
    }

    // 'ladder' color 'stops' stop+
    private ParsedValueImpl ladder(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
            final String msg = "Expected \'ladder\'";
            error(root, msg);
        }

        if (LOGGER.isLoggable(Level.WARNING)) {
            LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
        }

        Term term = root;

        if ((term = term.nextInSeries) == null) error(root, "Expected \'\'");
        final ParsedValueImpl color = parse(term);

        Term prev = term;

        if ((term = term.nextInSeries) == null) error(prev,  "Expected \'stops\'");
        if (term.token == null ||
            term.token.getType() != CssLexer.IDENT ||
            !"stops".equalsIgnoreCase(term.token.getText())) error(term,  "Expected \'stops\'");

        prev = term;

        if ((term = term.nextInSeries) == null) error(prev, "Expected \'(, )\'");

        int nStops = 0;
        Term temp = term;
        do {
            nStops += 1;
            // if next token type is IDENT, then we have CycleMethod
        } while (((temp = temp.nextInSeries) != null) &&
                 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));

        ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
        values[0] = color;
        int stopIndex = 1;
        do {
            ParsedValueImpl stop = stop(term);
            if (stop != null) values[stopIndex++] = stop;
            prev = term;
        } while(((term = term.nextInSeries) != null) &&
                 (term.token.getType() == CssLexer.LPAREN));

        // if term is not null and the last term was not an lparen,
        // then term starts a new series of Paint. Point
        // root.nextInSeries to term so the next loop skips over the
        // already parsed ladder bits.
        if (term != null) {
            root.nextInSeries = term;
        }

        // if term is null, then we are at the end of a series.
        // root points to 'ladder', now we want the next term after root
        // to be the term after the last stop, which may be another layer
        else {
            root.nextInSeries = null;
            root.nextLayer = prev.nextLayer;
        }

        return new ParsedValueImpl<>(values, LadderConverter.getInstance());
    }

    //  = ladder(, [, ]+ )
    private ParsedValueImpl parseLadder(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
            final String msg = "Expected \'ladder\'";
            error(root, msg);
        }

        Term term = root;

        if ((term = term.firstArg) == null) error(root, "Expected \'\'");
        final ParsedValueImpl color = parse(term);

        Term prev = term;

        if ((term = term.nextArg) == null)
            error(prev,  "Expected \'[, ]+\'");

        ParsedValueImpl[] stops = parseColorStops(term);

        ParsedValueImpl[] values = new ParsedValueImpl[stops.length+1];
        values[0] = color;
        System.arraycopy(stops, 0, values, 1, stops.length);
        return new ParsedValueImpl<>(values, LadderConverter.getInstance());
    }

    // parse (, )+
    // root.token should be a size
    // root.token.next should be a color
    private ParsedValueImpl stop(final Term root)
            throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"(".equals(fn)) {
            final String msg = "Expected \'(\'";
            error(root, msg);
        }

        Term arg = null;

        if ((arg = root.firstArg) == null) error(root,  "Expected \'\'");

        ParsedValueImpl size = parseSize(arg);

        Term prev = arg;
        if ((arg = arg.nextArg) == null) error(prev,  "Expected \'\'");

        ParsedValueImpl color = parseColor(arg);

        ParsedValueImpl[] values = new ParsedValueImpl[] { size, color };
        return new ParsedValueImpl<>(values, StopConverter.getInstance());

    }

    // http://dev.w3.org/csswg/css3-images/#color-stop-syntax
    //  =  [  |  ]?
    private ParsedValueImpl[] parseColorStops(final Term root)
            throws ParseException {

        int nArgs = 1;
        Term temp = root;
        while(temp != null) {
            if (temp.nextArg != null) {
                nArgs += 1;
                temp = temp.nextArg;
            } else if (temp.nextInSeries != null) {
                temp = temp.nextInSeries;
            } else {
                break;
            }
        }

        if (nArgs < 2) {
            error(root, "Expected \'\'");
        }

        ParsedValueImpl[] colors = new ParsedValueImpl[nArgs];
        Size[] positions = new Size[nArgs];
        java.util.Arrays.fill(positions, null);

        Term stop = root;
        Term prev = root;
        SizeUnits units = null;
        for (int n = 0; n\' and \'\'");
                        }
                    }
                } else {
                    error(prev, "Expected \'\' or \'\'");
                }
                prev = term;
                stop = term.nextArg;
            } else {
                prev = stop;
                stop = stop.nextArg;
            }

        }

        //
        // normalize positions according to
        // http://dev.w3.org/csswg/css3-images/#color-stop-syntax
        //
        // If the first color-stop does not have a position, set its
        // position to 0%. If the last color-stop does not have a position,
        // set its position to 100%.
        if (positions[0] == null) positions[0] = new Size(0, SizeUnits.PERCENT);
        if (positions[nArgs-1] == null) positions[nArgs-1] = new Size(100, SizeUnits.PERCENT);

        // If a color-stop has a position that is less than the specified
        // position of any color-stop before it in the list, set its
        // position to be equal to the largest specified position of any
        // color-stop before it.
        Size max = null;
        for (int n = 1 ; n -1) {

                    int nWithout = n - withoutIndex;
                    double precedingValue = preceding.getValue();
                    double delta =
                        (pos.getValue() - precedingValue) / (nWithout + 1);

                    while(withoutIndex < n) {
                        precedingValue += delta;
                        positions[withoutIndex++] =
                            new Size(precedingValue, pos.getUnits());
                    }
                    withoutIndex = -1;
                    preceding = pos;
                } else {
                    preceding = pos;
                }
            }
        }

        ParsedValueImpl[] stops = new ParsedValueImpl[nArgs];
        for (int n=0; n(
                new ParsedValueImpl[] {
                    new ParsedValueImpl(positions[n], null),
                    colors[n]
                },
                StopConverter.getInstance()
            );
        }

        return stops;

    }

    // parse (, )
    private ParsedValueImpl[] point(final Term root) throws ParseException {

        if (root.token == null ||
            root.token.getType() != CssLexer.LPAREN) error(root, "Expected \'(, )\'");

        final String fn = root.token.getText();
        if (fn == null || !"(".equalsIgnoreCase(fn)) {
            final String msg = "Expected \'(\'";
            error(root, msg);
        }

        Term arg = null;

        // no 
        if ((arg = root.firstArg) == null)  error(root, "Expected \'\'");

        final ParsedValueImpl ptX = parseSize(arg);

        final Term prev = arg;

        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        final ParsedValueImpl ptY = parseSize(arg);

        return new ParsedValueImpl[] { ptX, ptY };
    }

    private ParsedValueImpl parseFunction(final Term root) throws ParseException {

        // Text from parser is function name plus the lparen, e.g., 'derive('
        final String fcn = (root.token != null) ? root.token.getText() : null;
        if (fcn == null) {
            error(root, "Expected function name");
        } else if ("rgb".regionMatches(true, 0, fcn, 0, 3)) {
            return rgb(root);
        } else if ("hsb".regionMatches(true, 0, fcn, 0, 3)) {
            return hsb(root);
        } else if ("derive".regionMatches(true, 0, fcn, 0, 6)) {
            return derive(root);
        } else if ("innershadow".regionMatches(true, 0, fcn, 0, 11)) {
            return innershadow(root);
        } else if ("dropshadow".regionMatches(true, 0, fcn, 0, 10)) {
            return dropshadow(root);
        } else if ("linear-gradient".regionMatches(true, 0, fcn, 0, 15)) {
            return parseLinearGradient(root);
        } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
            return parseRadialGradient(root);
        } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
            return parseImagePattern(root);
        } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
            return parseRepeatingImagePattern(root);
        } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
            return parseLadder(root);
        } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
            return parseRegion(root);
        } else {
            error(root, "Unexpected function \'" + fcn + "\'");
        }
        return null;
    }

    private ParsedValueImpl blurType(final Term root) throws ParseException {

        if (root == null) return null;
        if (root.token == null ||
            root.token.getType() != CssLexer.IDENT ||
            root.token.getText() == null ||
            root.token.getText().isEmpty()) {
            final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
            error(root, msg);
        }
        final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
        BlurType blurType = BlurType.THREE_PASS_BOX;
        if ("gaussian".equals(blurStr)) {
            blurType = BlurType.GAUSSIAN;
        } else if ("one-pass-box".equals(blurStr)) {
            blurType = BlurType.ONE_PASS_BOX;
        } else if ("two-pass-box".equals(blurStr)) {
            blurType = BlurType.TWO_PASS_BOX;
        } else if ("three-pass-box".equals(blurStr)) {
            blurType = BlurType.THREE_PASS_BOX;
        } else {
            final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
            error(root, msg);
        }
        return new ParsedValueImpl<>(blurType.name(), new EnumConverter<>(BlurType.class));
    }

    // innershadow      
    private ParsedValueImpl innershadow(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (!"innershadow".regionMatches(true, 0, fn, 0, 11)) {
            final String msg = "Expected \'innershadow\'";
            error(root, msg);
        }

        Term arg;

        if ((arg = root.firstArg) == null) error(root, "Expected \'\'");
        ParsedValueImpl blurVal = blurType(arg);

        Term prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl colorVal = parseColor(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl radiusVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl chokeVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl offsetXVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl offsetYVal = parseSize(arg);

        ParsedValueImpl[] values = new ParsedValueImpl[] {
            blurVal,
            colorVal,
            radiusVal,
            chokeVal,
            offsetXVal,
            offsetYVal
        };
        return new ParsedValueImpl<>(values, EffectConverter.InnerShadowConverter.getInstance());
    }

    // dropshadow      
    private ParsedValueImpl dropshadow(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (!"dropshadow".regionMatches(true, 0, fn, 0, 10)) {
            final String msg = "Expected \'dropshadow\'";
            error(root, msg);
        }

        Term arg;

        if ((arg = root.firstArg) == null) error(root, "Expected \'\'");
        ParsedValueImpl blurVal = blurType(arg);

        Term prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl colorVal = parseColor(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl radiusVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl spreadVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl offsetXVal = parseSize(arg);

        prev = arg;
        if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");

        ParsedValueImpl offsetYVal = parseSize(arg);

        ParsedValueImpl[] values = new ParsedValueImpl[] {
            blurVal,
            colorVal,
            radiusVal,
            spreadVal,
            offsetXVal,
            offsetYVal
        };
        return new ParsedValueImpl<>(values, EffectConverter.DropShadowConverter.getInstance());
    }

    // returns null if the Term is null or is not a cycle method.
    private ParsedValueImpl cycleMethod(final Term root) {
        CycleMethod cycleMethod = null;
        if (root != null && root.token.getType() == CssLexer.IDENT) {

            final String text = root.token.getText().toLowerCase(Locale.ROOT);
            if ("repeat".equals(text)) {
                cycleMethod = CycleMethod.REPEAT;
            } else if ("reflect".equals(text)) {
                cycleMethod = CycleMethod.REFLECT;
            } else if ("no-cycle".equals(text)) {
                cycleMethod = CycleMethod.NO_CYCLE;
            }
        }
        if (cycleMethod != null)
            return new ParsedValueImpl<>(cycleMethod.name(), new EnumConverter<>(CycleMethod.class));
        else
            return null;
    }

    // linear  TO  STOPS + cycleMethod?
    private ParsedValueImpl linearGradient(final Term root) throws ParseException {

        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"linear".equalsIgnoreCase(fn)) {
            final String msg = "Expected \'linear\'";
            error(root, msg);
        }

        if (LOGGER.isLoggable(Level.WARNING)) {
            LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
        }

        Term term = root;

        if ((term = term.nextInSeries) == null) error(root, "Expected \'(, )\'");

        final ParsedValueImpl[] startPt = point(term);

        Term prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
        if (term.token == null ||
            term.token.getType() != CssLexer.IDENT ||
            !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");

        prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'(, )\'");

        final ParsedValueImpl[] endPt = point(term);

        prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
        if (term.token == null ||
            term.token.getType() != CssLexer.IDENT ||
            !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");

        prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'(, )\'");

        int nStops = 0;
        Term temp = term;
        do {
            nStops += 1;
            // if next token type is IDENT, then we have CycleMethod
        } while (((temp = temp.nextInSeries) != null) &&
                 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));

        ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
        int stopIndex = 0;
        do {
            ParsedValueImpl stop = stop(term);
            if (stop != null) stops[stopIndex++] = stop;
            prev = term;
        } while(((term = term.nextInSeries) != null) &&
                (term.token.getType() == CssLexer.LPAREN));

        // term is either null or is a cycle method, or the start of another Paint.
        ParsedValueImpl cycleMethod = cycleMethod(term);

        if (cycleMethod == null) {

            cycleMethod = new ParsedValueImpl<>(CycleMethod.NO_CYCLE.name(), new EnumConverter<>(CycleMethod.class));

            // if term is not null and the last term was not a cycle method,
            // then term starts a new series or layer of Paint
            if (term != null) {
                root.nextInSeries = term;
            }

            // if term is null, then we are at the end of a series.
            // root points to 'linear', now we want the next term after root
            // to be the term after the last stop, which may be another layer
            else {
                root.nextInSeries = null;
                root.nextLayer = prev.nextLayer;
            }


        } else {
            // last term was a CycleMethod, so term is not null.
            // root points at 'linear', now we want the next term after root
            // to be the term after cyclemethod, which may be another series
            // of paint or another layer.
            //
            root.nextInSeries = term.nextInSeries;
            root.nextLayer = term.nextLayer;
        }

        ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length];
        int index = 0;
        values[index++] = (startPt != null) ? startPt[0] : null;
        values[index++] = (startPt != null) ? startPt[1] : null;
        values[index++] = (endPt != null) ? endPt[0] : null;
        values[index++] = (endPt != null) ? endPt[1] : null;
        values[index++] = cycleMethod;
        for (int n=0; n(values, PaintConverter.LinearGradientConverter.getInstance());
    }

    // Based off http://dev.w3.org/csswg/css3-images/#linear-gradients
    //
    //  = linear-gradient(
    //        [ [from  to ] | [ to  ] ] ,]? [ [ repeat | reflect ] ,]?
    //        [, ]+
    // )
    //
    //
    //  =   |  
    //  = [left | right] || [top | bottom]
    //
    // If neither repeat nor reflect are given, then the CycleMethod defaults "NO_CYCLE".
    // If neither [from  to ] nor [ to  ] are given,
    // then the gradient direction defaults to 'to bottom'.
    // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax.
    private ParsedValueImpl parseLinearGradient(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (!"linear-gradient".regionMatches(true, 0, fn, 0, 15)) {
            final String msg = "Expected \'linear-gradient\'";
            error(root, msg);
        }

        Term arg;

        if ((arg = root.firstArg) == null ||
             arg.token == null ||
             arg.token.getText().isEmpty()) {
            error(root,
                "Expected \'from  to \' or \'to \' " +
                "or \'\' or \'\'");
        }

        Term prev = arg;
//        ParsedValueImpl angleVal = null;
        ParsedValueImpl[] startPt = null;
        ParsedValueImpl[] endPt = null;

        if ("from".equalsIgnoreCase(arg.token.getText())) {

            prev = arg;
            if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");

            ParsedValueImpl ptX = parseSize(arg);

            prev = arg;
            if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");

            ParsedValueImpl ptY = parseSize(arg);

            startPt = new ParsedValueImpl[] { ptX, ptY };

            prev = arg;
            if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
            if (arg.token == null ||
                arg.token.getType() != CssLexer.IDENT ||
                !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");

            prev = arg;
            if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");

            ptX = parseSize(arg);

            prev = arg;
            if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");

            ptY = parseSize(arg);

            endPt = new ParsedValueImpl[] { ptX, ptY };

            prev = arg;
            arg = arg.nextArg;

        } else if("to".equalsIgnoreCase(arg.token.getText())) {

            prev = arg;
            if ((arg = arg.nextInSeries) == null ||
                arg.token == null ||
                arg.token.getType() != CssLexer.IDENT ||
                arg.token.getText().isEmpty()) {
                error (prev, "Expected \'\'");
            }


            int startX = 0;
            int startY = 0;
            int endX = 0;
            int endY = 0;

            String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
            // The keywords denote the direction.
            if ("top".equals(sideOrCorner1)) {
                // going toward the top, then start at the bottom
                startY = 100;
                endY = 0;

            } else if ("bottom".equals(sideOrCorner1)) {
                // going toward the bottom, then start at the top
                startY = 0;
                endY = 100;

            } else if ("right".equals(sideOrCorner1)) {
                // going toward the right, then start at the left
                startX = 0;
                endX = 100;

            } else if ("left".equals(sideOrCorner1)) {
                // going toward the left, then start at the right
                startX = 100;
                endX = 0;

            } else {
                error(arg, "Invalid \'\'");
            }

            prev = arg;
            if (arg.nextInSeries != null) {
                arg = arg.nextInSeries;
                if (arg.token != null &&
                    arg.token.getType() == CssLexer.IDENT &&
                    !arg.token.getText().isEmpty()) {

                    String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);

                    // if right or left has already been given,
                    // then either startX or endX will not be zero.
                    if ("right".equals(sideOrCorner2) &&
                        startX == 0 && endX == 0) {
                        // start left, end right
                        startX = 0;
                        endX = 100;
                    } else if ("left".equals(sideOrCorner2) &&
                        startX == 0 && endX == 0) {
                        // start right, end left
                        startX = 100;
                        endX = 0;

                    // if top or bottom has already been given,
                    // then either startY or endY will not be zero.
                    } else if("top".equals(sideOrCorner2) &&
                        startY == 0 && endY == 0) {
                        // start bottom, end top
                        startY = 100;
                        endY = 0;
                    } else if ("bottom".equals(sideOrCorner2) &&
                        startY == 0 && endY == 0) {
                        // start top, end bottom
                        startY = 0;
                        endY = 100;

                    } else {
                        error(arg, "Invalid \'\'");
                    }

                } else {
                    error (prev, "Expected \'\'");
                }
            }


            startPt = new ParsedValueImpl[] {
                new ParsedValueImpl(new Size(startX, SizeUnits.PERCENT), null),
                new ParsedValueImpl(new Size(startY, SizeUnits.PERCENT), null)
            };

            endPt = new ParsedValueImpl[] {
                new ParsedValueImpl(new Size(endX, SizeUnits.PERCENT), null),
                new ParsedValueImpl(new Size(endY, SizeUnits.PERCENT), null)
            };

            prev = arg;
            arg = arg.nextArg;
        }

        if (startPt == null && endPt == null) {
            // spec says defaults to bottom
            startPt = new ParsedValueImpl[] {
                new ParsedValueImpl(new Size(0, SizeUnits.PERCENT), null),
                new ParsedValueImpl(new Size(0, SizeUnits.PERCENT), null)
            };

            endPt = new ParsedValueImpl[] {
                new ParsedValueImpl(new Size(0, SizeUnits.PERCENT), null),
                new ParsedValueImpl(new Size(100, SizeUnits.PERCENT), null)
            };
        }

        if (arg == null ||
            arg.token == null ||
            arg.token.getText().isEmpty()) {
            error(prev, "Expected \'\' or \'\'");
        }

        CycleMethod cycleMethod = CycleMethod.NO_CYCLE;
        if ("reflect".equalsIgnoreCase(arg.token.getText())) {
            cycleMethod = CycleMethod.REFLECT;
            prev = arg;
            arg = arg.nextArg;
        } else if ("repeat".equalsIgnoreCase(arg.token.getText())) {
            cycleMethod = CycleMethod.REPEAT;
            prev = arg;
            arg = arg.nextArg;
        }

        if (arg == null  ||
            arg.token == null ||
            arg.token.getText().isEmpty()) {
            error(prev, "Expected \'\'");
        }

        ParsedValueImpl[] stops = parseColorStops(arg);

        ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length];
        int index = 0;
        values[index++] = (startPt != null) ? startPt[0] : null;
        values[index++] = (startPt != null) ? startPt[1] : null;
        values[index++] = (endPt != null) ? endPt[0] : null;
        values[index++] = (endPt != null) ? endPt[1] : null;
        values[index++] = new ParsedValueImpl<>(cycleMethod.name(), new EnumConverter<>(CycleMethod.class));
        for (int n=0; n(values, PaintConverter.LinearGradientConverter.getInstance());

    }

    // radial [focus-angle ]? [focus-distance ]?
    // [center (,)]? 
    // stops [ (  ,  ) ]+ [ repeat | reflect ]?
    private ParsedValueImpl radialGradient(final Term root) throws ParseException {

        final String fn = (root.token != null) ? root.token.getText() : null;
        if (fn == null || !"radial".equalsIgnoreCase(fn)) {
            final String msg = "Expected \'radial\'";
            error(root, msg);
        }

        if (LOGGER.isLoggable(Level.WARNING)) {
            LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
        }

        Term term = root;
        Term prev = root;

        if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle \', \'focus-distance \', \'center (,)\' or \'\'");
        if (term.token == null) error(term, "Expected \'focus-angle \', \'focus-distance \', \'center (,)\' or \'\'");


        ParsedValueImpl focusAngle = null;
        if (term.token.getType() == CssLexer.IDENT) {
            final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
            if ("focus-angle".equals(keyword)) {

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected \'\'");
                if (term.token == null) error(prev, "Expected \'\'");

                focusAngle = parseSize(term);

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance \', \'center (,)\' or \'\'");
                if (term.token == null) error(term,  "Expected \'focus-distance \', \'center (,)\' or \'\'");
            }
        }

        ParsedValueImpl focusDistance = null;
        if (term.token.getType() == CssLexer.IDENT) {
            final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
            if ("focus-distance".equals(keyword)) {

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected \'\'");
                if (term.token == null) error(prev, "Expected \'\'");

                focusDistance = parseSize(term);

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected  \'center (,)\' or \'\'");
                if (term.token == null) error(term,  "Expected  \'center (,)\' or \'\'");
            }
        }

        ParsedValueImpl[] centerPoint = null;
        if (term.token.getType() == CssLexer.IDENT) {
            final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
            if ("center".equals(keyword)) {

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected \'(,)\'");
                if (term.token == null ||
                    term.token.getType() != CssLexer.LPAREN) error(term, "Expected \'(,)\'");

                centerPoint = point(term);

                prev = term;
                if ((term = term.nextInSeries) == null) error(prev, "Expected \'\'");
                if (term.token == null) error(term,  "Expected \'\'");
            }
        }

        ParsedValueImpl radius = parseSize(term);

        prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
        if (term.token == null ||
            term.token.getType() != CssLexer.IDENT) error(term, "Expected \'stops\' keyword");

        if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");

        prev = term;
        if ((term = term.nextInSeries) == null) error(prev, "Expected \'(, )\'");

        int nStops = 0;
        Term temp = term;
        do {
            nStops += 1;
            // if next token type is IDENT, then we have CycleMethod
        } while (((temp = temp.nextInSeries) != null) &&
                 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));

        ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
        int stopIndex = 0;
        do {
            ParsedValueImpl stop = stop(term);
            if (stop != null) stops[stopIndex++] = stop;
            prev = term;
        } while(((term = term.nextInSeries) != null) &&
                (term.token.getType() == CssLexer.LPAREN));

        // term is either null or is a cycle method, or the start of another Paint.
        ParsedValueImpl cycleMethod = cycleMethod(term);

        if (cycleMethod == null) {

            cycleMethod = new ParsedValueImpl<>(CycleMethod.NO_CYCLE.name(), new EnumConverter<>(CycleMethod.class));

            // if term is not null and the last term was not a cycle method,
            // then term starts a new series or layer of Paint
            if (term != null) {
                root.nextInSeries = term;
            }

            // if term is null, then we are at the end of a series.
            // root points to 'linear', now we want the next term after root
            // to be the term after the last stop, which may be another layer
            else {
                root.nextInSeries = null;
                root.nextLayer = prev.nextLayer;
            }


        } else {
            // last term was a CycleMethod, so term is not null.
            // root points at 'linear', now we want the next term after root
            // to be the term after cyclemethod, which may be another series
            // of paint or another layer.
            //
            root.nextInSeries = term.nextInSeries;
            root.nextLayer = term.nextLayer;
        }

        ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length];
        int index = 0;
        values[index++] = focusAngle;
        values[index++] = focusDistance;
        values[index++] = (centerPoint != null) ? centerPoint[0] : null;
        values[index++] = (centerPoint != null) ? centerPoint[1] : null;
        values[index++] = radius;
        values[index++] = cycleMethod;
        for (int n=0; n(values, PaintConverter.RadialGradientConverter.getInstance());
    }

    // Based off http://dev.w3.org/csswg/css3-images/#radial-gradients
    //
    //  = radial-gradient(
    //        [ focus-angle , ]?
    //        [ focus-distance , ]?
    //        [ center , ]?
    //        radius ,
    //        [ [ repeat | reflect ] ,]?
    //        [, ]+ )
    //
    // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax.
    private ParsedValueImpl parseRadialGradient(final Term root) throws ParseException {

        // first term in the chain is the function name...
        final String fn = (root.token != null) ? root.token.getText() : null;
        if (!"radial-gradient".regionMatches(true, 0, fn, 0, 15)) {
            final String msg = "Expected \'radial-gradient\'";
            error(root, msg);
        }

        Term arg;

        if ((arg = root.firstArg) == null ||
             arg.token == null ||
             arg.token.getText().isEmpty()) {
            error(root,
                "Expected \'focus-angle \' " +
                "or \'focus-distance \' " +
                "or \'center \' " +
                "or \'radius [ | ]\'");
        }

        Term prev = arg;
        ParsedValueImplfocusAngle = null;
        ParsedValueImplfocusDistance = null;
        ParsedValueImpl[] centerPoint = null;
        ParsedValueImplradius = null;

        if ("focus-angle".equalsIgnoreCase(arg.token.getText())) {

            prev = arg;
            if ((arg = arg.nextInSeries) == null ||
                    !isSize(arg.token)) error(prev, "Expected \'\'");

            Size angle = size(arg.token);
            switch(angle.getUnits()) {
                case DEG:
                case RAD:
                case GRAD:
                case TURN:
                case PX:
                    break;
                default:
                    error(arg, "Expected [deg | rad | grad | turn ]");
            }
            focusAngle = new ParsedValueImpl<>(angle, null);

            prev = arg;
            if ((arg = arg.nextArg) == null)
                error(prev, "Expected \'focus-distance \' " +
                            "or \'center \' " +
                            "or \'radius [ | ]\'");

        }

        if ("focus-distance".equalsIgnoreCase(arg.token.getText())) {

            prev = arg;
            if ((arg = arg.nextInSeries) == null ||
                    !isSize(arg.token)) error(prev, "Expected \'\'");

            Size distance = size(arg.token);

            // "The focus point is always specified relative to the center
            // point by an angle and a distance relative to the radius."
            switch(distance.getUnits()) {
                case PERCENT:
                    break;
                default:
                    error(arg, "Expected \'%\'");
            }
            focusDistance = new ParsedValueImpl<>(distance, null);

            prev = arg;
            if ((arg = arg.nextArg) == null)
                error(prev, "Expected \'center 
\' " + "or \'radius \'"); } if ("center".equalsIgnoreCase(arg.token.getText())) { prev = arg; if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'"); ParsedValueImpl ptX = parseSize(arg); prev = arg; if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'"); ParsedValueImpl ptY = parseSize(arg); centerPoint = new ParsedValueImpl[] { ptX, ptY }; prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'radius [ | ]\'"); } if ("radius".equalsIgnoreCase(arg.token.getText())) { prev = arg; if ((arg = arg.nextInSeries) == null || !isSize(arg.token)) error(prev, "Expected \'[ | ]\'"); radius = parseSize(arg); prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'radius [ | ]\'"); } CycleMethod cycleMethod = CycleMethod.NO_CYCLE; if ("reflect".equalsIgnoreCase(arg.token.getText())) { cycleMethod = CycleMethod.REFLECT; prev = arg; arg = arg.nextArg; } else if ("repeat".equalsIgnoreCase(arg.token.getText())) { cycleMethod = CycleMethod.REPEAT; prev = arg; arg = arg.nextArg; } if (arg == null || arg.token == null || arg.token.getText().isEmpty()) { error(prev, "Expected \'\'"); } ParsedValueImpl[] stops = parseColorStops(arg); ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length]; int index = 0; values[index++] = focusAngle; values[index++] = focusDistance; values[index++] = (centerPoint != null) ? centerPoint[0] : null; values[index++] = (centerPoint != null) ? centerPoint[1] : null; values[index++] = radius; values[index++] = new ParsedValueImpl<>(cycleMethod.name(), new EnumConverter<>(CycleMethod.class)); for (int n=0; n(values, PaintConverter.RadialGradientConverter.getInstance()); } // Based off ImagePattern constructor // // image-pattern([,,,,[,]?]?) // private ParsedValueImpl parseImagePattern(final Term root) throws ParseException { // first term in the chain is the function name... final String fn = (root.token != null) ? root.token.getText() : null; if (!"image-pattern".regionMatches(true, 0, fn, 0, 13)) { final String msg = "Expected \'image-pattern\'"; error(root, msg); } Term arg; if ((arg = root.firstArg) == null || arg.token == null || arg.token.getText().isEmpty()) { error(root, "Expected \'\'"); } Term prev = arg; final String uri = arg.token.getText(); ParsedValueImpl[] uriValues = new ParsedValueImpl[] { new ParsedValueImpl<>(uri, StringConverter.getInstance()), null // placeholder for Stylesheet URL }; ParsedValueImpl parsedURI = new ParsedValueImpl<>(uriValues, URLConverter.getInstance()); // If nextArg is null, then there are no remaining arguments, so we are done. if (arg.nextArg == null) { ParsedValueImpl[] values = new ParsedValueImpl[1]; values[0] = parsedURI; return new ParsedValueImpl<>(values, PaintConverter.ImagePatternConverter.getInstance()); } // There must now be 4 sizes in a row. Token token; if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'"); ParsedValueImpl x = parseSize(arg); prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'"); ParsedValueImpl y = parseSize(arg); prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'"); ParsedValueImpl w = parseSize(arg); prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'"); ParsedValueImpl h = parseSize(arg); // If there are no more args, then we are done. if (arg.nextArg == null) { ParsedValueImpl[] values = new ParsedValueImpl[5]; values[0] = parsedURI; values[1] = x; values[2] = y; values[3] = w; values[4] = h; return new ParsedValueImpl<>(values, PaintConverter.ImagePatternConverter.getInstance()); } prev = arg; if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'"); if ((token = arg.token) == null || token.getText() == null) error(arg, "Expected \'\'"); ParsedValueImpl[] values = new ParsedValueImpl[6]; values[0] = parsedURI; values[1] = x; values[2] = y; values[3] = w; values[4] = h; values[5] = new ParsedValueImpl(Boolean.parseBoolean(token.getText()), null); return new ParsedValueImpl<>(values, PaintConverter.ImagePatternConverter.getInstance()); } // For tiling ImagePatterns easily. // // repeating-image-pattern() // private ParsedValueImpl parseRepeatingImagePattern(final Term root) throws ParseException { // first term in the chain is the function name... final String fn = (root.token != null) ? root.token.getText() : null; if (!"repeating-image-pattern".regionMatches(true, 0, fn, 0, 23)) { final String msg = "Expected \'repeating-image-pattern\'"; error(root, msg); } Term arg; if ((arg = root.firstArg) == null || arg.token == null || arg.token.getText().isEmpty()) { error(root, "Expected \'\'"); } final String uri = arg.token.getText(); ParsedValueImpl[] uriValues = new ParsedValueImpl[] { new ParsedValueImpl<>(uri, StringConverter.getInstance()), null // placeholder for Stylesheet URL }; ParsedValueImpl parsedURI = new ParsedValueImpl<>(uriValues, URLConverter.getInstance()); ParsedValueImpl[] values = new ParsedValueImpl[1]; values[0] = parsedURI; return new ParsedValueImpl<>(values, PaintConverter.RepeatingImagePatternConverter.getInstance()); } // parse a series of paint values separated by commas. // i.e., [, ]* private ParsedValueImpl[],Paint[]> parsePaintLayers(Term root) throws ParseException { // how many paints in the series? int nPaints = numberOfLayers(root); ParsedValueImpl[] paints = new ParsedValueImpl[nPaints]; Term temp = root; int paint = 0; do { if (temp.token == null || temp.token.getText() == null || temp.token.getText().isEmpty()) error(temp, "Expected \'\'"); paints[paint++] = parse(temp); temp = nextLayer(temp); } while (temp != null); return new ParsedValueImpl<>(paints, PaintConverter.SequenceConverter.getInstance()); } // An size or a series of four size values // | private ParsedValueImpl[] parseSize1to4(final Term root) throws ParseException { Term temp = root; ParsedValueImpl[] sides = new ParsedValueImpl[4]; int side = 0; while (side < 4 && temp != null) { sides[side++] = parseSize(temp); temp = temp.nextInSeries; } if (side < 2) sides[1] = sides[0]; // right = top if (side < 3) sides[2] = sides[0]; // bottom = top if (side < 4) sides[3] = sides[1]; // left = right return sides; } // A series of inset or sets of four inset values // | [ , [ | ] ]* private ParsedValueImpl[], Insets[]> parseInsetsLayers(Term root) throws ParseException { int nLayers = numberOfLayers(root); Term temp = root; int layer = 0; ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; while(temp != null) { ParsedValueImpl[] sides = parseSize1to4(temp); layers[layer++] = new ParsedValueImpl<>(sides, InsetsConverter.getInstance()); while(temp.nextInSeries != null) { temp = temp.nextInSeries; } temp = nextLayer(temp); } return new ParsedValueImpl<>(layers, InsetsConverter.SequenceConverter.getInstance()); } // A single inset (1, 2, 3, or 4 digits) // | private ParsedValueImpl parseInsetsLayer(Term root) throws ParseException { Term temp = root; ParsedValueImpl layer = null; while(temp != null) { ParsedValueImpl[] sides = parseSize1to4(temp); layer = new ParsedValueImpl<>(sides, InsetsConverter.getInstance()); while(temp.nextInSeries != null) { temp = temp.nextInSeries; } temp = nextLayer(temp); } return layer; } // | private ParsedValueImpl[], Margins[]> parseMarginsLayers(Term root) throws ParseException { int nLayers = numberOfLayers(root); Term temp = root; int layer = 0; ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; while(temp != null) { ParsedValueImpl[] sides = parseSize1to4(temp); layers[layer++] = new ParsedValueImpl<>(sides, Margins.Converter.getInstance()); while(temp.nextInSeries != null) { temp = temp.nextInSeries; } temp = nextLayer(temp); } return new ParsedValueImpl<>(layers, Margins.SequenceConverter.getInstance()); } // | private ParsedValueImpl[] parseSizeSeries(Term root) throws ParseException { if (root.token == null) error(root, "Parse error"); List> sizes = new ArrayList<>(); Term term = root; while(term != null) { Token token = term.token; final int ttype = token.getType(); switch (ttype) { case CssLexer.NUMBER: case CssLexer.PERCENTAGE: case CssLexer.EMS: case CssLexer.EXS: case CssLexer.PX: case CssLexer.CM: case CssLexer.MM: case CssLexer.IN: case CssLexer.PT: case CssLexer.PC: case CssLexer.DEG: case CssLexer.GRAD: case CssLexer.RAD: case CssLexer.TURN: ParsedValueImpl sizeValue = new ParsedValueImpl(size(token), null); sizes.add(sizeValue); break; default: error (root, "expected series of "); } term = term.nextInSeries; } return sizes.toArray(new ParsedValueImpl[sizes.size()]); } // http://www.w3.org/TR/css3-background/#the-border-radius // {1,4} [ '/' {1,4}]? [',' {1,4} [ '/' {1,4}]?]? private ParsedValueImpl[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root) throws ParseException { int nLayers = numberOfLayers(root); Term term = root; int layer = 0; ParsedValueImpl[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers]; while(term != null) { int nHorizontalTerms = 0; Term temp = term; while (temp != null) { if (temp.token.getType() == CssLexer.SOLIDUS) { temp = temp.nextInSeries; break; } nHorizontalTerms += 1; temp = temp.nextInSeries; } int nVerticalTerms = 0; while (temp != null) { if (temp.token.getType() == CssLexer.SOLIDUS) { error(temp, "unexpected SOLIDUS"); break; } nVerticalTerms += 1; temp = temp.nextInSeries; } if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) { error(root, "expected [|]{1,4} [/ [|]{1,4}]?"); } // used as index into margins[]. horizontal = 0, vertical = 1 int orientation = 0; // at most, there should be four radii in the horizontal orientation and four in the vertical. ParsedValueImpl[][] radii = new ParsedValueImpl[2][4]; ParsedValueImpl zero = new ParsedValueImpl<>(new Size(0,SizeUnits.PX), null); for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; } int hr = 0; int vr = 0; Term lastTerm = term; while ((hr <= 4) && (vr <= 4) && (term != null)) { if (term.token.getType() == CssLexer.SOLIDUS) { orientation += 1; } else { ParsedValueImpl parsedValue = parseSize(term); if (orientation == 0) { radii[orientation][hr++] = parsedValue; } else { radii[orientation][vr++] = parsedValue; } } lastTerm = term; term = term.nextInSeries; } // // http://www.w3.org/TR/css3-background/#the-border-radius // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left. // If bottom-left is omitted it is the same as top-right. // If bottom-right is omitted it is the same as top-left. // If top-right is omitted it is the same as top-left. // // If there is no vertical component, then set both equally. // If either is zero, then both are zero. // // if hr == 0, then there were no horizontal radii (which would be an error caught above) if (hr != 0) { if (hr < 2) radii[0][1] = radii[0][0]; // top-right = top-left if (hr < 3) radii[0][2] = radii[0][0]; // bottom-right = top-left if (hr < 4) radii[0][3] = radii[0][1]; // bottom-left = top-right } else { assert(false); } // if vr == 0, then there were no vertical radii if (vr != 0) { if (vr < 2) radii[1][1] = radii[1][0]; // top-right = top-left if (vr < 3) radii[1][2] = radii[1][0]; // bottom-right = top-left if (vr < 4) radii[1][3] = radii[1][1]; // bottom-left = top-right } else { // if no vertical, the vertical value is same as horizontal radii[1][0] = radii[0][0]; radii[1][1] = radii[0][1]; radii[1][2] = radii[0][2]; radii[1][3] = radii[0][3]; } // if either is zero, both are zero if (zero.equals(radii[0][0]) || zero.equals(radii[1][0])) { radii[1][0] = radii[0][0] = zero; } if (zero.equals(radii[0][1]) || zero.equals(radii[1][1])) { radii[1][1] = radii[0][1] = zero; } if (zero.equals(radii[0][2]) || zero.equals(radii[1][2])) { radii[1][2] = radii[0][2] = zero; } if (zero.equals(radii[0][3]) || zero.equals(radii[1][3])) { radii[1][3] = radii[0][3] = zero; } layers[layer++] = new ParsedValueImpl<>(radii, null); term = nextLayer(lastTerm); } return new ParsedValueImpl<>(layers, CornerRadiiConverter.getInstance()); } /* Constant for background position */ private final static ParsedValueImpl ZERO_PERCENT = new ParsedValueImpl<>(new Size(0f, SizeUnits.PERCENT), null); /* Constant for background position */ private final static ParsedValueImpl FIFTY_PERCENT = new ParsedValueImpl<>(new Size(50f, SizeUnits.PERCENT), null); /* Constant for background position */ private final static ParsedValueImpl ONE_HUNDRED_PERCENT = new ParsedValueImpl<>(new Size(100f, SizeUnits.PERCENT), null); private static boolean isPositionKeyWord(String value) { return "center".equalsIgnoreCase(value) || "top".equalsIgnoreCase(value) || "bottom".equalsIgnoreCase(value) || "left".equalsIgnoreCase(value) || "right".equalsIgnoreCase(value); } /* * http://www.w3.org/TR/css3-background/#the-background-position * * = [ * [ top | bottom ] * | * [ | | left | center | right ] * [ | | top | center | bottom ]? * | * [ center | [ left | right ] [ | ]? ] && * [ center | [ top | bottom ] [ | ]? ] * ] * * From the W3 spec: * * returned ParsedValueImpl is [size, size, size, size] with the semantics * [top offset, right offset, bottom offset left offset] */ private ParsedValueImpl parseBackgroundPosition(Term term) throws ParseException { if (term.token == null || term.token.getText() == null || term.token.getText().isEmpty()) error(term, "Expected \'\'"); Term termOne = term; Token valueOne = term.token; Term termTwo = termOne.nextInSeries; Token valueTwo = (termTwo != null) ? termTwo.token : null; Term termThree = (termTwo != null) ? termTwo.nextInSeries : null; Token valueThree = (termThree != null) ? termThree.token : null; Term termFour = (termThree != null) ? termThree.nextInSeries : null; Token valueFour = (termFour != null) ? termFour.token : null; // are the horizontal and vertical exchanged if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { // 2 values filled String v1 = valueOne.getText(); String v2 = valueTwo.getText(); if( ("top".equals(v1) || "bottom".equals(v1)) && ("left".equals(v2) || "right".equals(v2) || "center".equals(v2)) ) { { Token tmp = valueTwo; valueTwo = valueOne; valueOne = tmp; } { Term tmp = termTwo; termTwo = termOne; termOne = tmp; } } } else if( valueOne != null && valueTwo != null && valueThree != null ) { Term[] termArray = null; Token[] tokeArray = null; // 4 values filled if( valueFour != null ) { if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) && ("left".equals(valueThree.getText()) || "right".equals(valueThree.getText())) ) { // e.g. top 50 left 20 termArray = new Term[] { termThree, termFour, termOne, termTwo }; tokeArray = new Token[] { valueThree, valueFour, valueOne, valueTwo }; } } else { if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) ) { if( ("left".equals(valueTwo.getText()) || "right".equals(valueTwo.getText())) ) { // e.g. top left 50 termArray = new Term[] { termTwo, termThree, termOne, null }; tokeArray = new Token[] { valueTwo, valueThree, valueOne, null }; } else { // e.g. top 50 left termArray = new Term[] { termThree, termOne, termTwo, null }; tokeArray = new Token[] { valueThree, valueOne, valueTwo, null }; } } } if( termArray != null ) { termOne = termArray[0]; termTwo = termArray[1]; termThree = termArray[2]; termFour = termArray[3]; valueOne = tokeArray[0]; valueTwo = tokeArray[1]; valueThree = tokeArray[2]; valueFour = tokeArray[3]; } } ParsedValueImpl top, right, bottom, left; top = right = bottom = left = ZERO_PERCENT; { if(valueOne == null && valueTwo == null && valueThree == null && valueFour == null) { error(term, "No value found for background-position"); } else if( valueOne != null && valueTwo == null && valueThree == null && valueFour == null ) { // Only one value String v1 = valueOne.getText(); if( "center".equals(v1) ) { left = FIFTY_PERCENT; right = ZERO_PERCENT; top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else if("left".equals(v1)) { left = ZERO_PERCENT; right = ZERO_PERCENT; top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else if( "right".equals(v1) ) { left = ONE_HUNDRED_PERCENT; right = ZERO_PERCENT; top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else if( "top".equals(v1) ) { left = FIFTY_PERCENT; right = ZERO_PERCENT; top = ZERO_PERCENT; bottom = ZERO_PERCENT; } else if( "bottom".equals(v1) ) { left = FIFTY_PERCENT; right = ZERO_PERCENT; top = ONE_HUNDRED_PERCENT; bottom = ZERO_PERCENT; } else { left = parseSize(termOne); right = ZERO_PERCENT; top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } } else if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { // 2 values String v1 = valueOne.getText().toLowerCase(Locale.ROOT); String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); if( ! isPositionKeyWord(v1) ) { left = parseSize(termOne); right = ZERO_PERCENT; if( "top".equals(v2) ) { top = ZERO_PERCENT; bottom = ZERO_PERCENT; } else if( "bottom".equals(v2) ) { top = ONE_HUNDRED_PERCENT; bottom = ZERO_PERCENT; } else if( "center".equals(v2) ) { top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else if( !isPositionKeyWord(v2) ) { top = parseSize(termTwo); bottom = ZERO_PERCENT; } else { error(termTwo,"Expected 'top', 'bottom', 'center' or "); } } else if( v1.equals("left") || v1.equals("right") ) { left = v1.equals("right") ? ONE_HUNDRED_PERCENT : ZERO_PERCENT; right = ZERO_PERCENT; if( ! isPositionKeyWord(v2) ) { top = parseSize(termTwo); bottom = ZERO_PERCENT; } else if( v2.equals("top") || v2.equals("bottom") || v2.equals("center") ) { if( v2.equals("top") ) { top = ZERO_PERCENT; bottom = ZERO_PERCENT; } else if(v2.equals("center")) { top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else { top = ONE_HUNDRED_PERCENT; bottom = ZERO_PERCENT; } } else { error(termTwo,"Expected 'top', 'bottom', 'center' or "); } } else if( v1.equals("center") ) { left = FIFTY_PERCENT; right = ZERO_PERCENT; if( v2.equals("top") ) { top = ZERO_PERCENT; bottom = ZERO_PERCENT; } else if( v2.equals("bottom") ) { top = ONE_HUNDRED_PERCENT; bottom = ZERO_PERCENT; } else if( v2.equals("center") ) { top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else if( ! isPositionKeyWord(v2) ) { top = parseSize(termTwo); bottom = ZERO_PERCENT; } else { error(termTwo,"Expected 'top', 'bottom', 'center' or "); } } } else if( valueOne != null && valueTwo != null && valueThree != null && valueFour == null ) { String v1 = valueOne.getText().toLowerCase(Locale.ROOT); String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); String v3 = valueThree.getText().toLowerCase(Locale.ROOT); if( ! isPositionKeyWord(v1) || "center".equals(v1) ) { // 1 is horizontal // means 2 & 3 are vertical if( "center".equals(v1) ) { left = FIFTY_PERCENT; } else { left = parseSize(termOne); } right = ZERO_PERCENT; if( !isPositionKeyWord(v3) ) { if( "top".equals(v2) ) { top = parseSize(termThree); bottom = ZERO_PERCENT; } else if( "bottom".equals(v2) ) { top = ZERO_PERCENT; bottom = parseSize(termThree); } else { error(termTwo,"Expected 'top' or 'bottom'"); } } else { error(termThree,"Expected "); } } else if( "left".equals(v1) || "right".equals(v1) ) { if( ! isPositionKeyWord(v2) ) { // 1 & 2 are horizontal // 3 is vertical if( "left".equals(v1) ) { left = parseSize(termTwo); right = ZERO_PERCENT; } else { left = ZERO_PERCENT; right = parseSize(termTwo); } if( "top".equals(v3) ) { top = ZERO_PERCENT; bottom = ZERO_PERCENT; } else if( "bottom".equals(v3) ) { top = ONE_HUNDRED_PERCENT; bottom = ZERO_PERCENT; } else if( "center".equals(v3) ) { top = FIFTY_PERCENT; bottom = ZERO_PERCENT; } else { error(termThree,"Expected 'top', 'bottom' or 'center'"); } } else { // 1 is horizontal // 2 & 3 are vertical if( "left".equals(v1) ) { left = ZERO_PERCENT; right = ZERO_PERCENT; } else { left = ONE_HUNDRED_PERCENT; right = ZERO_PERCENT; } if( ! isPositionKeyWord(v3) ) { if( "top".equals(v2) ) { top = parseSize(termThree); bottom = ZERO_PERCENT; } else if( "bottom".equals(v2) ) { top = ZERO_PERCENT; bottom = parseSize(termThree); } else { error(termTwo,"Expected 'top' or 'bottom'"); } } else { error(termThree,"Expected "); } } } } else { String v1 = valueOne.getText().toLowerCase(Locale.ROOT); String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); String v3 = valueThree.getText().toLowerCase(Locale.ROOT); String v4 = valueFour.getText().toLowerCase(Locale.ROOT); if( (v1.equals("left") || v1.equals("right")) && (v3.equals("top") || v3.equals("bottom") ) && ! isPositionKeyWord(v2) && ! isPositionKeyWord(v4) ) { if( v1.equals("left") ) { left = parseSize(termTwo); right = ZERO_PERCENT; } else { left = ZERO_PERCENT; right = parseSize(termTwo); } if( v3.equals("top") ) { top = parseSize(termFour); bottom = ZERO_PERCENT; } else { top = ZERO_PERCENT; bottom = parseSize(termFour); } } else { error(term,"Expected 'left' or 'right' followed by followed by 'top' or 'bottom' followed by "); } } } ParsedValueImpl[] values = new ParsedValueImpl[] {top, right, bottom, left}; return new ParsedValueImpl<>(values, BackgroundPositionConverter.getInstance()); } private ParsedValueImpl[], BackgroundPosition[]> parseBackgroundPositionLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBackgroundPosition(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, LayeredBackgroundPositionConverter.getInstance()); } /* http://www.w3.org/TR/css3-background/#the-background-repeat = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2} */ private ParsedValueImpl[] parseRepeatStyle(final Term root) throws ParseException { BackgroundRepeat xAxis, yAxis; xAxis = yAxis = BackgroundRepeat.NO_REPEAT; Term term = root; if (term.token == null || term.token.getType() != CssLexer.IDENT || term.token.getText() == null || term.token.getText().isEmpty()) error(term, "Expected \'\'"); String text = term.token.getText().toLowerCase(Locale.ROOT); if ("repeat-x".equals(text)) { xAxis = BackgroundRepeat.REPEAT; yAxis = BackgroundRepeat.NO_REPEAT; } else if ("repeat-y".equals(text)) { xAxis = BackgroundRepeat.NO_REPEAT; yAxis = BackgroundRepeat.REPEAT; } else if ("repeat".equals(text)) { xAxis = BackgroundRepeat.REPEAT; yAxis = BackgroundRepeat.REPEAT; } else if ("space".equals(text)) { xAxis = BackgroundRepeat.SPACE; yAxis = BackgroundRepeat.SPACE; } else if ("round".equals(text)) { xAxis = BackgroundRepeat.ROUND; yAxis = BackgroundRepeat.ROUND; } else if ("no-repeat".equals(text)) { xAxis = BackgroundRepeat.NO_REPEAT; yAxis = BackgroundRepeat.NO_REPEAT; } else if ("stretch".equals(text)) { xAxis = BackgroundRepeat.NO_REPEAT; yAxis = BackgroundRepeat.NO_REPEAT; } else { error(term, "Expected \'\' " + text); } if ((term = term.nextInSeries) != null && term.token != null && term.token.getType() == CssLexer.IDENT && term.token.getText() != null && !term.token.getText().isEmpty()) { text = term.token.getText().toLowerCase(Locale.ROOT); if ("repeat-x".equals(text)) { error(term, "Unexpected \'repeat-x\'"); } else if ("repeat-y".equals(text)) { error(term, "Unexpected \'repeat-y\'"); } else if ("repeat".equals(text)) { yAxis = BackgroundRepeat.REPEAT; } else if ("space".equals(text)) { yAxis = BackgroundRepeat.SPACE; } else if ("round".equals(text)) { yAxis = BackgroundRepeat.ROUND; } else if ("no-repeat".equals(text)) { yAxis = BackgroundRepeat.NO_REPEAT; } else if ("stretch".equals(text)) { yAxis = BackgroundRepeat.NO_REPEAT; } else { error(term, "Expected \'\'"); } } return new ParsedValueImpl[] { new ParsedValueImpl<>(xAxis.name(), new EnumConverter<>(BackgroundRepeat.class)), new ParsedValueImpl<>(yAxis.name(), new EnumConverter<>(BackgroundRepeat.class)) }; } private ParsedValueImpl[][],RepeatStruct[]> parseBorderImageRepeatStyleLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[][] layers = new ParsedValueImpl[nLayers][]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseRepeatStyle(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, RepeatStructConverter.getInstance()); } private ParsedValueImpl[][], RepeatStruct[]> parseBackgroundRepeatStyleLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[][] layers = new ParsedValueImpl[nLayers][]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseRepeatStyle(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, RepeatStructConverter.getInstance()); } /* http://www.w3.org/TR/css3-background/#the-background-size = [ | | auto ]{1,2} | cover | contain */ private ParsedValueImpl parseBackgroundSize(final Term root) throws ParseException { ParsedValueImpl height = null, width = null; boolean cover = false, contain = false; Term term = root; if (term.token == null) error(term, "Expected \'\'"); if (term.token.getType() == CssLexer.IDENT) { final String text = (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null; if ("auto".equals(text)) { // We don't do anything because width / height are already initialized } else if ("cover".equals(text)) { cover = true; } else if ("contain".equals(text)) { contain = true; } else if ("stretch".equals(text)) { width = ONE_HUNDRED_PERCENT; height = ONE_HUNDRED_PERCENT; } else { error(term, "Expected \'auto\', \'cover\', \'contain\', or \'stretch\'"); } } else if (isSize(term.token)) { width = parseSize(term); height = null; } else { error(term, "Expected \'\'"); } if ((term = term.nextInSeries) != null) { if (cover || contain) error(term, "Unexpected \'\'"); if (term.token.getType() == CssLexer.IDENT) { final String text = (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null; if ("auto".equals(text)) { height = null; } else if ("cover".equals(text)) { error(term, "Unexpected \'cover\'"); } else if ("contain".equals(text)) { error(term, "Unexpected \'contain\'"); } else if ("stretch".equals(text)) { height = ONE_HUNDRED_PERCENT; } else { error(term, "Expected \'auto\' or \'stretch\'"); } } else if (isSize(term.token)) { height = parseSize(term); } else { error(term, "Expected \'\'"); } } ParsedValueImpl[] values = new ParsedValueImpl[] { width, height, // TODO: handling of booleans is really bogus new ParsedValueImpl<>((cover ? "true" : "false"), BooleanConverter.getInstance()), new ParsedValueImpl<>((contain ? "true" : "false"), BooleanConverter.getInstance()) }; return new ParsedValueImpl<>(values, BackgroundSizeConverter.getInstance()); } private ParsedValueImpl[], BackgroundSize[]> parseBackgroundSizeLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBackgroundSize(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, LayeredBackgroundSizeConverter.getInstance()); } private ParsedValueImpl[], Paint[]> parseBorderPaint(final Term root) throws ParseException { Term term = root; ParsedValueImpl[] paints = new ParsedValueImpl[4]; int paint = 0; while(term != null) { if (term.token == null || paints.length <= paint) error(term, "Expected \'\'"); paints[paint++] = parse(term); term = term.nextInSeries; } if (paint < 2) paints[1] = paints[0]; // right = top if (paint < 3) paints[2] = paints[0]; // bottom = top if (paint < 4) paints[3] = paints[1]; // left = right return new ParsedValueImpl<>(paints, StrokeBorderPaintConverter.getInstance()); } private ParsedValueImpl[],Paint[]>[], Paint[][]> parseBorderPaintLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[],Paint[]>[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBorderPaint(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, LayeredBorderPaintConverter.getInstance()); } // borderStyle (borderStyle (borderStyle borderStyle?)?)? private ParsedValueImpl[],BorderStrokeStyle[]> parseBorderStyleSeries(final Term root) throws ParseException { Term term = root; ParsedValueImpl[] borders = new ParsedValueImpl[4]; int border = 0; while (term != null) { borders[border++] = parseBorderStyle(term); term = term.nextInSeries; } if (border < 2) borders[1] = borders[0]; // right = top if (border < 3) borders[2] = borders[0]; // bottom = top if (border < 4) borders[3] = borders[1]; // left = right return new ParsedValueImpl<>(borders, BorderStrokeStyleSequenceConverter.getInstance()); } private ParsedValueImpl[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]> parseBorderStyleLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBorderStyleSeries(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, LayeredBorderStyleConverter.getInstance()); } // Only meant to be used from parseBorderStyle, but might be useful elsewhere private String getKeyword(final Term term) { if (term != null && term.token != null && term.token.getType() == CssLexer.IDENT && term.token.getText() != null && !term.token.getText().isEmpty()) { return term.token.getText().toLowerCase(Locale.ROOT); } return null; } // [ , ]* // where = // [centered | inside | outside]? [line-join [miter | bevel | round]]? [line-cap [square | butt | round]]? // where = // [ none | solid | dotted | dashed ] private ParsedValueImpl parseBorderStyle(final Term root) throws ParseException { ParsedValue dashStyle = null; ParsedValue,Number> dashPhase = null; ParsedValue strokeType = null; ParsedValue strokeLineJoin = null; ParsedValue,Number> strokeMiterLimit = null; ParsedValue strokeLineCap = null; Term term = root; dashStyle = dashStyle(term); Term prev = term; term = term.nextInSeries; String keyword = getKeyword(term); // dash-style might be followed by 'phase ' if ("phase".equals(keyword)) { prev = term; if ((term = term.nextInSeries) == null || term.token == null || !isSize(term.token)) error(term, "Expected \'\'"); ParsedValueImpl sizeVal = parseSize(term); dashPhase = new ParsedValueImpl<>(sizeVal,SizeConverter.getInstance()); prev = term; term = term.nextInSeries; } // stroke type might be next strokeType = parseStrokeType(term); if (strokeType != null) { prev = term; term = term.nextInSeries; } keyword = getKeyword(term); if ("line-join".equals(keyword)) { prev = term; term = term.nextInSeries; ParsedValueImpl[] lineJoinValues = parseStrokeLineJoin(term); if (lineJoinValues != null) { strokeLineJoin = lineJoinValues[0]; strokeMiterLimit = lineJoinValues[1]; } else { error(term, "Expected \'miter ?\', \'bevel\' or \'round\'"); } prev = term; term = term.nextInSeries; keyword = getKeyword(term); } if ("line-cap".equals(keyword)) { prev = term; term = term.nextInSeries; strokeLineCap = parseStrokeLineCap(term); if (strokeLineCap == null) { error(term, "Expected \'square\', \'butt\' or \'round\'"); } prev = term; term = term.nextInSeries; } if (term != null) { // if term is not null, then we have gotten to the end of // one border style and term is start of another border style root.nextInSeries = term; } else { // If term is null, then we have gotten to the end of // a border style with no more to follow in this series. // But there may be another layer. root.nextInSeries = null; root.nextLayer = prev.nextLayer; } final ParsedValue[] values = new ParsedValue[]{ dashStyle, dashPhase, strokeType, strokeLineJoin, strokeMiterLimit, strokeLineCap }; return new ParsedValueImpl(values, BorderStyleConverter.getInstance()); } // // segments( [, ]+) | // private ParsedValue dashStyle(final Term root) throws ParseException { if (root.token == null) error(root, "Expected \'\'"); final int ttype = root.token.getType(); ParsedValue segments = null; if (ttype == CssLexer.IDENT) { segments = borderStyle(root); } else if (ttype == CssLexer.FUNCTION) { segments = segments(root); } else { error(root, "Expected \'\'"); } return segments; } /* = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset */ private ParsedValue borderStyle(Term root) throws ParseException { if (root.token == null || root.token.getType() != CssLexer.IDENT || root.token.getText() == null || root.token.getText().isEmpty()) error(root, "Expected \'\'"); final String text = root.token.getText().toLowerCase(Locale.ROOT); if ("none".equals(text)) { return BorderStyleConverter.NONE; } else if ("hidden".equals(text)) { // The "hidden" mode doesn't make sense for FX, because it is the // same as "none" except for border-collapsed CSS tables return BorderStyleConverter.NONE; } else if ("dotted".equals(text)) { return BorderStyleConverter.DOTTED; } else if ("dashed".equals(text)) { return BorderStyleConverter.DASHED; } else if ("solid".equals(text)) { return BorderStyleConverter.SOLID; } else if ("double".equals(text)) { error(root, "Unsupported \'double\'"); } else if ("groove".equals(text)) { error(root, "Unsupported \'groove\'"); } else if ("ridge".equals(text)) { error(root, "Unsupported \'ridge\'"); } else if ("inset".equals(text)) { error(root, "Unsupported \'inset\'"); } else if ("outset".equals(text)) { error(root, "Unsupported \'outset\'"); } else { error(root, "Unsupported \'" + text + "\'"); } // error throws so we should never get here. // but the compiler wants a return, so here it is. return BorderStyleConverter.SOLID; } private ParsedValueImpl segments(Term root) throws ParseException { // first term in the chain is the function name... final String fn = (root.token != null) ? root.token.getText() : null; if (!"segments".regionMatches(true, 0, fn, 0, 8)) { error(root,"Expected \'segments\'"); } Term arg = root.firstArg; if (arg == null) error(null, "Expected \'\'"); int nArgs = numberOfArgs(root); ParsedValueImpl[] segments = new ParsedValueImpl[nArgs]; int segment = 0; while(arg != null) { segments[segment++] = parseSize(arg); arg = arg.nextArg; } return new ParsedValueImpl<>(segments,SizeConverter.SequenceConverter.getInstance()); } private ParsedValueImpl parseStrokeType(final Term root) { final String keyword = getKeyword(root); if ("centered".equals(keyword) || "inside".equals(keyword) || "outside".equals(keyword)) { return new ParsedValueImpl(keyword, new EnumConverter(StrokeType.class)); } return null; } // Root term is the term just after the line-join keyword // Returns an array of two Values or null. // ParsedValueImpl[0] is ParsedValueImpl // ParsedValueImpl[1] is ParsedValueImpl,Number> if miter limit is given, null otherwise. // If the token is not a StrokeLineJoin, then null is returned. private ParsedValueImpl[] parseStrokeLineJoin(final Term root) throws ParseException { final String keyword = getKeyword(root); if ("miter".equals(keyword) || "bevel".equals(keyword) || "round".equals(keyword)) { ParsedValueImpl strokeLineJoin = new ParsedValueImpl(keyword, new EnumConverter(StrokeLineJoin.class)); ParsedValueImpl,Number> strokeMiterLimit = null; if ("miter".equals(keyword)) { Term next = root.nextInSeries; if (next != null && next.token != null && isSize(next.token)) { root.nextInSeries = next.nextInSeries; ParsedValueImpl sizeVal = parseSize(next); strokeMiterLimit = new ParsedValueImpl<>(sizeVal,SizeConverter.getInstance()); } } return new ParsedValueImpl[] { strokeLineJoin, strokeMiterLimit }; } return null; } // Root term is the term just after the line-cap keyword // If the token is not a StrokeLineCap, then null is returned. private ParsedValueImpl parseStrokeLineCap(final Term root) { final String keyword = getKeyword(root); if ("square".equals(keyword) || "butt".equals(keyword) || "round".equals(keyword)) { return new ParsedValueImpl(keyword, new EnumConverter(StrokeLineCap.class)); } return null; } /* * http://www.w3.org/TR/css3-background/#the-border-image-slice * [ | ]{1,4} && fill? */ private ParsedValueImpl parseBorderImageSlice(final Term root) throws ParseException { Term term = root; if (term.token == null || !isSize(term.token)) error(term, "Expected \'\'"); ParsedValueImpl[] insets = new ParsedValueImpl[4]; Boolean fill = Boolean.FALSE; int inset = 0; while (inset < 4 && term != null) { insets[inset++] = parseSize(term); if ((term = term.nextInSeries) != null && term.token != null && term.token.getType() == CssLexer.IDENT) { if("fill".equalsIgnoreCase(term.token.getText())) { fill = Boolean.TRUE; break; } } } if (inset < 2) insets[1] = insets[0]; // right = top if (inset < 3) insets[2] = insets[0]; // bottom = top if (inset < 4) insets[3] = insets[1]; // left = right ParsedValueImpl[] values = new ParsedValueImpl[] { new ParsedValueImpl<>(insets, InsetsConverter.getInstance()), new ParsedValueImpl(fill, null) }; return new ParsedValueImpl<>(values, BorderImageSliceConverter.getInstance()); } private ParsedValueImpl[],BorderImageSlices[]> parseBorderImageSliceLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBorderImageSlice(term); term = nextLayer(term); } return new ParsedValueImpl<> (layers, SliceSequenceConverter.getInstance()); } /* * http://www.w3.org/TR/css3-background/#border-image-width * [ | | | auto ]{1,4} */ private ParsedValueImpl parseBorderImageWidth(final Term root) throws ParseException { Term term = root; if (term.token == null || !isSize(term.token)) error(term, "Expected \'\'"); ParsedValueImpl[] insets = new ParsedValueImpl[4]; int inset = 0; while (inset < 4 && term != null) { insets[inset++] = parseSize(term); if ((term = term.nextInSeries) != null && term.token != null && term.token.getType() == CssLexer.IDENT) { } } if (inset < 2) insets[1] = insets[0]; // right = top if (inset < 3) insets[2] = insets[0]; // bottom = top if (inset < 4) insets[3] = insets[1]; // left = right return new ParsedValueImpl<>(insets, BorderImageWidthConverter.getInstance()); } private ParsedValueImpl[],BorderWidths[]> parseBorderImageWidthLayers(final Term root) throws ParseException { int nLayers = numberOfLayers(root); ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; int layer = 0; Term term = root; while (term != null) { layers[layer++] = parseBorderImageWidth(term); term = nextLayer(term); } return new ParsedValueImpl<>(layers, BorderImageWidthsSequenceConverter.getInstance()); } // parse a Region value // i.e., region(".styleClassForRegion") or region("#idForRegion") private static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:"; private ParsedValueImpl parseRegion(Term root) throws ParseException { // first term in the chain is the function name... final String fn = (root.token != null) ? root.token.getText() : null; if (!"region".regionMatches(true, 0, fn, 0, 6)) { error(root,"Expected \'region\'"); } Term arg = root.firstArg; if (arg == null) error(root, "Expected \'region(\"\")\'"); if (arg.token == null || arg.token.getType() != CssLexer.STRING || arg.token.getText() == null || arg.token.getText().isEmpty()) error(root, "Expected \'region(\"\")\'"); final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText()); return new ParsedValueImpl<>(styleClassOrId, StringConverter.getInstance()); } // url("") is tokenized by the lexer, so the root arg should be a URL token. private ParsedValueImpl parseURI(Term root) throws ParseException { if (root == null) error(root, "Expected \'url(\"\")\'"); if (root.token == null || root.token.getType() != CssLexer.URL || root.token.getText() == null || root.token.getText().isEmpty()) error(root, "Expected \'url(\"\")\'"); final String uri = root.token.getText(); ParsedValueImpl[] uriValues = new ParsedValueImpl[] { new ParsedValueImpl<>(uri, StringConverter.getInstance()), null // placeholder for Stylesheet URL }; return new ParsedValueImpl<>(uriValues, URLConverter.getInstance()); } // parse a series of URI values separated by commas. // i.e., [, ]* private ParsedValueImpl[],String[]> parseURILayers(Term root) throws ParseException { int nLayers = numberOfLayers(root); Term temp = root; int layer = 0; ParsedValueImpl[] layers = new ParsedValueImpl[nLayers]; while(temp != null) { layers[layer++] = parseURI(temp); temp = nextLayer(temp); } return new ParsedValueImpl<>(layers, URLConverter.SequenceConverter.getInstance()); } //-------------------------------------------------------------------------- // // http://www.w3.org/TR/css3-fonts // //-------------------------------------------------------------------------- /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */ private ParsedValueImpl,Number> parseFontSize(final Term root) throws ParseException { if (root == null) return null; final Token token = root.token; if (token == null || !isSize(token)) error(root, "Expected \'\'"); Size size = null; if (token.getType() == CssLexer.IDENT) { final String ident = token.getText().toLowerCase(Locale.ROOT); double value = -1; if ("inherit".equals(ident)) { value = 100; } else if ("xx-small".equals(ident)) { value = 60; } else if ("x-small".equals(ident)) { value = 75; } else if ("small".equals(ident)) { value = 80; } else if ("medium".equals(ident)) { value = 100; } else if ("large".equals(ident)) { value = 120; } else if ("x-large".equals(ident)) { value = 150; } else if ("xx-large".equals(ident)) { value = 200; } else if ("smaller".equals(ident)) { value = 80; } else if ("larger".equals(ident)) { value = 120; } if (value > -1) { size = new Size(value, SizeUnits.PERCENT); } } // if size is null, then size is not one of the keywords above. if (size == null) { size = size(token); } ParsedValueImpl svalue = new ParsedValueImpl<>(size, null); return new ParsedValueImpl<>(svalue, FontConverter.FontSizeConverter.getInstance()); } /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */ private ParsedValueImpl parseFontStyle(Term root) throws ParseException { if (root == null) return null; final Token token = root.token; if (token == null || token.getType() != CssLexer.IDENT || token.getText() == null || token.getText().isEmpty()) error(root, "Expected \'\'"); final String ident = token.getText().toLowerCase(Locale.ROOT); String posture = FontPosture.REGULAR.name(); if ("normal".equals(ident)) { posture = FontPosture.REGULAR.name(); } else if ("italic".equals(ident)) { posture = FontPosture.ITALIC.name(); } else if ("oblique".equals(ident)) { posture = FontPosture.ITALIC.name(); } else if ("inherit".equals(ident)) { posture = "inherit"; } else { return null; } return new ParsedValueImpl<>(posture, FontConverter.FontStyleConverter.getInstance()); } /* http://www.w3.org/TR/css3-fonts/#font-weight-the-font-weight-property */ private ParsedValueImpl parseFontWeight(Term root) throws ParseException { if (root == null) return null; final Token token = root.token; if (token == null || token.getText() == null || token.getText().isEmpty()) error(root, "Expected \'\'"); final String ident = token.getText().toLowerCase(Locale.ROOT); String weight = FontWeight.NORMAL.name(); if ("inherit".equals(ident)) { weight = FontWeight.NORMAL.name(); } else if ("normal".equals(ident)) { weight = FontWeight.NORMAL.name(); } else if ("bold".equals(ident)) { weight = FontWeight.BOLD.name(); } else if ("bolder".equals(ident)) { weight = FontWeight.BOLD.name(); } else if ("lighter".equals(ident)) { weight = FontWeight.LIGHT.name(); } else if ("100".equals(ident)) { weight = FontWeight.findByWeight(100).name(); } else if ("200".equals(ident)) { weight = FontWeight.findByWeight(200).name(); } else if ("300".equals(ident)) { weight = FontWeight.findByWeight(300).name(); } else if ("400".equals(ident)) { weight = FontWeight.findByWeight(400).name(); } else if ("500".equals(ident)) { weight = FontWeight.findByWeight(500).name(); } else if ("600".equals(ident)) { weight = FontWeight.findByWeight(600).name(); } else if ("700".equals(ident)) { weight = FontWeight.findByWeight(700).name(); } else if ("800".equals(ident)) { weight = FontWeight.findByWeight(800).name(); } else if ("900".equals(ident)) { weight = FontWeight.findByWeight(900).name(); } else { error(root, "Expected \'\'"); } return new ParsedValueImpl<>(weight, FontConverter.FontWeightConverter.getInstance()); } private ParsedValueImpl parseFontFamily(Term root) throws ParseException { if (root == null) return null; final Token token = root.token; String text = null; if (token == null || (token.getType() != CssLexer.IDENT && token.getType() != CssLexer.STRING) || (text = token.getText()) == null || text.isEmpty()) error(root, "Expected \'\'"); final String fam = stripQuotes(text.toLowerCase(Locale.ROOT)); if ("inherit".equals(fam)) { return new ParsedValueImpl<>("inherit", StringConverter.getInstance()); } else if ("serif".equals(fam) || "sans-serif".equals(fam) || "cursive".equals(fam) || "fantasy".equals(fam) || "monospace".equals(fam)) { return new ParsedValueImpl<>(fam, StringConverter.getInstance()); } else { return new ParsedValueImpl<>(token.getText(), StringConverter.getInstance()); } } // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily private ParsedValueImpl parseFont(Term root) throws ParseException { // Because style, variant, weight, size and family can inherit // AND style, variant and weight are optional, parsing this backwards // is easier. Term next = root.nextInSeries; root.nextInSeries = null; while (next != null) { Term temp = next.nextInSeries; next.nextInSeries = root; root = next; next = temp; } // Now, root should point to fontFamily Token token = root.token; int ttype = token.getType(); if (ttype != CssLexer.IDENT && ttype != CssLexer.STRING) error(root, "Expected \'\'"); ParsedValueImpl ffamily = parseFontFamily(root); Term term = root; if ((term = term.nextInSeries) == null) error(root, "Expected \'\'"); if (term.token == null || !isSize(term.token)) error(term, "Expected \'\'"); // Now, term could be the font size or it could be the line-height. // If the next term is a forward slash, then it's line-height. Term temp; if (((temp = term.nextInSeries) != null) && (temp.token != null && temp.token.getType() == CssLexer.SOLIDUS)) { root = temp; if ((term = temp.nextInSeries) == null) error(root, "Expected \'\'"); if (term.token == null || !isSize(term.token)) error(term, "Expected \'\'"); token = term.token; } ParsedValueImpl,Number> fsize = parseFontSize(term); if (fsize == null) error(root, "Expected \'\'"); ParsedValueImpl fstyle = null; ParsedValueImpl fweight = null; String fvariant = null; while ((term = term.nextInSeries) != null) { if (term.token == null || term.token.getType() != CssLexer.IDENT || term.token.getText() == null || term.token.getText().isEmpty()) error(term, "Expected \'\', \'\' or \'\'"); if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) { } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) { fvariant = term.token.getText(); } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) { } } ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle }; return new ParsedValueImpl<>(values, FontConverter.getInstance()); } // https://www.w3.org/TR/css-transitions-1/#transition-shorthand-property private ParsedValueImpl[], TransitionDefinition[]> parseTransitionLayers(Term term) throws ParseException { int nLayers = numberOfLayers(term); ParsedValue[] layers = new ParsedValue[nLayers]; for (int i = 0; i < nLayers; ++i) { layers[i] = parseTransition(term); term = nextLayer(term); } return new ParsedValueImpl[], TransitionDefinition[]>( layers, TransitionDefinitionConverter.SequenceConverter.getInstance()); } private ParsedValueImpl parseTransition(Term term) throws ParseException { ParsedValue parsedProperty = null; ParsedValue, Duration> parsedDuration = null; ParsedValue, Duration> parsedDelay = null; ParsedValue parsedTimingFunction = null; for (int i = 0; i < 4; ++i) { if (term == null) { break; } if (isEasingFunction(term.token)) { if (parsedTimingFunction != null) { error(term, "Expected \'\' or \'\'"); } parsedTimingFunction = parseEasingFunction(term); } else if (isTransitionProperty(term.token)) { if (parsedProperty != null) { error(term, "Expected \'\' or \'\'"); } parsedProperty = parseTransitionProperty(term); } else if (isTime(term.token)) { if (parsedDuration == null) { parsedDuration = parseDuration(term, false); } else if (parsedDelay == null) { parsedDelay = parseDuration(term, true); } } else { List args = new ArrayList<>(); if (parsedTimingFunction == null) args.add("\'\'"); if (parsedProperty == null) args.add("\'\'"); if (parsedDuration == null || parsedDelay == null) args.add("\'\'"); error(term, "Expected " + String.join(" or ", args)); } term = term.nextInSeries; } if (parsedProperty == null && parsedDuration == null && parsedTimingFunction == null) { error(term, "Expected \'#\'"); } return new ParsedValueImpl(new ParsedValue[] { parsedProperty, parsedDuration, parsedDelay, parsedTimingFunction }, TransitionDefinitionConverter.getInstance()); } /* * https://www.w3.org/TR/css-transitions-1/#transition-property-property */ private ParsedValueImpl[], String[]> parseTransitionPropertyLayers(Term term) throws ParseException { int nLayers = numberOfLayers(term); ParsedValue[] layers = new ParsedValue[nLayers]; for (int i = 0; i < nLayers; ++i) { layers[i] = parseTransitionProperty(term); term = nextLayer(term); } return new ParsedValueImpl[], String[]>( layers, StringConverter.SequenceConverter.getInstance()); } private ParsedValueImpl parseTransitionProperty(Term term) throws ParseException { if (term == null || !isTransitionProperty(term.token)) { error(term, "Expected \'\'"); } return new ParsedValueImpl(term.token.getText(), null); } private boolean isTransitionProperty(Token token) { int ttype; String str; return token != null && ((ttype = token.getType()) == CssLexer.STRING || ttype == CssLexer.IDENT) && (str = token.getText()) != null && !str.isEmpty(); } /* * https://www.w3.org/TR/css-easing-1/#easing-functions */ private ParsedValueImpl[], Interpolator[]> parseEasingFunctionLayers(Term term) throws ParseException { int nLayers = numberOfLayers(term); ParsedValue[] layers = new ParsedValue[nLayers]; for (int i = 0; i < nLayers; ++i) { layers[i] = parseEasingFunction(term); term = nextLayer(term); } return new ParsedValueImpl[], Interpolator[]>( layers, InterpolatorConverter.SequenceConverter.getInstance()); } private ParsedValueImpl parseEasingFunction(Term term) throws ParseException { if (term == null || !isEasingFunction(term.token)) { error(term, "Expected \'\'"); } return switch (term.token.getText()) { case "cubic-bezier(" -> { Double[] args = new Double[4]; Term arg = term.firstArg; for (int j = 0; j < 4; ++j, arg = arg.nextArg) { if (arg == null || arg.token == null || arg.token.getType() != CssLexer.NUMBER || (args[j] = Double.parseDouble(arg.token.getText())) < 0 || args[j] > 1) { error(arg != null ? arg : term, "Expected \'\'"); } } yield new ParsedValueImpl<>(new ParsedValueImpl[] { new ParsedValueImpl(term.token.getText(), null), new ParsedValueImpl(Arrays.asList(args), null) }, InterpolatorConverter.getInstance()); } case "steps(" -> { Object[] args = new Object[2]; Term arg = term.firstArg; if (arg == null || arg.token == null || arg.token.getType() != CssLexer.NUMBER) { error(arg, "Expected \'\'"); } else { args[0] = Integer.parseInt(arg.token.getText()); } arg = arg.nextArg; if (arg != null) { if (isStepPosition(arg.token)) { args[1] = arg.token.getText(); } else { error(arg != null ? arg : term, "Expected \'\'"); } } yield new ParsedValueImpl<>(new ParsedValueImpl[] { new ParsedValueImpl(term.token.getText(), null), new ParsedValueImpl(Arrays.asList(args), null) }, InterpolatorConverter.getInstance()); } default -> { yield new ParsedValueImpl<>( new ParsedValueImpl(term.token.getText(), null), InterpolatorConverter.getInstance()); } }; } // https://www.w3.org/TR/css-easing-1/#easing-functions // = linear | | private boolean isEasingFunction(Token token) throws ParseException { return token != null && switch (token.getText()) { case "linear" -> true; case "ease", "ease-in", "ease-out", "ease-in-out", "cubic-bezier(" -> true; case "step-start", "step-end", "steps(" -> true; case "-fx-ease-in", "-fx-ease-out", "-fx-ease-both" -> true; default -> false; }; } // https://www.w3.org/TR/css-easing-1/#step-easing-functions // = jump-start | jump-end | jump-none | jump-both | start | end private boolean isStepPosition(Token token) throws ParseException { return token != null && switch (token.getText()) { case "jump-start", "jump-end", "jump-none", "jump-both", "start", "end" -> true; default -> false; }; } // // Parser state machine // Token currentToken = null; // return the next token that is not whitespace. private Token nextToken(CssLexer lexer) { Token token = null; do { token = lexer.nextToken(); } while ((token != null) && (token.getType() == CssLexer.WS) || (token.getType() == CssLexer.NL)); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(token.toString()); } return token; } // keep track of what is in process of being parsed to avoid import loops private static Stack imports; private void parse(Stylesheet stylesheet, CssLexer lexer) { // need to read the first token currentToken = nextToken(lexer); while((currentToken != null) && (currentToken.getType() == CssLexer.AT_KEYWORD)) { currentToken = nextToken(lexer); if (currentToken == null || currentToken.getType() != CssLexer.IDENT) { // just using ParseException for a nice error message, not for throwing the exception. ParseException parseException = new ParseException("Expected IDENT", currentToken, this); final String msg = parseException.toString(); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); // get past EOL or SEMI do { currentToken = lexer.nextToken(); } while ((currentToken != null) && (currentToken.getType() == CssLexer.SEMI) || (currentToken.getType() == CssLexer.WS) || (currentToken.getType() == CssLexer.NL)); continue; } String keyword = currentToken.getText().toLowerCase(Locale.ROOT); if ("font-face".equals(keyword)) { FontFace fontFace = fontFace(lexer); if (fontFace != null) stylesheet.getFontFaces().add(fontFace); currentToken = nextToken(lexer); continue; } else if ("import".equals(keyword)) { if (CssParser.imports == null) { CssParser.imports = new Stack<>(); } if (!imports.contains(sourceOfStylesheet)) { imports.push(sourceOfStylesheet); Stylesheet importedStylesheet = handleImport(lexer); if (importedStylesheet != null) { stylesheet.importStylesheet(importedStylesheet); } imports.pop(); if (CssParser.imports.isEmpty()) { CssParser.imports = null; } } else { // Import imports import! final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]", line, pos, imports.peek()); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); } // get past EOL or SEMI do { currentToken = lexer.nextToken(); } while ((currentToken != null) && (currentToken.getType() == CssLexer.SEMI) || (currentToken.getType() == CssLexer.WS) || (currentToken.getType() == CssLexer.NL)); continue; } } while ((currentToken != null) && (currentToken.getType() != Token.EOF)) { List selectors = selectors(lexer); if (selectors == null) return; if ((currentToken == null) || (currentToken.getType() != CssLexer.LBRACE)) { final int line = currentToken != null ? currentToken.getLine() : -1; final int pos = currentToken != null ? currentToken.getOffset() : -1; final String msg = MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]", line, pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); currentToken = null; return; } // get past the LBRACE currentToken = nextToken(lexer); List declarations = declarations(lexer); if (declarations == null) return; if ((currentToken != null) && (currentToken.getType() != CssLexer.RBRACE)) { final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]", line, pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); currentToken = null; return; } stylesheet.getRules().add(new Rule(selectors, declarations)); currentToken = nextToken(lexer); } currentToken = null; } private FontFace fontFace(CssLexer lexer) { final Map descriptors = new HashMap<>(); final List sources = new ArrayList<>(); while(true) { currentToken = nextToken(lexer); if (currentToken.getType() == CssLexer.IDENT) { String key = currentToken.getText(); // ignore the colon that follows currentToken = nextToken(lexer); // get the next token after colon currentToken = nextToken(lexer); // ignore all but "src" if ("src".equalsIgnoreCase(key)) { while(true) { if((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) && (currentToken.getType() != CssLexer.RBRACE) && (currentToken.getType() != Token.EOF)) { if (currentToken.getType() == CssLexer.IDENT) { // simple reference to other font-family sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.REFERENCE,currentToken.getText())); } else if (currentToken.getType() == CssLexer.URL) { // let URLConverter do the conversion ParsedValueImpl[] uriValues = new ParsedValueImpl[] { new ParsedValueImpl<>(currentToken.getText(), StringConverter.getInstance()), new ParsedValueImpl(sourceOfStylesheet, null) }; ParsedValue parsedValue = new ParsedValueImpl<>(uriValues, URLConverter.getInstance()); String urlStr = parsedValue.convert(null); URL url = null; try { URI fontUri = new URI(urlStr); url = fontUri.toURL(); } catch (URISyntaxException | MalformedURLException malf) { final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]", line, pos, urlStr); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); // skip the rest. while(currentToken != null) { int ttype = currentToken.getType(); if (ttype == CssLexer.RBRACE || ttype == Token.EOF) { return null; } currentToken = nextToken(lexer); } } String format = null; while(true) { currentToken = nextToken(lexer); final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF; if (ttype == CssLexer.FUNCTION) { if ("format(".equalsIgnoreCase(currentToken.getText())) { continue; } else { break; } } else if (ttype == CssLexer.IDENT || ttype == CssLexer.STRING) { format = Utils.stripQuotes(currentToken.getText()); } else if (ttype == CssLexer.RPAREN) { continue; } else { break; } } sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.URL,url.toExternalForm(), format)); } else if (currentToken.getType() == CssLexer.FUNCTION) { if ("local(".equalsIgnoreCase(currentToken.getText())) { // consume the function token currentToken = nextToken(lexer); // parse function contents final StringBuilder localSb = new StringBuilder(); while(true) { if((currentToken != null) && (currentToken.getType() != CssLexer.RPAREN) && (currentToken.getType() != Token.EOF)) { localSb.append(currentToken.getText()); } else { break; } currentToken = nextToken(lexer); } int start = 0, end = localSb.length(); if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++; if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --; final String local = localSb.substring(start,end); sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.LOCAL,local)); } else { // error unknown fontface src type final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Unknown @font-face src type [" + currentToken.getText() + ")] at [{0,number,#},{1,number,#}]", line, pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); } } else if (currentToken.getType() == CssLexer.COMMA) { // ignore } else { // error unexpected token final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Unexpected TOKEN [" + currentToken.getText() + "] at [{0,number,#},{1,number,#}]", line, pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); } } else { break; } currentToken = nextToken(lexer); } } else { StringBuilder descriptorVal = new StringBuilder(); while(true) { if((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) && (currentToken.getType() != Token.EOF)) { descriptorVal.append(currentToken.getText()); } else { break; } currentToken = nextToken(lexer); } descriptors.put(key,descriptorVal.toString()); } // continue; } if ((currentToken == null) || (currentToken.getType() == CssLexer.RBRACE) || (currentToken.getType() == Token.EOF)) { break; } } return new FontFaceImpl(descriptors, sources); } private Stylesheet handleImport(CssLexer lexer) { currentToken = nextToken(lexer); if (currentToken == null || currentToken.getType() == Token.EOF) { return null; } int ttype = currentToken.getType(); String fname = null; if (ttype == CssLexer.STRING || ttype == CssLexer.URL) { fname = currentToken.getText(); } Stylesheet importedStylesheet = null; final String _sourceOfStylesheet = sourceOfStylesheet; if (fname != null) { // let URLConverter do the conversion ParsedValueImpl[] uriValues = new ParsedValueImpl[] { new ParsedValueImpl<>(fname, StringConverter.getInstance()), new ParsedValueImpl(sourceOfStylesheet, null) }; ParsedValue parsedValue = new ParsedValueImpl<>(uriValues, URLConverter.getInstance()); String urlString = parsedValue.convert(null); importedStylesheet = StyleManager.loadStylesheet(urlString); // When we load an imported stylesheet, the sourceOfStylesheet field // gets set to the new stylesheet. Once it is done loading we must reset // this field back to the previous value, otherwise we will potentially // run into problems (for example, see RT-40346). sourceOfStylesheet = _sourceOfStylesheet; } if (importedStylesheet == null) { final String msg = MessageFormat.format("Could not import {0}", fname); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); } return importedStylesheet; } private List selectors(CssLexer lexer) { List selectors = new ArrayList<>(); while(true) { Selector selector = selector(lexer); if (selector == null) { // some error happened, skip the rule... while ((currentToken != null) && (currentToken.getType() != CssLexer.RBRACE) && (currentToken.getType() != Token.EOF)) { currentToken = nextToken(lexer); } // current token is either RBRACE or EOF. Calling // currentToken will get the next token or EOF. currentToken = nextToken(lexer); // skipped the last rule? if (currentToken == null || currentToken.getType() == Token.EOF) { currentToken = null; return null; } continue; } selectors.add(selector); if ((currentToken != null) && (currentToken.getType() == CssLexer.COMMA)) { // get past the comma currentToken = nextToken(lexer); continue; } // currentToken was either null or not a comma // so we are done with selectors. break; } return selectors; } @SuppressWarnings("removal") private Selector selector(CssLexer lexer) { List combinators = null; List sels = null; SimpleSelector ancestor = simpleSelector(lexer); if (ancestor == null) return null; while (true) { Combinator comb = combinator(lexer); if (comb != null) { if (combinators == null) { combinators = new ArrayList<>(); } combinators.add(comb); SimpleSelector descendant = simpleSelector(lexer); if (descendant == null) return null; if (sels == null) { sels = new ArrayList<>(); sels.add(ancestor); } sels.add(descendant); } else { break; } } // RT-15473 // We might return from selector with a NL token instead of an // LBRACE, so skip past the NL here. if (currentToken != null && currentToken.getType() == CssLexer.NL) { currentToken = nextToken(lexer); } if (sels == null) { return ancestor; } else { return new CompoundSelector(sels,combinators); } } @SuppressWarnings("removal") private SimpleSelector simpleSelector(CssLexer lexer) { String esel = "*"; // element selector. default to universal String isel = ""; // id selector List csels = null; // class selector List pclasses = null; // pseudoclasses while (true) { final int ttype = (currentToken != null) ? currentToken.getType() : Token.INVALID; switch(ttype) { // element selector case CssLexer.STAR: case CssLexer.IDENT: esel = currentToken.getText(); break; // class selector case CssLexer.DOT: currentToken = nextToken(lexer); if (currentToken != null && currentToken.getType() == CssLexer.IDENT) { if (csels == null) { csels = new ArrayList<>(); } csels.add(currentToken.getText()); } else { currentToken = Token.INVALID_TOKEN; return null; } break; // id selector case CssLexer.HASH: isel = currentToken.getText().substring(1); break; case CssLexer.COLON: currentToken = nextToken(lexer); if (currentToken != null && pclasses == null) { pclasses = new ArrayList<>(); } if (currentToken.getType() == CssLexer.IDENT) { pclasses.add(currentToken.getText()); } else if (currentToken.getType() == CssLexer.FUNCTION){ String pclass = functionalPseudo(lexer); pclasses.add(pclass); } else { currentToken = Token.INVALID_TOKEN; } if (currentToken.getType() == Token.INVALID) { return null; } break; case CssLexer.NL: case CssLexer.WS: case CssLexer.COMMA: case CssLexer.GREATER: case CssLexer.LBRACE: case Token.EOF: return new SimpleSelector(esel, csels, pclasses, isel); default: return null; } // get the next token, but don't skip whitespace // since it may be a combinator currentToken = lexer.nextToken(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(currentToken.toString()); } } } // From http://www.w3.org/TR/selectors/#grammar // functional_pseudo // : FUNCTION S* expression ')' // ; // expression // /* In CSS3, the expressions are identifiers, strings, */ // /* or of the form "an+b" */ // : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ // ; private String functionalPseudo(CssLexer lexer) { // TODO: This is not how we should handle functional pseudo-classes in the long-run! StringBuilder pclass = new StringBuilder(currentToken.getText()); while(true) { currentToken = nextToken(lexer); switch(currentToken.getType()) { // TODO: lexer doesn't really scan right and isn't CSS3, // so PLUS, '-', NUMBER, etc are all useless at this point. case CssLexer.STRING: case CssLexer.IDENT: pclass.append(currentToken.getText()); break; case CssLexer.RPAREN: pclass.append(')'); return pclass.toString(); default: currentToken = Token.INVALID_TOKEN; return null; } } } private Combinator combinator(CssLexer lexer) { Combinator combinator = null; while (true) { final int ttype = (currentToken != null) ? currentToken.getType() : Token.INVALID; switch(ttype) { case CssLexer.WS: // need to check if combinator is null since child token // might be surrounded by whitespace. if (combinator == null && " ".equals(currentToken.getText())) { combinator = Combinator.DESCENDANT; } break; case CssLexer.GREATER: // no need to check if combinator is null here combinator = Combinator.CHILD; break; case CssLexer.STAR: case CssLexer.IDENT: case CssLexer.DOT: case CssLexer.HASH: case CssLexer.COLON: return combinator; default: // only selector is expected return null; } // get the next token, but don't skip whitespace currentToken = lexer.nextToken(); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(currentToken.toString()); } } } private List declarations(CssLexer lexer) { List declarations = new ArrayList<>(); while (true) { Declaration decl = declaration(lexer); if (decl != null) { declarations.add(decl); } else { // some error happened, skip the decl... while ((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) && (currentToken.getType() != CssLexer.RBRACE) && (currentToken.getType() != Token.EOF)) { currentToken = nextToken(lexer); } // current token is either SEMI, RBRACE or EOF. if (currentToken != null && currentToken.getType() != CssLexer.SEMI) return declarations; } // declaration; declaration; ??? // RT-17830 - allow declaration;; while ((currentToken != null) && (currentToken.getType() == CssLexer.SEMI)) { currentToken = nextToken(lexer); } // if it is delcaration; declaration, then the // next token should be an IDENT. if ((currentToken != null) && (currentToken.getType() == CssLexer.IDENT)) { continue; } break; } return declarations; } private Declaration declaration(CssLexer lexer) { final int ttype = (currentToken != null) ? currentToken.getType() : Token.INVALID; if ((currentToken == null) || (currentToken.getType() != CssLexer.IDENT)) { // // RT-16547: this warning was misleading because an empty rule // not invalid. Some people put in empty rules just as placeholders. // // if (LOGGER.isLoggable(PlatformLogger.WARNING)) { // final int line = currentToken != null ? currentToken.getLine() : -1; // final int pos = currentToken != null ? currentToken.getOffset() : -1; // final String url = // (stylesheet != null && stylesheet.getUrl() != null) ? // stylesheet.getUrl().toExternalForm() : "?"; // LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]", // url,line,pos); // } return null; } String property = currentToken.getText(); currentToken = nextToken(lexer); if ((currentToken == null) || (currentToken.getType() != CssLexer.COLON)) { final int line = currentToken.getLine(); final int pos = currentToken.getOffset(); final String msg = MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]", line, pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); return null; } currentToken = nextToken(lexer); Term root = expr(lexer); ParsedValueImpl value = null; try { value = (root != null) ? valueFor(property, root, lexer) : null; } catch (ParseException re) { Token badToken = re.tok; final int line = badToken != null ? badToken.getLine() : -1; final int pos = badToken != null ? badToken.getOffset() : -1; final String msg = MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]", line,pos,re.getMessage(),property); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); return null; } boolean important = currentToken.getType() == CssLexer.IMPORTANT_SYM; if (important) currentToken = nextToken(lexer); Declaration decl = (value != null) ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null; return decl; } private Term expr(CssLexer lexer) { final Term expr = term(lexer); Term current = expr; while(true) { // if current is null, then term returned null final int ttype = (current != null && currentToken != null) ? currentToken.getType() : Token.INVALID; if (ttype == Token.INVALID) { skipExpr(lexer); return null; } else if (ttype == CssLexer.SEMI || ttype == CssLexer.IMPORTANT_SYM || ttype == CssLexer.RBRACE || ttype == Token.EOF) { return expr; } else if (ttype == CssLexer.COMMA) { // comma breaks up sequences of terms. // next series of terms chains off the last term in // the current series. currentToken = nextToken(lexer); current = current.nextLayer = term(lexer); } else { current = current.nextInSeries = term(lexer); } } } private void skipExpr(CssLexer lexer) { while(true) { currentToken = nextToken(lexer); final int ttype = (currentToken != null) ? currentToken.getType() : Token.INVALID; if (ttype == CssLexer.SEMI || ttype == CssLexer.RBRACE || ttype == Token.EOF) { return; } } } private Term term(CssLexer lexer) { final int ttype = (currentToken != null) ? currentToken.getType() : Token.INVALID; switch (ttype) { case CssLexer.NUMBER: case CssLexer.CM: case CssLexer.EMS: case CssLexer.EXS: case CssLexer.IN: case CssLexer.MM: case CssLexer.PC: case CssLexer.PT: case CssLexer.PX: case CssLexer.DEG: case CssLexer.GRAD: case CssLexer.RAD: case CssLexer.TURN: case CssLexer.PERCENTAGE: case CssLexer.SECONDS: case CssLexer.MS: break; case CssLexer.STRING: break; case CssLexer.IDENT: break; case CssLexer.HASH: break; case CssLexer.FUNCTION: case CssLexer.LPAREN: Term function = new Term(currentToken); currentToken = nextToken(lexer); Term arg = term(lexer); function.firstArg = arg; while(true) { final int operator = currentToken != null ? currentToken.getType() : Token.INVALID; if (operator == CssLexer.RPAREN) { currentToken = nextToken(lexer); return function; } else if (operator == CssLexer.COMMA) { // comma breaks up sequences of terms. // next series of terms chains off the last term in // the current series. currentToken = nextToken(lexer); arg = arg.nextArg = term(lexer); } else { arg = arg.nextInSeries = term(lexer); } } case CssLexer.URL: break; case CssLexer.SOLIDUS: break; default: final int line = currentToken != null ? currentToken.getLine() : -1; final int pos = currentToken != null ? currentToken.getOffset() : -1; final String text = currentToken != null ? currentToken.getText() : ""; final String msg = MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]", "\'",text,line,pos); ParseError error = createError(msg); if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning(error.toString()); } reportError(error); return null; // currentToken = nextToken(lexer); // // return new Term(Token.INVALID_TOKEN); } Term term = new Term(currentToken); currentToken = nextToken(lexer); return term; } /** * List of errors that may have occurred during CSS processing. * @return an {@code ObservableList} of {@code ParseError} */ public static ObservableList errorsProperty() { return StyleManager.errorsProperty(); } /** * A class that encapsulates information about the source and nature * of errors encountered while parsing CSS or applying styles to Nodes. */ public static class ParseError { /** * Returns the error message. * @return the error message */ public final String getMessage() { return message; } /** * Constructs a {@code ParseError} object with the message. * @param message the message */ public ParseError(String message) { this.message = message; } final String message; @Override public String toString() { return "CSS Error: " + message; } /** Encapsulate errors arising from parsing of stylesheet files */ public final static class StylesheetParsingError extends ParseError { StylesheetParsingError(String url, String message) { super(message); this.url = url; } String getURL() { return url; } private final String url; @Override public String toString() { final String path = url != null ? url : "?"; // TBD: i18n return "CSS Error parsing " + path + ": " + message; } } /** Encapsulate errors arising from parsing of Node's style property */ public final static class InlineStyleParsingError extends ParseError { InlineStyleParsingError(Styleable styleable, String message) { super(message); this.styleable = styleable; } Styleable getStyleable() { return styleable; } private final Styleable styleable; @Override public String toString() { final String inlineStyle = styleable.getStyle(); final String source = styleable.toString(); // TBD: i18n return "CSS Error parsing in-line style \'" + inlineStyle + "\' from " + source + ": " + message; } } /** * Encapsulate errors arising from parsing when the style is not * an in-line style nor is the style from a stylesheet. Primarily to * support unit testing. */ public final static class StringParsingError extends ParseError { private final String style; StringParsingError(String style, String message) { super(message); this.style = style; } String getStyle() { return style; } @Override public String toString() { // TBD: i18n return "CSS Error parsing \'" + style + ": " + message; } } /** Encapsulates errors arising from applying a style to a Node. */ public final static class PropertySetError extends ParseError { private final CssMetaData styleableProperty; private final Styleable styleable; /** * Constructs a {@code PropertySetError} object. * @param styleableProperty CSS meta data * @param styleable styleable node * @param message parse error message */ public PropertySetError(CssMetaData styleableProperty, Styleable styleable, String message) { super(message); this.styleableProperty = styleableProperty; this.styleable = styleable; } Styleable getStyleable() { return styleable; } CssMetaData getProperty() { return styleableProperty; } @Override public String toString() { // TBD: i18n return "CSS Error parsing \'" + styleableProperty + ": " + message; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy