1 /* 2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.css; 27 28 import com.sun.javafx.css.Combinator; 29 import com.sun.javafx.css.FontFaceImpl; 30 import com.sun.javafx.css.ParsedValueImpl; 31 import com.sun.javafx.css.StyleManager; 32 import com.sun.javafx.util.Utils; 33 import javafx.css.converter.BooleanConverter; 34 import javafx.css.converter.DurationConverter; 35 import javafx.css.converter.EffectConverter; 36 import javafx.css.converter.EnumConverter; 37 import javafx.css.converter.FontConverter; 38 import javafx.css.converter.InsetsConverter; 39 import javafx.css.converter.PaintConverter; 40 import javafx.css.converter.SizeConverter; 41 import javafx.css.converter.SizeConverter.SequenceConverter; 42 import javafx.css.converter.StringConverter; 43 import javafx.css.converter.URLConverter; 44 import javafx.css.converter.DeriveColorConverter; 45 import javafx.css.converter.LadderConverter; 46 import javafx.css.converter.StopConverter; 47 import com.sun.javafx.css.parser.Token; 48 import com.sun.javafx.scene.layout.region.BackgroundPositionConverter; 49 import com.sun.javafx.scene.layout.region.BackgroundSizeConverter; 50 import com.sun.javafx.scene.layout.region.BorderImageSliceConverter; 51 import com.sun.javafx.scene.layout.region.BorderImageSlices; 52 import com.sun.javafx.scene.layout.region.BorderImageWidthConverter; 53 import com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter; 54 import com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter; 55 import com.sun.javafx.scene.layout.region.BorderStyleConverter; 56 import com.sun.javafx.scene.layout.region.CornerRadiiConverter; 57 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter; 58 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter; 59 import com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter; 60 import com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter; 61 import com.sun.javafx.scene.layout.region.Margins; 62 import com.sun.javafx.scene.layout.region.RepeatStruct; 63 import com.sun.javafx.scene.layout.region.RepeatStructConverter; 64 import com.sun.javafx.scene.layout.region.SliceSequenceConverter; 65 import com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter; 66 import javafx.collections.ObservableList; 67 import javafx.geometry.Insets; 68 import javafx.scene.effect.BlurType; 69 import javafx.scene.effect.Effect; 70 import javafx.scene.layout.BackgroundPosition; 71 import javafx.scene.layout.BackgroundRepeat; 72 import javafx.scene.layout.BackgroundSize; 73 import javafx.scene.layout.BorderStrokeStyle; 74 import javafx.scene.layout.BorderWidths; 75 import javafx.scene.layout.CornerRadii; 76 import javafx.scene.paint.Color; 77 import javafx.scene.paint.CycleMethod; 78 import javafx.scene.paint.Paint; 79 import javafx.scene.paint.Stop; 80 import javafx.scene.shape.StrokeLineCap; 81 import javafx.scene.shape.StrokeLineJoin; 82 import javafx.scene.shape.StrokeType; 83 import javafx.scene.text.Font; 84 import javafx.scene.text.FontPosture; 85 import javafx.scene.text.FontWeight; 86 import javafx.util.Duration; 87 import 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; 134 sourceOfStylesheet = url; 135 sourceOfInlineStyle = null; 136 } 137 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; 179 } 180 181 /** 182 * Creates a stylesheet from a CSS document string. 183 * 184 *@param stylesheetText the CSS document to parse 185 *@return the Stylesheet 186 */ 187 public Stylesheet parse(final String stylesheetText) { 188 final Stylesheet stylesheet = new Stylesheet(); 189 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) { 190 setInputSource(stylesheetText); 191 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) { 192 parse(stylesheet, reader); 193 } catch (IOException ioe) { 194 // this method doesn't explicitly throw IOException 195 } 196 } 197 return stylesheet; 198 } 199 200 /** 201 * Creates a stylesheet from a CSS document string using docbase as 202 * the base URL for resolving references within stylesheet. 203 * 204 *@param stylesheetText the CSS document to parse 205 *@return the Stylesheet 206 */ 207 public Stylesheet parse(final String docbase, final String stylesheetText) throws IOException { 208 final Stylesheet stylesheet = new Stylesheet(docbase); 209 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) { 210 setInputSource(docbase, stylesheetText); 211 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) { 212 parse(stylesheet, reader); 213 } 214 } 215 return stylesheet; 216 } 217 218 /** 219 * Updates the given stylesheet by reading a CSS document from a URL, 220 * assuming UTF-8 encoding. 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; 331 332 /* 333 * While parsing a declaration, tokens from parsing value (that is, 334 * the expr rule) are held in this tree structure which is then passed 335 * to methods which convert the tree into a ParsedValueImpl. 336 * 337 * Each term in expr is a Term. For simple terms, like HASH, the 338 * Term is just the Token. If the term is a function, then the 339 * Term is a linked-list of Term, the first being the function 340 * name and each nextArg being the arguments. 341 * 342 * If there is more than one term in the expr (insets, for example), 343 * then the terms are linked on nextInSequence. If there is more than one 344 * layer (sequence of terms), then each layer becomes the nextLayer 345 * to the last root in the previous sequence. 346 * 347 * The easiest way to think of it is that a comma starts a nextLayer (except 348 * when a function arg). 349 * 350 * The expr part of the declaration "-fx-padding 1 2, 3 4;" would look 351 * like this: 352 * [1 | nextLayer | nextInSeries]-->[2 | nextLayer | nextInSeries]-->null 353 * | | 354 * null | 355 * .---------------------------------' 356 * '-->[3 | nextLayer | nextInSeries]-->[4 | nextLayer | nextInSeries]-->null 357 * | | 358 * null null 359 * 360 * The first argument in a function needs to be distinct from the 361 * remaining args so that the args of a function in the middle of 362 * a function will not be omitted. Consider 'f0(a, f1(b, c), d)' 363 * If we relied only on nextArg, then the next arg of f0 would be a but 364 * the nextArg of f1 would be d. With firstArg, the firstArg of f0 is a, 365 * the nextArg of a is f1, the firstArg of f1 is b and the nextArg of f1 is d. 366 * 367 * TODO: now that the parser is the parser and not an adjunct to an ANTLR 368 * parser, this Term stuff shouldn't be needed. 369 */ 370 static class Term { 371 final Token token; 372 Term nextInSeries; 373 Term nextLayer; 374 Term firstArg; 375 Term nextArg; 376 Term(Token token) { 377 this.token = token; 378 this.nextLayer = null; 379 this.nextInSeries = null; 380 this.firstArg = null; 381 this.nextArg = null; 382 } 383 Term() { 384 this(null); 385 } 386 387 @Override public String toString() { 388 StringBuilder buf = new StringBuilder(); 389 if (token != null) buf.append(String.valueOf(token.getText())); 390 if (nextInSeries != null) { 391 buf.append("<nextInSeries>"); 392 buf.append(nextInSeries.toString()); 393 buf.append("</nextInSeries>\n"); 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 "); 449 buf.append(exception.getClass().getName()) 450 .append(" at:"); 451 int end = 0; 452 while(end < stea.length) { 453 // only report parser part of the stack trace. 454 if (!getClass().getName().equals(stea[end].getClassName())) { 455 break; 456 } 457 buf.append("\n\t") 458 .append(stea[end++].toString()); 459 } 460 LOGGER.warning(buf.toString()); 461 } 462 } 463 } 464 465 private String formatDeprecatedMessage(final Term root, final String syntax) { 466 final StringBuilder buf = 467 new StringBuilder("Using deprecated syntax for "); 468 buf.append(syntax); 469 if (sourceOfStylesheet != null){ 470 buf.append(" at ") 471 .append(sourceOfStylesheet) 472 .append("[") 473 .append(root.token.getLine()) 474 .append(',') 475 .append(root.token.getOffset()) 476 .append("]"); 477 } 478 buf.append(". Refer to the CSS Reference Guide."); 479 return buf.toString(); 480 } 481 482 // Assumes string is not a lookup! 483 private ParsedValueImpl<Color,Color> colorValueOfString(String str) { 484 485 if(str.startsWith("#") || str.startsWith("0x")) { 486 487 double a = 1.0f; 488 String c = str; 489 final int prefixLength = (str.startsWith("#")) ? 1 : 2; 490 491 final int len = c.length(); 492 // rgba or rrggbbaa - trim off the alpha 493 if ( (len-prefixLength) == 4) { 494 a = Integer.parseInt(c.substring(len-1), 16) / 15.0f; 495 c = c.substring(0,len-1); 496 } else if ((len-prefixLength) == 8) { 497 a = Integer.parseInt(c.substring(len-2), 16) / 255.0f; 498 c = c.substring(0,len-2); 499 } 500 // else color was rgb or rrggbb (no alpha) 501 return new ParsedValueImpl<Color,Color>(Color.web(c,a), null); 502 } 503 504 try { 505 return new ParsedValueImpl<Color,Color>(Color.web(str), null); 506 } catch (final IllegalArgumentException e) { 507 } catch (final NullPointerException e) { 508 } 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; 630 631 int nTerms = 0; 632 Term term = root; 633 do { 634 nTerms += 1; 635 term = term.nextInSeries; 636 } while (term != null); 637 return nTerms; 638 } 639 640 // Count the number of series of terms 641 private int numberOfLayers(final Term root) { 642 if (root == null) return 0; 643 644 int nLayers = 0; 645 Term term = root; 646 do { 647 nLayers += 1; 648 while (term.nextInSeries != null) { 649 term = term.nextInSeries; 650 } 651 term = term.nextLayer; 652 } while (term != null); 653 return nLayers; 654 } 655 656 // Count the number of args of terms. root is the function. 657 private int numberOfArgs(final Term root) { 658 if (root == null) return 0; 659 660 int nArgs = 0; 661 Term term = root.firstArg; 662 while (term != null) { 663 nArgs += 1; 664 term = term.nextArg; 665 } 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)) { 714 return parseURILayers(root); 715 } else if ("-fx-background-insets".equals(prop)) { 716 return parseInsetsLayers(root); 717 } else if ("-fx-opaque-insets".equals(prop)) { 718 return parseInsetsLayer(root); 719 } else if ("-fx-background-position".equals(prop)) { 720 return parseBackgroundPositionLayers(root); 721 } else if ("-fx-background-radius".equals(prop)) { 722 return parseCornerRadius(root); 723 } else if ("-fx-background-repeat".equals(prop)) { 724 return parseBackgroundRepeatStyleLayers(root); 725 } else if ("-fx-background-size".equals(prop)) { 726 return parseBackgroundSizeLayers(root); 727 } else if ("-fx-border-color".equals(prop)) { 728 return parseBorderPaintLayers(root); 729 } else if ("-fx-border-insets".equals(prop)) { 730 return parseInsetsLayers(root); 731 } else if ("-fx-border-radius".equals(prop)) { 732 return parseCornerRadius(root); 733 } else if ("-fx-border-style".equals(prop)) { 734 return parseBorderStyleLayers(root); 735 } else if ("-fx-border-width".equals(prop)) { 736 return parseMarginsLayers(root); 737 } else if ("-fx-border-image-insets".equals(prop)) { 738 return parseInsetsLayers(root); 739 } else if ("-fx-border-image-repeat".equals(prop)) { 740 return parseBorderImageRepeatStyleLayers(root); 741 } else if ("-fx-border-image-slice".equals(prop)) { 742 return parseBorderImageSliceLayers(root); 743 } else if ("-fx-border-image-source".equals(prop)) { 744 return parseURILayers(root); 745 } else if ("-fx-border-image-width".equals(prop)) { 746 return parseBorderImageWidthLayers(root); 747 } else if ("-fx-padding".equals(prop)) { 748 ParsedValueImpl<?,Size>[] sides = parseSize1to4(root); 749 return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 750 } else if ("-fx-label-padding".equals(prop)) { 751 ParsedValueImpl<?,Size>[] sides = parseSize1to4(root); 752 return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 753 } else if (prop.endsWith("font-family")) { 754 return parseFontFamily(root); 755 } else if (prop.endsWith("font-size")) { 756 ParsedValueImpl fsize = parseFontSize(root); 757 if (fsize == null) error(root, "Expected \'<font-size>\'"); 758 return fsize; 759 } else if (prop.endsWith("font-style")) { 760 ParsedValueImpl fstyle = parseFontStyle(root); 761 if (fstyle == null) error(root, "Expected \'<font-style>\'"); 762 return fstyle; 763 } else if (prop.endsWith("font-weight")) { 764 ParsedValueImpl fweight = parseFontWeight(root); 765 if (fweight == null) error(root, "Expected \'<font-style>\'"); 766 return fweight; 767 } else if (prop.endsWith("font")) { 768 return parseFont(root); 769 } else if ("-fx-stroke-dash-array".equals(prop)) { 770 // TODO: Figure out a way that these properties don't need to be 771 // special cased. 772 Term term = root; 773 int nArgs = numberOfTerms(term); 774 ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs]; 775 int segment = 0; 776 while(term != null) { 777 segments[segment++] = parseSize(term); 778 term = term.nextInSeries; 779 } 780 781 return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SequenceConverter.getInstance()); 782 783 } else if ("-fx-stroke-line-join".equals(prop)) { 784 // TODO: Figure out a way that these properties don't need to be 785 // special cased. 786 ParsedValueImpl[] values = parseStrokeLineJoin(root); 787 if (values == null) error(root, "Expected \'miter', \'bevel\' or \'round\'"); 788 return values[0]; 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) 1081 throws ParseException { 1082 1083 // first term in the chain is the function name... 1084 final String fn = (root.token != null) ? root.token.getText() : null; 1085 if (fn == null || !"derive".regionMatches(true, 0, fn, 0, 6)) { 1086 final String msg = "Expected \'derive\'"; 1087 error(root, msg); 1088 } 1089 1090 Term arg = root; 1091 if ((arg = arg.firstArg) == null) error(root, "Expected \'<color>\'"); 1092 1093 final ParsedValueImpl<?,Color> color = parseColor(arg); 1094 1095 final Term prev = arg; 1096 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<percent\'"); 1097 1098 final ParsedValueImpl<?,Size> brightness = parseSize(arg); 1099 1100 ParsedValueImpl[] values = new ParsedValueImpl[] { color, brightness }; 1101 return new ParsedValueImpl<ParsedValue[],Color>(values, DeriveColorConverter.getInstance()); 1102 } 1103 1104 // 'ladder' color 'stops' stop+ 1105 private ParsedValueImpl<ParsedValue[],Color> ladder(final Term root) throws ParseException { 1106 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 1171 // <ladder> = ladder(<color>, <color-stop>[, <color-stop>]+ ) 1172 private ParsedValueImpl<ParsedValue[],Color> parseLadder(final Term root) throws ParseException { 1173 1174 // first term in the chain is the function name... 1175 final String fn = (root.token != null) ? root.token.getText() : null; 1176 if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) { 1177 final String msg = "Expected \'ladder\'"; 1178 error(root, msg); 1179 } 1180 1181 Term term = root; 1182 1183 if ((term = term.firstArg) == null) error(root, "Expected \'<color>\'"); 1184 final ParsedValueImpl<?,Color> color = parse(term); 1185 1186 Term prev = term; 1187 1188 if ((term = term.nextArg) == null) 1189 error(prev, "Expected \'<color-stop>[, <color-stop>]+\'"); 1190 1191 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(term); 1192 1193 ParsedValueImpl[] values = new ParsedValueImpl[stops.length+1]; 1194 values[0] = color; 1195 System.arraycopy(stops, 0, values, 1, stops.length); 1196 return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance()); 1197 } 1198 1199 // parse (<number>, <color>)+ 1200 // root.token should be a size 1201 // root.token.next should be a color 1202 private ParsedValueImpl<ParsedValue[], Stop> stop(final Term root) 1203 throws ParseException { 1204 1205 // first term in the chain is the function name... 1206 final String fn = (root.token != null) ? root.token.getText() : null; 1207 if (fn == null || !"(".equals(fn)) { 1208 final String msg = "Expected \'(\'"; 1209 error(root, msg); 1210 } 1211 1212 Term arg = null; 1213 1214 if ((arg = root.firstArg) == null) error(root, "Expected \'<number>\'"); 1215 1216 ParsedValueImpl<?,Size> size = parseSize(arg); 1217 1218 Term prev = arg; 1219 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1220 1221 ParsedValueImpl<?,Color> color = parseColor(arg); 1222 1223 ParsedValueImpl[] values = new ParsedValueImpl[] { size, color }; 1224 return new ParsedValueImpl<ParsedValue[],Stop>(values, StopConverter.getInstance()); 1225 1226 } 1227 1228 // http://dev.w3.org/csswg/css3-images/#color-stop-syntax 1229 // <color-stop> = <color> [ <percentage> | <length> ]? 1230 private ParsedValueImpl<ParsedValue[], Stop>[] parseColorStops(final Term root) 1231 throws ParseException { 1232 1233 int nArgs = 1; 1234 Term temp = root; 1235 while(temp != null) { 1236 if (temp.nextArg != null) { 1237 nArgs += 1; 1238 temp = temp.nextArg; 1239 } else if (temp.nextInSeries != null) { 1240 temp = temp.nextInSeries; 1241 } else { 1242 break; 1243 } 1244 } 1245 1246 if (nArgs < 2) { 1247 error(root, "Expected \'<color-stop>\'"); 1248 } 1249 1250 ParsedValueImpl<?,Color>[] colors = new ParsedValueImpl[nArgs]; 1251 Size[] positions = new Size[nArgs]; 1252 java.util.Arrays.fill(positions, null); 1253 1254 Term stop = root; 1255 Term prev = root; 1256 SizeUnits units = null; 1257 for (int n = 0; n<nArgs; n++) { 1258 1259 colors[n] = parseColor(stop); 1260 1261 prev = stop; 1262 Term term = stop.nextInSeries; 1263 if (term != null) { 1264 if (isSize(term.token)) { 1265 positions[n] = size(term.token); 1266 if (units != null) { 1267 if (units != positions[n].getUnits()) { 1268 error(term, "Parser unable to handle mixed \'<percent>\' and \'<length>\'"); 1269 } 1270 } 1271 } else { 1272 error(prev, "Expected \'<percent>\' or \'<length>\'"); 1273 } 1274 prev = term; 1275 stop = term.nextArg; 1276 } else { 1277 prev = stop; 1278 stop = stop.nextArg; 1279 } 1280 1281 } 1282 1283 // 1284 // normalize positions according to 1285 // http://dev.w3.org/csswg/css3-images/#color-stop-syntax 1286 // 1287 // If the first color-stop does not have a position, set its 1288 // position to 0%. If the last color-stop does not have a position, 1289 // set its position to 100%. 1290 if (positions[0] == null) positions[0] = new Size(0, SizeUnits.PERCENT); 1291 if (positions[nArgs-1] == null) positions[nArgs-1] = new Size(100, SizeUnits.PERCENT); 1292 1293 // If a color-stop has a position that is less than the specified 1294 // position of any color-stop before it in the list, set its 1295 // position to be equal to the largest specified position of any 1296 // color-stop before it. 1297 Size max = null; 1298 for (int n = 1 ; n<nArgs; n++) { 1299 Size pos0 = positions[n-1]; 1300 if (pos0 == null) continue; 1301 if (max == null || max.getValue() < pos0.getValue()) { 1302 // TODO: this doesn't work with mixed length and percent 1303 max = pos0; 1304 } 1305 Size pos1 = positions[n]; 1306 if (pos1 == null) continue; 1307 1308 if (pos1.getValue() < max.getValue()) positions[n] = max; 1309 } 1310 1311 // If any color-stop still does not have a position, then, 1312 // for each run of adjacent color-stops without positions, set 1313 // their positions so that they are evenly spaced between the 1314 // preceding and following color-stops with positions. 1315 Size preceding = null; 1316 int withoutIndex = -1; 1317 for (int n = 0 ; n<nArgs; n++) { 1318 Size pos = positions[n]; 1319 if (pos == null) { 1320 if (withoutIndex == -1) withoutIndex = n; 1321 } else { 1322 if (withoutIndex > -1) { 1323 1324 int nWithout = n - withoutIndex; 1325 double precedingValue = preceding.getValue(); 1326 double delta = 1327 (pos.getValue() - precedingValue) / (nWithout + 1); 1328 1329 while(withoutIndex < n) { 1330 precedingValue += delta; 1331 positions[withoutIndex++] = 1332 new Size(precedingValue, pos.getUnits()); 1333 } 1334 withoutIndex = -1; 1335 preceding = pos; 1336 } else { 1337 preceding = pos; 1338 } 1339 } 1340 } 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 1382 return new ParsedValueImpl[] { ptX, ptY }; 1383 } 1384 1385 private ParsedValueImpl parseFunction(final Term root) throws ParseException { 1386 1387 // Text from parser is function name plus the lparen, e.g., 'derive(' 1388 final String fcn = (root.token != null) ? root.token.getText() : null; 1389 if (fcn == null) { 1390 error(root, "Expected function name"); 1391 } else if ("rgb".regionMatches(true, 0, fcn, 0, 3)) { 1392 return rgb(root); 1393 } else if ("hsb".regionMatches(true, 0, fcn, 0, 3)) { 1394 return hsb(root); 1395 } else if ("derive".regionMatches(true, 0, fcn, 0, 6)) { 1396 return derive(root); 1397 } else if ("innershadow".regionMatches(true, 0, fcn, 0, 11)) { 1398 return innershadow(root); 1399 } else if ("dropshadow".regionMatches(true, 0, fcn, 0, 10)) { 1400 return dropshadow(root); 1401 } else if ("linear-gradient".regionMatches(true, 0, fcn, 0, 15)) { 1402 return parseLinearGradient(root); 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)); 1444 } 1445 1446 // innershadow <blur-type> <color> <radius> <choke> <offset-x> <offset-y> 1447 private ParsedValueImpl innershadow(final Term root) throws ParseException { 1448 1449 // first term in the chain is the function name... 1450 final String fn = (root.token != null) ? root.token.getText() : null; 1451 if (!"innershadow".regionMatches(true, 0, fn, 0, 11)) { 1452 final String msg = "Expected \'innershadow\'"; 1453 error(root, msg); 1454 } 1455 1456 Term arg; 1457 1458 if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'"); 1459 ParsedValueImpl<String,BlurType> blurVal = blurType(arg); 1460 1461 Term prev = arg; 1462 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1463 1464 ParsedValueImpl<?,Color> colorVal = parseColor(arg); 1465 1466 prev = arg; 1467 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1468 1469 ParsedValueImpl<?,Size> radiusVal = parseSize(arg); 1470 1471 prev = arg; 1472 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1473 1474 ParsedValueImpl<?,Size> chokeVal = parseSize(arg); 1475 1476 prev = arg; 1477 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1478 1479 ParsedValueImpl<?,Size> offsetXVal = parseSize(arg); 1480 1481 prev = arg; 1482 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1483 1484 ParsedValueImpl<?,Size> offsetYVal = parseSize(arg); 1485 1486 ParsedValueImpl[] values = new ParsedValueImpl[] { 1487 blurVal, 1488 colorVal, 1489 radiusVal, 1490 chokeVal, 1491 offsetXVal, 1492 offsetYVal 1493 }; 1494 return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.InnerShadowConverter.getInstance()); 1495 } 1496 1497 // dropshadow <blur-type> <color> <radius> <spread> <offset-x> <offset-y> 1498 private ParsedValueImpl dropshadow(final Term root) throws ParseException { 1499 1500 // first term in the chain is the function name... 1501 final String fn = (root.token != null) ? root.token.getText() : null; 1502 if (!"dropshadow".regionMatches(true, 0, fn, 0, 10)) { 1503 final String msg = "Expected \'dropshadow\'"; 1504 error(root, msg); 1505 } 1506 1507 Term arg; 1508 1509 if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'"); 1510 ParsedValueImpl<String,BlurType> blurVal = blurType(arg); 1511 1512 Term prev = arg; 1513 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1514 1515 ParsedValueImpl<?,Color> colorVal = parseColor(arg); 1516 1517 prev = arg; 1518 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1519 1520 ParsedValueImpl<?,Size> radiusVal = parseSize(arg); 1521 1522 prev = arg; 1523 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1524 1525 ParsedValueImpl<?,Size> spreadVal = parseSize(arg); 1526 1527 prev = arg; 1528 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1529 1530 ParsedValueImpl<?,Size> offsetXVal = parseSize(arg); 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; 1643 } 1644 1645 1646 } else { 1647 // last term was a CycleMethod, so term is not null. 1648 // root points at 'linear', now we want the next term after root 1649 // to be the term after cyclemethod, which may be another series 1650 // of paint or another layer. 1651 // 1652 root.nextInSeries = term.nextInSeries; 1653 root.nextLayer = term.nextLayer; 1654 } 1655 1656 ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length]; 1657 int index = 0; 1658 values[index++] = (startPt != null) ? startPt[0] : null; 1659 values[index++] = (startPt != null) ? startPt[1] : null; 1660 values[index++] = (endPt != null) ? endPt[0] : null; 1661 values[index++] = (endPt != null) ? endPt[1] : null; 1662 values[index++] = cycleMethod; 1663 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 1664 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance()); 1665 } 1666 1667 // Based off http://dev.w3.org/csswg/css3-images/#linear-gradients 1668 // 1669 // <linear-gradient> = linear-gradient( 1670 // [ [from <point> to <point>] | [ to <side-or-corner> ] ] ,]? [ [ repeat | reflect ] ,]? 1671 // <color-stop>[, <color-stop>]+ 1672 // ) 1673 // 1674 // 1675 // <point> = <percentage> <percentage> | <length> <length> 1676 // <side-or-corner> = [left | right] || [top | bottom] 1677 // 1678 // If neither repeat nor reflect are given, then the CycleMethod defaults "NO_CYCLE". 1679 // If neither [from <point> to <point>] nor [ to <side-or-corner> ] are given, 1680 // then the gradient direction defaults to 'to bottom'. 1681 // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax. 1682 private ParsedValueImpl parseLinearGradient(final Term root) throws ParseException { 1683 1684 // first term in the chain is the function name... 1685 final String fn = (root.token != null) ? root.token.getText() : null; 1686 if (!"linear-gradient".regionMatches(true, 0, fn, 0, 15)) { 1687 final String msg = "Expected \'linear-gradient\'"; 1688 error(root, msg); 1689 } 1690 1691 Term arg; 1692 1693 if ((arg = root.firstArg) == null || 1694 arg.token == null || 1695 arg.token.getText().isEmpty()) { 1696 error(root, 1697 "Expected \'from <point> to <point>\' or \'to <side-or-corner>\' " + 1698 "or \'<cycle-method>\' or \'<color-stop>\'"); 1699 } 1700 1701 Term prev = arg; 1702 // ParsedValueImpl<Size,Size> angleVal = null; 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) && 1808 startY == 0 && endY == 0) { 1809 // start bottom, end top 1810 startY = 100; 1811 endY = 0; 1812 } else if ("bottom".equals(sideOrCorner2) && 1813 startY == 0 && endY == 0) { 1814 // start top, end bottom 1815 startY = 0; 1816 endY = 100; 1817 1818 } else { 1819 error(arg, "Invalid \'<side-or-corner>\'"); 1820 } 1821 1822 } else { 1823 error (prev, "Expected \'<side-or-corner>\'"); 1824 } 1825 } 1826 1827 1828 startPt = new ParsedValueImpl[] { 1829 new ParsedValueImpl<Size,Size>(new Size(startX, SizeUnits.PERCENT), null), 1830 new ParsedValueImpl<Size,Size>(new Size(startY, SizeUnits.PERCENT), null) 1831 }; 1832 1833 endPt = new ParsedValueImpl[] { 1834 new ParsedValueImpl<Size,Size>(new Size(endX, SizeUnits.PERCENT), null), 1835 new ParsedValueImpl<Size,Size>(new Size(endY, SizeUnits.PERCENT), null) 1836 }; 1837 1838 prev = arg; 1839 arg = arg.nextArg; 1840 } 1841 1842 if (startPt == null && endPt == null) { 1843 // spec says defaults to bottom 1844 startPt = new ParsedValueImpl[] { 1845 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null), 1846 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null) 1847 }; 1848 1849 endPt = new ParsedValueImpl[] { 1850 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null), 1851 new ParsedValueImpl<Size,Size>(new Size(100, SizeUnits.PERCENT), null) 1852 }; 1853 } 1854 1855 if (arg == null || 1856 arg.token == null || 1857 arg.token.getText().isEmpty()) { 1858 error(prev, "Expected \'<cycle-method>\' or \'<color-stop>\'"); 1859 } 1860 1861 CycleMethod cycleMethod = CycleMethod.NO_CYCLE; 1862 if ("reflect".equalsIgnoreCase(arg.token.getText())) { 1863 cycleMethod = CycleMethod.REFLECT; 1864 prev = arg; 1865 arg = arg.nextArg; 1866 } else if ("repeat".equalsIgnoreCase(arg.token.getText())) { 1867 cycleMethod = CycleMethod.REFLECT; 1868 prev = arg; 1869 arg = arg.nextArg; 1870 } 1871 1872 if (arg == null || 1873 arg.token == null || 1874 arg.token.getText().isEmpty()) { 1875 error(prev, "Expected \'<color-stop>\'"); 1876 } 1877 1878 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg); 1879 1880 ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length]; 1881 int index = 0; 1882 values[index++] = (startPt != null) ? startPt[0] : null; 1883 values[index++] = (startPt != null) ? startPt[1] : null; 1884 values[index++] = (endPt != null) ? endPt[0] : null; 1885 values[index++] = (endPt != null) ? endPt[1] : null; 1886 values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 1887 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 1888 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance()); 1889 1890 } 1891 1892 // radial [focus-angle <number | percent>]? [focus-distance <size>]? 1893 // [center (<size>,<size>)]? <size> 1894 // stops [ ( <number> , <color> ) ]+ [ repeat | reflect ]? 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; 2014 } 2015 2016 2017 } else { 2018 // last term was a CycleMethod, so term is not null. 2019 // root points at 'linear', now we want the next term after root 2020 // to be the term after cyclemethod, which may be another series 2021 // of paint or another layer. 2022 // 2023 root.nextInSeries = term.nextInSeries; 2024 root.nextLayer = term.nextLayer; 2025 } 2026 2027 ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length]; 2028 int index = 0; 2029 values[index++] = focusAngle; 2030 values[index++] = focusDistance; 2031 values[index++] = (centerPoint != null) ? centerPoint[0] : null; 2032 values[index++] = (centerPoint != null) ? centerPoint[1] : null; 2033 values[index++] = radius; 2034 values[index++] = cycleMethod; 2035 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 2036 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance()); 2037 } 2038 2039 // Based off http://dev.w3.org/csswg/css3-images/#radial-gradients 2040 // 2041 // <radial-gradient> = radial-gradient( 2042 // [ focus-angle <angle>, ]? 2043 // [ focus-distance <percentage>, ]? 2044 // [ center <point>, ]? 2045 // radius <length>, 2046 // [ [ repeat | reflect ] ,]? 2047 // <color-stop>[, <color-stop>]+ ) 2048 // 2049 // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax. 2050 private ParsedValueImpl parseRadialGradient(final Term root) throws ParseException { 2051 2052 // first term in the chain is the function name... 2053 final String fn = (root.token != null) ? root.token.getText() : null; 2054 if (!"radial-gradient".regionMatches(true, 0, fn, 0, 15)) { 2055 final String msg = "Expected \'radial-gradient\'"; 2056 error(root, msg); 2057 } 2058 2059 Term arg; 2060 2061 if ((arg = root.firstArg) == null || 2062 arg.token == null || 2063 arg.token.getText().isEmpty()) { 2064 error(root, 2065 "Expected \'focus-angle <angle>\' " + 2066 "or \'focus-distance <percentage>\' " + 2067 "or \'center <point>\' " + 2068 "or \'radius [<length> | <percentage>]\'"); 2069 } 2070 2071 Term prev = arg; 2072 ParsedValueImpl<?,Size>focusAngle = null; 2073 ParsedValueImpl<?,Size>focusDistance = null; 2074 ParsedValueImpl<?,Size>[] centerPoint = null; 2075 ParsedValueImpl<?,Size>radius = null; 2076 2077 if ("focus-angle".equalsIgnoreCase(arg.token.getText())) { 2078 2079 prev = arg; 2080 if ((arg = arg.nextInSeries) == null || 2081 !isSize(arg.token)) error(prev, "Expected \'<angle>\'"); 2082 2083 Size angle = size(arg.token); 2084 switch(angle.getUnits()) { 2085 case DEG: 2086 case RAD: 2087 case GRAD: 2088 case TURN: 2089 case PX: 2090 break; 2091 default: 2092 error(arg, "Expected [deg | rad | grad | turn ]"); 2093 } 2094 focusAngle = new ParsedValueImpl<Size,Size>(angle, null); 2095 2096 prev = arg; 2097 if ((arg = arg.nextArg) == null) 2098 error(prev, "Expected \'focus-distance <percentage>\' " + 2099 "or \'center <point>\' " + 2100 "or \'radius [<length> | <percentage>]\'"); 2101 2102 } 2103 2104 if ("focus-distance".equalsIgnoreCase(arg.token.getText())) { 2105 2106 prev = arg; 2107 if ((arg = arg.nextInSeries) == null || 2108 !isSize(arg.token)) error(prev, "Expected \'<percentage>\'"); 2109 2110 Size distance = size(arg.token); 2111 2112 // "The focus point is always specified relative to the center 2113 // point by an angle and a distance relative to the radius." 2114 switch(distance.getUnits()) { 2115 case PERCENT: 2116 break; 2117 default: 2118 error(arg, "Expected \'%\'"); 2119 } 2120 focusDistance = new ParsedValueImpl<Size,Size>(distance, null); 2121 2122 prev = arg; 2123 if ((arg = arg.nextArg) == null) 2124 error(prev, "Expected \'center <center>\' " + 2125 "or \'radius <length>\'"); 2126 2127 } 2128 2129 if ("center".equalsIgnoreCase(arg.token.getText())) { 2130 2131 prev = arg; 2132 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 2133 2134 ParsedValueImpl<?,Size> ptX = parseSize(arg); 2135 2136 prev = arg; 2137 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 2138 2139 ParsedValueImpl<?,Size> ptY = parseSize(arg); 2140 2141 centerPoint = new ParsedValueImpl[] { ptX, ptY }; 2142 2143 prev = arg; 2144 if ((arg = arg.nextArg) == null) 2145 error(prev, "Expected \'radius [<length> | <percentage>]\'"); 2146 } 2147 2148 if ("radius".equalsIgnoreCase(arg.token.getText())) { 2149 2150 prev = arg; 2151 if ((arg = arg.nextInSeries) == null || 2152 !isSize(arg.token)) error(prev, "Expected \'[<length> | <percentage>]\'"); 2153 2154 radius = parseSize(arg); 2155 2156 prev = arg; 2157 if ((arg = arg.nextArg) == null) 2158 error(prev, "Expected \'radius [<length> | <percentage>]\'"); 2159 } 2160 2161 CycleMethod cycleMethod = CycleMethod.NO_CYCLE; 2162 if ("reflect".equalsIgnoreCase(arg.token.getText())) { 2163 cycleMethod = CycleMethod.REFLECT; 2164 prev = arg; 2165 arg = arg.nextArg; 2166 } else if ("repeat".equalsIgnoreCase(arg.token.getText())) { 2167 cycleMethod = CycleMethod.REFLECT; 2168 prev = arg; 2169 arg = arg.nextArg; 2170 } 2171 2172 if (arg == null || 2173 arg.token == null || 2174 arg.token.getText().isEmpty()) { 2175 error(prev, "Expected \'<color-stop>\'"); 2176 } 2177 2178 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg); 2179 2180 ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length]; 2181 int index = 0; 2182 values[index++] = focusAngle; 2183 values[index++] = focusDistance; 2184 values[index++] = (centerPoint != null) ? centerPoint[0] : null; 2185 values[index++] = (centerPoint != null) ? centerPoint[1] : null; 2186 values[index++] = radius; 2187 values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 2188 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 2189 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance()); 2190 2191 } 2192 2193 // Based off ImagePattern constructor 2194 // 2195 // image-pattern(<uri-string>[,<size>,<size>,<size>,<size>[,<boolean>]?]?) 2196 // 2197 private ParsedValueImpl<ParsedValue[], Paint> parseImagePattern(final Term root) throws ParseException { 2198 2199 // first term in the chain is the function name... 2200 final String fn = (root.token != null) ? root.token.getText() : null; 2201 if (!"image-pattern".regionMatches(true, 0, fn, 0, 13)) { 2202 final String msg = "Expected \'image-pattern\'"; 2203 error(root, msg); 2204 } 2205 2206 Term arg; 2207 if ((arg = root.firstArg) == null || 2208 arg.token == null || 2209 arg.token.getText().isEmpty()) { 2210 error(root, 2211 "Expected \'<uri-string>\'"); 2212 } 2213 2214 Term prev = arg; 2215 2216 final String uri = arg.token.getText(); 2217 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 2218 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()), 2219 null // placeholder for Stylesheet URL 2220 }; 2221 ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance()); 2222 2223 // If nextArg is null, then there are no remaining arguments, so we are done. 2224 if (arg.nextArg == null) { 2225 ParsedValueImpl[] values = new ParsedValueImpl[1]; 2226 values[0] = parsedURI; 2227 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2228 } 2229 2230 // There must now be 4 sizes in a row. 2231 Token token; 2232 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2233 ParsedValueImpl<?, Size> x = parseSize(arg); 2234 2235 prev = arg; 2236 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2237 ParsedValueImpl<?, Size> y = parseSize(arg); 2238 2239 prev = arg; 2240 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2241 ParsedValueImpl<?, Size> w = parseSize(arg); 2242 2243 prev = arg; 2244 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2245 ParsedValueImpl<?, Size> h = parseSize(arg); 2246 2247 // If there are no more args, then we are done. 2248 if (arg.nextArg == null) { 2249 ParsedValueImpl[] values = new ParsedValueImpl[5]; 2250 values[0] = parsedURI; 2251 values[1] = x; 2252 values[2] = y; 2253 values[3] = w; 2254 values[4] = h; 2255 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2256 } 2257 2258 prev = arg; 2259 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<boolean>\'"); 2260 if ((token = arg.token) == null || token.getText() == null) error(arg, "Expected \'<boolean>\'"); 2261 2262 ParsedValueImpl[] values = new ParsedValueImpl[6]; 2263 values[0] = parsedURI; 2264 values[1] = x; 2265 values[2] = y; 2266 values[3] = w; 2267 values[4] = h; 2268 values[5] = new ParsedValueImpl<Boolean, Boolean>(Boolean.parseBoolean(token.getText()), null); 2269 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2270 } 2271 2272 // For tiling ImagePatterns easily. 2273 // 2274 // repeating-image-pattern(<uri-string>) 2275 // 2276 private ParsedValueImpl<ParsedValue[], Paint> parseRepeatingImagePattern(final Term root) throws ParseException { 2277 // first term in the chain is the function name... 2278 final String fn = (root.token != null) ? root.token.getText() : null; 2279 if (!"repeating-image-pattern".regionMatches(true, 0, fn, 0, 23)) { 2280 final String msg = "Expected \'repeating-image-pattern\'"; 2281 error(root, msg); 2282 } 2283 2284 Term arg; 2285 if ((arg = root.firstArg) == null || 2286 arg.token == null || 2287 arg.token.getText().isEmpty()) { 2288 error(root, 2289 "Expected \'<uri-string>\'"); 2290 } 2291 2292 final String uri = arg.token.getText(); 2293 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 2294 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()), 2295 null // placeholder for Stylesheet URL 2296 }; 2297 ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance()); 2298 ParsedValueImpl[] values = new ParsedValueImpl[1]; 2299 values[0] = parsedURI; 2300 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RepeatingImagePatternConverter.getInstance()); 2301 } 2302 2303 // parse a series of paint values separated by commas. 2304 // i.e., <paint> [, <paint>]* 2305 private ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]> parsePaintLayers(Term root) 2306 throws ParseException { 2307 2308 // how many paints in the series? 2309 int nPaints = numberOfLayers(root); 2310 2311 ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[nPaints]; 2312 2313 Term temp = root; 2314 int paint = 0; 2315 2316 do { 2317 if (temp.token == null || 2318 temp.token.getText() == null || 2319 temp.token.getText().isEmpty()) error(temp, "Expected \'<paint>\'"); 2320 2321 paints[paint++] = (ParsedValueImpl<?,Paint>)parse(temp); 2322 2323 temp = nextLayer(temp); 2324 } while (temp != null); 2325 2326 return new ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>(paints, PaintConverter.SequenceConverter.getInstance()); 2327 2328 } 2329 2330 // An size or a series of four size values 2331 // <size> | <size> <size> <size> <size> 2332 private ParsedValueImpl<?,Size>[] parseSize1to4(final Term root) 2333 throws ParseException { 2334 2335 Term temp = root; 2336 ParsedValueImpl<?,Size>[] sides = new ParsedValueImpl[4]; 2337 int side = 0; 2338 2339 while (side < 4 && temp != null) { 2340 sides[side++] = parseSize(temp); 2341 temp = temp.nextInSeries; 2342 } 2343 2344 if (side < 2) sides[1] = sides[0]; // right = top 2345 if (side < 3) sides[2] = sides[0]; // bottom = top 2346 if (side < 4) sides[3] = sides[1]; // left = right 2347 2348 return sides; 2349 } 2350 2351 // A series of inset or sets of four inset values 2352 // <size> | <size> <size> <size> <size> [ , [ <size> | <size> <size> <size> <size>] ]* 2353 private ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]> parseInsetsLayers(Term root) 2354 throws ParseException { 2355 2356 int nLayers = numberOfLayers(root); 2357 2358 Term temp = root; 2359 int layer = 0; 2360 ParsedValueImpl<ParsedValue[],Insets>[] layers = new ParsedValueImpl[nLayers]; 2361 2362 while(temp != null) { 2363 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2364 layers[layer++] = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 2365 while(temp.nextInSeries != null) { 2366 temp = temp.nextInSeries; 2367 } 2368 temp = nextLayer(temp); 2369 } 2370 2371 return new ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]>(layers, InsetsConverter.SequenceConverter.getInstance()); 2372 } 2373 2374 // A single inset (1, 2, 3, or 4 digits) 2375 // <size> | <size> <size> <size> <size> 2376 private ParsedValueImpl<ParsedValue[],Insets> parseInsetsLayer(Term root) 2377 throws ParseException { 2378 2379 Term temp = root; 2380 ParsedValueImpl<ParsedValue[],Insets> layer = null; 2381 2382 while(temp != null) { 2383 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2384 layer = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 2385 while(temp.nextInSeries != null) { 2386 temp = temp.nextInSeries; 2387 } 2388 temp = nextLayer(temp); 2389 } 2390 return layer; 2391 } 2392 2393 // <size> | <size> <size> <size> <size> 2394 private ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]> parseMarginsLayers(Term root) 2395 throws ParseException { 2396 2397 int nLayers = numberOfLayers(root); 2398 2399 Term temp = root; 2400 int layer = 0; 2401 ParsedValueImpl<ParsedValue[],Margins>[] layers = new ParsedValueImpl[nLayers]; 2402 2403 while(temp != null) { 2404 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2405 layers[layer++] = new ParsedValueImpl<ParsedValue[],Margins>(sides, Margins.Converter.getInstance()); 2406 while(temp.nextInSeries != null) { 2407 temp = temp.nextInSeries; 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 // 2529 // If there is no vertical component, then set both equally. 2530 // If either is zero, then both are zero. 2531 // 2532 2533 // if hr == 0, then there were no horizontal radii (which would be an error caught above) 2534 if (hr != 0) { 2535 if (hr < 2) radii[0][1] = radii[0][0]; // top-right = top-left 2536 if (hr < 3) radii[0][2] = radii[0][0]; // bottom-right = top-left 2537 if (hr < 4) radii[0][3] = radii[0][1]; // bottom-left = top-right 2538 } else { 2539 assert(false); 2540 } 2541 2542 // if vr == 0, then there were no vertical radii 2543 if (vr != 0) { 2544 if (vr < 2) radii[1][1] = radii[1][0]; // top-right = top-left 2545 if (vr < 3) radii[1][2] = radii[1][0]; // bottom-right = top-left 2546 if (vr < 4) radii[1][3] = radii[1][1]; // bottom-left = top-right 2547 } else { 2548 // if no vertical, the vertical value is same as horizontal 2549 radii[1][0] = radii[0][0]; 2550 radii[1][1] = radii[0][1]; 2551 radii[1][2] = radii[0][2]; 2552 radii[1][3] = radii[0][3]; 2553 } 2554 2555 // if either is zero, both are zero 2556 if (zero.equals(radii[0][0]) || zero.equals(radii[1][0])) { radii[1][0] = radii[0][0] = zero; } 2557 if (zero.equals(radii[0][1]) || zero.equals(radii[1][1])) { radii[1][1] = radii[0][1] = zero; } 2558 if (zero.equals(radii[0][2]) || zero.equals(radii[1][2])) { radii[1][2] = radii[0][2] = zero; } 2559 if (zero.equals(radii[0][3]) || zero.equals(radii[1][3])) { radii[1][3] = radii[0][3] = zero; } 2560 2561 layers[layer++] = new ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>(radii, null); 2562 2563 term = nextLayer(lastTerm); 2564 } 2565 return new ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]>(layers, CornerRadiiConverter.getInstance()); 2566 } 2567 2568 /* Constant for background position */ 2569 private final static ParsedValueImpl<Size,Size> ZERO_PERCENT = 2570 new ParsedValueImpl<Size,Size>(new Size(0f, SizeUnits.PERCENT), null); 2571 /* Constant for background position */ 2572 private final static ParsedValueImpl<Size,Size> FIFTY_PERCENT = 2573 new ParsedValueImpl<Size,Size>(new Size(50f, SizeUnits.PERCENT), null); 2574 /* Constant for background position */ 2575 private final static ParsedValueImpl<Size,Size> ONE_HUNDRED_PERCENT = 2576 new ParsedValueImpl<Size,Size>(new Size(100f, SizeUnits.PERCENT), null); 2577 2578 private static boolean isPositionKeyWord(String value) { 2579 return "center".equalsIgnoreCase(value) || "top".equalsIgnoreCase(value) || "bottom".equalsIgnoreCase(value) || "left".equalsIgnoreCase(value) || "right".equalsIgnoreCase(value); 2580 } 2581 2582 /* 2583 * http://www.w3.org/TR/css3-background/#the-background-position 2584 * 2585 * <bg-position> = [ 2586 * [ top | bottom ] 2587 * | 2588 * [ <percentage> | <length> | left | center | right ] 2589 * [ <percentage> | <length> | top | center | bottom ]? 2590 * | 2591 * [ center | [ left | right ] [ <percentage> | <length> ]? ] && 2592 * [ center | [ top | bottom ] [ <percentage> | <length> ]? ] 2593 * ] 2594 * 2595 * From the W3 spec: 2596 * 2597 * returned ParsedValueImpl is [size, size, size, size] with the semantics 2598 * [top offset, right offset, bottom offset left offset] 2599 */ 2600 private ParsedValueImpl<ParsedValue[], BackgroundPosition> parseBackgroundPosition(Term term) 2601 throws ParseException { 2602 2603 if (term.token == null || 2604 term.token.getText() == null || 2605 term.token.getText().isEmpty()) error(term, "Expected \'<bg-position>\'"); 2606 2607 Term termOne = term; 2608 Token valueOne = term.token; 2609 2610 Term termTwo = termOne.nextInSeries; 2611 Token valueTwo = (termTwo != null) ? termTwo.token : null; 2612 2613 Term termThree = (termTwo != null) ? termTwo.nextInSeries : null; 2614 Token valueThree = (termThree != null) ? termThree.token : null; 2615 2616 Term termFour = (termThree != null) ? termThree.nextInSeries : null; 2617 Token valueFour = (termFour != null) ? termFour.token : null; 2618 2619 // are the horizontal and vertical exchanged 2620 if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { 2621 // 2 values filled 2622 String v1 = valueOne.getText(); 2623 String v2 = valueTwo.getText(); 2624 if( ("top".equals(v1) || "bottom".equals(v1)) 2625 && ("left".equals(v2) || "right".equals(v2) || "center".equals(v2)) ) { 2626 { 2627 Token tmp = valueTwo; 2628 valueTwo = valueOne; 2629 valueOne = tmp; 2630 } 2631 2632 { 2633 Term tmp = termTwo; 2634 termTwo = termOne; 2635 termOne = tmp; 2636 } 2637 } 2638 } else if( valueOne != null && valueTwo != null && valueThree != null ) { 2639 Term[] termArray = null; 2640 Token[] tokeArray = null; 2641 // 4 values filled 2642 if( valueFour != null ) { 2643 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) 2644 && ("left".equals(valueThree.getText()) || "right".equals(valueThree.getText())) ) { 2645 // e.g. top 50 left 20 2646 termArray = new Term[] { termThree, termFour, termOne, termTwo }; 2647 tokeArray = new Token[] { valueThree, valueFour, valueOne, valueTwo }; 2648 } 2649 } else { 2650 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) ) { 2651 if( ("left".equals(valueTwo.getText()) || "right".equals(valueTwo.getText())) ) { 2652 // e.g. top left 50 2653 termArray = new Term[] { termTwo, termThree, termOne, null }; 2654 tokeArray = new Token[] { valueTwo, valueThree, valueOne, null }; 2655 } else { 2656 // e.g. top 50 left 2657 termArray = new Term[] { termThree, termOne, termTwo, null }; 2658 tokeArray = new Token[] { valueThree, valueOne, valueTwo, null }; 2659 } 2660 } 2661 } 2662 2663 if( termArray != null ) { 2664 termOne = termArray[0]; 2665 termTwo = termArray[1]; 2666 termThree = termArray[2]; 2667 termFour = termArray[3]; 2668 2669 valueOne = tokeArray[0]; 2670 valueTwo = tokeArray[1]; 2671 valueThree = tokeArray[2]; 2672 valueFour = tokeArray[3]; 2673 } 2674 } 2675 2676 2677 ParsedValueImpl<?,Size> top, right, bottom, left; 2678 top = right = bottom = left = ZERO_PERCENT; 2679 { 2680 if(valueOne == null && valueTwo == null && valueThree == null && valueFour == null) { 2681 error(term, "No value found for background-position"); 2682 } else if( valueOne != null && valueTwo == null && valueThree == null && valueFour == null ) { 2683 // Only one value 2684 String v1 = valueOne.getText(); 2685 2686 if( "center".equals(v1) ) { 2687 left = FIFTY_PERCENT; 2688 right = ZERO_PERCENT; 2689 2690 top = FIFTY_PERCENT; 2691 bottom = ZERO_PERCENT; 2692 2693 } else if("left".equals(v1)) { 2694 left = ZERO_PERCENT; 2695 right = ZERO_PERCENT; 2696 2697 top = FIFTY_PERCENT; 2698 bottom = ZERO_PERCENT; 2699 2700 } else if( "right".equals(v1) ) { 2701 left = ONE_HUNDRED_PERCENT; 2702 right = ZERO_PERCENT; 2703 2704 top = FIFTY_PERCENT; 2705 bottom = ZERO_PERCENT; 2706 2707 } else if( "top".equals(v1) ) { 2708 left = FIFTY_PERCENT; 2709 right = ZERO_PERCENT; 2710 2711 top = ZERO_PERCENT; 2712 bottom = ZERO_PERCENT; 2713 2714 } else if( "bottom".equals(v1) ) { 2715 left = FIFTY_PERCENT; 2716 right = ZERO_PERCENT; 2717 2718 top = ONE_HUNDRED_PERCENT; 2719 bottom = ZERO_PERCENT; 2720 } else { 2721 left = parseSize(termOne); 2722 right = ZERO_PERCENT; 2723 top = FIFTY_PERCENT; 2724 bottom = ZERO_PERCENT; 2725 } 2726 } else if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { 2727 // 2 values 2728 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2729 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2730 2731 if( ! isPositionKeyWord(v1) ) { 2732 left = parseSize(termOne); 2733 right = ZERO_PERCENT; 2734 2735 if( "top".equals(v2) ) { 2736 top = ZERO_PERCENT; 2737 bottom = ZERO_PERCENT; 2738 } else if( "bottom".equals(v2) ) { 2739 top = ONE_HUNDRED_PERCENT; 2740 bottom = ZERO_PERCENT; 2741 } else if( "center".equals(v2) ) { 2742 top = FIFTY_PERCENT; 2743 bottom = ZERO_PERCENT; 2744 } else if( !isPositionKeyWord(v2) ) { 2745 top = parseSize(termTwo); 2746 bottom = ZERO_PERCENT; 2747 } else { 2748 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2749 } 2750 } else if( v1.equals("left") || v1.equals("right") ) { 2751 left = v1.equals("right") ? ONE_HUNDRED_PERCENT : ZERO_PERCENT; 2752 right = ZERO_PERCENT; 2753 2754 if( ! isPositionKeyWord(v2) ) { 2755 top = parseSize(termTwo); 2756 bottom = ZERO_PERCENT; 2757 } else if( v2.equals("top") || v2.equals("bottom") || v2.equals("center") ) { 2758 if( v2.equals("top") ) { 2759 top = ZERO_PERCENT; 2760 bottom = ZERO_PERCENT; 2761 } else if(v2.equals("center")) { 2762 top = FIFTY_PERCENT; 2763 bottom = ZERO_PERCENT; 2764 } else { 2765 top = ONE_HUNDRED_PERCENT; 2766 bottom = ZERO_PERCENT; 2767 } 2768 } else { 2769 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2770 } 2771 } else if( v1.equals("center") ) { 2772 left = FIFTY_PERCENT; 2773 right = ZERO_PERCENT; 2774 2775 if( v2.equals("top") ) { 2776 top = ZERO_PERCENT; 2777 bottom = ZERO_PERCENT; 2778 } else if( v2.equals("bottom") ) { 2779 top = ONE_HUNDRED_PERCENT; 2780 bottom = ZERO_PERCENT; 2781 } else if( v2.equals("center") ) { 2782 top = FIFTY_PERCENT; 2783 bottom = ZERO_PERCENT; 2784 } else if( ! isPositionKeyWord(v2) ) { 2785 top = parseSize(termTwo); 2786 bottom = ZERO_PERCENT; 2787 } else { 2788 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2789 } 2790 } 2791 } else if( valueOne != null && valueTwo != null && valueThree != null && valueFour == null ) { 2792 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2793 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2794 String v3 = valueThree.getText().toLowerCase(Locale.ROOT); 2795 2796 if( ! isPositionKeyWord(v1) || "center".equals(v1) ) { 2797 // 1 is horizontal 2798 // means 2 & 3 are vertical 2799 if( "center".equals(v1) ) { 2800 left = FIFTY_PERCENT; 2801 } else { 2802 left = parseSize(termOne); 2803 } 2804 right = ZERO_PERCENT; 2805 2806 if( !isPositionKeyWord(v3) ) { 2807 if( "top".equals(v2) ) { 2808 top = parseSize(termThree); 2809 bottom = ZERO_PERCENT; 2810 } else if( "bottom".equals(v2) ) { 2811 top = ZERO_PERCENT; 2812 bottom = parseSize(termThree); 2813 } else { 2814 error(termTwo,"Expected 'top' or 'bottom'"); 2815 } 2816 } else { 2817 error(termThree,"Expected <size>"); 2818 } 2819 } else if( "left".equals(v1) || "right".equals(v1) ) { 2820 if( ! isPositionKeyWord(v2) ) { 2821 // 1 & 2 are horizontal 2822 // 3 is vertical 2823 if( "left".equals(v1) ) { 2824 left = parseSize(termTwo); 2825 right = ZERO_PERCENT; 2826 } else { 2827 left = ZERO_PERCENT; 2828 right = parseSize(termTwo); 2829 } 2830 2831 if( "top".equals(v3) ) { 2832 top = ZERO_PERCENT; 2833 bottom = ZERO_PERCENT; 2834 } else if( "bottom".equals(v3) ) { 2835 top = ONE_HUNDRED_PERCENT; 2836 bottom = ZERO_PERCENT; 2837 } else if( "center".equals(v3) ) { 2838 top = FIFTY_PERCENT; 2839 bottom = ZERO_PERCENT; 2840 } else { 2841 error(termThree,"Expected 'top', 'bottom' or 'center'"); 2842 } 2843 } else { 2844 // 1 is horizontal 2845 // 2 & 3 are vertical 2846 if( "left".equals(v1) ) { 2847 left = ZERO_PERCENT; 2848 right = ZERO_PERCENT; 2849 } else { 2850 left = ONE_HUNDRED_PERCENT; 2851 right = ZERO_PERCENT; 2852 } 2853 2854 if( ! isPositionKeyWord(v3) ) { 2855 if( "top".equals(v2) ) { 2856 top = parseSize(termThree); 2857 bottom = ZERO_PERCENT; 2858 } else if( "bottom".equals(v2) ) { 2859 top = ZERO_PERCENT; 2860 bottom = parseSize(termThree); 2861 } else { 2862 error(termTwo,"Expected 'top' or 'bottom'"); 2863 } 2864 } else { 2865 error(termThree,"Expected <size>"); 2866 } 2867 } 2868 } 2869 } else { 2870 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2871 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2872 String v3 = valueThree.getText().toLowerCase(Locale.ROOT); 2873 String v4 = valueFour.getText().toLowerCase(Locale.ROOT); 2874 2875 if( (v1.equals("left") || v1.equals("right")) && (v3.equals("top") || v3.equals("bottom") ) && ! isPositionKeyWord(v2) && ! isPositionKeyWord(v4) ) { 2876 if( v1.equals("left") ) { 2877 left = parseSize(termTwo); 2878 right = ZERO_PERCENT; 2879 } else { 2880 left = ZERO_PERCENT; 2881 right = parseSize(termTwo); 2882 } 2883 2884 if( v3.equals("top") ) { 2885 top = parseSize(termFour); 2886 bottom = ZERO_PERCENT; 2887 } else { 2888 top = ZERO_PERCENT; 2889 bottom = parseSize(termFour); 2890 } 2891 2892 } else { 2893 error(term,"Expected 'left' or 'right' followed by <size> followed by 'top' or 'bottom' followed by <size>"); 2894 } 2895 } 2896 } 2897 2898 ParsedValueImpl<?,Size>[] values = new ParsedValueImpl[] {top, right, bottom, left}; 2899 return new ParsedValueImpl<ParsedValue[], BackgroundPosition>(values, BackgroundPositionConverter.getInstance()); 2900 } 2901 2902 private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]> 2903 parseBackgroundPositionLayers(final Term root) throws ParseException { 2904 2905 int nLayers = numberOfLayers(root); 2906 ParsedValueImpl<ParsedValue[], BackgroundPosition>[] layers = new ParsedValueImpl[nLayers]; 2907 int layer = 0; 2908 Term term = root; 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>\'"); 2982 } 2983 } 2984 2985 return new ParsedValueImpl[] { 2986 new ParsedValueImpl<String,BackgroundRepeat>(xAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class)), 2987 new ParsedValueImpl<String,BackgroundRepeat>(yAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class)) 2988 }; 2989 } 2990 2991 private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]> 2992 parseBorderImageRepeatStyleLayers(final Term root) throws ParseException { 2993 2994 int nLayers = numberOfLayers(root); 2995 ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][]; 2996 int layer = 0; 2997 Term term = root; 2998 while (term != null) { 2999 layers[layer++] = parseRepeatStyle(term); 3000 term = nextLayer(term); 3001 } 3002 return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]>(layers, RepeatStructConverter.getInstance()); 3003 } 3004 3005 3006 private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]> 3007 parseBackgroundRepeatStyleLayers(final Term root) throws ParseException { 3008 3009 int nLayers = numberOfLayers(root); 3010 ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][]; 3011 int layer = 0; 3012 Term term = root; 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 3080 } 3081 3082 ParsedValueImpl[] values = new ParsedValueImpl[] { 3083 width, 3084 height, 3085 // TODO: handling of booleans is really bogus 3086 new ParsedValueImpl<String,Boolean>((cover ? "true" : "false"), BooleanConverter.getInstance()), 3087 new ParsedValueImpl<String,Boolean>((contain ? "true" : "false"), BooleanConverter.getInstance()) 3088 }; 3089 return new ParsedValueImpl<ParsedValue[], BackgroundSize>(values, BackgroundSizeConverter.getInstance()); 3090 } 3091 3092 private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[], BackgroundSize[]> 3093 parseBackgroundSizeLayers(final Term root) throws ParseException { 3094 3095 int nLayers = numberOfLayers(root); 3096 ParsedValueImpl<ParsedValue[], BackgroundSize>[] layers = new ParsedValueImpl[nLayers]; 3097 int layer = 0; 3098 Term term = root; 3099 while (term != null) { 3100 layers[layer++] = parseBackgroundSize(term); 3101 term = nextLayer(term); 3102 } 3103 return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[], BackgroundSize[]>(layers, LayeredBackgroundSizeConverter.getInstance()); 3104 } 3105 3106 private ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]> parseBorderPaint(final Term root) 3107 throws ParseException { 3108 3109 Term term = root; 3110 ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[4]; 3111 int paint = 0; 3112 3113 while(term != null) { 3114 if (term.token == null || paints.length <= paint) error(term, "Expected \'<paint>\'"); 3115 paints[paint++] = parse(term); 3116 term = term.nextInSeries; 3117 } 3118 3119 if (paint < 2) paints[1] = paints[0]; // right = top 3120 if (paint < 3) paints[2] = paints[0]; // bottom = top 3121 if (paint < 4) paints[3] = paints[1]; // left = right 3122 3123 return new ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]>(paints, StrokeBorderPaintConverter.getInstance()); 3124 } 3125 3126 private ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]> parseBorderPaintLayers(final Term root) 3127 throws ParseException { 3128 3129 int nLayers = numberOfLayers(root); 3130 ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>[] layers = new ParsedValueImpl[nLayers]; 3131 int layer = 0; 3132 Term term = root; 3133 while (term != null) { 3134 layers[layer++] = parseBorderPaint(term); 3135 term = nextLayer(term); 3136 } 3137 return new ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]>(layers, LayeredBorderPaintConverter.getInstance()); 3138 } 3139 3140 // borderStyle (borderStyle (borderStyle borderStyle?)?)? 3141 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]> parseBorderStyleSeries(final Term root) 3142 throws ParseException { 3143 3144 Term term = root; 3145 ParsedValueImpl<ParsedValue[],BorderStrokeStyle>[] borders = new ParsedValueImpl[4]; 3146 int border = 0; 3147 while (term != null) { 3148 borders[border++] = parseBorderStyle(term); 3149 term = term.nextInSeries; 3150 } 3151 3152 if (border < 2) borders[1] = borders[0]; // right = top 3153 if (border < 3) borders[2] = borders[0]; // bottom = top 3154 if (border < 4) borders[3] = borders[1]; // left = right 3155 3156 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>(borders, BorderStrokeStyleSequenceConverter.getInstance()); 3157 } 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; 3199 ParsedValue<String,StrokeLineJoin> strokeLineJoin = null; 3200 ParsedValue<ParsedValue<?,Size>,Number> strokeMiterLimit = null; 3201 ParsedValue<String,StrokeLineCap> strokeLineCap = null; 3202 3203 Term term = root; 3204 3205 dashStyle = dashStyle(term); 3206 3207 Term prev = term; 3208 term = term.nextInSeries; 3209 String keyword = getKeyword(term); 3210 3211 // dash-style might be followed by 'phase <size>' 3212 if ("phase".equals(keyword)) { 3213 3214 prev = term; 3215 if ((term = term.nextInSeries) == null || 3216 term.token == null || 3217 !isSize(term.token)) error(term, "Expected \'<size>\'"); 3218 3219 ParsedValueImpl<?,Size> sizeVal = parseSize(term); 3220 dashPhase = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance()); 3221 3222 prev = term; 3223 term = term.nextInSeries; 3224 } 3225 3226 // stroke type might be next 3227 strokeType = parseStrokeType(term); 3228 if (strokeType != null) { 3229 prev = term; 3230 term = term.nextInSeries; 3231 } 3232 3233 keyword = getKeyword(term); 3234 3235 if ("line-join".equals(keyword)) { 3236 3237 prev = term; 3238 term = term.nextInSeries; 3239 3240 ParsedValueImpl[] lineJoinValues = parseStrokeLineJoin(term); 3241 if (lineJoinValues != null) { 3242 strokeLineJoin = lineJoinValues[0]; 3243 strokeMiterLimit = lineJoinValues[1]; 3244 } else { 3245 error(term, "Expected \'miter <size>?\', \'bevel\' or \'round\'"); 3246 } 3247 prev = term; 3248 term = term.nextInSeries; 3249 keyword = getKeyword(term); 3250 } 3251 3252 if ("line-cap".equals(keyword)) { 3253 3254 prev = term; 3255 term = term.nextInSeries; 3256 3257 strokeLineCap = parseStrokeLineCap(term); 3258 if (strokeLineCap == null) { 3259 error(term, "Expected \'square\', \'butt\' or \'round\'"); 3260 } 3261 3262 prev = term; 3263 term = term.nextInSeries; 3264 } 3265 3266 if (term != null) { 3267 // if term is not null, then we have gotten to the end of 3268 // one border style and term is start of another border style 3269 root.nextInSeries = term; 3270 } else { 3271 // If term is null, then we have gotten to the end of 3272 // a border style with no more to follow in this series. 3273 // But there may be another layer. 3274 root.nextInSeries = null; 3275 root.nextLayer = prev.nextLayer; 3276 } 3277 3278 final ParsedValue[] values = new ParsedValue[]{ 3279 dashStyle, 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)) { 3339 error(root, "Unsupported <border-style> \'groove\'"); 3340 } else if ("ridge".equals(text)) { 3341 error(root, "Unsupported <border-style> \'ridge\'"); 3342 } else if ("inset".equals(text)) { 3343 error(root, "Unsupported <border-style> \'inset\'"); 3344 } else if ("outset".equals(text)) { 3345 error(root, "Unsupported <border-style> \'outset\'"); 3346 } else { 3347 error(root, "Unsupported <border-style> \'" + text + "\'"); 3348 } 3349 // error throws so we should never get here. 3350 // but the compiler wants a return, so here it is. 3351 return BorderStyleConverter.SOLID; 3352 } 3353 3354 private ParsedValueImpl<ParsedValue[],Number[]> segments(Term root) 3355 throws ParseException { 3356 3357 // first term in the chain is the function name... 3358 final String fn = (root.token != null) ? root.token.getText() : null; 3359 if (!"segments".regionMatches(true, 0, fn, 0, 8)) { 3360 error(root,"Expected \'segments\'"); 3361 } 3362 3363 Term arg = root.firstArg; 3364 if (arg == null) error(null, "Expected \'<size>\'"); 3365 3366 int nArgs = numberOfArgs(root); 3367 ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs]; 3368 int segment = 0; 3369 while(arg != null) { 3370 segments[segment++] = parseSize(arg); 3371 arg = arg.nextArg; 3372 } 3373 3374 return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SizeConverter.SequenceConverter.getInstance()); 3375 3376 } 3377 3378 private ParsedValueImpl<String,StrokeType> parseStrokeType(final Term root) 3379 throws ParseException { 3380 3381 final String keyword = getKeyword(root); 3382 3383 3384 if ("centered".equals(keyword) || 3385 "inside".equals(keyword) || 3386 "outside".equals(keyword)) { 3387 3388 return new ParsedValueImpl<String,StrokeType>(keyword, new EnumConverter(StrokeType.class)); 3389 3390 } 3391 return null; 3392 } 3393 3394 // Root term is the term just after the line-join keyword 3395 // Returns an array of two Values or null. 3396 // ParsedValueImpl[0] is ParsedValueImpl<StrokeLineJoin,StrokeLineJoin> 3397 // ParsedValueImpl[1] is ParsedValueImpl<Value<?,Size>,Number> if miter limit is given, null otherwise. 3398 // If the token is not a StrokeLineJoin, then null is returned. 3399 private ParsedValueImpl[] parseStrokeLineJoin(final Term root) 3400 throws ParseException { 3401 3402 final String keyword = getKeyword(root); 3403 3404 if ("miter".equals(keyword) || 3405 "bevel".equals(keyword) || 3406 "round".equals(keyword)) { 3407 3408 ParsedValueImpl<String,StrokeLineJoin> strokeLineJoin = 3409 new ParsedValueImpl<String,StrokeLineJoin>(keyword, new EnumConverter(StrokeLineJoin.class)); 3410 3411 ParsedValueImpl<ParsedValue<?,Size>,Number> strokeMiterLimit = null; 3412 if ("miter".equals(keyword)) { 3413 3414 Term next = root.nextInSeries; 3415 if (next != null && 3416 next.token != null && 3417 isSize(next.token)) { 3418 3419 root.nextInSeries = next.nextInSeries; 3420 ParsedValueImpl<?,Size> sizeVal = parseSize(next); 3421 strokeMiterLimit = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance()); 3422 } 3423 3424 } 3425 3426 return new ParsedValueImpl[] { strokeLineJoin, strokeMiterLimit }; 3427 } 3428 return null; 3429 } 3430 3431 // Root term is the term just after the line-cap keyword 3432 // If the token is not a StrokeLineCap, then null is returned. 3433 private ParsedValueImpl<String,StrokeLineCap> parseStrokeLineCap(final Term root) 3434 throws ParseException { 3435 3436 final String keyword = getKeyword(root); 3437 3438 if ("square".equals(keyword) || 3439 "butt".equals(keyword) || 3440 "round".equals(keyword)) { 3441 3442 return new ParsedValueImpl<String,StrokeLineCap>(keyword, new EnumConverter(StrokeLineCap.class)); 3443 } 3444 return null; 3445 } 3446 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[]> 3488 parseBorderImageSliceLayers(final Term root) throws ParseException { 3489 3490 int nLayers = numberOfLayers(root); 3491 ParsedValueImpl<ParsedValue[], BorderImageSlices>[] layers = new ParsedValueImpl[nLayers]; 3492 int layer = 0; 3493 Term term = root; 3494 while (term != null) { 3495 layers[layer++] = parseBorderImageSlice(term); 3496 term = nextLayer(term); 3497 } 3498 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]> (layers, SliceSequenceConverter.getInstance()); 3499 } 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; 3596 ParsedValueImpl<ParsedValue[],String>[] layers = new ParsedValueImpl[nLayers]; 3597 3598 while(temp != null) { 3599 layers[layer++] = parseURI(temp); 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; 3641 } else if ("larger".equals(ident)) { 3642 value = 120; 3643 } 3644 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 } 3686 3687 /* http://www.w3.org/TR/css3-fonts/#font-weight-the-font-weight-property */ 3688 private ParsedValueImpl<String, FontWeight> parseFontWeight(Term root) throws ParseException { 3689 3690 if (root == null) return null; 3691 final Token token = root.token; 3692 if (token == null || 3693 token.getText() == null || 3694 token.getText().isEmpty()) error(root, "Expected \'<font-weight>\'"); 3695 3696 final String ident = token.getText().toLowerCase(Locale.ROOT); 3697 String weight = FontWeight.NORMAL.name(); 3698 3699 if ("inherit".equals(ident)) { 3700 weight = FontWeight.NORMAL.name(); 3701 } else if ("normal".equals(ident)) { 3702 weight = FontWeight.NORMAL.name(); 3703 } else if ("bold".equals(ident)) { 3704 weight = FontWeight.BOLD.name(); 3705 } else if ("bolder".equals(ident)) { 3706 weight = FontWeight.BOLD.name(); 3707 } else if ("lighter".equals(ident)) { 3708 weight = FontWeight.LIGHT.name(); 3709 } else if ("100".equals(ident)) { 3710 weight = FontWeight.findByWeight(100).name(); 3711 } else if ("200".equals(ident)) { 3712 weight = FontWeight.findByWeight(200).name(); 3713 } else if ("300".equals(ident)) { 3714 weight = FontWeight.findByWeight(300).name(); 3715 } else if ("400".equals(ident)) { 3716 weight = FontWeight.findByWeight(400).name(); 3717 } else if ("500".equals(ident)) { 3718 weight = FontWeight.findByWeight(500).name(); 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 }