Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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,Size>[] sides = parseSize1to4(root);
return new ParsedValueImpl<>(sides, InsetsConverter.getInstance());
} else if ("-fx-label-padding".equals(prop)) {
ParsedValueImpl,Size>[] 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,Size>[] 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,Size> parseSize(final Term root) throws ParseException {
if (root.token == null || !isSize(root.token)) error(root, "Expected \'\'");
ParsedValueImpl,Size> 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, Size> 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, Size> 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,Color> parseColor(final Term root) throws ParseException {
ParsedValueImpl,Color> 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> 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> 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> 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> size = parseSize(arg);
Term prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Color> 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,Color>[] 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,Size> ptX = parseSize(arg);
final Term prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
final ParsedValueImpl,Size> 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,Color> colorVal = parseColor(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> radiusVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> chokeVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> offsetXVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> 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,Color> colorVal = parseColor(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> radiusVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> spreadVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> offsetXVal = parseSize(arg);
prev = arg;
if ((arg = arg.nextArg) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> 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,Size>[] 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,Size>[] 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,Size>[] startPt = null;
ParsedValueImpl,Size>[] endPt = null;
if ("from".equalsIgnoreCase(arg.token.getText())) {
prev = arg;
if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> ptX = parseSize(arg);
prev = arg;
if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'\'");
ParsedValueImpl,Size> 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,Size> 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,Size> 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,Size>[] 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,Size> 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;
ParsedValueImpl,Size>focusAngle = null;
ParsedValueImpl,Size>focusDistance = null;
ParsedValueImpl,Size>[] centerPoint = null;
ParsedValueImpl,Size>radius = 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