6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package com.sun.javafx.css.parser;
27
28 import com.sun.javafx.util.Utils;
29 import com.sun.javafx.css.Combinator;
30 import com.sun.javafx.css.CompoundSelector;
31 import com.sun.javafx.css.CssError;
32 import com.sun.javafx.css.Declaration;
33 import com.sun.javafx.css.FontFace;
34 import com.sun.javafx.css.ParsedValueImpl;
35 import com.sun.javafx.css.Rule;
36 import com.sun.javafx.css.Selector;
37 import com.sun.javafx.css.SimpleSelector;
38 import com.sun.javafx.css.Size;
39 import com.sun.javafx.css.SizeUnits;
40 import com.sun.javafx.css.StyleManager;
41 import com.sun.javafx.css.Stylesheet;
42 import com.sun.javafx.css.converters.BooleanConverter;
43 import com.sun.javafx.css.converters.DurationConverter;
44 import com.sun.javafx.css.converters.EffectConverter;
45 import com.sun.javafx.css.converters.EnumConverter;
46 import com.sun.javafx.css.converters.FontConverter;
47 import com.sun.javafx.css.converters.InsetsConverter;
48 import com.sun.javafx.css.converters.PaintConverter;
49 import com.sun.javafx.css.converters.SizeConverter;
50 import com.sun.javafx.css.converters.SizeConverter.SequenceConverter;
51 import com.sun.javafx.css.converters.StringConverter;
52 import com.sun.javafx.css.converters.URLConverter;
53 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
54 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
55 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
56 import com.sun.javafx.scene.layout.region.BorderImageSlices;
57 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
58 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
59 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
60 import com.sun.javafx.scene.layout.region.BorderStyleConverter;
61 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
62 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
63 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
64 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
65 import com.sun.javafx.scene.layout.region.Margins;
66 import com.sun.javafx.scene.layout.region.RepeatStruct;
67 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
68 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
69 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
70 import javafx.css.ParsedValue;
71 import javafx.css.StyleConverter;
72 import javafx.css.Styleable;
73 import javafx.geometry.Insets;
74 import javafx.scene.effect.BlurType;
75 import javafx.scene.effect.Effect;
76 import javafx.scene.layout.BackgroundPosition;
77 import javafx.scene.layout.BackgroundRepeat;
78 import javafx.scene.layout.BackgroundSize;
79 import javafx.scene.layout.BorderStrokeStyle;
80 import javafx.scene.layout.BorderWidths;
81 import javafx.scene.layout.CornerRadii;
82 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
83 import javafx.scene.paint.Color;
84 import javafx.scene.paint.CycleMethod;
85 import javafx.scene.paint.Paint;
86 import javafx.scene.paint.Stop;
87 import javafx.scene.shape.StrokeLineCap;
88 import javafx.scene.shape.StrokeLineJoin;
89 import javafx.scene.shape.StrokeType;
90 import javafx.scene.text.Font;
91 import javafx.scene.text.FontPosture;
92 import javafx.scene.text.FontWeight;
93 import javafx.util.Duration;
94 import sun.util.logging.PlatformLogger;
95 import sun.util.logging.PlatformLogger.Level;
96
97 import java.io.BufferedReader;
98 import java.io.CharArrayReader;
99 import java.io.IOException;
100 import java.io.InputStreamReader;
101 import java.io.Reader;
102 import java.net.MalformedURLException;
103 import java.net.URI;
104 import java.net.URISyntaxException;
105 import java.net.URL;
106 import java.text.MessageFormat;
107 import java.util.ArrayList;
108 import java.util.Collections;
109 import java.util.HashMap;
110 import java.util.List;
111 import java.util.Locale;
112 import java.util.Map;
113 import java.util.Stack;
114
115 final public class CSSParser {
116
117 /**
118 * @deprecated As of 8u40, use new CSSParser() instead.
119 */
120 @Deprecated
121 public static CSSParser getInstance() {
122 return new CSSParser();
123 }
124
125 public CSSParser() {
126 properties = new HashMap<String,String>();
127 }
128
129 // stylesheet as a string from parse method. This will be null if the
130 // stylesheet is being parsed from a file; otherwise, the parser is parsing
131 // a string and this is that string.
132 private String stylesheetAsText;
133
134 // the url of the stylesheet file, or the docbase of an applet. This will
135 // be null if the source is not a file or from an applet.
136 private String sourceOfStylesheet;
137
138 // the Styleable from the node with an in-line style. This will be null
139 // unless the source of the styles is a Node's styleProperty. In this case,
140 // the stylesheetString will also be set.
141 private Styleable sourceOfInlineStyle;
142
143 // source is a file
144 private void setInputSource(String url, String str) {
145 stylesheetAsText = str;
150 // source as string only
151 private void setInputSource(String str) {
152 stylesheetAsText = str;
153 sourceOfStylesheet = null;
154 sourceOfInlineStyle = null;
155 }
156
157 // source is in-line style
158 private void setInputSource(Styleable styleable) {
159 stylesheetAsText = styleable != null ? styleable.getStyle() : null;
160 sourceOfStylesheet = null;
161 sourceOfInlineStyle = styleable;
162 }
163
164 private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
165
166 private static final class ParseException extends Exception {
167 ParseException(String message) {
168 this(message,null,null);
169 }
170 ParseException(String message, Token tok, CSSParser parser) {
171 super(message);
172 this.tok = tok;
173 if (parser.sourceOfStylesheet != null) {
174 source = parser.sourceOfStylesheet;
175 } else if (parser.sourceOfInlineStyle != null) {
176 source = parser.sourceOfInlineStyle.toString();
177 } else if (parser.stylesheetAsText != null) {
178 source = parser.stylesheetAsText;
179 } else {
180 source = "?";
181 }
182 }
183 @Override public String toString() {
184 StringBuilder builder = new StringBuilder(super.getMessage());
185 builder.append(source);
186 if (tok != null) builder.append(": ").append(tok.toString());
187 return builder.toString();
188 }
189 private final Token tok;
190 private final String source;
233 *
234 *@param url URL of the stylesheet to parse
235 *@return the stylesheet
236 *@throws IOException
237 */
238 public Stylesheet parse(final URL url) throws IOException {
239
240 final String path = url != null ? url.toExternalForm() : null;
241 final Stylesheet stylesheet = new Stylesheet(path);
242 if (url != null) {
243 setInputSource(path, null);
244 try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
245 parse(stylesheet, reader);
246 }
247 }
248 return stylesheet;
249 }
250
251 /* All of the other function calls should wind up here */
252 private void parse(final Stylesheet stylesheet, final Reader reader) {
253
254 // CSSLexer lex = CSSLexer.getInstance();
255 CSSLexer lex = new CSSLexer();
256 lex.setReader(reader);
257
258 try {
259 this.parse(stylesheet, lex);
260 } catch (Exception ex) {
261 // Sometimes bad syntax causes an exception. The code should be
262 // fixed to handle the bad syntax, but the fallback is
263 // to handle the exception here. Uncaught, the exception can cause
264 // problems like RT-20311
265 reportException(ex);
266 }
267
268 }
269
270 /** Parse an in-line style from a Node */
271 public Stylesheet parseInlineStyle(final Styleable node) {
272
273 Stylesheet stylesheet = new Stylesheet();
274
275 final String stylesheetText = (node != null) ? node.getStyle() : null;
276 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
277 setInputSource(node);
278 final List<Rule> rules = new ArrayList<Rule>();
279 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
280 final CSSLexer lexer = CSSLexer.getInstance();
281 lexer.setReader(reader);
282 currentToken = nextToken(lexer);
283 final List<Declaration> declarations = declarations(lexer);
284 if (declarations != null && !declarations.isEmpty()) {
285 final Selector selector = Selector.getUniversalSelector();
286 final Rule rule = new Rule(
287 Collections.singletonList(selector),
288 declarations
289 );
290 rules.add(rule);
291 }
292 } catch (IOException ioe) {
293 } catch (Exception ex) {
294 // Sometimes bad syntax causes an exception. The code should be
295 // fixed to handle the bad syntax, but the fallback is
296 // to handle the exception here. Uncaught, the exception can cause
297 // problems like RT-20311
298 reportException(ex);
299 }
300 stylesheet.getRules().addAll(rules);
301 }
302
303 // don't retain reference to the styleable
304 setInputSource((Styleable) null);
305
306 return stylesheet;
307 }
308
309 /** convenience method for unit tests */
310 public ParsedValueImpl parseExpr(String property, String expr) {
311 if (property == null || expr == null) return null;
312
313 ParsedValueImpl value = null;
314 setInputSource(null, property + ": " + expr);
315 char buf[] = new char[expr.length() + 1];
316 System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
317 buf[buf.length-1] = ';';
318
319 try (Reader reader = new CharArrayReader(buf)) {
320 CSSLexer lex = CSSLexer.getInstance();
321 lex.setReader(reader);
322
323 currentToken = nextToken(lex);
324 CSSParser.Term term = this.expr(lex);
325 value = valueFor(property, term, lex);
326 } catch (IOException ioe) {
327 } catch (ParseException e) {
328 if (LOGGER.isLoggable(Level.WARNING)) {
329 LOGGER.warning("\"" +property + ": " + expr + "\" " + e.toString());
330 }
331 } catch (Exception ex) {
332 // Sometimes bad syntax causes an exception. The code should be
333 // fixed to handle the bad syntax, but the fallback is
334 // to handle the exception here. Uncaught, the exception can cause
335 // problems like RT-20311
336 reportException(ex);
337 }
338 return value;
339 }
340 /*
341 * Map of property names found while parsing. If a value matches a
342 * property name, then the value is a lookup.
343 */
344 private final Map<String,String> properties;
408 }
409 if (nextLayer != null) {
410 buf.append("<nextLayer>");
411 buf.append(nextLayer.toString());
412 buf.append("</nextLayer>\n");
413 }
414 if (firstArg != null) {
415 buf.append("<args>");
416 buf.append(firstArg.toString());
417 if (nextArg != null) {
418 buf.append(nextArg.toString());
419 }
420 buf.append("</args>");
421 }
422
423 return buf.toString();
424 }
425
426 }
427
428 private CssError createError(String msg) {
429
430 CssError error = null;
431 if (sourceOfStylesheet != null) {
432 error = new CssError.StylesheetParsingError(sourceOfStylesheet, msg);
433 } else if (sourceOfInlineStyle != null) {
434 error = new CssError.InlineStyleParsingError(sourceOfInlineStyle, msg);
435 } else {
436 error = new CssError.StringParsingError(stylesheetAsText, msg);
437 }
438 return error;
439 }
440
441 private void reportError(CssError error) {
442 List<CssError> errors = null;
443 if ((errors = StyleManager.getErrors()) != null) {
444 errors.add(error);
445 }
446 }
447
448 private void error(final Term root, final String msg) throws ParseException {
449
450 final Token token = root != null ? root.token : null;
451 final ParseException pe = new ParseException(msg,token,this);
452 reportError(createError(pe.toString()));
453 throw pe;
454 }
455
456 private void reportException(Exception exception) {
457
458 if (LOGGER.isLoggable(Level.WARNING)) {
459 final StackTraceElement[] stea = exception.getStackTrace();
460 if (stea.length > 0) {
461 final StringBuilder buf =
462 new StringBuilder("Please report ");
523
524 // not a color
525 return null;
526 }
527
528 private String stripQuotes(String string) {
529 return com.sun.javafx.util.Utils.stripQuotes(string);
530 }
531
532 private double clamp(double min, double val, double max) {
533 if (val < min) return min;
534 if (max < val) return max;
535 return val;
536 }
537
538 // Return true if the token is a size type or an identifier
539 // (which would indicate a lookup).
540 private boolean isSize(Token token) {
541 final int ttype = token.getType();
542 switch (ttype) {
543 case CSSLexer.NUMBER:
544 case CSSLexer.PERCENTAGE:
545 case CSSLexer.EMS:
546 case CSSLexer.EXS:
547 case CSSLexer.PX:
548 case CSSLexer.CM:
549 case CSSLexer.MM:
550 case CSSLexer.IN:
551 case CSSLexer.PT:
552 case CSSLexer.PC:
553 case CSSLexer.DEG:
554 case CSSLexer.GRAD:
555 case CSSLexer.RAD:
556 case CSSLexer.TURN:
557 return true;
558 default:
559 return token.getType() == CSSLexer.IDENT;
560 }
561 }
562
563 private Size size(final Token token) throws ParseException {
564 SizeUnits units = SizeUnits.PX;
565 // Amount to trim off the suffix, if any. Most are 2 chars.
566 int trim = 2;
567 final String sval = token.getText().trim();
568 final int len = sval.length();
569 final int ttype = token.getType();
570 switch (ttype) {
571 case CSSLexer.NUMBER:
572 units = SizeUnits.PX;
573 trim = 0;
574 break;
575 case CSSLexer.PERCENTAGE:
576 units = SizeUnits.PERCENT;
577 trim = 1;
578 break;
579 case CSSLexer.EMS:
580 units = SizeUnits.EM;
581 break;
582 case CSSLexer.EXS:
583 units = SizeUnits.EX;
584 break;
585 case CSSLexer.PX:
586 units = SizeUnits.PX;
587 break;
588 case CSSLexer.CM:
589 units = SizeUnits.CM;
590 break;
591 case CSSLexer.MM:
592 units = SizeUnits.MM;
593 break;
594 case CSSLexer.IN:
595 units = SizeUnits.IN;
596 break;
597 case CSSLexer.PT:
598 units = SizeUnits.PT;
599 break;
600 case CSSLexer.PC:
601 units = SizeUnits.PC;
602 break;
603 case CSSLexer.DEG:
604 units = SizeUnits.DEG;
605 trim = 3;
606 break;
607 case CSSLexer.GRAD:
608 units = SizeUnits.GRAD;
609 trim = 4;
610 break;
611 case CSSLexer.RAD:
612 units = SizeUnits.RAD;
613 trim = 3;
614 break;
615 case CSSLexer.TURN:
616 units = SizeUnits.TURN;
617 trim = 4;
618 break;
619 case CSSLexer.SECONDS:
620 units = SizeUnits.S;
621 trim = 1;
622 break;
623 case CSSLexer.MS:
624 units = SizeUnits.MS;
625 break;
626 default:
627 if (LOGGER.isLoggable(Level.FINEST)) {
628 LOGGER.finest("Expected \'<number>\'");
629 }
630 ParseException re = new ParseException("Expected \'<number>\'",token, this);
631 reportError(createError(re.toString()));
632 throw re;
633 }
634 // TODO: Handle NumberFormatException
635 return new Size(
636 Double.parseDouble(sval.substring(0,len-trim)),
637 units
638 );
639 }
640
641 // Count the number of terms in a series
642 private int numberOfTerms(final Term root) {
643 if (root == null) return 0;
680 return nArgs;
681 }
682
683 // Get the next layer following this term, which may be null
684 private Term nextLayer(final Term root) {
685 if (root == null) return null;
686
687 Term term = root;
688 while (term.nextInSeries != null) {
689 term = term.nextInSeries;
690 }
691 return term.nextLayer;
692 }
693
694 ////////////////////////////////////////////////////////////////////////////
695 //
696 // Parsing routines
697 //
698 ////////////////////////////////////////////////////////////////////////////
699
700 ParsedValueImpl valueFor(String property, Term root, CSSLexer lexer) throws ParseException {
701 final String prop = property.toLowerCase(Locale.ROOT);
702 properties.put(prop, prop);
703 if (root == null || root.token == null) {
704 error(root, "Expected value for property \'" + prop + "\'");
705 }
706
707 if (root.token.getType() == CSSLexer.IDENT) {
708 final String txt = root.token.getText();
709 if ("inherit".equalsIgnoreCase(txt)) {
710 return new ParsedValueImpl<String,String>("inherit", null);
711 } else if ("null".equalsIgnoreCase(txt)
712 || "none".equalsIgnoreCase(txt)) {
713 return new ParsedValueImpl<String,String>("null", null);
714 }
715 }
716 if ("-fx-fill".equals(prop)) {
717 ParsedValueImpl pv = parse(root);
718 if (pv.getConverter() == StyleConverter.getUrlConverter()) {
719 // ImagePatternConverter expects array of ParsedValue where element 0 is the URL
720 // Pending RT-33574
721 pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance());
722 }
723 return pv;
724 }
725 else if ("-fx-background-color".equals(prop)) {
726 return parsePaintLayers(root);
727 } else if ("-fx-background-image".equals(prop)) {
803 } else if ("-fx-stroke-line-cap".equals(prop)) {
804 // TODO: Figure out a way that these properties don't need to be
805 // special cased.
806 ParsedValueImpl value = parseStrokeLineCap(root);
807 if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
808 return value;
809 } else if ("-fx-stroke-type".equals(prop)) {
810 // TODO: Figure out a way that these properties don't need to be
811 // special cased.
812 ParsedValueImpl value = parseStrokeType(root);
813 if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
814 return value;
815 } else if ("-fx-font-smoothing-type".equals(prop)) {
816 // TODO: Figure out a way that these properties don't need to be
817 // special cased.
818 String str = null;
819 int ttype = -1;
820 final Token token = root.token;
821
822 if (root.token == null
823 || ((ttype = root.token.getType()) != CSSLexer.STRING
824 && ttype != CSSLexer.IDENT)
825 || (str = root.token.getText()) == null
826 || str.isEmpty()) {
827 error(root, "Expected STRING or IDENT");
828 }
829 return new ParsedValueImpl<String, String>(stripQuotes(str), null, false);
830 }
831 return parse(root);
832 }
833
834 private ParsedValueImpl parse(Term root) throws ParseException {
835
836 if (root.token == null) error(root, "Parse error");
837 final Token token = root.token;
838 ParsedValueImpl value = null; // value to return;
839
840 final int ttype = token.getType();
841 switch (ttype) {
842 case CSSLexer.NUMBER:
843 case CSSLexer.PERCENTAGE:
844 case CSSLexer.EMS:
845 case CSSLexer.EXS:
846 case CSSLexer.PX:
847 case CSSLexer.CM:
848 case CSSLexer.MM:
849 case CSSLexer.IN:
850 case CSSLexer.PT:
851 case CSSLexer.PC:
852 case CSSLexer.DEG:
853 case CSSLexer.GRAD:
854 case CSSLexer.RAD:
855 case CSSLexer.TURN:
856 if (root.nextInSeries == null) {
857 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null);
858 value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance());
859 } else {
860 ParsedValueImpl<Size,Size>[] sizeValue = parseSizeSeries(root);
861 value = new ParsedValueImpl<ParsedValue[],Number[]>(sizeValue, SizeConverter.SequenceConverter.getInstance());
862 }
863 break;
864 case CSSLexer.SECONDS:
865 case CSSLexer.MS: {
866 ParsedValue<Size, Size> sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
867 value = new ParsedValueImpl<ParsedValue<?, Size>, Duration>(sizeValue, DurationConverter.getInstance());
868 break;
869 }
870 case CSSLexer.STRING:
871 case CSSLexer.IDENT:
872 boolean isIdent = ttype == CSSLexer.IDENT;
873 final String str = stripQuotes(token.getText());
874 final String text = str.toLowerCase(Locale.ROOT);
875 if ("ladder".equals(text)) {
876 value = ladder(root);
877 } else if ("linear".equals(text) && (root.nextInSeries) != null) {
878 // if nextInSeries is null, then assume this is _not_ an old-style linear gradient
879 value = linearGradient(root);
880 } else if ("radial".equals(text) && (root.nextInSeries) != null) {
881 // if nextInSeries is null, then assume this is _not_ an old-style radial gradient
882 value = radialGradient(root);
883 } else if ("infinity".equals(text)) {
884 Size size = new Size(Double.MAX_VALUE, SizeUnits.PX);
885 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size, null);
886 value = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeValue, SizeConverter.getInstance());
887 } else if ("indefinite".equals(text)) {
888 Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX);
889 ParsedValueImpl<Size,Size> sizeValue = new ParsedValueImpl<>(size, null);
890 value = new ParsedValueImpl<ParsedValue<?,Size>,Duration>(sizeValue, DurationConverter.getInstance());
891 } else if ("true".equals(text)) {
892 // TODO: handling of boolean is really bogus
893 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance());
894 } else if ("false".equals(text)) {
895 // TODO: handling of boolean is really bogus
896 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance());
897 } else {
898 // if the property value is another property, then it needs to be looked up.
899 boolean needsLookup = isIdent && properties.containsKey(text);
900 if (needsLookup || ((value = colorValueOfString(str)) == null )) {
901 // If the value is a lookup, make sure to use the lower-case text so it matches the property
902 // in the Declaration. If the value is not a lookup, then use str since the value might
903 // be a string which could have some case sensitive meaning
904 //
905 // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201
906 value = new ParsedValueImpl<String,String>(needsLookup ? text : str, null, isIdent || needsLookup);
907 }
908 }
909 break;
910 case CSSLexer.HASH:
911 final String clr = token.getText();
912 try {
913 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null);
914 } catch (final IllegalArgumentException e) {
915 error(root, e.getMessage());
916 }
917 break;
918 case CSSLexer.FUNCTION:
919 return parseFunction(root);
920 case CSSLexer.URL:
921 return parseURI(root);
922 default:
923 final String msg = "Unknown token type: \'" + ttype + "\'";
924 error(root, msg);
925 }
926 return value;
927
928 }
929
930 /* Parse size.
931 * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
932 */
933 private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException {
934
935 if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'");
936
937 ParsedValueImpl<?,Size> value = null;
938
939 if (root.token.getType() != CSSLexer.IDENT) {
940
941 Size size = size(root.token);
942 value = new ParsedValueImpl<Size,Size>(size, null);
943
944 } else {
945
946 String key = root.token.getText();
947 value = new ParsedValueImpl<String,Size>(key, null, true);
948
949 }
950
951 return value;
952 }
953
954 private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException {
955
956 ParsedValueImpl<?,Color> color = null;
957 if (root.token != null &&
958 (root.token.getType() == CSSLexer.IDENT ||
959 root.token.getType() == CSSLexer.HASH ||
960 root.token.getType() == CSSLexer.FUNCTION)) {
961
962 color = parse(root);
963
964 } else {
965 error(root, "Expected \'<color>\'");
966 }
967 return color;
968 }
969
970 // rgb(NUMBER, NUMBER, NUMBER)
971 // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
972 // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
973 // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
974 private ParsedValueImpl rgb(Term root) throws ParseException {
975
976 // first term in the chain is the function name...
977 final String fn = (root.token != null) ? root.token.getText() : null;
978 if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
979 final String msg = "Expected \'rgb\' or \'rgba\'";
980 error(root, msg);
981 }
982
983 Term arg = root;
984 Token rtok, gtok, btok, atok;
985
986 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
987 if ((rtok = arg.token) == null ||
988 (rtok.getType() != CSSLexer.NUMBER &&
989 rtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
990
991 root = arg;
992
993 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
994 if ((gtok = arg.token) == null ||
995 (gtok.getType() != CSSLexer.NUMBER &&
996 gtok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
997
998 root = arg;
999
1000 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
1001 if ((btok = arg.token) == null ||
1002 (btok.getType() != CSSLexer.NUMBER &&
1003 btok.getType() != CSSLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
1004
1005 root = arg;
1006
1007 if ((arg = arg.nextArg) != null) {
1008 if ((atok = arg.token) == null ||
1009 atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1010 } else {
1011 atok = null;
1012 }
1013
1014 int argType = rtok.getType();
1015 if (argType != gtok.getType() || argType != btok.getType() ||
1016 (argType != CSSLexer.NUMBER && argType != CSSLexer.PERCENTAGE)) {
1017 error(root, "Argument type mistmatch");
1018 }
1019
1020 final String rtext = rtok.getText();
1021 final String gtext = gtok.getText();
1022 final String btext = btok.getText();
1023
1024 double rval = 0;
1025 double gval = 0;
1026 double bval = 0;
1027 if (argType == CSSLexer.NUMBER) {
1028 rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
1029 gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
1030 bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
1031 } else {
1032 rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
1033 gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
1034 bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
1035 }
1036
1037 final String atext = (atok != null) ? atok.getText() : null;
1038 final double aval = (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;
1039
1040 return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null);
1041
1042 }
1043
1044 // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
1045 // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
1046 private ParsedValueImpl hsb(Term root) throws ParseException {
1047
1048 // first term in the chain is the function name...
1049 final String fn = (root.token != null) ? root.token.getText() : null;
1050 if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
1051 final String msg = "Expected \'hsb\' or \'hsba\'";
1052 error(root, msg);
1053 }
1054
1055 Term arg = root;
1056 Token htok, stok, btok, atok;
1057
1058 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'");
1059 if ((htok = arg.token) == null || htok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1060
1061 root = arg;
1062
1063 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1064 if ((stok = arg.token) == null || stok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1065
1066 root = arg;
1067
1068 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1069 if ((btok = arg.token) == null || btok.getType() != CSSLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1070
1071 root = arg;
1072
1073 if ((arg = arg.nextArg) != null) {
1074 if ((atok = arg.token) == null || atok.getType() != CSSLexer.NUMBER) error(arg, "Expected \'<number>\'");
1075 } else {
1076 atok = null;
1077 }
1078
1079 final Size hval = size(htok);
1080 final Size sval = size(stok);
1081 final Size bval = size(btok);
1082
1083 final double hue = hval.pixels(); // no clamp - hue can be negative
1084 final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
1085 final double brightness = clamp(0.0f, bval.pixels(), 1.0f);
1086
1087 final Size aval = (atok != null) ? size(atok) : null;
1088 final double opacity = (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;
1089
1090 return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null);
1091 }
1092
1093 // derive(color, pct)
1094 private ParsedValueImpl<ParsedValue[],Color> derive(final Term root)
1121 // first term in the chain is the function name...
1122 final String fn = (root.token != null) ? root.token.getText() : null;
1123 if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
1124 final String msg = "Expected \'ladder\'";
1125 error(root, msg);
1126 }
1127
1128 if (LOGGER.isLoggable(Level.WARNING)) {
1129 LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
1130 }
1131
1132 Term term = root;
1133
1134 if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'");
1135 final ParsedValueImpl<?,Color> color = parse(term);
1136
1137 Term prev = term;
1138
1139 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1140 if (term.token == null ||
1141 term.token.getType() != CSSLexer.IDENT ||
1142 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1143
1144 prev = term;
1145
1146 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'");
1147
1148 int nStops = 0;
1149 Term temp = term;
1150 do {
1151 nStops += 1;
1152 // if next token type is IDENT, then we have CycleMethod
1153 } while (((temp = temp.nextInSeries) != null) &&
1154 ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1155
1156 ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
1157 values[0] = color;
1158 int stopIndex = 1;
1159 do {
1160 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1161 if (stop != null) values[stopIndex++] = stop;
1162 prev = term;
1163 } while(((term = term.nextInSeries) != null) &&
1164 (term.token.getType() == CSSLexer.LPAREN));
1165
1166 // if term is not null and the last term was not an lparen,
1167 // then term starts a new series of Paint. Point
1168 // root.nextInSeries to term so the next loop skips over the
1169 // already parsed ladder bits.
1170 if (term != null) {
1171 root.nextInSeries = term;
1172 }
1173
1174 // if term is null, then we are at the end of a series.
1175 // root points to 'ladder', now we want the next term after root
1176 // to be the term after the last stop, which may be another layer
1177 else {
1178 root.nextInSeries = null;
1179 root.nextLayer = prev.nextLayer;
1180 }
1181
1182 return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
1183 }
1184
1355
1356 ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs];
1357 for (int n=0; n<nArgs; n++) {
1358 stops[n] = new ParsedValueImpl<ParsedValue[],Stop>(
1359 new ParsedValueImpl[] {
1360 new ParsedValueImpl<Size,Size>(positions[n], null),
1361 colors[n]
1362 },
1363 StopConverter.getInstance()
1364 );
1365 }
1366
1367 return stops;
1368
1369 }
1370
1371 // parse (<number>, <number>)
1372 private ParsedValueImpl[] point(final Term root) throws ParseException {
1373
1374 if (root.token == null ||
1375 root.token.getType() != CSSLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'");
1376
1377 final String fn = root.token.getText();
1378 if (fn == null || !"(".equalsIgnoreCase(fn)) {
1379 final String msg = "Expected \'(\'";
1380 error(root, msg);
1381 }
1382
1383 Term arg = null;
1384
1385 // no <number>
1386 if ((arg = root.firstArg) == null) error(root, "Expected \'<number>\'");
1387
1388 final ParsedValueImpl<?,Size> ptX = parseSize(arg);
1389
1390 final Term prev = arg;
1391
1392 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1393
1394 final ParsedValueImpl<?,Size> ptY = parseSize(arg);
1395
1417 } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
1418 return parseRadialGradient(root);
1419 } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
1420 return parseImagePattern(root);
1421 } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
1422 return parseRepeatingImagePattern(root);
1423 } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
1424 return parseLadder(root);
1425 } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
1426 return parseRegion(root);
1427 } else {
1428 error(root, "Unexpected function \'" + fcn + "\'");
1429 }
1430 return null;
1431 }
1432
1433 private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException {
1434
1435 if (root == null) return null;
1436 if (root.token == null ||
1437 root.token.getType() != CSSLexer.IDENT ||
1438 root.token.getText() == null ||
1439 root.token.getText().isEmpty()) {
1440 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1441 error(root, msg);
1442 }
1443 final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
1444 BlurType blurType = BlurType.THREE_PASS_BOX;
1445 if ("gaussian".equals(blurStr)) {
1446 blurType = BlurType.GAUSSIAN;
1447 } else if ("one-pass-box".equals(blurStr)) {
1448 blurType = BlurType.ONE_PASS_BOX;
1449 } else if ("two-pass-box".equals(blurStr)) {
1450 blurType = BlurType.TWO_PASS_BOX;
1451 } else if ("three-pass-box".equals(blurStr)) {
1452 blurType = BlurType.THREE_PASS_BOX;
1453 } else {
1454 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1455 error(root, msg);
1456 }
1457 return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class));
1545
1546 prev = arg;
1547 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1548
1549 ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
1550
1551 ParsedValueImpl[] values = new ParsedValueImpl[] {
1552 blurVal,
1553 colorVal,
1554 radiusVal,
1555 spreadVal,
1556 offsetXVal,
1557 offsetYVal
1558 };
1559 return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance());
1560 }
1561
1562 // returns null if the Term is null or is not a cycle method.
1563 private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) {
1564 CycleMethod cycleMethod = null;
1565 if (root != null && root.token.getType() == CSSLexer.IDENT) {
1566
1567 final String text = root.token.getText().toLowerCase(Locale.ROOT);
1568 if ("repeat".equals(text)) {
1569 cycleMethod = CycleMethod.REPEAT;
1570 } else if ("reflect".equals(text)) {
1571 cycleMethod = CycleMethod.REFLECT;
1572 } else if ("no-cycle".equals(text)) {
1573 cycleMethod = CycleMethod.NO_CYCLE;
1574 }
1575 }
1576 if (cycleMethod != null)
1577 return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1578 else
1579 return null;
1580 }
1581
1582 // linear <point> TO <point> STOPS <stop>+ cycleMethod?
1583 private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException {
1584
1585 final String fn = (root.token != null) ? root.token.getText() : null;
1586 if (fn == null || !"linear".equalsIgnoreCase(fn)) {
1587 final String msg = "Expected \'linear\'";
1588 error(root, msg);
1589 }
1590
1591 if (LOGGER.isLoggable(Level.WARNING)) {
1592 LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
1593 }
1594
1595 Term term = root;
1596
1597 if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'");
1598
1599 final ParsedValueImpl<?,Size>[] startPt = point(term);
1600
1601 Term prev = term;
1602 if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
1603 if (term.token == null ||
1604 term.token.getType() != CSSLexer.IDENT ||
1605 !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");
1606
1607 prev = term;
1608 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1609
1610 final ParsedValueImpl<?,Size>[] endPt = point(term);
1611
1612 prev = term;
1613 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1614 if (term.token == null ||
1615 term.token.getType() != CSSLexer.IDENT ||
1616 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1617
1618 prev = term;
1619 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1620
1621 int nStops = 0;
1622 Term temp = term;
1623 do {
1624 nStops += 1;
1625 // if next token type is IDENT, then we have CycleMethod
1626 } while (((temp = temp.nextInSeries) != null) &&
1627 ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1628
1629 ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1630 int stopIndex = 0;
1631 do {
1632 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1633 if (stop != null) stops[stopIndex++] = stop;
1634 prev = term;
1635 } while(((term = term.nextInSeries) != null) &&
1636 (term.token.getType() == CSSLexer.LPAREN));
1637
1638 // term is either null or is a cycle method, or the start of another Paint.
1639 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1640
1641 if (cycleMethod == null) {
1642
1643 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1644
1645 // if term is not null and the last term was not a cycle method,
1646 // then term starts a new series or layer of Paint
1647 if (term != null) {
1648 root.nextInSeries = term;
1649 }
1650
1651 // if term is null, then we are at the end of a series.
1652 // root points to 'linear', now we want the next term after root
1653 // to be the term after the last stop, which may be another layer
1654 else {
1655 root.nextInSeries = null;
1656 root.nextLayer = prev.nextLayer;
1717 ParsedValueImpl<?,Size>[] startPt = null;
1718 ParsedValueImpl<?,Size>[] endPt = null;
1719
1720 if ("from".equalsIgnoreCase(arg.token.getText())) {
1721
1722 prev = arg;
1723 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1724
1725 ParsedValueImpl<?,Size> ptX = parseSize(arg);
1726
1727 prev = arg;
1728 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1729
1730 ParsedValueImpl<?,Size> ptY = parseSize(arg);
1731
1732 startPt = new ParsedValueImpl[] { ptX, ptY };
1733
1734 prev = arg;
1735 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
1736 if (arg.token == null ||
1737 arg.token.getType() != CSSLexer.IDENT ||
1738 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");
1739
1740 prev = arg;
1741 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1742
1743 ptX = parseSize(arg);
1744
1745 prev = arg;
1746 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1747
1748 ptY = parseSize(arg);
1749
1750 endPt = new ParsedValueImpl[] { ptX, ptY };
1751
1752 prev = arg;
1753 arg = arg.nextArg;
1754
1755 } else if("to".equalsIgnoreCase(arg.token.getText())) {
1756
1757 prev = arg;
1758 if ((arg = arg.nextInSeries) == null ||
1759 arg.token == null ||
1760 arg.token.getType() != CSSLexer.IDENT ||
1761 arg.token.getText().isEmpty()) {
1762 error (prev, "Expected \'<side-or-corner>\'");
1763 }
1764
1765
1766 int startX = 0;
1767 int startY = 0;
1768 int endX = 0;
1769 int endY = 0;
1770
1771 String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
1772 // The keywords denote the direction.
1773 if ("top".equals(sideOrCorner1)) {
1774 // going toward the top, then start at the bottom
1775 startY = 100;
1776 endY = 0;
1777
1778 } else if ("bottom".equals(sideOrCorner1)) {
1779 // going toward the bottom, then start at the top
1780 startY = 0;
1781 endY = 100;
1782
1783 } else if ("right".equals(sideOrCorner1)) {
1784 // going toward the right, then start at the left
1785 startX = 0;
1786 endX = 100;
1787
1788 } else if ("left".equals(sideOrCorner1)) {
1789 // going toward the left, then start at the right
1790 startX = 100;
1791 endX = 0;
1792
1793 } else {
1794 error(arg, "Invalid \'<side-or-corner>\'");
1795 }
1796
1797 prev = arg;
1798 if (arg.nextInSeries != null) {
1799 arg = arg.nextInSeries;
1800 if (arg.token != null &&
1801 arg.token.getType() == CSSLexer.IDENT &&
1802 !arg.token.getText().isEmpty()) {
1803
1804 String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);
1805
1806 // if right or left has already been given,
1807 // then either startX or endX will not be zero.
1808 if ("right".equals(sideOrCorner2) &&
1809 startX == 0 && endX == 0) {
1810 // start left, end right
1811 startX = 0;
1812 endX = 100;
1813 } else if ("left".equals(sideOrCorner2) &&
1814 startX == 0 && endX == 0) {
1815 // start right, end left
1816 startX = 100;
1817 endX = 0;
1818
1819 // if top or bottom has already been given,
1820 // then either startY or endY will not be zero.
1821 } else if("top".equals(sideOrCorner2) &&
1909 private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException {
1910
1911 final String fn = (root.token != null) ? root.token.getText() : null;
1912 if (fn == null || !"radial".equalsIgnoreCase(fn)) {
1913 final String msg = "Expected \'radial\'";
1914 error(root, msg);
1915 }
1916
1917 if (LOGGER.isLoggable(Level.WARNING)) {
1918 LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
1919 }
1920
1921 Term term = root;
1922 Term prev = root;
1923
1924 if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1925 if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1926
1927
1928 ParsedValueImpl<?,Size> focusAngle = null;
1929 if (term.token.getType() == CSSLexer.IDENT) {
1930 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1931 if ("focus-angle".equals(keyword)) {
1932
1933 prev = term;
1934 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1935 if (term.token == null) error(prev, "Expected \'<number>\'");
1936
1937 focusAngle = parseSize(term);
1938
1939 prev = term;
1940 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1941 if (term.token == null) error(term, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1942 }
1943 }
1944
1945 ParsedValueImpl<?,Size> focusDistance = null;
1946 if (term.token.getType() == CSSLexer.IDENT) {
1947 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1948 if ("focus-distance".equals(keyword)) {
1949
1950 prev = term;
1951 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1952 if (term.token == null) error(prev, "Expected \'<number>\'");
1953
1954 focusDistance = parseSize(term);
1955
1956 prev = term;
1957 if ((term = term.nextInSeries) == null) error(prev, "Expected \'center (<number>,<number>)\' or \'<size>\'");
1958 if (term.token == null) error(term, "Expected \'center (<number>,<number>)\' or \'<size>\'");
1959 }
1960 }
1961
1962 ParsedValueImpl<?,Size>[] centerPoint = null;
1963 if (term.token.getType() == CSSLexer.IDENT) {
1964 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1965 if ("center".equals(keyword)) {
1966
1967 prev = term;
1968 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'");
1969 if (term.token == null ||
1970 term.token.getType() != CSSLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'");
1971
1972 centerPoint = point(term);
1973
1974 prev = term;
1975 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'");
1976 if (term.token == null) error(term, "Expected \'<size>\'");
1977 }
1978 }
1979
1980 ParsedValueImpl<?,Size> radius = parseSize(term);
1981
1982 prev = term;
1983 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
1984 if (term.token == null ||
1985 term.token.getType() != CSSLexer.IDENT) error(term, "Expected \'stops\' keyword");
1986
1987 if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1988
1989 prev = term;
1990 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1991
1992 int nStops = 0;
1993 Term temp = term;
1994 do {
1995 nStops += 1;
1996 // if next token type is IDENT, then we have CycleMethod
1997 } while (((temp = temp.nextInSeries) != null) &&
1998 ((temp.token != null) && (temp.token.getType() == CSSLexer.LPAREN)));
1999
2000 ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
2001 int stopIndex = 0;
2002 do {
2003 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
2004 if (stop != null) stops[stopIndex++] = stop;
2005 prev = term;
2006 } while(((term = term.nextInSeries) != null) &&
2007 (term.token.getType() == CSSLexer.LPAREN));
2008
2009 // term is either null or is a cycle method, or the start of another Paint.
2010 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
2011
2012 if (cycleMethod == null) {
2013
2014 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
2015
2016 // if term is not null and the last term was not a cycle method,
2017 // then term starts a new series or layer of Paint
2018 if (term != null) {
2019 root.nextInSeries = term;
2020 }
2021
2022 // if term is null, then we are at the end of a series.
2023 // root points to 'linear', now we want the next term after root
2024 // to be the term after the last stop, which may be another layer
2025 else {
2026 root.nextInSeries = null;
2027 root.nextLayer = prev.nextLayer;
2422 }
2423 temp = nextLayer(temp);
2424 }
2425
2426 return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance());
2427 }
2428
2429 // <size> | <size> <size> <size> <size>
2430 private ParsedValueImpl<Size, Size>[] parseSizeSeries(Term root)
2431 throws ParseException {
2432
2433 if (root.token == null) error(root, "Parse error");
2434
2435 List<ParsedValueImpl<Size,Size>> sizes = new ArrayList<>();
2436
2437 Term term = root;
2438 while(term != null) {
2439 Token token = term.token;
2440 final int ttype = token.getType();
2441 switch (ttype) {
2442 case CSSLexer.NUMBER:
2443 case CSSLexer.PERCENTAGE:
2444 case CSSLexer.EMS:
2445 case CSSLexer.EXS:
2446 case CSSLexer.PX:
2447 case CSSLexer.CM:
2448 case CSSLexer.MM:
2449 case CSSLexer.IN:
2450 case CSSLexer.PT:
2451 case CSSLexer.PC:
2452 case CSSLexer.DEG:
2453 case CSSLexer.GRAD:
2454 case CSSLexer.RAD:
2455 case CSSLexer.TURN:
2456 ParsedValueImpl sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
2457 sizes.add(sizeValue);
2458 break;
2459 default:
2460 error (root, "expected series of <size>");
2461 }
2462 term = term.nextInSeries;
2463 }
2464 return sizes.toArray(new ParsedValueImpl[sizes.size()]);
2465
2466 }
2467
2468 // http://www.w3.org/TR/css3-background/#the-border-radius
2469 // <size>{1,4} [ '/' <size>{1,4}]? [',' <size>{1,4} [ '/' <size>{1,4}]?]?
2470 private ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root)
2471 throws ParseException {
2472
2473
2474 int nLayers = numberOfLayers(root);
2475
2476 Term term = root;
2477 int layer = 0;
2478 ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers];
2479
2480 while(term != null) {
2481
2482 int nHorizontalTerms = 0;
2483 Term temp = term;
2484 while (temp != null) {
2485 if (temp.token.getType() == CSSLexer.SOLIDUS) {
2486 temp = temp.nextInSeries;
2487 break;
2488 }
2489 nHorizontalTerms += 1;
2490 temp = temp.nextInSeries;
2491 };
2492
2493 int nVerticalTerms = 0;
2494 while (temp != null) {
2495 if (temp.token.getType() == CSSLexer.SOLIDUS) {
2496 error(temp, "unexpected SOLIDUS");
2497 break;
2498 }
2499 nVerticalTerms += 1;
2500 temp = temp.nextInSeries;
2501 }
2502
2503 if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) {
2504 error(root, "expected [<length>|<percentage>]{1,4} [/ [<length>|<percentage>]{1,4}]?");
2505 }
2506
2507 // used as index into margins[]. horizontal = 0, vertical = 1
2508 int orientation = 0;
2509
2510 // at most, there should be four radii in the horizontal orientation and four in the vertical.
2511 ParsedValueImpl<?,Size>[][] radii = new ParsedValueImpl[2][4];
2512
2513 ParsedValueImpl<?,Size> zero = new ParsedValueImpl<Size,Size>(new Size(0,SizeUnits.PX), null);
2514 for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; }
2515
2516 int hr = 0;
2517 int vr = 0;
2518
2519 Term lastTerm = term;
2520 while ((hr <= 4) && (vr <= 4) && (term != null)) {
2521
2522 if (term.token.getType() == CSSLexer.SOLIDUS) {
2523 orientation += 1;
2524 } else {
2525 ParsedValueImpl<?,Size> parsedValue = parseSize(term);
2526 if (orientation == 0) {
2527 radii[orientation][hr++] = parsedValue;
2528 } else {
2529 radii[orientation][vr++] = parsedValue;
2530 }
2531 }
2532 lastTerm = term;
2533 term = term.nextInSeries;
2534 }
2535
2536 //
2537 // http://www.w3.org/TR/css3-background/#the-border-radius
2538 // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left.
2539 // If bottom-left is omitted it is the same as top-right.
2540 // If bottom-right is omitted it is the same as top-left.
2541 // If top-right is omitted it is the same as top-left.
2542 //
2923 while (term != null) {
2924 layers[layer++] = parseBackgroundPosition(term);
2925 term = nextLayer(term);
2926 }
2927 return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance());
2928 }
2929
2930 /*
2931 http://www.w3.org/TR/css3-background/#the-background-repeat
2932 <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
2933 */
2934 private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root)
2935 throws ParseException {
2936
2937 BackgroundRepeat xAxis, yAxis;
2938 xAxis = yAxis = BackgroundRepeat.NO_REPEAT;
2939
2940 Term term = root;
2941
2942 if (term.token == null ||
2943 term.token.getType() != CSSLexer.IDENT ||
2944 term.token.getText() == null ||
2945 term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'");
2946
2947 String text = term.token.getText().toLowerCase(Locale.ROOT);
2948 if ("repeat-x".equals(text)) {
2949 xAxis = BackgroundRepeat.REPEAT;
2950 yAxis = BackgroundRepeat.NO_REPEAT;
2951 } else if ("repeat-y".equals(text)) {
2952 xAxis = BackgroundRepeat.NO_REPEAT;
2953 yAxis = BackgroundRepeat.REPEAT;
2954 } else if ("repeat".equals(text)) {
2955 xAxis = BackgroundRepeat.REPEAT;
2956 yAxis = BackgroundRepeat.REPEAT;
2957 } else if ("space".equals(text)) {
2958 xAxis = BackgroundRepeat.SPACE;
2959 yAxis = BackgroundRepeat.SPACE;
2960 } else if ("round".equals(text)) {
2961 xAxis = BackgroundRepeat.ROUND;
2962 yAxis = BackgroundRepeat.ROUND;
2963 } else if ("no-repeat".equals(text)) {
2964 xAxis = BackgroundRepeat.NO_REPEAT;
2965 yAxis = BackgroundRepeat.NO_REPEAT;
2966 } else if ("stretch".equals(text)) {
2967 xAxis = BackgroundRepeat.NO_REPEAT;
2968 yAxis = BackgroundRepeat.NO_REPEAT;
2969 } else {
2970 error(term, "Expected \'<repeat-style>\' " + text);
2971 }
2972
2973 if ((term = term.nextInSeries) != null &&
2974 term.token != null &&
2975 term.token.getType() == CSSLexer.IDENT &&
2976 term.token.getText() != null &&
2977 !term.token.getText().isEmpty()) {
2978
2979 text = term.token.getText().toLowerCase(Locale.ROOT);
2980 if ("repeat-x".equals(text)) {
2981 error(term, "Unexpected \'repeat-x\'");
2982 } else if ("repeat-y".equals(text)) {
2983 error(term, "Unexpected \'repeat-y\'");
2984 } else if ("repeat".equals(text)) {
2985 yAxis = BackgroundRepeat.REPEAT;
2986 } else if ("space".equals(text)) {
2987 yAxis = BackgroundRepeat.SPACE;
2988 } else if ("round".equals(text)) {
2989 yAxis = BackgroundRepeat.ROUND;
2990 } else if ("no-repeat".equals(text)) {
2991 yAxis = BackgroundRepeat.NO_REPEAT;
2992 } else if ("stretch".equals(text)) {
2993 yAxis = BackgroundRepeat.NO_REPEAT;
2994 } else {
2995 error(term, "Expected \'<repeat-style>\'");
3027 while (term != null) {
3028 layers[layer++] = parseRepeatStyle(term);
3029 term = nextLayer(term);
3030 }
3031 return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
3032 }
3033
3034 /*
3035 http://www.w3.org/TR/css3-background/#the-background-size
3036 <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
3037 */
3038 private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root)
3039 throws ParseException {
3040
3041 ParsedValueImpl<?,Size> height = null, width = null;
3042 boolean cover = false, contain = false;
3043
3044 Term term = root;
3045 if (term.token == null) error(term, "Expected \'<bg-size>\'");
3046
3047 if (term.token.getType() == CSSLexer.IDENT) {
3048 final String text =
3049 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3050
3051 if ("auto".equals(text)) {
3052 // We don't do anything because width / height are already initialized
3053 } else if ("cover".equals(text)) {
3054 cover = true;
3055 } else if ("contain".equals(text)) {
3056 contain = true;
3057 } else if ("stretch".equals(text)) {
3058 width = ONE_HUNDRED_PERCENT;
3059 height = ONE_HUNDRED_PERCENT;
3060 } else {
3061 error(term, "Expected \'auto\', \'cover\', \'contain\', or \'stretch\'");
3062 }
3063 } else if (isSize(term.token)) {
3064 width = parseSize(term);
3065 height = null;
3066 } else {
3067 error(term, "Expected \'<bg-size>\'");
3068 }
3069
3070 if ((term = term.nextInSeries) != null) {
3071 if (cover || contain) error(term, "Unexpected \'<bg-size>\'");
3072
3073 if (term.token.getType() == CSSLexer.IDENT) {
3074 final String text =
3075 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3076
3077 if ("auto".equals(text)) {
3078 height = null;
3079 } else if ("cover".equals(text)) {
3080 error(term, "Unexpected \'cover\'");
3081 } else if ("contain".equals(text)) {
3082 error(term, "Unexpected \'contain\'");
3083 } else if ("stretch".equals(text)) {
3084 height = ONE_HUNDRED_PERCENT;
3085 } else {
3086 error(term, "Expected \'auto\' or \'stretch\'");
3087 }
3088 } else if (isSize(term.token)) {
3089 height = parseSize(term);
3090 } else {
3091 error(term, "Expected \'<bg-size>\'");
3092 }
3093
3172
3173
3174 private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>
3175 parseBorderStyleLayers(final Term root) throws ParseException {
3176
3177 int nLayers = numberOfLayers(root);
3178 ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers];
3179 int layer = 0;
3180 Term term = root;
3181 while (term != null) {
3182 layers[layer++] = parseBorderStyleSeries(term);
3183 term = nextLayer(term);
3184 }
3185 return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance());
3186 }
3187
3188 // Only meant to be used from parseBorderStyle, but might be useful elsewhere
3189 private String getKeyword(final Term term) {
3190 if (term != null &&
3191 term.token != null &&
3192 term.token.getType() == CSSLexer.IDENT &&
3193 term.token.getText() != null &&
3194 !term.token.getText().isEmpty()) {
3195
3196 return term.token.getText().toLowerCase(Locale.ROOT);
3197 }
3198 return null;
3199 }
3200
3201 //<border-style> [ , <border-style> ]*
3202 // where <border-style> =
3203 // <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]?
3204 // where <dash-style> =
3205 // [ none | solid | dotted | dashed ]
3206 private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root)
3207 throws ParseException {
3208
3209
3210 ParsedValue<ParsedValue[],Number[]> dashStyle = null;
3211 ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null;
3212 ParsedValue<String,StrokeType> strokeType = null;
3294 dashPhase,
3295 strokeType,
3296 strokeLineJoin,
3297 strokeMiterLimit,
3298 strokeLineCap
3299 };
3300
3301 return new ParsedValueImpl(values, BorderStyleConverter.getInstance());
3302 }
3303
3304 //
3305 // segments(<size> [, <size>]+) | <border-style>
3306 //
3307 private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException {
3308
3309 if (root.token == null) error(root, "Expected \'<dash-style>\'");
3310
3311 final int ttype = root.token.getType();
3312
3313 ParsedValue<ParsedValue[],Number[]> segments = null;
3314 if (ttype == CSSLexer.IDENT) {
3315 segments = borderStyle(root);
3316 } else if (ttype == CSSLexer.FUNCTION) {
3317 segments = segments(root);
3318 } else {
3319 error(root, "Expected \'<dash-style>\'");
3320 }
3321
3322 return segments;
3323 }
3324
3325 /*
3326 <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
3327 */
3328 private ParsedValue<ParsedValue[],Number[]> borderStyle(Term root)
3329 throws ParseException {
3330
3331 if (root.token == null ||
3332 root.token.getType() != CSSLexer.IDENT ||
3333 root.token.getText() == null ||
3334 root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'");
3335
3336 final String text = root.token.getText().toLowerCase(Locale.ROOT);
3337
3338 if ("none".equals(text)) {
3339 return BorderStyleConverter.NONE;
3340 } else if ("hidden".equals(text)) {
3341 // The "hidden" mode doesn't make sense for FX, because it is the
3342 // same as "none" except for border-collapsed CSS tables
3343 return BorderStyleConverter.NONE;
3344 } else if ("dotted".equals(text)) {
3345 return BorderStyleConverter.DOTTED;
3346 } else if ("dashed".equals(text)) {
3347 return BorderStyleConverter.DASHED;
3348 } else if ("solid".equals(text)) {
3349 return BorderStyleConverter.SOLID;
3350 } else if ("double".equals(text)) {
3351 error(root, "Unsupported <border-style> \'double\'");
3352 } else if ("groove".equals(text)) {
3461 /*
3462 * http://www.w3.org/TR/css3-background/#the-border-image-slice
3463 * [<number> | <percentage>]{1,4} && fill?
3464 */
3465 private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root)
3466 throws ParseException {
3467
3468 Term term = root;
3469 if (term.token == null || !isSize(term.token))
3470 error(term, "Expected \'<size>\'");
3471
3472 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3473 Boolean fill = Boolean.FALSE;
3474
3475 int inset = 0;
3476 while (inset < 4 && term != null) {
3477 insets[inset++] = parseSize(term);
3478
3479 if ((term = term.nextInSeries) != null &&
3480 term.token != null &&
3481 term.token.getType() == CSSLexer.IDENT) {
3482
3483 if("fill".equalsIgnoreCase(term.token.getText())) {
3484 fill = Boolean.TRUE;
3485 break;
3486 }
3487 }
3488 }
3489
3490 if (inset < 2) insets[1] = insets[0]; // right = top
3491 if (inset < 3) insets[2] = insets[0]; // bottom = top
3492 if (inset < 4) insets[3] = insets[1]; // left = right
3493
3494 ParsedValueImpl[] values = new ParsedValueImpl[] {
3495 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()),
3496 new ParsedValueImpl<Boolean,Boolean>(fill, null)
3497 };
3498 return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance());
3499 }
3500
3501 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]>
3514
3515 /*
3516 * http://www.w3.org/TR/css3-background/#border-image-width
3517 * [ <length> | <percentage> | <number> | auto ]{1,4}
3518 */
3519 private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root)
3520 throws ParseException {
3521
3522 Term term = root;
3523 if (term.token == null || !isSize(term.token))
3524 error(term, "Expected \'<size>\'");
3525
3526 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3527
3528 int inset = 0;
3529 while (inset < 4 && term != null) {
3530 insets[inset++] = parseSize(term);
3531
3532 if ((term = term.nextInSeries) != null &&
3533 term.token != null &&
3534 term.token.getType() == CSSLexer.IDENT) {
3535 }
3536 }
3537
3538 if (inset < 2) insets[1] = insets[0]; // right = top
3539 if (inset < 3) insets[2] = insets[0]; // bottom = top
3540 if (inset < 4) insets[3] = insets[1]; // left = right
3541
3542 return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance());
3543 }
3544
3545 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]>
3546 parseBorderImageWidthLayers(final Term root) throws ParseException {
3547
3548 int nLayers = numberOfLayers(root);
3549 ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers];
3550 int layer = 0;
3551 Term term = root;
3552 while (term != null) {
3553 layers[layer++] = parseBorderImageWidth(term);
3554 term = nextLayer(term);
3555 }
3556 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance());
3557 }
3558
3559 // parse a Region value
3560 // i.e., region(".styleClassForRegion") or region("#idForRegion")
3561 public static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:";
3562 private ParsedValueImpl<String,String> parseRegion(Term root)
3563 throws ParseException {
3564 // first term in the chain is the function name...
3565 final String fn = (root.token != null) ? root.token.getText() : null;
3566 if (!"region".regionMatches(true, 0, fn, 0, 6)) {
3567 error(root,"Expected \'region\'");
3568 }
3569
3570 Term arg = root.firstArg;
3571 if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3572
3573 if (arg.token == null ||
3574 arg.token.getType() != CSSLexer.STRING ||
3575 arg.token.getText() == null ||
3576 arg.token.getText().isEmpty()) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3577
3578 final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText());
3579 return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance());
3580 }
3581
3582 // url("<uri>") is tokenized by the lexer, so the root arg should be a URL token.
3583 private ParsedValueImpl<ParsedValue[],String> parseURI(Term root)
3584 throws ParseException {
3585
3586 if (root == null) error(root, "Expected \'url(\"<uri-string>\")\'");
3587
3588 if (root.token == null ||
3589 root.token.getType() != CSSLexer.URL ||
3590 root.token.getText() == null ||
3591 root.token.getText().isEmpty()) error(root, "Expected \'url(\"<uri-string>\")\'");
3592
3593 final String uri = root.token.getText();
3594 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
3595 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
3596 null // placeholder for Stylesheet URL
3597 };
3598 return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
3599 }
3600
3601 // parse a series of URI values separated by commas.
3602 // i.e., <uri> [, <uri>]*
3603 private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root)
3604 throws ParseException {
3605
3606 int nLayers = numberOfLayers(root);
3607
3608 Term temp = root;
3609 int layer = 0;
3614 temp = nextLayer(temp);
3615 }
3616
3617 return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance());
3618 }
3619
3620 ////////////////////////////////////////////////////////////////////////////
3621 //
3622 // http://www.w3.org/TR/css3-fonts
3623 //
3624 ////////////////////////////////////////////////////////////////////////////
3625
3626 /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */
3627 private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException {
3628
3629 if (root == null) return null;
3630 final Token token = root.token;
3631 if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'");
3632
3633 Size size = null;
3634 if (token.getType() == CSSLexer.IDENT) {
3635 final String ident = token.getText().toLowerCase(Locale.ROOT);
3636 double value = -1;
3637 if ("inherit".equals(ident)) {
3638 value = 100;
3639 } else if ("xx-small".equals(ident)) {
3640 value = 60;
3641 } else if ("x-small".equals(ident)) {
3642 value = 75;
3643 } else if ("small".equals(ident)) {
3644 value = 80;
3645 } else if ("medium".equals(ident)) {
3646 value = 100;
3647 } else if ("large".equals(ident)) {
3648 value = 120;
3649 } else if ("x-large".equals(ident)) {
3650 value = 150;
3651 } else if ("xx-large".equals(ident)) {
3652 value = 200;
3653 } else if ("smaller".equals(ident)) {
3654 value = 80;
3659 if (value > -1) {
3660 size = new Size(value, SizeUnits.PERCENT);
3661 }
3662 }
3663
3664 // if size is null, then size is not one of the keywords above.
3665 if (size == null) {
3666 size = size(token);
3667 }
3668
3669 ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null);
3670 return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance());
3671 }
3672
3673 /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */
3674 private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException {
3675
3676 if (root == null) return null;
3677 final Token token = root.token;
3678 if (token == null ||
3679 token.getType() != CSSLexer.IDENT ||
3680 token.getText() == null ||
3681 token.getText().isEmpty()) error(root, "Expected \'<font-style>\'");
3682
3683 final String ident = token.getText().toLowerCase(Locale.ROOT);
3684 String posture = FontPosture.REGULAR.name();
3685
3686 if ("normal".equals(ident)) {
3687 posture = FontPosture.REGULAR.name();
3688 } else if ("italic".equals(ident)) {
3689 posture = FontPosture.ITALIC.name();
3690 } else if ("oblique".equals(ident)) {
3691 posture = FontPosture.ITALIC.name();
3692 } else if ("inherit".equals(ident)) {
3693 posture = "inherit";
3694 } else {
3695 return null;
3696 }
3697
3698 return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance());
3699 }
3733 } else if ("600".equals(ident)) {
3734 weight = FontWeight.findByWeight(600).name();
3735 } else if ("700".equals(ident)) {
3736 weight = FontWeight.findByWeight(700).name();
3737 } else if ("800".equals(ident)) {
3738 weight = FontWeight.findByWeight(800).name();
3739 } else if ("900".equals(ident)) {
3740 weight = FontWeight.findByWeight(900).name();
3741 } else {
3742 error(root, "Expected \'<font-weight>\'");
3743 }
3744 return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance());
3745 }
3746
3747 private ParsedValueImpl<String,String> parseFontFamily(Term root) throws ParseException {
3748
3749 if (root == null) return null;
3750 final Token token = root.token;
3751 String text = null;
3752 if (token == null ||
3753 (token.getType() != CSSLexer.IDENT &&
3754 token.getType() != CSSLexer.STRING) ||
3755 (text = token.getText()) == null ||
3756 text.isEmpty()) error(root, "Expected \'<font-family>\'");
3757
3758 final String fam = stripQuotes(text.toLowerCase(Locale.ROOT));
3759 if ("inherit".equals(fam)) {
3760 return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance());
3761 } else if ("serif".equals(fam) ||
3762 "sans-serif".equals(fam) ||
3763 "cursive".equals(fam) ||
3764 "fantasy".equals(fam) ||
3765 "monospace".equals(fam)) {
3766 return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance());
3767 } else {
3768 return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance());
3769 }
3770 }
3771
3772 // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily
3773 private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException {
3774
3775 // Because style, variant, weight, size and family can inherit
3776 // AND style, variant and weight are optional, parsing this backwards
3777 // is easier.
3778 Term next = root.nextInSeries;
3779 root.nextInSeries = null;
3780 while (next != null) {
3781 Term temp = next.nextInSeries;
3782 next.nextInSeries = root;
3783 root = next;
3784 next = temp;
3785 }
3786
3787 // Now, root should point to fontFamily
3788 Token token = root.token;
3789 int ttype = token.getType();
3790 if (ttype != CSSLexer.IDENT &&
3791 ttype != CSSLexer.STRING) error(root, "Expected \'<font-family>\'");
3792 ParsedValueImpl<String,String> ffamily = parseFontFamily(root);
3793
3794 Term term = root;
3795 if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'");
3796 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3797
3798 // Now, term could be the font size or it could be the line-height.
3799 // If the next term is a forward slash, then it's line-height.
3800 Term temp;
3801 if (((temp = term.nextInSeries) != null) &&
3802 (temp.token != null && temp.token.getType() == CSSLexer.SOLIDUS)) {
3803
3804 root = temp;
3805
3806 if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'");
3807 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3808
3809 token = term.token;
3810 }
3811
3812 ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term);
3813 if (fsize == null) error(root, "Expected \'<size>\'");
3814
3815 ParsedValueImpl<String,FontPosture> fstyle = null;
3816 ParsedValueImpl<String,FontWeight> fweight = null;
3817 String fvariant = null;
3818
3819 while ((term = term.nextInSeries) != null) {
3820
3821 if (term.token == null ||
3822 term.token.getType() != CSSLexer.IDENT ||
3823 term.token.getText() == null ||
3824 term.token.getText().isEmpty())
3825 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'");
3826
3827 if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) {
3828 ;
3829 } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) {
3830 fvariant = term.token.getText();
3831 } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) {
3832 ;
3833 }
3834 }
3835
3836 ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle };
3837 return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance());
3838 }
3839
3840 //
3841 // Parser state machine
3842 //
3843 Token currentToken = null;
3844
3845 // return the next token that is not whitespace.
3846 private Token nextToken(CSSLexer lexer) {
3847
3848 Token token = null;
3849
3850 do {
3851 token = lexer.nextToken();
3852 } while ((token != null) &&
3853 (token.getType() == CSSLexer.WS) ||
3854 (token.getType() == CSSLexer.NL));
3855
3856 if (LOGGER.isLoggable(Level.FINEST)) {
3857 LOGGER.finest(token.toString());
3858 }
3859
3860 return token;
3861
3862 }
3863
3864 // keep track of what is in process of being parsed to avoid import loops
3865 private static Stack<String> imports;
3866
3867 private void parse(Stylesheet stylesheet, CSSLexer lexer) {
3868
3869 // need to read the first token
3870 currentToken = nextToken(lexer);
3871
3872 while((currentToken != null) &&
3873 (currentToken.getType() == CSSLexer.AT_KEYWORD)) {
3874
3875 currentToken = nextToken(lexer);
3876
3877 if (currentToken == null || currentToken.getType() != CSSLexer.IDENT) {
3878
3879 // just using ParseException for a nice error message, not for throwing the exception.
3880 ParseException parseException = new ParseException("Expected IDENT", currentToken, this);
3881 final String msg = parseException.toString();
3882 CssError error = createError(msg);
3883 if (LOGGER.isLoggable(Level.WARNING)) {
3884 LOGGER.warning(error.toString());
3885 }
3886 reportError(error);
3887
3888 // get past EOL or SEMI
3889 do {
3890 currentToken = lexer.nextToken();
3891 } while ((currentToken != null) &&
3892 (currentToken.getType() == CSSLexer.SEMI) ||
3893 (currentToken.getType() == CSSLexer.WS) ||
3894 (currentToken.getType() == CSSLexer.NL));
3895 continue;
3896 }
3897
3898 String keyword = currentToken.getText().toLowerCase(Locale.ROOT);
3899 if ("font-face".equals(keyword)) {
3900 FontFace fontFace = fontFace(lexer);
3901 if (fontFace != null) stylesheet.getFontFaces().add(fontFace);
3902 currentToken = nextToken(lexer);
3903 continue;
3904
3905 } else if ("import".equals(keyword)) {
3906
3907 if (CSSParser.imports == null) {
3908 CSSParser.imports = new Stack<>();
3909 }
3910
3911 if (!imports.contains(sourceOfStylesheet)) {
3912
3913 imports.push(sourceOfStylesheet);
3914
3915 Stylesheet importedStylesheet = handleImport(lexer);
3916
3917 if (importedStylesheet != null) {
3918 stylesheet.importStylesheet(importedStylesheet);
3919 }
3920
3921 imports.pop();
3922
3923 if (CSSParser.imports.isEmpty()) {
3924 CSSParser.imports = null;
3925 }
3926
3927 } else {
3928 // Import imports import!
3929 final int line = currentToken.getLine();
3930 final int pos = currentToken.getOffset();
3931 final String msg =
3932 MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]",
3933 line, pos, imports.peek());
3934 CssError error = createError(msg);
3935 if (LOGGER.isLoggable(Level.WARNING)) {
3936 LOGGER.warning(error.toString());
3937 }
3938 reportError(error);
3939 }
3940
3941 // get past EOL or SEMI
3942 do {
3943 currentToken = lexer.nextToken();
3944 } while ((currentToken != null) &&
3945 (currentToken.getType() == CSSLexer.SEMI) ||
3946 (currentToken.getType() == CSSLexer.WS) ||
3947 (currentToken.getType() == CSSLexer.NL));
3948
3949 continue;
3950
3951 }
3952 }
3953
3954 while ((currentToken != null) &&
3955 (currentToken.getType() != Token.EOF)) {
3956
3957 List<Selector> selectors = selectors(lexer);
3958 if (selectors == null) return;
3959
3960 if ((currentToken == null) ||
3961 (currentToken.getType() != CSSLexer.LBRACE)) {
3962 final int line = currentToken != null ? currentToken.getLine() : -1;
3963 final int pos = currentToken != null ? currentToken.getOffset() : -1;
3964 final String msg =
3965 MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]",
3966 line, pos);
3967 CssError error = createError(msg);
3968 if (LOGGER.isLoggable(Level.WARNING)) {
3969 LOGGER.warning(error.toString());
3970 }
3971 reportError(error);
3972 currentToken = null;
3973 return;
3974 }
3975
3976 // get past the LBRACE
3977 currentToken = nextToken(lexer);
3978
3979 List<Declaration> declarations = declarations(lexer);
3980 if (declarations == null) return;
3981
3982 if ((currentToken != null) &&
3983 (currentToken.getType() != CSSLexer.RBRACE)) {
3984 final int line = currentToken.getLine();
3985 final int pos = currentToken.getOffset();
3986 final String msg =
3987 MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]",
3988 line,pos);
3989 CssError error = createError(msg);
3990 if (LOGGER.isLoggable(Level.WARNING)) {
3991 LOGGER.warning(error.toString());
3992 }
3993 reportError(error);
3994 currentToken = null;
3995 return;
3996 }
3997
3998 stylesheet.getRules().add(new Rule(selectors, declarations));
3999
4000 currentToken = nextToken(lexer);
4001
4002 }
4003 currentToken = null;
4004 }
4005
4006 private FontFace fontFace(CSSLexer lexer) {
4007 final Map<String,String> descriptors = new HashMap<String,String>();
4008 final List<FontFace.FontFaceSrc> sources = new ArrayList<FontFace.FontFaceSrc>();
4009 while(true) {
4010 currentToken = nextToken(lexer);
4011 if (currentToken.getType() == CSSLexer.IDENT) {
4012 String key = currentToken.getText();
4013 // ignore the colon that follows
4014 currentToken = nextToken(lexer);
4015 // get the next token after colon
4016 currentToken = nextToken(lexer);
4017 // ignore all but "src"
4018 if ("src".equalsIgnoreCase(key)) {
4019 while(true) {
4020 if((currentToken != null) &&
4021 (currentToken.getType() != CSSLexer.SEMI) &&
4022 (currentToken.getType() != CSSLexer.RBRACE) &&
4023 (currentToken.getType() != Token.EOF)) {
4024
4025 if (currentToken.getType() == CSSLexer.IDENT) {
4026 // simple reference to other font-family
4027 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.REFERENCE,currentToken.getText()));
4028
4029 } else if (currentToken.getType() == CSSLexer.URL) {
4030
4031 // let URLConverter do the conversion
4032 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4033 new ParsedValueImpl<String,String>(currentToken.getText(), StringConverter.getInstance()),
4034 new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4035 };
4036 ParsedValue<ParsedValue[], String> parsedValue =
4037 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4038 String urlStr = parsedValue.convert(null);
4039
4040 URL url = null;
4041 try {
4042 URI fontUri = new URI(urlStr);
4043 url = fontUri.toURL();
4044 } catch (URISyntaxException | MalformedURLException malf) {
4045
4046 final int line = currentToken.getLine();
4047 final int pos = currentToken.getOffset();
4048 final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]",line,pos,urlStr);
4049 CssError error = createError(msg);
4050 if (LOGGER.isLoggable(Level.WARNING)) {
4051 LOGGER.warning(error.toString());
4052 }
4053 reportError(error);
4054
4055 // skip the rest.
4056 while(currentToken != null) {
4057 int ttype = currentToken.getType();
4058 if (ttype == CSSLexer.RBRACE ||
4059 ttype == Token.EOF) {
4060 return null;
4061 }
4062 currentToken = nextToken(lexer);
4063 }
4064 }
4065
4066 String format = null;
4067 while(true) {
4068 currentToken = nextToken(lexer);
4069 final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF;
4070 if (ttype == CSSLexer.FUNCTION) {
4071 if ("format(".equalsIgnoreCase(currentToken.getText())) {
4072 continue;
4073 } else {
4074 break;
4075 }
4076 } else if (ttype == CSSLexer.IDENT ||
4077 ttype == CSSLexer.STRING) {
4078
4079 format = Utils.stripQuotes(currentToken.getText());
4080 } else if (ttype == CSSLexer.RPAREN) {
4081 continue;
4082 } else {
4083 break;
4084 }
4085 }
4086 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.URL,url.toExternalForm(), format));
4087
4088 } else if (currentToken.getType() == CSSLexer.FUNCTION) {
4089 if ("local(".equalsIgnoreCase(currentToken.getText())) {
4090 // consume the function token
4091 currentToken = nextToken(lexer);
4092 // parse function contents
4093 final StringBuilder localSb = new StringBuilder();
4094 while(true) {
4095 if((currentToken != null) && (currentToken.getType() != CSSLexer.RPAREN) &&
4096 (currentToken.getType() != Token.EOF)) {
4097 localSb.append(currentToken.getText());
4098 } else {
4099 break;
4100 }
4101 currentToken = nextToken(lexer);
4102 }
4103 int start = 0, end = localSb.length();
4104 if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++;
4105 if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --;
4106 final String local = localSb.substring(start,end);
4107 sources.add(new FontFace.FontFaceSrc(FontFace.FontFaceSrcType.LOCAL,local));
4108 } else {
4109 // error unknown fontface src type
4110 final int line = currentToken.getLine();
4111 final int pos = currentToken.getOffset();
4112 final String msg = MessageFormat.format("Unknown @font-face src type ["+currentToken.getText()+")] at [{0,number,#},{1,number,#}]",line,pos);
4113 CssError error = createError(msg);
4114 if (LOGGER.isLoggable(Level.WARNING)) {
4115 LOGGER.warning(error.toString());
4116 }
4117 reportError(error);
4118
4119 }
4120 } else if (currentToken.getType() == CSSLexer.COMMA) {
4121 // ignore
4122 } else {
4123 // error unexpected token
4124 final int line = currentToken.getLine();
4125 final int pos = currentToken.getOffset();
4126 final String msg = MessageFormat.format("Unexpected TOKEN ["+currentToken.getText()+"] at [{0,number,#},{1,number,#}]",line,pos);
4127 CssError error = createError(msg);
4128 if (LOGGER.isLoggable(Level.WARNING)) {
4129 LOGGER.warning(error.toString());
4130 }
4131 reportError(error);
4132 }
4133 } else {
4134 break;
4135 }
4136 currentToken = nextToken(lexer);
4137 }
4138 } else {
4139 StringBuilder descriptorVal = new StringBuilder();
4140 while(true) {
4141 if((currentToken != null) && (currentToken.getType() != CSSLexer.SEMI) &&
4142 (currentToken.getType() != Token.EOF)) {
4143 descriptorVal.append(currentToken.getText());
4144 } else {
4145 break;
4146 }
4147 currentToken = nextToken(lexer);
4148 }
4149 descriptors.put(key,descriptorVal.toString());
4150 }
4151 // continue;
4152 }
4153
4154 if ((currentToken == null) ||
4155 (currentToken.getType() == CSSLexer.RBRACE) ||
4156 (currentToken.getType() == Token.EOF)) {
4157 break;
4158 }
4159
4160 }
4161 return new FontFace(descriptors, sources);
4162 }
4163
4164 private Stylesheet handleImport(CSSLexer lexer) {
4165 currentToken = nextToken(lexer);
4166
4167 if (currentToken == null || currentToken.getType() == Token.EOF) {
4168 return null;
4169 }
4170
4171 int ttype = currentToken.getType();
4172
4173 String fname = null;
4174 if (ttype == CSSLexer.STRING || ttype == CSSLexer.URL) {
4175 fname = currentToken.getText();
4176 }
4177
4178 Stylesheet importedStylesheet = null;
4179 final String _sourceOfStylesheet = sourceOfStylesheet;
4180
4181 if (fname != null) {
4182 // let URLConverter do the conversion
4183 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4184 new ParsedValueImpl<String,String>(fname, StringConverter.getInstance()),
4185 new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4186 };
4187 ParsedValue<ParsedValue[], String> parsedValue =
4188 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4189
4190 String urlString = parsedValue.convert(null);
4191 importedStylesheet = StyleManager.loadStylesheet(urlString);
4192
4193 // When we load an imported stylesheet, the sourceOfStylesheet field
4194 // gets set to the new stylesheet. Once it is done loading we must reset
4195 // this field back to the previous value, otherwise we will potentially
4196 // run into problems (for example, see RT-40346).
4197 sourceOfStylesheet = _sourceOfStylesheet;
4198 }
4199 if (importedStylesheet == null) {
4200 final String msg =
4201 MessageFormat.format("Could not import {0}", fname);
4202 CssError error = createError(msg);
4203 if (LOGGER.isLoggable(Level.WARNING)) {
4204 LOGGER.warning(error.toString());
4205 }
4206 reportError(error);
4207 }
4208 return importedStylesheet;
4209 }
4210
4211 private List<Selector> selectors(CSSLexer lexer) {
4212
4213 List<Selector> selectors = new ArrayList<Selector>();
4214
4215 while(true) {
4216 Selector selector = selector(lexer);
4217 if (selector == null) {
4218 // some error happened, skip the rule...
4219 while ((currentToken != null) &&
4220 (currentToken.getType() != CSSLexer.RBRACE) &&
4221 (currentToken.getType() != Token.EOF)) {
4222 currentToken = nextToken(lexer);
4223 }
4224
4225 // current token is either RBRACE or EOF. Calling
4226 // currentToken will get the next token or EOF.
4227 currentToken = nextToken(lexer);
4228
4229 // skipped the last rule?
4230 if (currentToken == null || currentToken.getType() == Token.EOF) {
4231 currentToken = null;
4232 return null;
4233 }
4234
4235 continue;
4236 }
4237 selectors.add(selector);
4238
4239 if ((currentToken != null) &&
4240 (currentToken.getType() == CSSLexer.COMMA)) {
4241 // get past the comma
4242 currentToken = nextToken(lexer);
4243 continue;
4244 }
4245
4246 // currentToken was either null or not a comma
4247 // so we are done with selectors.
4248 break;
4249 }
4250
4251 return selectors;
4252 }
4253
4254 private Selector selector(CSSLexer lexer) {
4255
4256 List<Combinator> combinators = null;
4257 List<SimpleSelector> sels = null;
4258
4259 SimpleSelector ancestor = simpleSelector(lexer);
4260 if (ancestor == null) return null;
4261
4262 while (true) {
4263 Combinator comb = combinator(lexer);
4264 if (comb != null) {
4265 if (combinators == null) {
4266 combinators = new ArrayList<Combinator>();
4267 }
4268 combinators.add(comb);
4269 SimpleSelector descendant = simpleSelector(lexer);
4270 if (descendant == null) return null;
4271 if (sels == null) {
4272 sels = new ArrayList<SimpleSelector>();
4273 sels.add(ancestor);
4274 }
4275 sels.add(descendant);
4276 } else {
4277 break;
4278 }
4279 }
4280
4281 // RT-15473
4282 // We might return from selector with a NL token instead of an
4283 // LBRACE, so skip past the NL here.
4284 if (currentToken != null && currentToken.getType() == CSSLexer.NL) {
4285 currentToken = nextToken(lexer);
4286 }
4287
4288
4289 if (sels == null) {
4290 return ancestor;
4291 } else {
4292 return new CompoundSelector(sels,combinators);
4293 }
4294
4295 }
4296
4297 private SimpleSelector simpleSelector(CSSLexer lexer) {
4298
4299 String esel = "*"; // element selector. default to universal
4300 String isel = ""; // id selector
4301 List<String> csels = null; // class selector
4302 List<String> pclasses = null; // pseudoclasses
4303
4304 while (true) {
4305
4306 final int ttype =
4307 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4308
4309 switch(ttype) {
4310 // element selector
4311 case CSSLexer.STAR:
4312 case CSSLexer.IDENT:
4313 esel = currentToken.getText();
4314 break;
4315
4316 // class selector
4317 case CSSLexer.DOT:
4318 currentToken = nextToken(lexer);
4319 if (currentToken != null &&
4320 currentToken.getType() == CSSLexer.IDENT) {
4321 if (csels == null) {
4322 csels = new ArrayList<String>();
4323 }
4324 csels.add(currentToken.getText());
4325 } else {
4326 currentToken = Token.INVALID_TOKEN;
4327 return null;
4328 }
4329 break;
4330
4331 // id selector
4332 case CSSLexer.HASH:
4333 isel = currentToken.getText().substring(1);
4334 break;
4335
4336 case CSSLexer.COLON:
4337 currentToken = nextToken(lexer);
4338 if (currentToken != null && pclasses == null) {
4339 pclasses = new ArrayList<String>();
4340 }
4341
4342 if (currentToken.getType() == CSSLexer.IDENT) {
4343 pclasses.add(currentToken.getText());
4344 } else if (currentToken.getType() == CSSLexer.FUNCTION){
4345 String pclass = functionalPseudo(lexer);
4346 pclasses.add(pclass);
4347 } else {
4348 currentToken = Token.INVALID_TOKEN;
4349 }
4350
4351 if (currentToken.getType() == Token.INVALID) {
4352 return null;
4353 }
4354 break;
4355
4356 case CSSLexer.NL:
4357 case CSSLexer.WS:
4358 case CSSLexer.COMMA:
4359 case CSSLexer.GREATER:
4360 case CSSLexer.LBRACE:
4361 case Token.EOF:
4362 return new SimpleSelector(esel, csels, pclasses, isel);
4363
4364 default:
4365 return null;
4366
4367
4368 }
4369
4370 // get the next token, but don't skip whitespace
4371 // since it may be a combinator
4372 currentToken = lexer.nextToken();
4373 if (LOGGER.isLoggable(Level.FINEST)) {
4374 LOGGER.finest(currentToken.toString());
4375 }
4376 }
4377 }
4378
4379 // From http://www.w3.org/TR/selectors/#grammar
4380 // functional_pseudo
4381 // : FUNCTION S* expression ')'
4382 // ;
4383 // expression
4384 // /* In CSS3, the expressions are identifiers, strings, */
4385 // /* or of the form "an+b" */
4386 // : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
4387 // ;
4388 private String functionalPseudo(CSSLexer lexer) {
4389
4390 // TODO: This is not how we should handle functional pseudo-classes in the long-run!
4391
4392 StringBuilder pclass = new StringBuilder(currentToken.getText());
4393
4394 while(true) {
4395
4396 currentToken = nextToken(lexer);
4397
4398 switch(currentToken.getType()) {
4399
4400 // TODO: lexer doesn't really scan right and isn't CSS3,
4401 // so PLUS, '-', NUMBER, etc are all useless at this point.
4402 case CSSLexer.STRING:
4403 case CSSLexer.IDENT:
4404 pclass.append(currentToken.getText());
4405 break;
4406
4407 case CSSLexer.RPAREN:
4408 pclass.append(')');
4409 return pclass.toString();
4410
4411 default:
4412 currentToken = Token.INVALID_TOKEN;
4413 return null;
4414 }
4415 }
4416
4417 }
4418
4419 private Combinator combinator(CSSLexer lexer) {
4420
4421 Combinator combinator = null;
4422
4423 while (true) {
4424
4425 final int ttype =
4426 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4427
4428 switch(ttype) {
4429
4430 case CSSLexer.WS:
4431 // need to check if combinator is null since child token
4432 // might be surrounded by whitespace.
4433 if (combinator == null && " ".equals(currentToken.getText())) {
4434 combinator = Combinator.DESCENDANT;
4435 }
4436 break;
4437
4438 case CSSLexer.GREATER:
4439 // no need to check if combinator is null here
4440 combinator = Combinator.CHILD;
4441 break;
4442
4443 case CSSLexer.STAR:
4444 case CSSLexer.IDENT:
4445 case CSSLexer.DOT:
4446 case CSSLexer.HASH:
4447 case CSSLexer.COLON:
4448 return combinator;
4449
4450 default:
4451 // only selector is expected
4452 return null;
4453
4454 }
4455
4456 // get the next token, but don't skip whitespace
4457 currentToken = lexer.nextToken();
4458 if (LOGGER.isLoggable(Level.FINEST)) {
4459 LOGGER.finest(currentToken.toString());
4460 }
4461 }
4462 }
4463
4464 private List<Declaration> declarations(CSSLexer lexer) {
4465
4466 List<Declaration> declarations = new ArrayList<Declaration>();
4467
4468 while (true) {
4469
4470 Declaration decl = declaration(lexer);
4471 if (decl != null) {
4472 declarations.add(decl);
4473 } else {
4474 // some error happened, skip the decl...
4475 while ((currentToken != null) &&
4476 (currentToken.getType() != CSSLexer.SEMI) &&
4477 (currentToken.getType() != CSSLexer.RBRACE) &&
4478 (currentToken.getType() != Token.EOF)) {
4479 currentToken = nextToken(lexer);
4480 }
4481
4482 // current token is either SEMI, RBRACE or EOF.
4483 if (currentToken != null &&
4484 currentToken.getType() != CSSLexer.SEMI)
4485 return declarations;
4486 }
4487
4488 // declaration; declaration; ???
4489 // RT-17830 - allow declaration;;
4490 while ((currentToken != null) &&
4491 (currentToken.getType() == CSSLexer.SEMI)) {
4492 currentToken = nextToken(lexer);
4493 }
4494
4495 // if it is delcaration; declaration, then the
4496 // next token should be an IDENT.
4497 if ((currentToken != null) &&
4498 (currentToken.getType() == CSSLexer.IDENT)) {
4499 continue;
4500 }
4501
4502 break;
4503 }
4504
4505 return declarations;
4506 }
4507
4508 private Declaration declaration(CSSLexer lexer) {
4509
4510 final int ttype =
4511 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4512
4513 if ((currentToken == null) ||
4514 (currentToken.getType() != CSSLexer.IDENT)) {
4515 //
4516 // RT-16547: this warning was misleading because an empty rule
4517 // not invalid. Some people put in empty rules just as placeholders.
4518 //
4519 // if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
4520 // final int line = currentToken != null ? currentToken.getLine() : -1;
4521 // final int pos = currentToken != null ? currentToken.getOffset() : -1;
4522 // final String url =
4523 // (stylesheet != null && stylesheet.getUrl() != null) ?
4524 // stylesheet.getUrl().toExternalForm() : "?";
4525 // LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]",
4526 // url,line,pos);
4527 // }
4528 return null;
4529 }
4530
4531 String property = currentToken.getText();
4532
4533 currentToken = nextToken(lexer);
4534
4535 if ((currentToken == null) ||
4536 (currentToken.getType() != CSSLexer.COLON)) {
4537 final int line = currentToken.getLine();
4538 final int pos = currentToken.getOffset();
4539 final String msg =
4540 MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]",
4541 line,pos);
4542 CssError error = createError(msg);
4543 if (LOGGER.isLoggable(Level.WARNING)) {
4544 LOGGER.warning(error.toString());
4545 }
4546 reportError(error);
4547 return null;
4548 }
4549
4550 currentToken = nextToken(lexer);
4551
4552 Term root = expr(lexer);
4553 ParsedValueImpl value = null;
4554 try {
4555 value = (root != null) ? valueFor(property, root, lexer) : null;
4556 } catch (ParseException re) {
4557 Token badToken = re.tok;
4558 final int line = badToken != null ? badToken.getLine() : -1;
4559 final int pos = badToken != null ? badToken.getOffset() : -1;
4560 final String msg =
4561 MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]",
4562 line,pos,re.getMessage(),property);
4563 CssError error = createError(msg);
4564 if (LOGGER.isLoggable(Level.WARNING)) {
4565 LOGGER.warning(error.toString());
4566 }
4567 reportError(error);
4568 return null;
4569 }
4570
4571 boolean important = currentToken.getType() == CSSLexer.IMPORTANT_SYM;
4572 if (important) currentToken = nextToken(lexer);
4573
4574 Declaration decl = (value != null)
4575 ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null;
4576 return decl;
4577 }
4578
4579 private Term expr(CSSLexer lexer) {
4580
4581 final Term expr = term(lexer);
4582 Term current = expr;
4583
4584 while(true) {
4585
4586 // if current is null, then term returned null
4587 final int ttype =
4588 (current != null && currentToken != null)
4589 ? currentToken.getType() : Token.INVALID;
4590
4591 if (ttype == Token.INVALID) {
4592 skipExpr(lexer);
4593 return null;
4594 } else if (ttype == CSSLexer.SEMI ||
4595 ttype == CSSLexer.IMPORTANT_SYM ||
4596 ttype == CSSLexer.RBRACE ||
4597 ttype == Token.EOF) {
4598 return expr;
4599 } else if (ttype == CSSLexer.COMMA) {
4600 // comma breaks up sequences of terms.
4601 // next series of terms chains off the last term in
4602 // the current series.
4603 currentToken = nextToken(lexer);
4604 current = current.nextLayer = term(lexer);
4605 } else {
4606 current = current.nextInSeries = term(lexer);
4607 }
4608
4609 }
4610 }
4611
4612 private void skipExpr(CSSLexer lexer) {
4613
4614 while(true) {
4615
4616 currentToken = nextToken(lexer);
4617
4618 final int ttype =
4619 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4620
4621 if (ttype == CSSLexer.SEMI ||
4622 ttype == CSSLexer.RBRACE ||
4623 ttype == Token.EOF) {
4624 return;
4625 }
4626 }
4627 }
4628
4629 private Term term(CSSLexer lexer) {
4630
4631 final int ttype =
4632 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4633
4634 switch (ttype) {
4635
4636 case CSSLexer.NUMBER:
4637 case CSSLexer.CM:
4638 case CSSLexer.EMS:
4639 case CSSLexer.EXS:
4640 case CSSLexer.IN:
4641 case CSSLexer.MM:
4642 case CSSLexer.PC:
4643 case CSSLexer.PT:
4644 case CSSLexer.PX:
4645 case CSSLexer.DEG:
4646 case CSSLexer.GRAD:
4647 case CSSLexer.RAD:
4648 case CSSLexer.TURN:
4649 case CSSLexer.PERCENTAGE:
4650 case CSSLexer.SECONDS:
4651 case CSSLexer.MS:
4652 break;
4653
4654 case CSSLexer.STRING:
4655 break;
4656 case CSSLexer.IDENT:
4657 break;
4658
4659 case CSSLexer.HASH:
4660 break;
4661
4662 case CSSLexer.FUNCTION:
4663 case CSSLexer.LPAREN:
4664
4665 Term function = new Term(currentToken);
4666 currentToken = nextToken(lexer);
4667
4668 Term arg = term(lexer);
4669 function.firstArg = arg;
4670
4671 while(true) {
4672
4673 final int operator =
4674 currentToken != null ? currentToken.getType() : Token.INVALID;
4675
4676 if (operator == CSSLexer.RPAREN) {
4677 currentToken = nextToken(lexer);
4678 return function;
4679 } else if (operator == CSSLexer.COMMA) {
4680 // comma breaks up sequences of terms.
4681 // next series of terms chains off the last term in
4682 // the current series.
4683 currentToken = nextToken(lexer);
4684 arg = arg.nextArg = term(lexer);
4685
4686 } else {
4687 arg = arg.nextInSeries = term(lexer);
4688 }
4689
4690 }
4691
4692 case CSSLexer.URL:
4693 break;
4694
4695 case CSSLexer.SOLIDUS:
4696 break;
4697
4698 default:
4699 final int line = currentToken != null ? currentToken.getLine() : -1;
4700 final int pos = currentToken != null ? currentToken.getOffset() : -1;
4701 final String text = currentToken != null ? currentToken.getText() : "";
4702 final String msg =
4703 MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]",
4704 "\'",text,line,pos);
4705 CssError error = createError(msg);
4706 if (LOGGER.isLoggable(Level.WARNING)) {
4707 LOGGER.warning(error.toString());
4708 }
4709 reportError(error);
4710 return null;
4711 // currentToken = nextToken(lexer);
4712 //
4713 // return new Term(Token.INVALID_TOKEN);
4714 }
4715
4716 Term term = new Term(currentToken);
4717 currentToken = nextToken(lexer);
4718 return term;
4719 }
4720 }
|
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.css;
27
28 import com.sun.javafx.css.Combinator;
29 import com.sun.javafx.css.FontFaceImpl;
30 import com.sun.javafx.css.ParsedValueImpl;
31 import com.sun.javafx.css.StyleManager;
32 import com.sun.javafx.util.Utils;
33 import javafx.css.converter.BooleanConverter;
34 import javafx.css.converter.DurationConverter;
35 import javafx.css.converter.EffectConverter;
36 import javafx.css.converter.EnumConverter;
37 import javafx.css.converter.FontConverter;
38 import javafx.css.converter.InsetsConverter;
39 import javafx.css.converter.PaintConverter;
40 import javafx.css.converter.SizeConverter;
41 import javafx.css.converter.SizeConverter.SequenceConverter;
42 import javafx.css.converter.StringConverter;
43 import javafx.css.converter.URLConverter;
44 import javafx.css.converter.DeriveColorConverter;
45 import javafx.css.converter.LadderConverter;
46 import javafx.css.converter.StopConverter;
47 import com.sun.javafx.css.parser.Token;
48 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter;
49 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter;
50 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter;
51 import com.sun.javafx.scene.layout.region.BorderImageSlices;
52 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter;
53 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter;
54 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter;
55 import com.sun.javafx.scene.layout.region.BorderStyleConverter;
56 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
57 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
58 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
59 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter;
60 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter;
61 import com.sun.javafx.scene.layout.region.Margins;
62 import com.sun.javafx.scene.layout.region.RepeatStruct;
63 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
64 import com.sun.javafx.scene.layout.region.SliceSequenceConverter;
65 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter;
66 import javafx.collections.ObservableList;
67 import javafx.geometry.Insets;
68 import javafx.scene.effect.BlurType;
69 import javafx.scene.effect.Effect;
70 import javafx.scene.layout.BackgroundPosition;
71 import javafx.scene.layout.BackgroundRepeat;
72 import javafx.scene.layout.BackgroundSize;
73 import javafx.scene.layout.BorderStrokeStyle;
74 import javafx.scene.layout.BorderWidths;
75 import javafx.scene.layout.CornerRadii;
76 import javafx.scene.paint.Color;
77 import javafx.scene.paint.CycleMethod;
78 import javafx.scene.paint.Paint;
79 import javafx.scene.paint.Stop;
80 import javafx.scene.shape.StrokeLineCap;
81 import javafx.scene.shape.StrokeLineJoin;
82 import javafx.scene.shape.StrokeType;
83 import javafx.scene.text.Font;
84 import javafx.scene.text.FontPosture;
85 import javafx.scene.text.FontWeight;
86 import javafx.util.Duration;
87 import sun.util.logging.PlatformLogger;
88 import sun.util.logging.PlatformLogger.Level;
89
90 import java.io.BufferedReader;
91 import java.io.CharArrayReader;
92 import java.io.IOException;
93 import java.io.InputStreamReader;
94 import java.io.Reader;
95 import java.net.MalformedURLException;
96 import java.net.URI;
97 import java.net.URISyntaxException;
98 import java.net.URL;
99 import java.text.MessageFormat;
100 import java.util.ArrayList;
101 import java.util.Collections;
102 import java.util.HashMap;
103 import java.util.List;
104 import java.util.Locale;
105 import java.util.Map;
106 import java.util.Stack;
107
108 /**
109 * @since 9
110 */
111 final public class CssParser {
112
113 public CssParser() {
114 properties = new HashMap<String,String>();
115 }
116
117 // stylesheet as a string from parse method. This will be null if the
118 // stylesheet is being parsed from a file; otherwise, the parser is parsing
119 // a string and this is that string.
120 private String stylesheetAsText;
121
122 // the url of the stylesheet file, or the docbase of an applet. This will
123 // be null if the source is not a file or from an applet.
124 private String sourceOfStylesheet;
125
126 // the Styleable from the node with an in-line style. This will be null
127 // unless the source of the styles is a Node's styleProperty. In this case,
128 // the stylesheetString will also be set.
129 private Styleable sourceOfInlineStyle;
130
131 // source is a file
132 private void setInputSource(String url, String str) {
133 stylesheetAsText = str;
138 // source as string only
139 private void setInputSource(String str) {
140 stylesheetAsText = str;
141 sourceOfStylesheet = null;
142 sourceOfInlineStyle = null;
143 }
144
145 // source is in-line style
146 private void setInputSource(Styleable styleable) {
147 stylesheetAsText = styleable != null ? styleable.getStyle() : null;
148 sourceOfStylesheet = null;
149 sourceOfInlineStyle = styleable;
150 }
151
152 private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
153
154 private static final class ParseException extends Exception {
155 ParseException(String message) {
156 this(message,null,null);
157 }
158 ParseException(String message, Token tok, CssParser parser) {
159 super(message);
160 this.tok = tok;
161 if (parser.sourceOfStylesheet != null) {
162 source = parser.sourceOfStylesheet;
163 } else if (parser.sourceOfInlineStyle != null) {
164 source = parser.sourceOfInlineStyle.toString();
165 } else if (parser.stylesheetAsText != null) {
166 source = parser.stylesheetAsText;
167 } else {
168 source = "?";
169 }
170 }
171 @Override public String toString() {
172 StringBuilder builder = new StringBuilder(super.getMessage());
173 builder.append(source);
174 if (tok != null) builder.append(": ").append(tok.toString());
175 return builder.toString();
176 }
177 private final Token tok;
178 private final String source;
221 *
222 *@param url URL of the stylesheet to parse
223 *@return the stylesheet
224 *@throws IOException
225 */
226 public Stylesheet parse(final URL url) throws IOException {
227
228 final String path = url != null ? url.toExternalForm() : null;
229 final Stylesheet stylesheet = new Stylesheet(path);
230 if (url != null) {
231 setInputSource(path, null);
232 try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
233 parse(stylesheet, reader);
234 }
235 }
236 return stylesheet;
237 }
238
239 /* All of the other function calls should wind up here */
240 private void parse(final Stylesheet stylesheet, final Reader reader) {
241 CssLexer lex = new CssLexer();
242 lex.setReader(reader);
243
244 try {
245 this.parse(stylesheet, lex);
246 } catch (Exception ex) {
247 // Sometimes bad syntax causes an exception. The code should be
248 // fixed to handle the bad syntax, but the fallback is
249 // to handle the exception here. Uncaught, the exception can cause
250 // problems like RT-20311
251 reportException(ex);
252 }
253
254 }
255
256 /** Parse an in-line style from a Node */
257 public Stylesheet parseInlineStyle(final Styleable node) {
258
259 Stylesheet stylesheet = new Stylesheet();
260
261 final String stylesheetText = (node != null) ? node.getStyle() : null;
262 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) {
263 setInputSource(node);
264 final List<Rule> rules = new ArrayList<Rule>();
265 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) {
266 final CssLexer lexer = new CssLexer();
267 lexer.setReader(reader);
268 currentToken = nextToken(lexer);
269 final List<Declaration> declarations = declarations(lexer);
270 if (declarations != null && !declarations.isEmpty()) {
271 final Selector selector = Selector.getUniversalSelector();
272 final Rule rule = new Rule(
273 Collections.singletonList(selector),
274 declarations
275 );
276 rules.add(rule);
277 }
278 } catch (IOException ioe) {
279 } catch (Exception ex) {
280 // Sometimes bad syntax causes an exception. The code should be
281 // fixed to handle the bad syntax, but the fallback is
282 // to handle the exception here. Uncaught, the exception can cause
283 // problems like RT-20311
284 reportException(ex);
285 }
286 stylesheet.getRules().addAll(rules);
287 }
288
289 // don't retain reference to the styleable
290 setInputSource((Styleable) null);
291
292 return stylesheet;
293 }
294
295 /** convenience method for unit tests */
296 public ParsedValue parseExpr(String property, String expr) {
297 if (property == null || expr == null) return null;
298
299 ParsedValueImpl value = null;
300 setInputSource(null, property + ": " + expr);
301 char buf[] = new char[expr.length() + 1];
302 System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length());
303 buf[buf.length-1] = ';';
304
305 try (Reader reader = new CharArrayReader(buf)) {
306 CssLexer lex = new CssLexer();
307 lex.setReader(reader);
308
309 currentToken = nextToken(lex);
310 CssParser.Term term = this.expr(lex);
311 value = valueFor(property, term, lex);
312 } catch (IOException ioe) {
313 } catch (ParseException e) {
314 if (LOGGER.isLoggable(Level.WARNING)) {
315 LOGGER.warning("\"" +property + ": " + expr + "\" " + e.toString());
316 }
317 } catch (Exception ex) {
318 // Sometimes bad syntax causes an exception. The code should be
319 // fixed to handle the bad syntax, but the fallback is
320 // to handle the exception here. Uncaught, the exception can cause
321 // problems like RT-20311
322 reportException(ex);
323 }
324 return value;
325 }
326 /*
327 * Map of property names found while parsing. If a value matches a
328 * property name, then the value is a lookup.
329 */
330 private final Map<String,String> properties;
394 }
395 if (nextLayer != null) {
396 buf.append("<nextLayer>");
397 buf.append(nextLayer.toString());
398 buf.append("</nextLayer>\n");
399 }
400 if (firstArg != null) {
401 buf.append("<args>");
402 buf.append(firstArg.toString());
403 if (nextArg != null) {
404 buf.append(nextArg.toString());
405 }
406 buf.append("</args>");
407 }
408
409 return buf.toString();
410 }
411
412 }
413
414 private ParseError createError(String msg) {
415
416 ParseError error = null;
417 if (sourceOfStylesheet != null) {
418 error = new ParseError.StylesheetParsingError(sourceOfStylesheet, msg);
419 } else if (sourceOfInlineStyle != null) {
420 error = new ParseError.InlineStyleParsingError(sourceOfInlineStyle, msg);
421 } else {
422 error = new ParseError.StringParsingError(stylesheetAsText, msg);
423 }
424 return error;
425 }
426
427 private void reportError(ParseError error) {
428 List<ParseError> errors = null;
429 if ((errors = StyleManager.getErrors()) != null) {
430 errors.add(error);
431 }
432 }
433
434 private void error(final Term root, final String msg) throws ParseException {
435
436 final Token token = root != null ? root.token : null;
437 final ParseException pe = new ParseException(msg,token,this);
438 reportError(createError(pe.toString()));
439 throw pe;
440 }
441
442 private void reportException(Exception exception) {
443
444 if (LOGGER.isLoggable(Level.WARNING)) {
445 final StackTraceElement[] stea = exception.getStackTrace();
446 if (stea.length > 0) {
447 final StringBuilder buf =
448 new StringBuilder("Please report ");
509
510 // not a color
511 return null;
512 }
513
514 private String stripQuotes(String string) {
515 return com.sun.javafx.util.Utils.stripQuotes(string);
516 }
517
518 private double clamp(double min, double val, double max) {
519 if (val < min) return min;
520 if (max < val) return max;
521 return val;
522 }
523
524 // Return true if the token is a size type or an identifier
525 // (which would indicate a lookup).
526 private boolean isSize(Token token) {
527 final int ttype = token.getType();
528 switch (ttype) {
529 case CssLexer.NUMBER:
530 case CssLexer.PERCENTAGE:
531 case CssLexer.EMS:
532 case CssLexer.EXS:
533 case CssLexer.PX:
534 case CssLexer.CM:
535 case CssLexer.MM:
536 case CssLexer.IN:
537 case CssLexer.PT:
538 case CssLexer.PC:
539 case CssLexer.DEG:
540 case CssLexer.GRAD:
541 case CssLexer.RAD:
542 case CssLexer.TURN:
543 return true;
544 default:
545 return token.getType() == CssLexer.IDENT;
546 }
547 }
548
549 private Size size(final Token token) throws ParseException {
550 SizeUnits units = SizeUnits.PX;
551 // Amount to trim off the suffix, if any. Most are 2 chars.
552 int trim = 2;
553 final String sval = token.getText().trim();
554 final int len = sval.length();
555 final int ttype = token.getType();
556 switch (ttype) {
557 case CssLexer.NUMBER:
558 units = SizeUnits.PX;
559 trim = 0;
560 break;
561 case CssLexer.PERCENTAGE:
562 units = SizeUnits.PERCENT;
563 trim = 1;
564 break;
565 case CssLexer.EMS:
566 units = SizeUnits.EM;
567 break;
568 case CssLexer.EXS:
569 units = SizeUnits.EX;
570 break;
571 case CssLexer.PX:
572 units = SizeUnits.PX;
573 break;
574 case CssLexer.CM:
575 units = SizeUnits.CM;
576 break;
577 case CssLexer.MM:
578 units = SizeUnits.MM;
579 break;
580 case CssLexer.IN:
581 units = SizeUnits.IN;
582 break;
583 case CssLexer.PT:
584 units = SizeUnits.PT;
585 break;
586 case CssLexer.PC:
587 units = SizeUnits.PC;
588 break;
589 case CssLexer.DEG:
590 units = SizeUnits.DEG;
591 trim = 3;
592 break;
593 case CssLexer.GRAD:
594 units = SizeUnits.GRAD;
595 trim = 4;
596 break;
597 case CssLexer.RAD:
598 units = SizeUnits.RAD;
599 trim = 3;
600 break;
601 case CssLexer.TURN:
602 units = SizeUnits.TURN;
603 trim = 4;
604 break;
605 case CssLexer.SECONDS:
606 units = SizeUnits.S;
607 trim = 1;
608 break;
609 case CssLexer.MS:
610 units = SizeUnits.MS;
611 break;
612 default:
613 if (LOGGER.isLoggable(Level.FINEST)) {
614 LOGGER.finest("Expected \'<number>\'");
615 }
616 ParseException re = new ParseException("Expected \'<number>\'",token, this);
617 reportError(createError(re.toString()));
618 throw re;
619 }
620 // TODO: Handle NumberFormatException
621 return new Size(
622 Double.parseDouble(sval.substring(0,len-trim)),
623 units
624 );
625 }
626
627 // Count the number of terms in a series
628 private int numberOfTerms(final Term root) {
629 if (root == null) return 0;
666 return nArgs;
667 }
668
669 // Get the next layer following this term, which may be null
670 private Term nextLayer(final Term root) {
671 if (root == null) return null;
672
673 Term term = root;
674 while (term.nextInSeries != null) {
675 term = term.nextInSeries;
676 }
677 return term.nextLayer;
678 }
679
680 ////////////////////////////////////////////////////////////////////////////
681 //
682 // Parsing routines
683 //
684 ////////////////////////////////////////////////////////////////////////////
685
686 ParsedValueImpl valueFor(String property, Term root, CssLexer lexer) throws ParseException {
687 final String prop = property.toLowerCase(Locale.ROOT);
688 properties.put(prop, prop);
689 if (root == null || root.token == null) {
690 error(root, "Expected value for property \'" + prop + "\'");
691 }
692
693 if (root.token.getType() == CssLexer.IDENT) {
694 final String txt = root.token.getText();
695 if ("inherit".equalsIgnoreCase(txt)) {
696 return new ParsedValueImpl<String,String>("inherit", null);
697 } else if ("null".equalsIgnoreCase(txt)
698 || "none".equalsIgnoreCase(txt)) {
699 return new ParsedValueImpl<String,String>("null", null);
700 }
701 }
702 if ("-fx-fill".equals(prop)) {
703 ParsedValueImpl pv = parse(root);
704 if (pv.getConverter() == StyleConverter.getUrlConverter()) {
705 // ImagePatternConverter expects array of ParsedValue where element 0 is the URL
706 // Pending RT-33574
707 pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance());
708 }
709 return pv;
710 }
711 else if ("-fx-background-color".equals(prop)) {
712 return parsePaintLayers(root);
713 } else if ("-fx-background-image".equals(prop)) {
789 } else if ("-fx-stroke-line-cap".equals(prop)) {
790 // TODO: Figure out a way that these properties don't need to be
791 // special cased.
792 ParsedValueImpl value = parseStrokeLineCap(root);
793 if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'");
794 return value;
795 } else if ("-fx-stroke-type".equals(prop)) {
796 // TODO: Figure out a way that these properties don't need to be
797 // special cased.
798 ParsedValueImpl value = parseStrokeType(root);
799 if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'");
800 return value;
801 } else if ("-fx-font-smoothing-type".equals(prop)) {
802 // TODO: Figure out a way that these properties don't need to be
803 // special cased.
804 String str = null;
805 int ttype = -1;
806 final Token token = root.token;
807
808 if (root.token == null
809 || ((ttype = root.token.getType()) != CssLexer.STRING
810 && ttype != CssLexer.IDENT)
811 || (str = root.token.getText()) == null
812 || str.isEmpty()) {
813 error(root, "Expected STRING or IDENT");
814 }
815 return new ParsedValueImpl<String, String>(stripQuotes(str), null, false);
816 }
817 return parse(root);
818 }
819
820 private ParsedValueImpl parse(Term root) throws ParseException {
821
822 if (root.token == null) error(root, "Parse error");
823 final Token token = root.token;
824 ParsedValueImpl value = null; // value to return;
825
826 final int ttype = token.getType();
827 switch (ttype) {
828 case CssLexer.NUMBER:
829 case CssLexer.PERCENTAGE:
830 case CssLexer.EMS:
831 case CssLexer.EXS:
832 case CssLexer.PX:
833 case CssLexer.CM:
834 case CssLexer.MM:
835 case CssLexer.IN:
836 case CssLexer.PT:
837 case CssLexer.PC:
838 case CssLexer.DEG:
839 case CssLexer.GRAD:
840 case CssLexer.RAD:
841 case CssLexer.TURN:
842 if (root.nextInSeries == null) {
843 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null);
844 value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance());
845 } else {
846 ParsedValueImpl<Size,Size>[] sizeValue = parseSizeSeries(root);
847 value = new ParsedValueImpl<ParsedValue[],Number[]>(sizeValue, SizeConverter.SequenceConverter.getInstance());
848 }
849 break;
850 case CssLexer.SECONDS:
851 case CssLexer.MS: {
852 ParsedValue<Size, Size> sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
853 value = new ParsedValueImpl<ParsedValue<?, Size>, Duration>(sizeValue, DurationConverter.getInstance());
854 break;
855 }
856 case CssLexer.STRING:
857 case CssLexer.IDENT:
858 boolean isIdent = ttype == CssLexer.IDENT;
859 final String str = stripQuotes(token.getText());
860 final String text = str.toLowerCase(Locale.ROOT);
861 if ("ladder".equals(text)) {
862 value = ladder(root);
863 } else if ("linear".equals(text) && (root.nextInSeries) != null) {
864 // if nextInSeries is null, then assume this is _not_ an old-style linear gradient
865 value = linearGradient(root);
866 } else if ("radial".equals(text) && (root.nextInSeries) != null) {
867 // if nextInSeries is null, then assume this is _not_ an old-style radial gradient
868 value = radialGradient(root);
869 } else if ("infinity".equals(text)) {
870 Size size = new Size(Double.MAX_VALUE, SizeUnits.PX);
871 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size, null);
872 value = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeValue, SizeConverter.getInstance());
873 } else if ("indefinite".equals(text)) {
874 Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX);
875 ParsedValueImpl<Size,Size> sizeValue = new ParsedValueImpl<>(size, null);
876 value = new ParsedValueImpl<ParsedValue<?,Size>,Duration>(sizeValue, DurationConverter.getInstance());
877 } else if ("true".equals(text)) {
878 // TODO: handling of boolean is really bogus
879 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance());
880 } else if ("false".equals(text)) {
881 // TODO: handling of boolean is really bogus
882 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance());
883 } else {
884 // if the property value is another property, then it needs to be looked up.
885 boolean needsLookup = isIdent && properties.containsKey(text);
886 if (needsLookup || ((value = colorValueOfString(str)) == null )) {
887 // If the value is a lookup, make sure to use the lower-case text so it matches the property
888 // in the Declaration. If the value is not a lookup, then use str since the value might
889 // be a string which could have some case sensitive meaning
890 //
891 // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201
892 value = new ParsedValueImpl<String,String>(needsLookup ? text : str, null, isIdent || needsLookup);
893 }
894 }
895 break;
896 case CssLexer.HASH:
897 final String clr = token.getText();
898 try {
899 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null);
900 } catch (final IllegalArgumentException e) {
901 error(root, e.getMessage());
902 }
903 break;
904 case CssLexer.FUNCTION:
905 return parseFunction(root);
906 case CssLexer.URL:
907 return parseURI(root);
908 default:
909 final String msg = "Unknown token type: \'" + ttype + "\'";
910 error(root, msg);
911 }
912 return value;
913
914 }
915
916 /* Parse size.
917 * @throw RecongnitionExcpetion if the token is not a size type or a lookup.
918 */
919 private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException {
920
921 if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'");
922
923 ParsedValueImpl<?,Size> value = null;
924
925 if (root.token.getType() != CssLexer.IDENT) {
926
927 Size size = size(root.token);
928 value = new ParsedValueImpl<Size,Size>(size, null);
929
930 } else {
931
932 String key = root.token.getText();
933 value = new ParsedValueImpl<String,Size>(key, null, true);
934
935 }
936
937 return value;
938 }
939
940 private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException {
941
942 ParsedValueImpl<?,Color> color = null;
943 if (root.token != null &&
944 (root.token.getType() == CssLexer.IDENT ||
945 root.token.getType() == CssLexer.HASH ||
946 root.token.getType() == CssLexer.FUNCTION)) {
947
948 color = parse(root);
949
950 } else {
951 error(root, "Expected \'<color>\'");
952 }
953 return color;
954 }
955
956 // rgb(NUMBER, NUMBER, NUMBER)
957 // rgba(NUMBER, NUMBER, NUMBER, NUMBER)
958 // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE)
959 // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER)
960 private ParsedValueImpl rgb(Term root) throws ParseException {
961
962 // first term in the chain is the function name...
963 final String fn = (root.token != null) ? root.token.getText() : null;
964 if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) {
965 final String msg = "Expected \'rgb\' or \'rgba\'";
966 error(root, msg);
967 }
968
969 Term arg = root;
970 Token rtok, gtok, btok, atok;
971
972 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
973 if ((rtok = arg.token) == null ||
974 (rtok.getType() != CssLexer.NUMBER &&
975 rtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
976
977 root = arg;
978
979 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
980 if ((gtok = arg.token) == null ||
981 (gtok.getType() != CssLexer.NUMBER &&
982 gtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
983
984 root = arg;
985
986 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'");
987 if ((btok = arg.token) == null ||
988 (btok.getType() != CssLexer.NUMBER &&
989 btok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'");
990
991 root = arg;
992
993 if ((arg = arg.nextArg) != null) {
994 if ((atok = arg.token) == null ||
995 atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
996 } else {
997 atok = null;
998 }
999
1000 int argType = rtok.getType();
1001 if (argType != gtok.getType() || argType != btok.getType() ||
1002 (argType != CssLexer.NUMBER && argType != CssLexer.PERCENTAGE)) {
1003 error(root, "Argument type mistmatch");
1004 }
1005
1006 final String rtext = rtok.getText();
1007 final String gtext = gtok.getText();
1008 final String btext = btok.getText();
1009
1010 double rval = 0;
1011 double gval = 0;
1012 double bval = 0;
1013 if (argType == CssLexer.NUMBER) {
1014 rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f);
1015 gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f);
1016 bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f);
1017 } else {
1018 rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f);
1019 gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f);
1020 bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f);
1021 }
1022
1023 final String atext = (atok != null) ? atok.getText() : null;
1024 final double aval = (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0;
1025
1026 return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null);
1027
1028 }
1029
1030 // hsb(NUMBER, PERCENTAGE, PERCENTAGE)
1031 // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)
1032 private ParsedValueImpl hsb(Term root) throws ParseException {
1033
1034 // first term in the chain is the function name...
1035 final String fn = (root.token != null) ? root.token.getText() : null;
1036 if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) {
1037 final String msg = "Expected \'hsb\' or \'hsba\'";
1038 error(root, msg);
1039 }
1040
1041 Term arg = root;
1042 Token htok, stok, btok, atok;
1043
1044 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'");
1045 if ((htok = arg.token) == null || htok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
1046
1047 root = arg;
1048
1049 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1050 if ((stok = arg.token) == null || stok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1051
1052 root = arg;
1053
1054 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'");
1055 if ((btok = arg.token) == null || btok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'");
1056
1057 root = arg;
1058
1059 if ((arg = arg.nextArg) != null) {
1060 if ((atok = arg.token) == null || atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'");
1061 } else {
1062 atok = null;
1063 }
1064
1065 final Size hval = size(htok);
1066 final Size sval = size(stok);
1067 final Size bval = size(btok);
1068
1069 final double hue = hval.pixels(); // no clamp - hue can be negative
1070 final double saturation = clamp(0.0f, sval.pixels(), 1.0f);
1071 final double brightness = clamp(0.0f, bval.pixels(), 1.0f);
1072
1073 final Size aval = (atok != null) ? size(atok) : null;
1074 final double opacity = (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0;
1075
1076 return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null);
1077 }
1078
1079 // derive(color, pct)
1080 private ParsedValueImpl<ParsedValue[],Color> derive(final Term root)
1107 // first term in the chain is the function name...
1108 final String fn = (root.token != null) ? root.token.getText() : null;
1109 if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) {
1110 final String msg = "Expected \'ladder\'";
1111 error(root, msg);
1112 }
1113
1114 if (LOGGER.isLoggable(Level.WARNING)) {
1115 LOGGER.warning(formatDeprecatedMessage(root, "ladder"));
1116 }
1117
1118 Term term = root;
1119
1120 if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'");
1121 final ParsedValueImpl<?,Color> color = parse(term);
1122
1123 Term prev = term;
1124
1125 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1126 if (term.token == null ||
1127 term.token.getType() != CssLexer.IDENT ||
1128 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1129
1130 prev = term;
1131
1132 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'");
1133
1134 int nStops = 0;
1135 Term temp = term;
1136 do {
1137 nStops += 1;
1138 // if next token type is IDENT, then we have CycleMethod
1139 } while (((temp = temp.nextInSeries) != null) &&
1140 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1141
1142 ParsedValueImpl[] values = new ParsedValueImpl[nStops+1];
1143 values[0] = color;
1144 int stopIndex = 1;
1145 do {
1146 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1147 if (stop != null) values[stopIndex++] = stop;
1148 prev = term;
1149 } while(((term = term.nextInSeries) != null) &&
1150 (term.token.getType() == CssLexer.LPAREN));
1151
1152 // if term is not null and the last term was not an lparen,
1153 // then term starts a new series of Paint. Point
1154 // root.nextInSeries to term so the next loop skips over the
1155 // already parsed ladder bits.
1156 if (term != null) {
1157 root.nextInSeries = term;
1158 }
1159
1160 // if term is null, then we are at the end of a series.
1161 // root points to 'ladder', now we want the next term after root
1162 // to be the term after the last stop, which may be another layer
1163 else {
1164 root.nextInSeries = null;
1165 root.nextLayer = prev.nextLayer;
1166 }
1167
1168 return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance());
1169 }
1170
1341
1342 ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs];
1343 for (int n=0; n<nArgs; n++) {
1344 stops[n] = new ParsedValueImpl<ParsedValue[],Stop>(
1345 new ParsedValueImpl[] {
1346 new ParsedValueImpl<Size,Size>(positions[n], null),
1347 colors[n]
1348 },
1349 StopConverter.getInstance()
1350 );
1351 }
1352
1353 return stops;
1354
1355 }
1356
1357 // parse (<number>, <number>)
1358 private ParsedValueImpl[] point(final Term root) throws ParseException {
1359
1360 if (root.token == null ||
1361 root.token.getType() != CssLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'");
1362
1363 final String fn = root.token.getText();
1364 if (fn == null || !"(".equalsIgnoreCase(fn)) {
1365 final String msg = "Expected \'(\'";
1366 error(root, msg);
1367 }
1368
1369 Term arg = null;
1370
1371 // no <number>
1372 if ((arg = root.firstArg) == null) error(root, "Expected \'<number>\'");
1373
1374 final ParsedValueImpl<?,Size> ptX = parseSize(arg);
1375
1376 final Term prev = arg;
1377
1378 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1379
1380 final ParsedValueImpl<?,Size> ptY = parseSize(arg);
1381
1403 } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) {
1404 return parseRadialGradient(root);
1405 } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) {
1406 return parseImagePattern(root);
1407 } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) {
1408 return parseRepeatingImagePattern(root);
1409 } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) {
1410 return parseLadder(root);
1411 } else if ("region".regionMatches(true, 0, fcn, 0, 6)) {
1412 return parseRegion(root);
1413 } else {
1414 error(root, "Unexpected function \'" + fcn + "\'");
1415 }
1416 return null;
1417 }
1418
1419 private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException {
1420
1421 if (root == null) return null;
1422 if (root.token == null ||
1423 root.token.getType() != CssLexer.IDENT ||
1424 root.token.getText() == null ||
1425 root.token.getText().isEmpty()) {
1426 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1427 error(root, msg);
1428 }
1429 final String blurStr = root.token.getText().toLowerCase(Locale.ROOT);
1430 BlurType blurType = BlurType.THREE_PASS_BOX;
1431 if ("gaussian".equals(blurStr)) {
1432 blurType = BlurType.GAUSSIAN;
1433 } else if ("one-pass-box".equals(blurStr)) {
1434 blurType = BlurType.ONE_PASS_BOX;
1435 } else if ("two-pass-box".equals(blurStr)) {
1436 blurType = BlurType.TWO_PASS_BOX;
1437 } else if ("three-pass-box".equals(blurStr)) {
1438 blurType = BlurType.THREE_PASS_BOX;
1439 } else {
1440 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'";
1441 error(root, msg);
1442 }
1443 return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class));
1531
1532 prev = arg;
1533 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'");
1534
1535 ParsedValueImpl<?,Size> offsetYVal = parseSize(arg);
1536
1537 ParsedValueImpl[] values = new ParsedValueImpl[] {
1538 blurVal,
1539 colorVal,
1540 radiusVal,
1541 spreadVal,
1542 offsetXVal,
1543 offsetYVal
1544 };
1545 return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance());
1546 }
1547
1548 // returns null if the Term is null or is not a cycle method.
1549 private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) {
1550 CycleMethod cycleMethod = null;
1551 if (root != null && root.token.getType() == CssLexer.IDENT) {
1552
1553 final String text = root.token.getText().toLowerCase(Locale.ROOT);
1554 if ("repeat".equals(text)) {
1555 cycleMethod = CycleMethod.REPEAT;
1556 } else if ("reflect".equals(text)) {
1557 cycleMethod = CycleMethod.REFLECT;
1558 } else if ("no-cycle".equals(text)) {
1559 cycleMethod = CycleMethod.NO_CYCLE;
1560 }
1561 }
1562 if (cycleMethod != null)
1563 return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1564 else
1565 return null;
1566 }
1567
1568 // linear <point> TO <point> STOPS <stop>+ cycleMethod?
1569 private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException {
1570
1571 final String fn = (root.token != null) ? root.token.getText() : null;
1572 if (fn == null || !"linear".equalsIgnoreCase(fn)) {
1573 final String msg = "Expected \'linear\'";
1574 error(root, msg);
1575 }
1576
1577 if (LOGGER.isLoggable(Level.WARNING)) {
1578 LOGGER.warning(formatDeprecatedMessage(root, "linear gradient"));
1579 }
1580
1581 Term term = root;
1582
1583 if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'");
1584
1585 final ParsedValueImpl<?,Size>[] startPt = point(term);
1586
1587 Term prev = term;
1588 if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'");
1589 if (term.token == null ||
1590 term.token.getType() != CssLexer.IDENT ||
1591 !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'");
1592
1593 prev = term;
1594 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1595
1596 final ParsedValueImpl<?,Size>[] endPt = point(term);
1597
1598 prev = term;
1599 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'");
1600 if (term.token == null ||
1601 term.token.getType() != CssLexer.IDENT ||
1602 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1603
1604 prev = term;
1605 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1606
1607 int nStops = 0;
1608 Term temp = term;
1609 do {
1610 nStops += 1;
1611 // if next token type is IDENT, then we have CycleMethod
1612 } while (((temp = temp.nextInSeries) != null) &&
1613 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1614
1615 ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1616 int stopIndex = 0;
1617 do {
1618 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1619 if (stop != null) stops[stopIndex++] = stop;
1620 prev = term;
1621 } while(((term = term.nextInSeries) != null) &&
1622 (term.token.getType() == CssLexer.LPAREN));
1623
1624 // term is either null or is a cycle method, or the start of another Paint.
1625 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1626
1627 if (cycleMethod == null) {
1628
1629 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
1630
1631 // if term is not null and the last term was not a cycle method,
1632 // then term starts a new series or layer of Paint
1633 if (term != null) {
1634 root.nextInSeries = term;
1635 }
1636
1637 // if term is null, then we are at the end of a series.
1638 // root points to 'linear', now we want the next term after root
1639 // to be the term after the last stop, which may be another layer
1640 else {
1641 root.nextInSeries = null;
1642 root.nextLayer = prev.nextLayer;
1703 ParsedValueImpl<?,Size>[] startPt = null;
1704 ParsedValueImpl<?,Size>[] endPt = null;
1705
1706 if ("from".equalsIgnoreCase(arg.token.getText())) {
1707
1708 prev = arg;
1709 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1710
1711 ParsedValueImpl<?,Size> ptX = parseSize(arg);
1712
1713 prev = arg;
1714 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1715
1716 ParsedValueImpl<?,Size> ptY = parseSize(arg);
1717
1718 startPt = new ParsedValueImpl[] { ptX, ptY };
1719
1720 prev = arg;
1721 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'");
1722 if (arg.token == null ||
1723 arg.token.getType() != CssLexer.IDENT ||
1724 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'");
1725
1726 prev = arg;
1727 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1728
1729 ptX = parseSize(arg);
1730
1731 prev = arg;
1732 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'");
1733
1734 ptY = parseSize(arg);
1735
1736 endPt = new ParsedValueImpl[] { ptX, ptY };
1737
1738 prev = arg;
1739 arg = arg.nextArg;
1740
1741 } else if("to".equalsIgnoreCase(arg.token.getText())) {
1742
1743 prev = arg;
1744 if ((arg = arg.nextInSeries) == null ||
1745 arg.token == null ||
1746 arg.token.getType() != CssLexer.IDENT ||
1747 arg.token.getText().isEmpty()) {
1748 error (prev, "Expected \'<side-or-corner>\'");
1749 }
1750
1751
1752 int startX = 0;
1753 int startY = 0;
1754 int endX = 0;
1755 int endY = 0;
1756
1757 String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT);
1758 // The keywords denote the direction.
1759 if ("top".equals(sideOrCorner1)) {
1760 // going toward the top, then start at the bottom
1761 startY = 100;
1762 endY = 0;
1763
1764 } else if ("bottom".equals(sideOrCorner1)) {
1765 // going toward the bottom, then start at the top
1766 startY = 0;
1767 endY = 100;
1768
1769 } else if ("right".equals(sideOrCorner1)) {
1770 // going toward the right, then start at the left
1771 startX = 0;
1772 endX = 100;
1773
1774 } else if ("left".equals(sideOrCorner1)) {
1775 // going toward the left, then start at the right
1776 startX = 100;
1777 endX = 0;
1778
1779 } else {
1780 error(arg, "Invalid \'<side-or-corner>\'");
1781 }
1782
1783 prev = arg;
1784 if (arg.nextInSeries != null) {
1785 arg = arg.nextInSeries;
1786 if (arg.token != null &&
1787 arg.token.getType() == CssLexer.IDENT &&
1788 !arg.token.getText().isEmpty()) {
1789
1790 String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT);
1791
1792 // if right or left has already been given,
1793 // then either startX or endX will not be zero.
1794 if ("right".equals(sideOrCorner2) &&
1795 startX == 0 && endX == 0) {
1796 // start left, end right
1797 startX = 0;
1798 endX = 100;
1799 } else if ("left".equals(sideOrCorner2) &&
1800 startX == 0 && endX == 0) {
1801 // start right, end left
1802 startX = 100;
1803 endX = 0;
1804
1805 // if top or bottom has already been given,
1806 // then either startY or endY will not be zero.
1807 } else if("top".equals(sideOrCorner2) &&
1895 private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException {
1896
1897 final String fn = (root.token != null) ? root.token.getText() : null;
1898 if (fn == null || !"radial".equalsIgnoreCase(fn)) {
1899 final String msg = "Expected \'radial\'";
1900 error(root, msg);
1901 }
1902
1903 if (LOGGER.isLoggable(Level.WARNING)) {
1904 LOGGER.warning(formatDeprecatedMessage(root, "radial gradient"));
1905 }
1906
1907 Term term = root;
1908 Term prev = root;
1909
1910 if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1911 if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1912
1913
1914 ParsedValueImpl<?,Size> focusAngle = null;
1915 if (term.token.getType() == CssLexer.IDENT) {
1916 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1917 if ("focus-angle".equals(keyword)) {
1918
1919 prev = term;
1920 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1921 if (term.token == null) error(prev, "Expected \'<number>\'");
1922
1923 focusAngle = parseSize(term);
1924
1925 prev = term;
1926 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1927 if (term.token == null) error(term, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'");
1928 }
1929 }
1930
1931 ParsedValueImpl<?,Size> focusDistance = null;
1932 if (term.token.getType() == CssLexer.IDENT) {
1933 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1934 if ("focus-distance".equals(keyword)) {
1935
1936 prev = term;
1937 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'");
1938 if (term.token == null) error(prev, "Expected \'<number>\'");
1939
1940 focusDistance = parseSize(term);
1941
1942 prev = term;
1943 if ((term = term.nextInSeries) == null) error(prev, "Expected \'center (<number>,<number>)\' or \'<size>\'");
1944 if (term.token == null) error(term, "Expected \'center (<number>,<number>)\' or \'<size>\'");
1945 }
1946 }
1947
1948 ParsedValueImpl<?,Size>[] centerPoint = null;
1949 if (term.token.getType() == CssLexer.IDENT) {
1950 final String keyword = term.token.getText().toLowerCase(Locale.ROOT);
1951 if ("center".equals(keyword)) {
1952
1953 prev = term;
1954 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'");
1955 if (term.token == null ||
1956 term.token.getType() != CssLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'");
1957
1958 centerPoint = point(term);
1959
1960 prev = term;
1961 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'");
1962 if (term.token == null) error(term, "Expected \'<size>\'");
1963 }
1964 }
1965
1966 ParsedValueImpl<?,Size> radius = parseSize(term);
1967
1968 prev = term;
1969 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword");
1970 if (term.token == null ||
1971 term.token.getType() != CssLexer.IDENT) error(term, "Expected \'stops\' keyword");
1972
1973 if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'");
1974
1975 prev = term;
1976 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'");
1977
1978 int nStops = 0;
1979 Term temp = term;
1980 do {
1981 nStops += 1;
1982 // if next token type is IDENT, then we have CycleMethod
1983 } while (((temp = temp.nextInSeries) != null) &&
1984 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN)));
1985
1986 ParsedValueImpl[] stops = new ParsedValueImpl[nStops];
1987 int stopIndex = 0;
1988 do {
1989 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term);
1990 if (stop != null) stops[stopIndex++] = stop;
1991 prev = term;
1992 } while(((term = term.nextInSeries) != null) &&
1993 (term.token.getType() == CssLexer.LPAREN));
1994
1995 // term is either null or is a cycle method, or the start of another Paint.
1996 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term);
1997
1998 if (cycleMethod == null) {
1999
2000 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class));
2001
2002 // if term is not null and the last term was not a cycle method,
2003 // then term starts a new series or layer of Paint
2004 if (term != null) {
2005 root.nextInSeries = term;
2006 }
2007
2008 // if term is null, then we are at the end of a series.
2009 // root points to 'linear', now we want the next term after root
2010 // to be the term after the last stop, which may be another layer
2011 else {
2012 root.nextInSeries = null;
2013 root.nextLayer = prev.nextLayer;
2408 }
2409 temp = nextLayer(temp);
2410 }
2411
2412 return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance());
2413 }
2414
2415 // <size> | <size> <size> <size> <size>
2416 private ParsedValueImpl<Size, Size>[] parseSizeSeries(Term root)
2417 throws ParseException {
2418
2419 if (root.token == null) error(root, "Parse error");
2420
2421 List<ParsedValueImpl<Size,Size>> sizes = new ArrayList<>();
2422
2423 Term term = root;
2424 while(term != null) {
2425 Token token = term.token;
2426 final int ttype = token.getType();
2427 switch (ttype) {
2428 case CssLexer.NUMBER:
2429 case CssLexer.PERCENTAGE:
2430 case CssLexer.EMS:
2431 case CssLexer.EXS:
2432 case CssLexer.PX:
2433 case CssLexer.CM:
2434 case CssLexer.MM:
2435 case CssLexer.IN:
2436 case CssLexer.PT:
2437 case CssLexer.PC:
2438 case CssLexer.DEG:
2439 case CssLexer.GRAD:
2440 case CssLexer.RAD:
2441 case CssLexer.TURN:
2442 ParsedValueImpl sizeValue = new ParsedValueImpl<Size, Size>(size(token), null);
2443 sizes.add(sizeValue);
2444 break;
2445 default:
2446 error (root, "expected series of <size>");
2447 }
2448 term = term.nextInSeries;
2449 }
2450 return sizes.toArray(new ParsedValueImpl[sizes.size()]);
2451
2452 }
2453
2454 // http://www.w3.org/TR/css3-background/#the-border-radius
2455 // <size>{1,4} [ '/' <size>{1,4}]? [',' <size>{1,4} [ '/' <size>{1,4}]?]?
2456 private ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root)
2457 throws ParseException {
2458
2459
2460 int nLayers = numberOfLayers(root);
2461
2462 Term term = root;
2463 int layer = 0;
2464 ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers];
2465
2466 while(term != null) {
2467
2468 int nHorizontalTerms = 0;
2469 Term temp = term;
2470 while (temp != null) {
2471 if (temp.token.getType() == CssLexer.SOLIDUS) {
2472 temp = temp.nextInSeries;
2473 break;
2474 }
2475 nHorizontalTerms += 1;
2476 temp = temp.nextInSeries;
2477 };
2478
2479 int nVerticalTerms = 0;
2480 while (temp != null) {
2481 if (temp.token.getType() == CssLexer.SOLIDUS) {
2482 error(temp, "unexpected SOLIDUS");
2483 break;
2484 }
2485 nVerticalTerms += 1;
2486 temp = temp.nextInSeries;
2487 }
2488
2489 if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) {
2490 error(root, "expected [<length>|<percentage>]{1,4} [/ [<length>|<percentage>]{1,4}]?");
2491 }
2492
2493 // used as index into margins[]. horizontal = 0, vertical = 1
2494 int orientation = 0;
2495
2496 // at most, there should be four radii in the horizontal orientation and four in the vertical.
2497 ParsedValueImpl<?,Size>[][] radii = new ParsedValueImpl[2][4];
2498
2499 ParsedValueImpl<?,Size> zero = new ParsedValueImpl<Size,Size>(new Size(0,SizeUnits.PX), null);
2500 for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; }
2501
2502 int hr = 0;
2503 int vr = 0;
2504
2505 Term lastTerm = term;
2506 while ((hr <= 4) && (vr <= 4) && (term != null)) {
2507
2508 if (term.token.getType() == CssLexer.SOLIDUS) {
2509 orientation += 1;
2510 } else {
2511 ParsedValueImpl<?,Size> parsedValue = parseSize(term);
2512 if (orientation == 0) {
2513 radii[orientation][hr++] = parsedValue;
2514 } else {
2515 radii[orientation][vr++] = parsedValue;
2516 }
2517 }
2518 lastTerm = term;
2519 term = term.nextInSeries;
2520 }
2521
2522 //
2523 // http://www.w3.org/TR/css3-background/#the-border-radius
2524 // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left.
2525 // If bottom-left is omitted it is the same as top-right.
2526 // If bottom-right is omitted it is the same as top-left.
2527 // If top-right is omitted it is the same as top-left.
2528 //
2909 while (term != null) {
2910 layers[layer++] = parseBackgroundPosition(term);
2911 term = nextLayer(term);
2912 }
2913 return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance());
2914 }
2915
2916 /*
2917 http://www.w3.org/TR/css3-background/#the-background-repeat
2918 <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2}
2919 */
2920 private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root)
2921 throws ParseException {
2922
2923 BackgroundRepeat xAxis, yAxis;
2924 xAxis = yAxis = BackgroundRepeat.NO_REPEAT;
2925
2926 Term term = root;
2927
2928 if (term.token == null ||
2929 term.token.getType() != CssLexer.IDENT ||
2930 term.token.getText() == null ||
2931 term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'");
2932
2933 String text = term.token.getText().toLowerCase(Locale.ROOT);
2934 if ("repeat-x".equals(text)) {
2935 xAxis = BackgroundRepeat.REPEAT;
2936 yAxis = BackgroundRepeat.NO_REPEAT;
2937 } else if ("repeat-y".equals(text)) {
2938 xAxis = BackgroundRepeat.NO_REPEAT;
2939 yAxis = BackgroundRepeat.REPEAT;
2940 } else if ("repeat".equals(text)) {
2941 xAxis = BackgroundRepeat.REPEAT;
2942 yAxis = BackgroundRepeat.REPEAT;
2943 } else if ("space".equals(text)) {
2944 xAxis = BackgroundRepeat.SPACE;
2945 yAxis = BackgroundRepeat.SPACE;
2946 } else if ("round".equals(text)) {
2947 xAxis = BackgroundRepeat.ROUND;
2948 yAxis = BackgroundRepeat.ROUND;
2949 } else if ("no-repeat".equals(text)) {
2950 xAxis = BackgroundRepeat.NO_REPEAT;
2951 yAxis = BackgroundRepeat.NO_REPEAT;
2952 } else if ("stretch".equals(text)) {
2953 xAxis = BackgroundRepeat.NO_REPEAT;
2954 yAxis = BackgroundRepeat.NO_REPEAT;
2955 } else {
2956 error(term, "Expected \'<repeat-style>\' " + text);
2957 }
2958
2959 if ((term = term.nextInSeries) != null &&
2960 term.token != null &&
2961 term.token.getType() == CssLexer.IDENT &&
2962 term.token.getText() != null &&
2963 !term.token.getText().isEmpty()) {
2964
2965 text = term.token.getText().toLowerCase(Locale.ROOT);
2966 if ("repeat-x".equals(text)) {
2967 error(term, "Unexpected \'repeat-x\'");
2968 } else if ("repeat-y".equals(text)) {
2969 error(term, "Unexpected \'repeat-y\'");
2970 } else if ("repeat".equals(text)) {
2971 yAxis = BackgroundRepeat.REPEAT;
2972 } else if ("space".equals(text)) {
2973 yAxis = BackgroundRepeat.SPACE;
2974 } else if ("round".equals(text)) {
2975 yAxis = BackgroundRepeat.ROUND;
2976 } else if ("no-repeat".equals(text)) {
2977 yAxis = BackgroundRepeat.NO_REPEAT;
2978 } else if ("stretch".equals(text)) {
2979 yAxis = BackgroundRepeat.NO_REPEAT;
2980 } else {
2981 error(term, "Expected \'<repeat-style>\'");
3013 while (term != null) {
3014 layers[layer++] = parseRepeatStyle(term);
3015 term = nextLayer(term);
3016 }
3017 return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance());
3018 }
3019
3020 /*
3021 http://www.w3.org/TR/css3-background/#the-background-size
3022 <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain
3023 */
3024 private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root)
3025 throws ParseException {
3026
3027 ParsedValueImpl<?,Size> height = null, width = null;
3028 boolean cover = false, contain = false;
3029
3030 Term term = root;
3031 if (term.token == null) error(term, "Expected \'<bg-size>\'");
3032
3033 if (term.token.getType() == CssLexer.IDENT) {
3034 final String text =
3035 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3036
3037 if ("auto".equals(text)) {
3038 // We don't do anything because width / height are already initialized
3039 } else if ("cover".equals(text)) {
3040 cover = true;
3041 } else if ("contain".equals(text)) {
3042 contain = true;
3043 } else if ("stretch".equals(text)) {
3044 width = ONE_HUNDRED_PERCENT;
3045 height = ONE_HUNDRED_PERCENT;
3046 } else {
3047 error(term, "Expected \'auto\', \'cover\', \'contain\', or \'stretch\'");
3048 }
3049 } else if (isSize(term.token)) {
3050 width = parseSize(term);
3051 height = null;
3052 } else {
3053 error(term, "Expected \'<bg-size>\'");
3054 }
3055
3056 if ((term = term.nextInSeries) != null) {
3057 if (cover || contain) error(term, "Unexpected \'<bg-size>\'");
3058
3059 if (term.token.getType() == CssLexer.IDENT) {
3060 final String text =
3061 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null;
3062
3063 if ("auto".equals(text)) {
3064 height = null;
3065 } else if ("cover".equals(text)) {
3066 error(term, "Unexpected \'cover\'");
3067 } else if ("contain".equals(text)) {
3068 error(term, "Unexpected \'contain\'");
3069 } else if ("stretch".equals(text)) {
3070 height = ONE_HUNDRED_PERCENT;
3071 } else {
3072 error(term, "Expected \'auto\' or \'stretch\'");
3073 }
3074 } else if (isSize(term.token)) {
3075 height = parseSize(term);
3076 } else {
3077 error(term, "Expected \'<bg-size>\'");
3078 }
3079
3158
3159
3160 private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>
3161 parseBorderStyleLayers(final Term root) throws ParseException {
3162
3163 int nLayers = numberOfLayers(root);
3164 ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers];
3165 int layer = 0;
3166 Term term = root;
3167 while (term != null) {
3168 layers[layer++] = parseBorderStyleSeries(term);
3169 term = nextLayer(term);
3170 }
3171 return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance());
3172 }
3173
3174 // Only meant to be used from parseBorderStyle, but might be useful elsewhere
3175 private String getKeyword(final Term term) {
3176 if (term != null &&
3177 term.token != null &&
3178 term.token.getType() == CssLexer.IDENT &&
3179 term.token.getText() != null &&
3180 !term.token.getText().isEmpty()) {
3181
3182 return term.token.getText().toLowerCase(Locale.ROOT);
3183 }
3184 return null;
3185 }
3186
3187 //<border-style> [ , <border-style> ]*
3188 // where <border-style> =
3189 // <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]?
3190 // where <dash-style> =
3191 // [ none | solid | dotted | dashed ]
3192 private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root)
3193 throws ParseException {
3194
3195
3196 ParsedValue<ParsedValue[],Number[]> dashStyle = null;
3197 ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null;
3198 ParsedValue<String,StrokeType> strokeType = null;
3280 dashPhase,
3281 strokeType,
3282 strokeLineJoin,
3283 strokeMiterLimit,
3284 strokeLineCap
3285 };
3286
3287 return new ParsedValueImpl(values, BorderStyleConverter.getInstance());
3288 }
3289
3290 //
3291 // segments(<size> [, <size>]+) | <border-style>
3292 //
3293 private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException {
3294
3295 if (root.token == null) error(root, "Expected \'<dash-style>\'");
3296
3297 final int ttype = root.token.getType();
3298
3299 ParsedValue<ParsedValue[],Number[]> segments = null;
3300 if (ttype == CssLexer.IDENT) {
3301 segments = borderStyle(root);
3302 } else if (ttype == CssLexer.FUNCTION) {
3303 segments = segments(root);
3304 } else {
3305 error(root, "Expected \'<dash-style>\'");
3306 }
3307
3308 return segments;
3309 }
3310
3311 /*
3312 <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset
3313 */
3314 private ParsedValue<ParsedValue[],Number[]> borderStyle(Term root)
3315 throws ParseException {
3316
3317 if (root.token == null ||
3318 root.token.getType() != CssLexer.IDENT ||
3319 root.token.getText() == null ||
3320 root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'");
3321
3322 final String text = root.token.getText().toLowerCase(Locale.ROOT);
3323
3324 if ("none".equals(text)) {
3325 return BorderStyleConverter.NONE;
3326 } else if ("hidden".equals(text)) {
3327 // The "hidden" mode doesn't make sense for FX, because it is the
3328 // same as "none" except for border-collapsed CSS tables
3329 return BorderStyleConverter.NONE;
3330 } else if ("dotted".equals(text)) {
3331 return BorderStyleConverter.DOTTED;
3332 } else if ("dashed".equals(text)) {
3333 return BorderStyleConverter.DASHED;
3334 } else if ("solid".equals(text)) {
3335 return BorderStyleConverter.SOLID;
3336 } else if ("double".equals(text)) {
3337 error(root, "Unsupported <border-style> \'double\'");
3338 } else if ("groove".equals(text)) {
3447 /*
3448 * http://www.w3.org/TR/css3-background/#the-border-image-slice
3449 * [<number> | <percentage>]{1,4} && fill?
3450 */
3451 private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root)
3452 throws ParseException {
3453
3454 Term term = root;
3455 if (term.token == null || !isSize(term.token))
3456 error(term, "Expected \'<size>\'");
3457
3458 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3459 Boolean fill = Boolean.FALSE;
3460
3461 int inset = 0;
3462 while (inset < 4 && term != null) {
3463 insets[inset++] = parseSize(term);
3464
3465 if ((term = term.nextInSeries) != null &&
3466 term.token != null &&
3467 term.token.getType() == CssLexer.IDENT) {
3468
3469 if("fill".equalsIgnoreCase(term.token.getText())) {
3470 fill = Boolean.TRUE;
3471 break;
3472 }
3473 }
3474 }
3475
3476 if (inset < 2) insets[1] = insets[0]; // right = top
3477 if (inset < 3) insets[2] = insets[0]; // bottom = top
3478 if (inset < 4) insets[3] = insets[1]; // left = right
3479
3480 ParsedValueImpl[] values = new ParsedValueImpl[] {
3481 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()),
3482 new ParsedValueImpl<Boolean,Boolean>(fill, null)
3483 };
3484 return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance());
3485 }
3486
3487 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]>
3500
3501 /*
3502 * http://www.w3.org/TR/css3-background/#border-image-width
3503 * [ <length> | <percentage> | <number> | auto ]{1,4}
3504 */
3505 private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root)
3506 throws ParseException {
3507
3508 Term term = root;
3509 if (term.token == null || !isSize(term.token))
3510 error(term, "Expected \'<size>\'");
3511
3512 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4];
3513
3514 int inset = 0;
3515 while (inset < 4 && term != null) {
3516 insets[inset++] = parseSize(term);
3517
3518 if ((term = term.nextInSeries) != null &&
3519 term.token != null &&
3520 term.token.getType() == CssLexer.IDENT) {
3521 }
3522 }
3523
3524 if (inset < 2) insets[1] = insets[0]; // right = top
3525 if (inset < 3) insets[2] = insets[0]; // bottom = top
3526 if (inset < 4) insets[3] = insets[1]; // left = right
3527
3528 return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance());
3529 }
3530
3531 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]>
3532 parseBorderImageWidthLayers(final Term root) throws ParseException {
3533
3534 int nLayers = numberOfLayers(root);
3535 ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers];
3536 int layer = 0;
3537 Term term = root;
3538 while (term != null) {
3539 layers[layer++] = parseBorderImageWidth(term);
3540 term = nextLayer(term);
3541 }
3542 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance());
3543 }
3544
3545 // parse a Region value
3546 // i.e., region(".styleClassForRegion") or region("#idForRegion")
3547 private static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:";
3548 private ParsedValueImpl<String,String> parseRegion(Term root)
3549 throws ParseException {
3550 // first term in the chain is the function name...
3551 final String fn = (root.token != null) ? root.token.getText() : null;
3552 if (!"region".regionMatches(true, 0, fn, 0, 6)) {
3553 error(root,"Expected \'region\'");
3554 }
3555
3556 Term arg = root.firstArg;
3557 if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3558
3559 if (arg.token == null ||
3560 arg.token.getType() != CssLexer.STRING ||
3561 arg.token.getText() == null ||
3562 arg.token.getText().isEmpty()) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'");
3563
3564 final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText());
3565 return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance());
3566 }
3567
3568 // url("<uri>") is tokenized by the lexer, so the root arg should be a URL token.
3569 private ParsedValueImpl<ParsedValue[],String> parseURI(Term root)
3570 throws ParseException {
3571
3572 if (root == null) error(root, "Expected \'url(\"<uri-string>\")\'");
3573
3574 if (root.token == null ||
3575 root.token.getType() != CssLexer.URL ||
3576 root.token.getText() == null ||
3577 root.token.getText().isEmpty()) error(root, "Expected \'url(\"<uri-string>\")\'");
3578
3579 final String uri = root.token.getText();
3580 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
3581 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()),
3582 null // placeholder for Stylesheet URL
3583 };
3584 return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance());
3585 }
3586
3587 // parse a series of URI values separated by commas.
3588 // i.e., <uri> [, <uri>]*
3589 private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root)
3590 throws ParseException {
3591
3592 int nLayers = numberOfLayers(root);
3593
3594 Term temp = root;
3595 int layer = 0;
3600 temp = nextLayer(temp);
3601 }
3602
3603 return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance());
3604 }
3605
3606 ////////////////////////////////////////////////////////////////////////////
3607 //
3608 // http://www.w3.org/TR/css3-fonts
3609 //
3610 ////////////////////////////////////////////////////////////////////////////
3611
3612 /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */
3613 private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException {
3614
3615 if (root == null) return null;
3616 final Token token = root.token;
3617 if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'");
3618
3619 Size size = null;
3620 if (token.getType() == CssLexer.IDENT) {
3621 final String ident = token.getText().toLowerCase(Locale.ROOT);
3622 double value = -1;
3623 if ("inherit".equals(ident)) {
3624 value = 100;
3625 } else if ("xx-small".equals(ident)) {
3626 value = 60;
3627 } else if ("x-small".equals(ident)) {
3628 value = 75;
3629 } else if ("small".equals(ident)) {
3630 value = 80;
3631 } else if ("medium".equals(ident)) {
3632 value = 100;
3633 } else if ("large".equals(ident)) {
3634 value = 120;
3635 } else if ("x-large".equals(ident)) {
3636 value = 150;
3637 } else if ("xx-large".equals(ident)) {
3638 value = 200;
3639 } else if ("smaller".equals(ident)) {
3640 value = 80;
3645 if (value > -1) {
3646 size = new Size(value, SizeUnits.PERCENT);
3647 }
3648 }
3649
3650 // if size is null, then size is not one of the keywords above.
3651 if (size == null) {
3652 size = size(token);
3653 }
3654
3655 ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null);
3656 return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance());
3657 }
3658
3659 /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */
3660 private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException {
3661
3662 if (root == null) return null;
3663 final Token token = root.token;
3664 if (token == null ||
3665 token.getType() != CssLexer.IDENT ||
3666 token.getText() == null ||
3667 token.getText().isEmpty()) error(root, "Expected \'<font-style>\'");
3668
3669 final String ident = token.getText().toLowerCase(Locale.ROOT);
3670 String posture = FontPosture.REGULAR.name();
3671
3672 if ("normal".equals(ident)) {
3673 posture = FontPosture.REGULAR.name();
3674 } else if ("italic".equals(ident)) {
3675 posture = FontPosture.ITALIC.name();
3676 } else if ("oblique".equals(ident)) {
3677 posture = FontPosture.ITALIC.name();
3678 } else if ("inherit".equals(ident)) {
3679 posture = "inherit";
3680 } else {
3681 return null;
3682 }
3683
3684 return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance());
3685 }
3719 } else if ("600".equals(ident)) {
3720 weight = FontWeight.findByWeight(600).name();
3721 } else if ("700".equals(ident)) {
3722 weight = FontWeight.findByWeight(700).name();
3723 } else if ("800".equals(ident)) {
3724 weight = FontWeight.findByWeight(800).name();
3725 } else if ("900".equals(ident)) {
3726 weight = FontWeight.findByWeight(900).name();
3727 } else {
3728 error(root, "Expected \'<font-weight>\'");
3729 }
3730 return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance());
3731 }
3732
3733 private ParsedValueImpl<String,String> parseFontFamily(Term root) throws ParseException {
3734
3735 if (root == null) return null;
3736 final Token token = root.token;
3737 String text = null;
3738 if (token == null ||
3739 (token.getType() != CssLexer.IDENT &&
3740 token.getType() != CssLexer.STRING) ||
3741 (text = token.getText()) == null ||
3742 text.isEmpty()) error(root, "Expected \'<font-family>\'");
3743
3744 final String fam = stripQuotes(text.toLowerCase(Locale.ROOT));
3745 if ("inherit".equals(fam)) {
3746 return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance());
3747 } else if ("serif".equals(fam) ||
3748 "sans-serif".equals(fam) ||
3749 "cursive".equals(fam) ||
3750 "fantasy".equals(fam) ||
3751 "monospace".equals(fam)) {
3752 return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance());
3753 } else {
3754 return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance());
3755 }
3756 }
3757
3758 // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily
3759 private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException {
3760
3761 // Because style, variant, weight, size and family can inherit
3762 // AND style, variant and weight are optional, parsing this backwards
3763 // is easier.
3764 Term next = root.nextInSeries;
3765 root.nextInSeries = null;
3766 while (next != null) {
3767 Term temp = next.nextInSeries;
3768 next.nextInSeries = root;
3769 root = next;
3770 next = temp;
3771 }
3772
3773 // Now, root should point to fontFamily
3774 Token token = root.token;
3775 int ttype = token.getType();
3776 if (ttype != CssLexer.IDENT &&
3777 ttype != CssLexer.STRING) error(root, "Expected \'<font-family>\'");
3778 ParsedValueImpl<String,String> ffamily = parseFontFamily(root);
3779
3780 Term term = root;
3781 if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'");
3782 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3783
3784 // Now, term could be the font size or it could be the line-height.
3785 // If the next term is a forward slash, then it's line-height.
3786 Term temp;
3787 if (((temp = term.nextInSeries) != null) &&
3788 (temp.token != null && temp.token.getType() == CssLexer.SOLIDUS)) {
3789
3790 root = temp;
3791
3792 if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'");
3793 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'");
3794
3795 token = term.token;
3796 }
3797
3798 ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term);
3799 if (fsize == null) error(root, "Expected \'<size>\'");
3800
3801 ParsedValueImpl<String,FontPosture> fstyle = null;
3802 ParsedValueImpl<String,FontWeight> fweight = null;
3803 String fvariant = null;
3804
3805 while ((term = term.nextInSeries) != null) {
3806
3807 if (term.token == null ||
3808 term.token.getType() != CssLexer.IDENT ||
3809 term.token.getText() == null ||
3810 term.token.getText().isEmpty())
3811 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'");
3812
3813 if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) {
3814 ;
3815 } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) {
3816 fvariant = term.token.getText();
3817 } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) {
3818 ;
3819 }
3820 }
3821
3822 ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle };
3823 return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance());
3824 }
3825
3826 //
3827 // Parser state machine
3828 //
3829 Token currentToken = null;
3830
3831 // return the next token that is not whitespace.
3832 private Token nextToken(CssLexer lexer) {
3833
3834 Token token = null;
3835
3836 do {
3837 token = lexer.nextToken();
3838 } while ((token != null) &&
3839 (token.getType() == CssLexer.WS) ||
3840 (token.getType() == CssLexer.NL));
3841
3842 if (LOGGER.isLoggable(Level.FINEST)) {
3843 LOGGER.finest(token.toString());
3844 }
3845
3846 return token;
3847
3848 }
3849
3850 // keep track of what is in process of being parsed to avoid import loops
3851 private static Stack<String> imports;
3852
3853 private void parse(Stylesheet stylesheet, CssLexer lexer) {
3854
3855 // need to read the first token
3856 currentToken = nextToken(lexer);
3857
3858 while((currentToken != null) &&
3859 (currentToken.getType() == CssLexer.AT_KEYWORD)) {
3860
3861 currentToken = nextToken(lexer);
3862
3863 if (currentToken == null || currentToken.getType() != CssLexer.IDENT) {
3864
3865 // just using ParseException for a nice error message, not for throwing the exception.
3866 ParseException parseException = new ParseException("Expected IDENT", currentToken, this);
3867 final String msg = parseException.toString();
3868 ParseError error = createError(msg);
3869 if (LOGGER.isLoggable(Level.WARNING)) {
3870 LOGGER.warning(error.toString());
3871 }
3872 reportError(error);
3873
3874 // get past EOL or SEMI
3875 do {
3876 currentToken = lexer.nextToken();
3877 } while ((currentToken != null) &&
3878 (currentToken.getType() == CssLexer.SEMI) ||
3879 (currentToken.getType() == CssLexer.WS) ||
3880 (currentToken.getType() == CssLexer.NL));
3881 continue;
3882 }
3883
3884 String keyword = currentToken.getText().toLowerCase(Locale.ROOT);
3885 if ("font-face".equals(keyword)) {
3886 FontFace fontFace = fontFace(lexer);
3887 if (fontFace != null) stylesheet.getFontFaces().add(fontFace);
3888 currentToken = nextToken(lexer);
3889 continue;
3890
3891 } else if ("import".equals(keyword)) {
3892
3893 if (CssParser.imports == null) {
3894 CssParser.imports = new Stack<>();
3895 }
3896
3897 if (!imports.contains(sourceOfStylesheet)) {
3898
3899 imports.push(sourceOfStylesheet);
3900
3901 Stylesheet importedStylesheet = handleImport(lexer);
3902
3903 if (importedStylesheet != null) {
3904 stylesheet.importStylesheet(importedStylesheet);
3905 }
3906
3907 imports.pop();
3908
3909 if (CssParser.imports.isEmpty()) {
3910 CssParser.imports = null;
3911 }
3912
3913 } else {
3914 // Import imports import!
3915 final int line = currentToken.getLine();
3916 final int pos = currentToken.getOffset();
3917 final String msg =
3918 MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]",
3919 line, pos, imports.peek());
3920 ParseError error = createError(msg);
3921 if (LOGGER.isLoggable(Level.WARNING)) {
3922 LOGGER.warning(error.toString());
3923 }
3924 reportError(error);
3925 }
3926
3927 // get past EOL or SEMI
3928 do {
3929 currentToken = lexer.nextToken();
3930 } while ((currentToken != null) &&
3931 (currentToken.getType() == CssLexer.SEMI) ||
3932 (currentToken.getType() == CssLexer.WS) ||
3933 (currentToken.getType() == CssLexer.NL));
3934
3935 continue;
3936
3937 }
3938 }
3939
3940 while ((currentToken != null) &&
3941 (currentToken.getType() != Token.EOF)) {
3942
3943 List<Selector> selectors = selectors(lexer);
3944 if (selectors == null) return;
3945
3946 if ((currentToken == null) ||
3947 (currentToken.getType() != CssLexer.LBRACE)) {
3948 final int line = currentToken != null ? currentToken.getLine() : -1;
3949 final int pos = currentToken != null ? currentToken.getOffset() : -1;
3950 final String msg =
3951 MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]",
3952 line, pos);
3953 ParseError error = createError(msg);
3954 if (LOGGER.isLoggable(Level.WARNING)) {
3955 LOGGER.warning(error.toString());
3956 }
3957 reportError(error);
3958 currentToken = null;
3959 return;
3960 }
3961
3962 // get past the LBRACE
3963 currentToken = nextToken(lexer);
3964
3965 List<Declaration> declarations = declarations(lexer);
3966 if (declarations == null) return;
3967
3968 if ((currentToken != null) &&
3969 (currentToken.getType() != CssLexer.RBRACE)) {
3970 final int line = currentToken.getLine();
3971 final int pos = currentToken.getOffset();
3972 final String msg =
3973 MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]",
3974 line, pos);
3975 ParseError error = createError(msg);
3976 if (LOGGER.isLoggable(Level.WARNING)) {
3977 LOGGER.warning(error.toString());
3978 }
3979 reportError(error);
3980 currentToken = null;
3981 return;
3982 }
3983
3984 stylesheet.getRules().add(new Rule(selectors, declarations));
3985
3986 currentToken = nextToken(lexer);
3987
3988 }
3989 currentToken = null;
3990 }
3991
3992 private FontFace fontFace(CssLexer lexer) {
3993 final Map<String,String> descriptors = new HashMap<String,String>();
3994 final List<FontFaceImpl.FontFaceSrc> sources = new ArrayList<FontFaceImpl.FontFaceSrc>();
3995 while(true) {
3996 currentToken = nextToken(lexer);
3997 if (currentToken.getType() == CssLexer.IDENT) {
3998 String key = currentToken.getText();
3999 // ignore the colon that follows
4000 currentToken = nextToken(lexer);
4001 // get the next token after colon
4002 currentToken = nextToken(lexer);
4003 // ignore all but "src"
4004 if ("src".equalsIgnoreCase(key)) {
4005 while(true) {
4006 if((currentToken != null) &&
4007 (currentToken.getType() != CssLexer.SEMI) &&
4008 (currentToken.getType() != CssLexer.RBRACE) &&
4009 (currentToken.getType() != Token.EOF)) {
4010
4011 if (currentToken.getType() == CssLexer.IDENT) {
4012 // simple reference to other font-family
4013 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.REFERENCE,currentToken.getText()));
4014
4015 } else if (currentToken.getType() == CssLexer.URL) {
4016
4017 // let URLConverter do the conversion
4018 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4019 new ParsedValueImpl<String,String>(currentToken.getText(), StringConverter.getInstance()),
4020 new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4021 };
4022 ParsedValue<ParsedValue[], String> parsedValue =
4023 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4024 String urlStr = parsedValue.convert(null);
4025
4026 URL url = null;
4027 try {
4028 URI fontUri = new URI(urlStr);
4029 url = fontUri.toURL();
4030 } catch (URISyntaxException | MalformedURLException malf) {
4031
4032 final int line = currentToken.getLine();
4033 final int pos = currentToken.getOffset();
4034 final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]", line, pos, urlStr);
4035 ParseError error = createError(msg);
4036 if (LOGGER.isLoggable(Level.WARNING)) {
4037 LOGGER.warning(error.toString());
4038 }
4039 reportError(error);
4040
4041 // skip the rest.
4042 while(currentToken != null) {
4043 int ttype = currentToken.getType();
4044 if (ttype == CssLexer.RBRACE ||
4045 ttype == Token.EOF) {
4046 return null;
4047 }
4048 currentToken = nextToken(lexer);
4049 }
4050 }
4051
4052 String format = null;
4053 while(true) {
4054 currentToken = nextToken(lexer);
4055 final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF;
4056 if (ttype == CssLexer.FUNCTION) {
4057 if ("format(".equalsIgnoreCase(currentToken.getText())) {
4058 continue;
4059 } else {
4060 break;
4061 }
4062 } else if (ttype == CssLexer.IDENT ||
4063 ttype == CssLexer.STRING) {
4064
4065 format = Utils.stripQuotes(currentToken.getText());
4066 } else if (ttype == CssLexer.RPAREN) {
4067 continue;
4068 } else {
4069 break;
4070 }
4071 }
4072 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.URL,url.toExternalForm(), format));
4073
4074 } else if (currentToken.getType() == CssLexer.FUNCTION) {
4075 if ("local(".equalsIgnoreCase(currentToken.getText())) {
4076 // consume the function token
4077 currentToken = nextToken(lexer);
4078 // parse function contents
4079 final StringBuilder localSb = new StringBuilder();
4080 while(true) {
4081 if((currentToken != null) && (currentToken.getType() != CssLexer.RPAREN) &&
4082 (currentToken.getType() != Token.EOF)) {
4083 localSb.append(currentToken.getText());
4084 } else {
4085 break;
4086 }
4087 currentToken = nextToken(lexer);
4088 }
4089 int start = 0, end = localSb.length();
4090 if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++;
4091 if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --;
4092 final String local = localSb.substring(start,end);
4093 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.LOCAL,local));
4094 } else {
4095 // error unknown fontface src type
4096 final int line = currentToken.getLine();
4097 final int pos = currentToken.getOffset();
4098 final String msg = MessageFormat.format("Unknown @font-face src type [" + currentToken.getText() + ")] at [{0,number,#},{1,number,#}]", line, pos);
4099 ParseError error = createError(msg);
4100 if (LOGGER.isLoggable(Level.WARNING)) {
4101 LOGGER.warning(error.toString());
4102 }
4103 reportError(error);
4104
4105 }
4106 } else if (currentToken.getType() == CssLexer.COMMA) {
4107 // ignore
4108 } else {
4109 // error unexpected token
4110 final int line = currentToken.getLine();
4111 final int pos = currentToken.getOffset();
4112 final String msg = MessageFormat.format("Unexpected TOKEN [" + currentToken.getText() + "] at [{0,number,#},{1,number,#}]", line, pos);
4113 ParseError error = createError(msg);
4114 if (LOGGER.isLoggable(Level.WARNING)) {
4115 LOGGER.warning(error.toString());
4116 }
4117 reportError(error);
4118 }
4119 } else {
4120 break;
4121 }
4122 currentToken = nextToken(lexer);
4123 }
4124 } else {
4125 StringBuilder descriptorVal = new StringBuilder();
4126 while(true) {
4127 if((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) &&
4128 (currentToken.getType() != Token.EOF)) {
4129 descriptorVal.append(currentToken.getText());
4130 } else {
4131 break;
4132 }
4133 currentToken = nextToken(lexer);
4134 }
4135 descriptors.put(key,descriptorVal.toString());
4136 }
4137 // continue;
4138 }
4139
4140 if ((currentToken == null) ||
4141 (currentToken.getType() == CssLexer.RBRACE) ||
4142 (currentToken.getType() == Token.EOF)) {
4143 break;
4144 }
4145
4146 }
4147 return new FontFaceImpl(descriptors, sources);
4148 }
4149
4150 private Stylesheet handleImport(CssLexer lexer) {
4151 currentToken = nextToken(lexer);
4152
4153 if (currentToken == null || currentToken.getType() == Token.EOF) {
4154 return null;
4155 }
4156
4157 int ttype = currentToken.getType();
4158
4159 String fname = null;
4160 if (ttype == CssLexer.STRING || ttype == CssLexer.URL) {
4161 fname = currentToken.getText();
4162 }
4163
4164 Stylesheet importedStylesheet = null;
4165 final String _sourceOfStylesheet = sourceOfStylesheet;
4166
4167 if (fname != null) {
4168 // let URLConverter do the conversion
4169 ParsedValueImpl[] uriValues = new ParsedValueImpl[] {
4170 new ParsedValueImpl<String,String>(fname, StringConverter.getInstance()),
4171 new ParsedValueImpl<String,String>(sourceOfStylesheet, null)
4172 };
4173 ParsedValue<ParsedValue[], String> parsedValue =
4174 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance());
4175
4176 String urlString = parsedValue.convert(null);
4177 importedStylesheet = StyleManager.loadStylesheet(urlString);
4178
4179 // When we load an imported stylesheet, the sourceOfStylesheet field
4180 // gets set to the new stylesheet. Once it is done loading we must reset
4181 // this field back to the previous value, otherwise we will potentially
4182 // run into problems (for example, see RT-40346).
4183 sourceOfStylesheet = _sourceOfStylesheet;
4184 }
4185 if (importedStylesheet == null) {
4186 final String msg =
4187 MessageFormat.format("Could not import {0}", fname);
4188 ParseError error = createError(msg);
4189 if (LOGGER.isLoggable(Level.WARNING)) {
4190 LOGGER.warning(error.toString());
4191 }
4192 reportError(error);
4193 }
4194 return importedStylesheet;
4195 }
4196
4197 private List<Selector> selectors(CssLexer lexer) {
4198
4199 List<Selector> selectors = new ArrayList<Selector>();
4200
4201 while(true) {
4202 Selector selector = selector(lexer);
4203 if (selector == null) {
4204 // some error happened, skip the rule...
4205 while ((currentToken != null) &&
4206 (currentToken.getType() != CssLexer.RBRACE) &&
4207 (currentToken.getType() != Token.EOF)) {
4208 currentToken = nextToken(lexer);
4209 }
4210
4211 // current token is either RBRACE or EOF. Calling
4212 // currentToken will get the next token or EOF.
4213 currentToken = nextToken(lexer);
4214
4215 // skipped the last rule?
4216 if (currentToken == null || currentToken.getType() == Token.EOF) {
4217 currentToken = null;
4218 return null;
4219 }
4220
4221 continue;
4222 }
4223 selectors.add(selector);
4224
4225 if ((currentToken != null) &&
4226 (currentToken.getType() == CssLexer.COMMA)) {
4227 // get past the comma
4228 currentToken = nextToken(lexer);
4229 continue;
4230 }
4231
4232 // currentToken was either null or not a comma
4233 // so we are done with selectors.
4234 break;
4235 }
4236
4237 return selectors;
4238 }
4239
4240 private Selector selector(CssLexer lexer) {
4241
4242 List<Combinator> combinators = null;
4243 List<SimpleSelector> sels = null;
4244
4245 SimpleSelector ancestor = simpleSelector(lexer);
4246 if (ancestor == null) return null;
4247
4248 while (true) {
4249 Combinator comb = combinator(lexer);
4250 if (comb != null) {
4251 if (combinators == null) {
4252 combinators = new ArrayList<Combinator>();
4253 }
4254 combinators.add(comb);
4255 SimpleSelector descendant = simpleSelector(lexer);
4256 if (descendant == null) return null;
4257 if (sels == null) {
4258 sels = new ArrayList<SimpleSelector>();
4259 sels.add(ancestor);
4260 }
4261 sels.add(descendant);
4262 } else {
4263 break;
4264 }
4265 }
4266
4267 // RT-15473
4268 // We might return from selector with a NL token instead of an
4269 // LBRACE, so skip past the NL here.
4270 if (currentToken != null && currentToken.getType() == CssLexer.NL) {
4271 currentToken = nextToken(lexer);
4272 }
4273
4274
4275 if (sels == null) {
4276 return ancestor;
4277 } else {
4278 return new CompoundSelector(sels,combinators);
4279 }
4280
4281 }
4282
4283 private SimpleSelector simpleSelector(CssLexer lexer) {
4284
4285 String esel = "*"; // element selector. default to universal
4286 String isel = ""; // id selector
4287 List<String> csels = null; // class selector
4288 List<String> pclasses = null; // pseudoclasses
4289
4290 while (true) {
4291
4292 final int ttype =
4293 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4294
4295 switch(ttype) {
4296 // element selector
4297 case CssLexer.STAR:
4298 case CssLexer.IDENT:
4299 esel = currentToken.getText();
4300 break;
4301
4302 // class selector
4303 case CssLexer.DOT:
4304 currentToken = nextToken(lexer);
4305 if (currentToken != null &&
4306 currentToken.getType() == CssLexer.IDENT) {
4307 if (csels == null) {
4308 csels = new ArrayList<String>();
4309 }
4310 csels.add(currentToken.getText());
4311 } else {
4312 currentToken = Token.INVALID_TOKEN;
4313 return null;
4314 }
4315 break;
4316
4317 // id selector
4318 case CssLexer.HASH:
4319 isel = currentToken.getText().substring(1);
4320 break;
4321
4322 case CssLexer.COLON:
4323 currentToken = nextToken(lexer);
4324 if (currentToken != null && pclasses == null) {
4325 pclasses = new ArrayList<String>();
4326 }
4327
4328 if (currentToken.getType() == CssLexer.IDENT) {
4329 pclasses.add(currentToken.getText());
4330 } else if (currentToken.getType() == CssLexer.FUNCTION){
4331 String pclass = functionalPseudo(lexer);
4332 pclasses.add(pclass);
4333 } else {
4334 currentToken = Token.INVALID_TOKEN;
4335 }
4336
4337 if (currentToken.getType() == Token.INVALID) {
4338 return null;
4339 }
4340 break;
4341
4342 case CssLexer.NL:
4343 case CssLexer.WS:
4344 case CssLexer.COMMA:
4345 case CssLexer.GREATER:
4346 case CssLexer.LBRACE:
4347 case Token.EOF:
4348 return new SimpleSelector(esel, csels, pclasses, isel);
4349
4350 default:
4351 return null;
4352
4353
4354 }
4355
4356 // get the next token, but don't skip whitespace
4357 // since it may be a combinator
4358 currentToken = lexer.nextToken();
4359 if (LOGGER.isLoggable(Level.FINEST)) {
4360 LOGGER.finest(currentToken.toString());
4361 }
4362 }
4363 }
4364
4365 // From http://www.w3.org/TR/selectors/#grammar
4366 // functional_pseudo
4367 // : FUNCTION S* expression ')'
4368 // ;
4369 // expression
4370 // /* In CSS3, the expressions are identifiers, strings, */
4371 // /* or of the form "an+b" */
4372 // : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
4373 // ;
4374 private String functionalPseudo(CssLexer lexer) {
4375
4376 // TODO: This is not how we should handle functional pseudo-classes in the long-run!
4377
4378 StringBuilder pclass = new StringBuilder(currentToken.getText());
4379
4380 while(true) {
4381
4382 currentToken = nextToken(lexer);
4383
4384 switch(currentToken.getType()) {
4385
4386 // TODO: lexer doesn't really scan right and isn't CSS3,
4387 // so PLUS, '-', NUMBER, etc are all useless at this point.
4388 case CssLexer.STRING:
4389 case CssLexer.IDENT:
4390 pclass.append(currentToken.getText());
4391 break;
4392
4393 case CssLexer.RPAREN:
4394 pclass.append(')');
4395 return pclass.toString();
4396
4397 default:
4398 currentToken = Token.INVALID_TOKEN;
4399 return null;
4400 }
4401 }
4402
4403 }
4404
4405 private Combinator combinator(CssLexer lexer) {
4406
4407 Combinator combinator = null;
4408
4409 while (true) {
4410
4411 final int ttype =
4412 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4413
4414 switch(ttype) {
4415
4416 case CssLexer.WS:
4417 // need to check if combinator is null since child token
4418 // might be surrounded by whitespace.
4419 if (combinator == null && " ".equals(currentToken.getText())) {
4420 combinator = Combinator.DESCENDANT;
4421 }
4422 break;
4423
4424 case CssLexer.GREATER:
4425 // no need to check if combinator is null here
4426 combinator = Combinator.CHILD;
4427 break;
4428
4429 case CssLexer.STAR:
4430 case CssLexer.IDENT:
4431 case CssLexer.DOT:
4432 case CssLexer.HASH:
4433 case CssLexer.COLON:
4434 return combinator;
4435
4436 default:
4437 // only selector is expected
4438 return null;
4439
4440 }
4441
4442 // get the next token, but don't skip whitespace
4443 currentToken = lexer.nextToken();
4444 if (LOGGER.isLoggable(Level.FINEST)) {
4445 LOGGER.finest(currentToken.toString());
4446 }
4447 }
4448 }
4449
4450 private List<Declaration> declarations(CssLexer lexer) {
4451
4452 List<Declaration> declarations = new ArrayList<Declaration>();
4453
4454 while (true) {
4455
4456 Declaration decl = declaration(lexer);
4457 if (decl != null) {
4458 declarations.add(decl);
4459 } else {
4460 // some error happened, skip the decl...
4461 while ((currentToken != null) &&
4462 (currentToken.getType() != CssLexer.SEMI) &&
4463 (currentToken.getType() != CssLexer.RBRACE) &&
4464 (currentToken.getType() != Token.EOF)) {
4465 currentToken = nextToken(lexer);
4466 }
4467
4468 // current token is either SEMI, RBRACE or EOF.
4469 if (currentToken != null &&
4470 currentToken.getType() != CssLexer.SEMI)
4471 return declarations;
4472 }
4473
4474 // declaration; declaration; ???
4475 // RT-17830 - allow declaration;;
4476 while ((currentToken != null) &&
4477 (currentToken.getType() == CssLexer.SEMI)) {
4478 currentToken = nextToken(lexer);
4479 }
4480
4481 // if it is delcaration; declaration, then the
4482 // next token should be an IDENT.
4483 if ((currentToken != null) &&
4484 (currentToken.getType() == CssLexer.IDENT)) {
4485 continue;
4486 }
4487
4488 break;
4489 }
4490
4491 return declarations;
4492 }
4493
4494 private Declaration declaration(CssLexer lexer) {
4495
4496 final int ttype =
4497 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4498
4499 if ((currentToken == null) ||
4500 (currentToken.getType() != CssLexer.IDENT)) {
4501 //
4502 // RT-16547: this warning was misleading because an empty rule
4503 // not invalid. Some people put in empty rules just as placeholders.
4504 //
4505 // if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
4506 // final int line = currentToken != null ? currentToken.getLine() : -1;
4507 // final int pos = currentToken != null ? currentToken.getOffset() : -1;
4508 // final String url =
4509 // (stylesheet != null && stylesheet.getUrl() != null) ?
4510 // stylesheet.getUrl().toExternalForm() : "?";
4511 // LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]",
4512 // url,line,pos);
4513 // }
4514 return null;
4515 }
4516
4517 String property = currentToken.getText();
4518
4519 currentToken = nextToken(lexer);
4520
4521 if ((currentToken == null) ||
4522 (currentToken.getType() != CssLexer.COLON)) {
4523 final int line = currentToken.getLine();
4524 final int pos = currentToken.getOffset();
4525 final String msg =
4526 MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]",
4527 line, pos);
4528 ParseError error = createError(msg);
4529 if (LOGGER.isLoggable(Level.WARNING)) {
4530 LOGGER.warning(error.toString());
4531 }
4532 reportError(error);
4533 return null;
4534 }
4535
4536 currentToken = nextToken(lexer);
4537
4538 Term root = expr(lexer);
4539 ParsedValueImpl value = null;
4540 try {
4541 value = (root != null) ? valueFor(property, root, lexer) : null;
4542 } catch (ParseException re) {
4543 Token badToken = re.tok;
4544 final int line = badToken != null ? badToken.getLine() : -1;
4545 final int pos = badToken != null ? badToken.getOffset() : -1;
4546 final String msg =
4547 MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]",
4548 line,pos,re.getMessage(),property);
4549 ParseError error = createError(msg);
4550 if (LOGGER.isLoggable(Level.WARNING)) {
4551 LOGGER.warning(error.toString());
4552 }
4553 reportError(error);
4554 return null;
4555 }
4556
4557 boolean important = currentToken.getType() == CssLexer.IMPORTANT_SYM;
4558 if (important) currentToken = nextToken(lexer);
4559
4560 Declaration decl = (value != null)
4561 ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null;
4562 return decl;
4563 }
4564
4565 private Term expr(CssLexer lexer) {
4566
4567 final Term expr = term(lexer);
4568 Term current = expr;
4569
4570 while(true) {
4571
4572 // if current is null, then term returned null
4573 final int ttype =
4574 (current != null && currentToken != null)
4575 ? currentToken.getType() : Token.INVALID;
4576
4577 if (ttype == Token.INVALID) {
4578 skipExpr(lexer);
4579 return null;
4580 } else if (ttype == CssLexer.SEMI ||
4581 ttype == CssLexer.IMPORTANT_SYM ||
4582 ttype == CssLexer.RBRACE ||
4583 ttype == Token.EOF) {
4584 return expr;
4585 } else if (ttype == CssLexer.COMMA) {
4586 // comma breaks up sequences of terms.
4587 // next series of terms chains off the last term in
4588 // the current series.
4589 currentToken = nextToken(lexer);
4590 current = current.nextLayer = term(lexer);
4591 } else {
4592 current = current.nextInSeries = term(lexer);
4593 }
4594
4595 }
4596 }
4597
4598 private void skipExpr(CssLexer lexer) {
4599
4600 while(true) {
4601
4602 currentToken = nextToken(lexer);
4603
4604 final int ttype =
4605 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4606
4607 if (ttype == CssLexer.SEMI ||
4608 ttype == CssLexer.RBRACE ||
4609 ttype == Token.EOF) {
4610 return;
4611 }
4612 }
4613 }
4614
4615 private Term term(CssLexer lexer) {
4616
4617 final int ttype =
4618 (currentToken != null) ? currentToken.getType() : Token.INVALID;
4619
4620 switch (ttype) {
4621
4622 case CssLexer.NUMBER:
4623 case CssLexer.CM:
4624 case CssLexer.EMS:
4625 case CssLexer.EXS:
4626 case CssLexer.IN:
4627 case CssLexer.MM:
4628 case CssLexer.PC:
4629 case CssLexer.PT:
4630 case CssLexer.PX:
4631 case CssLexer.DEG:
4632 case CssLexer.GRAD:
4633 case CssLexer.RAD:
4634 case CssLexer.TURN:
4635 case CssLexer.PERCENTAGE:
4636 case CssLexer.SECONDS:
4637 case CssLexer.MS:
4638 break;
4639
4640 case CssLexer.STRING:
4641 break;
4642 case CssLexer.IDENT:
4643 break;
4644
4645 case CssLexer.HASH:
4646 break;
4647
4648 case CssLexer.FUNCTION:
4649 case CssLexer.LPAREN:
4650
4651 Term function = new Term(currentToken);
4652 currentToken = nextToken(lexer);
4653
4654 Term arg = term(lexer);
4655 function.firstArg = arg;
4656
4657 while(true) {
4658
4659 final int operator =
4660 currentToken != null ? currentToken.getType() : Token.INVALID;
4661
4662 if (operator == CssLexer.RPAREN) {
4663 currentToken = nextToken(lexer);
4664 return function;
4665 } else if (operator == CssLexer.COMMA) {
4666 // comma breaks up sequences of terms.
4667 // next series of terms chains off the last term in
4668 // the current series.
4669 currentToken = nextToken(lexer);
4670 arg = arg.nextArg = term(lexer);
4671
4672 } else {
4673 arg = arg.nextInSeries = term(lexer);
4674 }
4675
4676 }
4677
4678 case CssLexer.URL:
4679 break;
4680
4681 case CssLexer.SOLIDUS:
4682 break;
4683
4684 default:
4685 final int line = currentToken != null ? currentToken.getLine() : -1;
4686 final int pos = currentToken != null ? currentToken.getOffset() : -1;
4687 final String text = currentToken != null ? currentToken.getText() : "";
4688 final String msg =
4689 MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]",
4690 "\'",text,line,pos);
4691 ParseError error = createError(msg);
4692 if (LOGGER.isLoggable(Level.WARNING)) {
4693 LOGGER.warning(error.toString());
4694 }
4695 reportError(error);
4696 return null;
4697 // currentToken = nextToken(lexer);
4698 //
4699 // return new Term(Token.INVALID_TOKEN);
4700 }
4701
4702 Term term = new Term(currentToken);
4703 currentToken = nextToken(lexer);
4704 return term;
4705 }
4706
4707 public static ObservableList<ParseError> errorsProperty() {
4708 return StyleManager.errorsProperty();
4709 }
4710
4711
4712
4713 /**
4714 * Encapsulate information about the source and nature of errors encountered
4715 * while parsing CSS or applying styles to Nodes.
4716 */
4717 public static class ParseError {
4718
4719 /** @return The error message from the CSS code. */
4720 public final String getMessage() {
4721 return message;
4722 }
4723
4724 public ParseError(String message) {
4725 this.message = message;
4726 }
4727
4728 final String message;
4729
4730 @Override public String toString() {
4731 return "CSS Error: " + message;
4732 }
4733
4734 /** Encapsulate errors arising from parsing of stylesheet files */
4735 public final static class StylesheetParsingError extends ParseError {
4736
4737 StylesheetParsingError(String url, String message) {
4738 super(message);
4739 this.url = url;
4740 }
4741
4742 String getURL() {
4743 return url;
4744 }
4745
4746 private final String url;
4747
4748 @Override public String toString() {
4749 final String path = url != null ? url : "?";
4750 // TBD: i18n
4751 return "CSS Error parsing " + path + ": " + message;
4752 }
4753
4754 }
4755
4756 /** Encapsulate errors arising from parsing of Node's style property */
4757 public final static class InlineStyleParsingError extends ParseError {
4758
4759 InlineStyleParsingError(Styleable styleable, String message) {
4760 super(message);
4761 this.styleable = styleable;
4762 }
4763
4764 Styleable getStyleable() {
4765 return styleable;
4766 }
4767
4768 private final Styleable styleable;
4769
4770 @Override public String toString() {
4771 final String inlineStyle = styleable.getStyle();
4772 final String source = styleable.toString();
4773 // TBD: i18n
4774 return "CSS Error parsing in-line style \'" + inlineStyle +
4775 "\' from " + source + ": " + message;
4776 }
4777 }
4778
4779 /**
4780 * Encapsulate errors arising from parsing when the style is not
4781 * an in-line style nor is the style from a stylesheet. Primarily to
4782 * support unit testing.
4783 */
4784 public final static class StringParsingError extends ParseError {
4785 private final String style;
4786
4787 StringParsingError(String style, String message) {
4788 super(message);
4789 this.style = style;
4790 }
4791
4792 String getStyle() {
4793 return style;
4794 }
4795
4796 @Override public String toString() {
4797 // TBD: i18n
4798 return "CSS Error parsing \'" + style + ": " + message;
4799 }
4800 }
4801
4802 /** Encapsulates errors arising from applying a style to a Node. */
4803 public final static class PropertySetError extends ParseError {
4804 private final CssMetaData styleableProperty;
4805 private final Styleable styleable;
4806
4807 public PropertySetError(CssMetaData styleableProperty,
4808 Styleable styleable, String message) {
4809 super(message);
4810 this.styleableProperty = styleableProperty;
4811 this.styleable = styleable;
4812 }
4813
4814 Styleable getStyleable() {
4815 return styleable;
4816 }
4817
4818 CssMetaData getProperty() {
4819 return styleableProperty;
4820 }
4821
4822 @Override public String toString() {
4823 // TBD: i18n
4824 return "CSS Error parsing \'" + styleableProperty + ": " + message;
4825 }
4826 }
4827 }
4828 }
|