1 /* 2 * Copyright (c) 2011, 2018, 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 com.sun.javafx.logging.PlatformLogger; 88 import com.sun.javafx.logging.PlatformLogger.Level; 89 90 91 import java.io.BufferedReader; 92 import java.io.CharArrayReader; 93 import java.io.IOException; 94 import java.io.InputStreamReader; 95 import java.io.Reader; 96 import java.net.MalformedURLException; 97 import java.net.URI; 98 import java.net.URISyntaxException; 99 import java.net.URL; 100 import java.text.MessageFormat; 101 import java.util.ArrayList; 102 import java.util.Collections; 103 import java.util.HashMap; 104 import java.util.List; 105 import java.util.Locale; 106 import java.util.Map; 107 import java.util.Stack; 108 109 /** 110 * @since 9 111 */ 112 final public class CssParser { 113 114 public CssParser() { 115 properties = new HashMap<String,String>(); 116 } 117 118 // stylesheet as a string from parse method. This will be null if the 119 // stylesheet is being parsed from a file; otherwise, the parser is parsing 120 // a string and this is that string. 121 private String stylesheetAsText; 122 123 // the url of the stylesheet file, or the docbase of an applet. This will 124 // be null if the source is not a file or from an applet. 125 private String sourceOfStylesheet; 126 127 // the Styleable from the node with an in-line style. This will be null 128 // unless the source of the styles is a Node's styleProperty. In this case, 129 // the stylesheetString will also be set. 130 private Styleable sourceOfInlineStyle; 131 132 // source is a file 133 private void setInputSource(String url, String str) { 134 stylesheetAsText = str; 135 sourceOfStylesheet = url; 136 sourceOfInlineStyle = null; 137 } 138 139 // source as string only 140 private void setInputSource(String str) { 141 stylesheetAsText = str; 142 sourceOfStylesheet = null; 143 sourceOfInlineStyle = null; 144 } 145 146 // source is in-line style 147 private void setInputSource(Styleable styleable) { 148 stylesheetAsText = styleable != null ? styleable.getStyle() : null; 149 sourceOfStylesheet = null; 150 sourceOfInlineStyle = styleable; 151 } 152 153 private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger(); 154 155 private static final class ParseException extends Exception { 156 ParseException(String message) { 157 this(message,null,null); 158 } 159 ParseException(String message, Token tok, CssParser parser) { 160 super(message); 161 this.tok = tok; 162 if (parser.sourceOfStylesheet != null) { 163 source = parser.sourceOfStylesheet; 164 } else if (parser.sourceOfInlineStyle != null) { 165 source = parser.sourceOfInlineStyle.toString(); 166 } else if (parser.stylesheetAsText != null) { 167 source = parser.stylesheetAsText; 168 } else { 169 source = "?"; 170 } 171 } 172 @Override public String toString() { 173 StringBuilder builder = new StringBuilder(super.getMessage()); 174 builder.append(source); 175 if (tok != null) builder.append(": ").append(tok.toString()); 176 return builder.toString(); 177 } 178 private final Token tok; 179 private final String source; 180 } 181 182 /** 183 * Creates a stylesheet from a CSS document string. 184 * 185 * @param stylesheetText the CSS document to parse 186 * @return the Stylesheet 187 */ 188 public Stylesheet parse(final String stylesheetText) { 189 final Stylesheet stylesheet = new Stylesheet(); 190 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) { 191 setInputSource(stylesheetText); 192 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) { 193 parse(stylesheet, reader); 194 } catch (IOException ioe) { 195 // this method doesn't explicitly throw IOException 196 } 197 } 198 return stylesheet; 199 } 200 201 /** 202 * Creates a stylesheet from a CSS document string using docbase as the base 203 * URL for resolving references within stylesheet. 204 * 205 * @param docbase the doc base for resolving URL references 206 * @param stylesheetText the CSS document to parse 207 * @return the Stylesheet 208 * @throws java.io.IOException the exception 209 */ 210 public Stylesheet parse(final String docbase, final String stylesheetText) throws IOException { 211 final Stylesheet stylesheet = new Stylesheet(docbase); 212 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) { 213 setInputSource(docbase, stylesheetText); 214 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) { 215 parse(stylesheet, reader); 216 } 217 } 218 return stylesheet; 219 } 220 221 /** 222 * Updates the given stylesheet by reading a CSS document from a URL, 223 * assuming UTF-8 encoding. 224 * 225 *@param url URL of the stylesheet to parse 226 *@return the stylesheet 227 *@throws IOException the exception 228 */ 229 public Stylesheet parse(final URL url) throws IOException { 230 231 final String path = url != null ? url.toExternalForm() : null; 232 final Stylesheet stylesheet = new Stylesheet(path); 233 if (url != null) { 234 setInputSource(path, null); 235 try (Reader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { 236 parse(stylesheet, reader); 237 } 238 } 239 return stylesheet; 240 } 241 242 /* All of the other function calls should wind up here */ 243 private void parse(final Stylesheet stylesheet, final Reader reader) { 244 CssLexer lex = new CssLexer(); 245 lex.setReader(reader); 246 247 try { 248 this.parse(stylesheet, lex); 249 } catch (Exception ex) { 250 // Sometimes bad syntax causes an exception. The code should be 251 // fixed to handle the bad syntax, but the fallback is 252 // to handle the exception here. Uncaught, the exception can cause 253 // problems like RT-20311 254 reportException(ex); 255 } 256 257 } 258 259 /** Parse an in-line style from a Node. 260 * @param node the styleable node 261 * @return the style sheet 262 */ 263 public Stylesheet parseInlineStyle(final Styleable node) { 264 265 Stylesheet stylesheet = new Stylesheet(); 266 267 final String stylesheetText = (node != null) ? node.getStyle() : null; 268 if (stylesheetText != null && !stylesheetText.trim().isEmpty()) { 269 setInputSource(node); 270 final List<Rule> rules = new ArrayList<Rule>(); 271 try (Reader reader = new CharArrayReader(stylesheetText.toCharArray())) { 272 final CssLexer lexer = new CssLexer(); 273 lexer.setReader(reader); 274 currentToken = nextToken(lexer); 275 final List<Declaration> declarations = declarations(lexer); 276 if (declarations != null && !declarations.isEmpty()) { 277 final Selector selector = Selector.getUniversalSelector(); 278 final Rule rule = new Rule( 279 Collections.singletonList(selector), 280 declarations 281 ); 282 rules.add(rule); 283 } 284 } catch (IOException ioe) { 285 } catch (Exception ex) { 286 // Sometimes bad syntax causes an exception. The code should be 287 // fixed to handle the bad syntax, but the fallback is 288 // to handle the exception here. Uncaught, the exception can cause 289 // problems like RT-20311 290 reportException(ex); 291 } 292 stylesheet.getRules().addAll(rules); 293 } 294 295 // don't retain reference to the styleable 296 setInputSource((Styleable) null); 297 298 return stylesheet; 299 } 300 301 /** 302 * Convenience method for unit tests. 303 * @param property the property 304 * @param expr the expression 305 * @return the parsed value 306 */ 307 ParsedValue parseExpr(String property, String expr) { 308 if (property == null || expr == null) return null; 309 310 ParsedValueImpl value = null; 311 setInputSource(null, property + ": " + expr); 312 char buf[] = new char[expr.length() + 1]; 313 System.arraycopy(expr.toCharArray(), 0, buf, 0, expr.length()); 314 buf[buf.length-1] = ';'; 315 316 try (Reader reader = new CharArrayReader(buf)) { 317 CssLexer lex = new CssLexer(); 318 lex.setReader(reader); 319 320 currentToken = nextToken(lex); 321 CssParser.Term term = this.expr(lex); 322 value = valueFor(property, term, lex); 323 } catch (IOException ioe) { 324 } catch (ParseException e) { 325 if (LOGGER.isLoggable(Level.WARNING)) { 326 LOGGER.warning("\"" +property + ": " + expr + "\" " + e.toString()); 327 } 328 } catch (Exception ex) { 329 // Sometimes bad syntax causes an exception. The code should be 330 // fixed to handle the bad syntax, but the fallback is 331 // to handle the exception here. Uncaught, the exception can cause 332 // problems like RT-20311 333 reportException(ex); 334 } 335 return value; 336 } 337 /* 338 * Map of property names found while parsing. If a value matches a 339 * property name, then the value is a lookup. 340 */ 341 private final Map<String,String> properties; 342 343 /* 344 * While parsing a declaration, tokens from parsing value (that is, 345 * the expr rule) are held in this tree structure which is then passed 346 * to methods which convert the tree into a ParsedValueImpl. 347 * 348 * Each term in expr is a Term. For simple terms, like HASH, the 349 * Term is just the Token. If the term is a function, then the 350 * Term is a linked-list of Term, the first being the function 351 * name and each nextArg being the arguments. 352 * 353 * If there is more than one term in the expr (insets, for example), 354 * then the terms are linked on nextInSequence. If there is more than one 355 * layer (sequence of terms), then each layer becomes the nextLayer 356 * to the last root in the previous sequence. 357 * 358 * The easiest way to think of it is that a comma starts a nextLayer (except 359 * when a function arg). 360 * 361 * The expr part of the declaration "-fx-padding 1 2, 3 4;" would look 362 * like this: 363 * [1 | nextLayer | nextInSeries]-->[2 | nextLayer | nextInSeries]-->null 364 * | | 365 * null | 366 * .---------------------------------' 367 * '-->[3 | nextLayer | nextInSeries]-->[4 | nextLayer | nextInSeries]-->null 368 * | | 369 * null null 370 * 371 * The first argument in a function needs to be distinct from the 372 * remaining args so that the args of a function in the middle of 373 * a function will not be omitted. Consider 'f0(a, f1(b, c), d)' 374 * If we relied only on nextArg, then the next arg of f0 would be a but 375 * the nextArg of f1 would be d. With firstArg, the firstArg of f0 is a, 376 * the nextArg of a is f1, the firstArg of f1 is b and the nextArg of f1 is d. 377 * 378 * TODO: now that the parser is the parser and not an adjunct to an ANTLR 379 * parser, this Term stuff shouldn't be needed. 380 */ 381 static class Term { 382 final Token token; 383 Term nextInSeries; 384 Term nextLayer; 385 Term firstArg; 386 Term nextArg; 387 Term(Token token) { 388 this.token = token; 389 this.nextLayer = null; 390 this.nextInSeries = null; 391 this.firstArg = null; 392 this.nextArg = null; 393 } 394 Term() { 395 this(null); 396 } 397 398 @Override public String toString() { 399 StringBuilder buf = new StringBuilder(); 400 if (token != null) buf.append(String.valueOf(token.getText())); 401 if (nextInSeries != null) { 402 buf.append("<nextInSeries>"); 403 buf.append(nextInSeries.toString()); 404 buf.append("</nextInSeries>\n"); 405 } 406 if (nextLayer != null) { 407 buf.append("<nextLayer>"); 408 buf.append(nextLayer.toString()); 409 buf.append("</nextLayer>\n"); 410 } 411 if (firstArg != null) { 412 buf.append("<args>"); 413 buf.append(firstArg.toString()); 414 if (nextArg != null) { 415 buf.append(nextArg.toString()); 416 } 417 buf.append("</args>"); 418 } 419 420 return buf.toString(); 421 } 422 423 } 424 425 private ParseError createError(String msg) { 426 427 ParseError error = null; 428 if (sourceOfStylesheet != null) { 429 error = new ParseError.StylesheetParsingError(sourceOfStylesheet, msg); 430 } else if (sourceOfInlineStyle != null) { 431 error = new ParseError.InlineStyleParsingError(sourceOfInlineStyle, msg); 432 } else { 433 error = new ParseError.StringParsingError(stylesheetAsText, msg); 434 } 435 return error; 436 } 437 438 private void reportError(ParseError error) { 439 List<ParseError> errors = null; 440 if ((errors = StyleManager.getErrors()) != null) { 441 errors.add(error); 442 } 443 } 444 445 private void error(final Term root, final String msg) throws ParseException { 446 447 final Token token = root != null ? root.token : null; 448 final ParseException pe = new ParseException(msg,token,this); 449 reportError(createError(pe.toString())); 450 throw pe; 451 } 452 453 private void reportException(Exception exception) { 454 455 if (LOGGER.isLoggable(Level.WARNING)) { 456 final StackTraceElement[] stea = exception.getStackTrace(); 457 if (stea.length > 0) { 458 final StringBuilder buf = 459 new StringBuilder("Please report "); 460 buf.append(exception.getClass().getName()) 461 .append(" at:"); 462 int end = 0; 463 while(end < stea.length) { 464 // only report parser part of the stack trace. 465 if (!getClass().getName().equals(stea[end].getClassName())) { 466 break; 467 } 468 buf.append("\n\t") 469 .append(stea[end++].toString()); 470 } 471 LOGGER.warning(buf.toString()); 472 } 473 } 474 } 475 476 private String formatDeprecatedMessage(final Term root, final String syntax) { 477 final StringBuilder buf = 478 new StringBuilder("Using deprecated syntax for "); 479 buf.append(syntax); 480 if (sourceOfStylesheet != null){ 481 buf.append(" at ") 482 .append(sourceOfStylesheet) 483 .append("[") 484 .append(root.token.getLine()) 485 .append(',') 486 .append(root.token.getOffset()) 487 .append("]"); 488 } 489 buf.append(". Refer to the CSS Reference Guide."); 490 return buf.toString(); 491 } 492 493 // Assumes string is not a lookup! 494 private ParsedValueImpl<Color,Color> colorValueOfString(String str) { 495 496 if(str.startsWith("#") || str.startsWith("0x")) { 497 498 double a = 1.0f; 499 String c = str; 500 final int prefixLength = (str.startsWith("#")) ? 1 : 2; 501 502 final int len = c.length(); 503 // rgba or rrggbbaa - trim off the alpha 504 if ( (len-prefixLength) == 4) { 505 a = Integer.parseInt(c.substring(len-1), 16) / 15.0f; 506 c = c.substring(0,len-1); 507 } else if ((len-prefixLength) == 8) { 508 a = Integer.parseInt(c.substring(len-2), 16) / 255.0f; 509 c = c.substring(0,len-2); 510 } 511 // else color was rgb or rrggbb (no alpha) 512 return new ParsedValueImpl<Color,Color>(Color.web(c,a), null); 513 } 514 515 try { 516 return new ParsedValueImpl<Color,Color>(Color.web(str), null); 517 } catch (final IllegalArgumentException e) { 518 } catch (final NullPointerException e) { 519 } 520 521 // not a color 522 return null; 523 } 524 525 private String stripQuotes(String string) { 526 return com.sun.javafx.util.Utils.stripQuotes(string); 527 } 528 529 private double clamp(double min, double val, double max) { 530 if (val < min) return min; 531 if (max < val) return max; 532 return val; 533 } 534 535 // Return true if the token is a size type or an identifier 536 // (which would indicate a lookup). 537 private boolean isSize(Token token) { 538 final int ttype = token.getType(); 539 switch (ttype) { 540 case CssLexer.NUMBER: 541 case CssLexer.PERCENTAGE: 542 case CssLexer.EMS: 543 case CssLexer.EXS: 544 case CssLexer.PX: 545 case CssLexer.CM: 546 case CssLexer.MM: 547 case CssLexer.IN: 548 case CssLexer.PT: 549 case CssLexer.PC: 550 case CssLexer.DEG: 551 case CssLexer.GRAD: 552 case CssLexer.RAD: 553 case CssLexer.TURN: 554 return true; 555 default: 556 return token.getType() == CssLexer.IDENT; 557 } 558 } 559 560 private Size size(final Token token) throws ParseException { 561 SizeUnits units = SizeUnits.PX; 562 // Amount to trim off the suffix, if any. Most are 2 chars. 563 int trim = 2; 564 final String sval = token.getText().trim(); 565 final int len = sval.length(); 566 final int ttype = token.getType(); 567 switch (ttype) { 568 case CssLexer.NUMBER: 569 units = SizeUnits.PX; 570 trim = 0; 571 break; 572 case CssLexer.PERCENTAGE: 573 units = SizeUnits.PERCENT; 574 trim = 1; 575 break; 576 case CssLexer.EMS: 577 units = SizeUnits.EM; 578 break; 579 case CssLexer.EXS: 580 units = SizeUnits.EX; 581 break; 582 case CssLexer.PX: 583 units = SizeUnits.PX; 584 break; 585 case CssLexer.CM: 586 units = SizeUnits.CM; 587 break; 588 case CssLexer.MM: 589 units = SizeUnits.MM; 590 break; 591 case CssLexer.IN: 592 units = SizeUnits.IN; 593 break; 594 case CssLexer.PT: 595 units = SizeUnits.PT; 596 break; 597 case CssLexer.PC: 598 units = SizeUnits.PC; 599 break; 600 case CssLexer.DEG: 601 units = SizeUnits.DEG; 602 trim = 3; 603 break; 604 case CssLexer.GRAD: 605 units = SizeUnits.GRAD; 606 trim = 4; 607 break; 608 case CssLexer.RAD: 609 units = SizeUnits.RAD; 610 trim = 3; 611 break; 612 case CssLexer.TURN: 613 units = SizeUnits.TURN; 614 trim = 4; 615 break; 616 case CssLexer.SECONDS: 617 units = SizeUnits.S; 618 trim = 1; 619 break; 620 case CssLexer.MS: 621 units = SizeUnits.MS; 622 break; 623 default: 624 if (LOGGER.isLoggable(Level.FINEST)) { 625 LOGGER.finest("Expected \'<number>\'"); 626 } 627 ParseException re = new ParseException("Expected \'<number>\'",token, this); 628 reportError(createError(re.toString())); 629 throw re; 630 } 631 // TODO: Handle NumberFormatException 632 return new Size( 633 Double.parseDouble(sval.substring(0,len-trim)), 634 units 635 ); 636 } 637 638 // Count the number of terms in a series 639 private int numberOfTerms(final Term root) { 640 if (root == null) return 0; 641 642 int nTerms = 0; 643 Term term = root; 644 do { 645 nTerms += 1; 646 term = term.nextInSeries; 647 } while (term != null); 648 return nTerms; 649 } 650 651 // Count the number of series of terms 652 private int numberOfLayers(final Term root) { 653 if (root == null) return 0; 654 655 int nLayers = 0; 656 Term term = root; 657 do { 658 nLayers += 1; 659 while (term.nextInSeries != null) { 660 term = term.nextInSeries; 661 } 662 term = term.nextLayer; 663 } while (term != null); 664 return nLayers; 665 } 666 667 // Count the number of args of terms. root is the function. 668 private int numberOfArgs(final Term root) { 669 if (root == null) return 0; 670 671 int nArgs = 0; 672 Term term = root.firstArg; 673 while (term != null) { 674 nArgs += 1; 675 term = term.nextArg; 676 } 677 return nArgs; 678 } 679 680 // Get the next layer following this term, which may be null 681 private Term nextLayer(final Term root) { 682 if (root == null) return null; 683 684 Term term = root; 685 while (term.nextInSeries != null) { 686 term = term.nextInSeries; 687 } 688 return term.nextLayer; 689 } 690 691 //////////////////////////////////////////////////////////////////////////// 692 // 693 // Parsing routines 694 // 695 //////////////////////////////////////////////////////////////////////////// 696 697 ParsedValueImpl valueFor(String property, Term root, CssLexer lexer) throws ParseException { 698 final String prop = property.toLowerCase(Locale.ROOT); 699 properties.put(prop, prop); 700 if (root == null || root.token == null) { 701 error(root, "Expected value for property \'" + prop + "\'"); 702 } 703 704 if (root.token.getType() == CssLexer.IDENT) { 705 final String txt = root.token.getText(); 706 if ("inherit".equalsIgnoreCase(txt)) { 707 return new ParsedValueImpl<String,String>("inherit", null); 708 } else if ("null".equalsIgnoreCase(txt) 709 || "none".equalsIgnoreCase(txt)) { 710 return new ParsedValueImpl<String,String>("null", null); 711 } 712 } 713 if ("-fx-fill".equals(prop)) { 714 ParsedValueImpl pv = parse(root); 715 if (pv.getConverter() == StyleConverter.getUrlConverter()) { 716 // ImagePatternConverter expects array of ParsedValue where element 0 is the URL 717 // Pending RT-33574 718 pv = new ParsedValueImpl(new ParsedValue[] {pv},PaintConverter.ImagePatternConverter.getInstance()); 719 } 720 return pv; 721 } 722 else if ("-fx-background-color".equals(prop)) { 723 return parsePaintLayers(root); 724 } else if ("-fx-background-image".equals(prop)) { 725 return parseURILayers(root); 726 } else if ("-fx-background-insets".equals(prop)) { 727 return parseInsetsLayers(root); 728 } else if ("-fx-opaque-insets".equals(prop)) { 729 return parseInsetsLayer(root); 730 } else if ("-fx-background-position".equals(prop)) { 731 return parseBackgroundPositionLayers(root); 732 } else if ("-fx-background-radius".equals(prop)) { 733 return parseCornerRadius(root); 734 } else if ("-fx-background-repeat".equals(prop)) { 735 return parseBackgroundRepeatStyleLayers(root); 736 } else if ("-fx-background-size".equals(prop)) { 737 return parseBackgroundSizeLayers(root); 738 } else if ("-fx-border-color".equals(prop)) { 739 return parseBorderPaintLayers(root); 740 } else if ("-fx-border-insets".equals(prop)) { 741 return parseInsetsLayers(root); 742 } else if ("-fx-border-radius".equals(prop)) { 743 return parseCornerRadius(root); 744 } else if ("-fx-border-style".equals(prop)) { 745 return parseBorderStyleLayers(root); 746 } else if ("-fx-border-width".equals(prop)) { 747 return parseMarginsLayers(root); 748 } else if ("-fx-border-image-insets".equals(prop)) { 749 return parseInsetsLayers(root); 750 } else if ("-fx-border-image-repeat".equals(prop)) { 751 return parseBorderImageRepeatStyleLayers(root); 752 } else if ("-fx-border-image-slice".equals(prop)) { 753 return parseBorderImageSliceLayers(root); 754 } else if ("-fx-border-image-source".equals(prop)) { 755 return parseURILayers(root); 756 } else if ("-fx-border-image-width".equals(prop)) { 757 return parseBorderImageWidthLayers(root); 758 } else if ("-fx-padding".equals(prop)) { 759 ParsedValueImpl<?,Size>[] sides = parseSize1to4(root); 760 return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 761 } else if ("-fx-label-padding".equals(prop)) { 762 ParsedValueImpl<?,Size>[] sides = parseSize1to4(root); 763 return new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 764 } else if (prop.endsWith("font-family")) { 765 return parseFontFamily(root); 766 } else if (prop.endsWith("font-size")) { 767 ParsedValueImpl fsize = parseFontSize(root); 768 if (fsize == null) error(root, "Expected \'<font-size>\'"); 769 return fsize; 770 } else if (prop.endsWith("font-style")) { 771 ParsedValueImpl fstyle = parseFontStyle(root); 772 if (fstyle == null) error(root, "Expected \'<font-style>\'"); 773 return fstyle; 774 } else if (prop.endsWith("font-weight")) { 775 ParsedValueImpl fweight = parseFontWeight(root); 776 if (fweight == null) error(root, "Expected \'<font-style>\'"); 777 return fweight; 778 } else if (prop.endsWith("font")) { 779 return parseFont(root); 780 } else if ("-fx-stroke-dash-array".equals(prop)) { 781 // TODO: Figure out a way that these properties don't need to be 782 // special cased. 783 Term term = root; 784 int nArgs = numberOfTerms(term); 785 ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs]; 786 int segment = 0; 787 while(term != null) { 788 segments[segment++] = parseSize(term); 789 term = term.nextInSeries; 790 } 791 792 return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SequenceConverter.getInstance()); 793 794 } else if ("-fx-stroke-line-join".equals(prop)) { 795 // TODO: Figure out a way that these properties don't need to be 796 // special cased. 797 ParsedValueImpl[] values = parseStrokeLineJoin(root); 798 if (values == null) error(root, "Expected \'miter', \'bevel\' or \'round\'"); 799 return values[0]; 800 } else if ("-fx-stroke-line-cap".equals(prop)) { 801 // TODO: Figure out a way that these properties don't need to be 802 // special cased. 803 ParsedValueImpl value = parseStrokeLineCap(root); 804 if (value == null) error(root, "Expected \'square', \'butt\' or \'round\'"); 805 return value; 806 } else if ("-fx-stroke-type".equals(prop)) { 807 // TODO: Figure out a way that these properties don't need to be 808 // special cased. 809 ParsedValueImpl value = parseStrokeType(root); 810 if (value == null) error(root, "Expected \'centered', \'inside\' or \'outside\'"); 811 return value; 812 } else if ("-fx-font-smoothing-type".equals(prop)) { 813 // TODO: Figure out a way that these properties don't need to be 814 // special cased. 815 String str = null; 816 int ttype = -1; 817 final Token token = root.token; 818 819 if (root.token == null 820 || ((ttype = root.token.getType()) != CssLexer.STRING 821 && ttype != CssLexer.IDENT) 822 || (str = root.token.getText()) == null 823 || str.isEmpty()) { 824 error(root, "Expected STRING or IDENT"); 825 } 826 return new ParsedValueImpl<String, String>(stripQuotes(str), null, false); 827 } 828 return parse(root); 829 } 830 831 private ParsedValueImpl parse(Term root) throws ParseException { 832 833 if (root.token == null) error(root, "Parse error"); 834 final Token token = root.token; 835 ParsedValueImpl value = null; // value to return; 836 837 final int ttype = token.getType(); 838 switch (ttype) { 839 case CssLexer.NUMBER: 840 case CssLexer.PERCENTAGE: 841 case CssLexer.EMS: 842 case CssLexer.EXS: 843 case CssLexer.PX: 844 case CssLexer.CM: 845 case CssLexer.MM: 846 case CssLexer.IN: 847 case CssLexer.PT: 848 case CssLexer.PC: 849 case CssLexer.DEG: 850 case CssLexer.GRAD: 851 case CssLexer.RAD: 852 case CssLexer.TURN: 853 if (root.nextInSeries == null) { 854 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size(token), null); 855 value = new ParsedValueImpl<ParsedValue<?,Size>, Number>(sizeValue, SizeConverter.getInstance()); 856 } else { 857 ParsedValueImpl<Size,Size>[] sizeValue = parseSizeSeries(root); 858 value = new ParsedValueImpl<ParsedValue[],Number[]>(sizeValue, SizeConverter.SequenceConverter.getInstance()); 859 } 860 break; 861 case CssLexer.SECONDS: 862 case CssLexer.MS: { 863 ParsedValue<Size, Size> sizeValue = new ParsedValueImpl<Size, Size>(size(token), null); 864 value = new ParsedValueImpl<ParsedValue<?, Size>, Duration>(sizeValue, DurationConverter.getInstance()); 865 break; 866 } 867 case CssLexer.STRING: 868 case CssLexer.IDENT: 869 boolean isIdent = ttype == CssLexer.IDENT; 870 final String str = stripQuotes(token.getText()); 871 final String text = str.toLowerCase(Locale.ROOT); 872 if ("ladder".equals(text)) { 873 value = ladder(root); 874 } else if ("linear".equals(text) && (root.nextInSeries) != null) { 875 // if nextInSeries is null, then assume this is _not_ an old-style linear gradient 876 value = linearGradient(root); 877 } else if ("radial".equals(text) && (root.nextInSeries) != null) { 878 // if nextInSeries is null, then assume this is _not_ an old-style radial gradient 879 value = radialGradient(root); 880 } else if ("infinity".equals(text)) { 881 Size size = new Size(Double.MAX_VALUE, SizeUnits.PX); 882 ParsedValueImpl sizeValue = new ParsedValueImpl<Size,Number>(size, null); 883 value = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeValue, SizeConverter.getInstance()); 884 } else if ("indefinite".equals(text)) { 885 Size size = new Size(Double.POSITIVE_INFINITY, SizeUnits.PX); 886 ParsedValueImpl<Size,Size> sizeValue = new ParsedValueImpl<>(size, null); 887 value = new ParsedValueImpl<ParsedValue<?,Size>,Duration>(sizeValue, DurationConverter.getInstance()); 888 } else if ("true".equals(text)) { 889 // TODO: handling of boolean is really bogus 890 value = new ParsedValueImpl<String,Boolean>("true",BooleanConverter.getInstance()); 891 } else if ("false".equals(text)) { 892 // TODO: handling of boolean is really bogus 893 value = new ParsedValueImpl<String,Boolean>("false",BooleanConverter.getInstance()); 894 } else { 895 // if the property value is another property, then it needs to be looked up. 896 boolean needsLookup = isIdent && properties.containsKey(text); 897 if (needsLookup || ((value = colorValueOfString(str)) == null )) { 898 // If the value is a lookup, make sure to use the lower-case text so it matches the property 899 // in the Declaration. If the value is not a lookup, then use str since the value might 900 // be a string which could have some case sensitive meaning 901 // 902 // TODO: isIdent is needed here because of RT-38345. This effectively undoes RT-38201 903 value = new ParsedValueImpl<String,String>(needsLookup ? text : str, null, isIdent || needsLookup); 904 } 905 } 906 break; 907 case CssLexer.HASH: 908 final String clr = token.getText(); 909 try { 910 value = new ParsedValueImpl<Color,Color>(Color.web(clr), null); 911 } catch (final IllegalArgumentException e) { 912 error(root, e.getMessage()); 913 } 914 break; 915 case CssLexer.FUNCTION: 916 return parseFunction(root); 917 case CssLexer.URL: 918 return parseURI(root); 919 default: 920 final String msg = "Unknown token type: \'" + ttype + "\'"; 921 error(root, msg); 922 } 923 return value; 924 925 } 926 927 /* Parse size. 928 * @throw RecongnitionExcpetion if the token is not a size type or a lookup. 929 */ 930 private ParsedValueImpl<?,Size> parseSize(final Term root) throws ParseException { 931 932 if (root.token == null || !isSize(root.token)) error(root, "Expected \'<size>\'"); 933 934 ParsedValueImpl<?,Size> value = null; 935 936 if (root.token.getType() != CssLexer.IDENT) { 937 938 Size size = size(root.token); 939 value = new ParsedValueImpl<Size,Size>(size, null); 940 941 } else { 942 943 String key = root.token.getText(); 944 value = new ParsedValueImpl<String,Size>(key, null, true); 945 946 } 947 948 return value; 949 } 950 951 private ParsedValueImpl<?,Color> parseColor(final Term root) throws ParseException { 952 953 ParsedValueImpl<?,Color> color = null; 954 if (root.token != null && 955 (root.token.getType() == CssLexer.IDENT || 956 root.token.getType() == CssLexer.HASH || 957 root.token.getType() == CssLexer.FUNCTION)) { 958 959 color = parse(root); 960 961 } else { 962 error(root, "Expected \'<color>\'"); 963 } 964 return color; 965 } 966 967 // rgb(NUMBER, NUMBER, NUMBER) 968 // rgba(NUMBER, NUMBER, NUMBER, NUMBER) 969 // rgb(PERCENTAGE, PERCENTAGE, PERCENTAGE) 970 // rgba(PERCENTAGE, PERCENTAGE, PERCENTAGE, NUMBER) 971 private ParsedValueImpl rgb(Term root) throws ParseException { 972 973 // first term in the chain is the function name... 974 final String fn = (root.token != null) ? root.token.getText() : null; 975 if (fn == null || !"rgb".regionMatches(true, 0, fn, 0, 3)) { 976 final String msg = "Expected \'rgb\' or \'rgba\'"; 977 error(root, msg); 978 } 979 980 Term arg = root; 981 Token rtok, gtok, btok, atok; 982 983 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'"); 984 if ((rtok = arg.token) == null || 985 (rtok.getType() != CssLexer.NUMBER && 986 rtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'"); 987 988 root = arg; 989 990 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'"); 991 if ((gtok = arg.token) == null || 992 (gtok.getType() != CssLexer.NUMBER && 993 gtok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'"); 994 995 root = arg; 996 997 if ((arg = arg.nextArg) == null) error(root, "Expected \'<number>\' or \'<percentage>\'"); 998 if ((btok = arg.token) == null || 999 (btok.getType() != CssLexer.NUMBER && 1000 btok.getType() != CssLexer.PERCENTAGE)) error(arg, "Expected \'<number>\' or \'<percentage>\'"); 1001 1002 root = arg; 1003 1004 if ((arg = arg.nextArg) != null) { 1005 if ((atok = arg.token) == null || 1006 atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'"); 1007 } else { 1008 atok = null; 1009 } 1010 1011 int argType = rtok.getType(); 1012 if (argType != gtok.getType() || argType != btok.getType() || 1013 (argType != CssLexer.NUMBER && argType != CssLexer.PERCENTAGE)) { 1014 error(root, "Argument type mistmatch"); 1015 } 1016 1017 final String rtext = rtok.getText(); 1018 final String gtext = gtok.getText(); 1019 final String btext = btok.getText(); 1020 1021 double rval = 0; 1022 double gval = 0; 1023 double bval = 0; 1024 if (argType == CssLexer.NUMBER) { 1025 rval = clamp(0.0f, Double.parseDouble(rtext) / 255.0f, 1.0f); 1026 gval = clamp(0.0f, Double.parseDouble(gtext) / 255.0f, 1.0f); 1027 bval = clamp(0.0f, Double.parseDouble(btext) / 255.0f, 1.0f); 1028 } else { 1029 rval = clamp(0.0f, Double.parseDouble(rtext.substring(0,rtext.length()-1)) / 100.0f, 1.0f); 1030 gval = clamp(0.0f, Double.parseDouble(gtext.substring(0,gtext.length()-1)) / 100.0f, 1.0f); 1031 bval = clamp(0.0f, Double.parseDouble(btext.substring(0,btext.length()-1)) / 100.0f, 1.0f); 1032 } 1033 1034 final String atext = (atok != null) ? atok.getText() : null; 1035 final double aval = (atext != null) ? clamp(0.0f, Double.parseDouble(atext), 1.0f) : 1.0; 1036 1037 return new ParsedValueImpl<Color,Color>(Color.color(rval,gval,bval,aval), null); 1038 1039 } 1040 1041 // hsb(NUMBER, PERCENTAGE, PERCENTAGE) 1042 // hsba(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) 1043 private ParsedValueImpl hsb(Term root) throws ParseException { 1044 1045 // first term in the chain is the function name... 1046 final String fn = (root.token != null) ? root.token.getText() : null; 1047 if (fn == null || !"hsb".regionMatches(true, 0, fn, 0, 3)) { 1048 final String msg = "Expected \'hsb\' or \'hsba\'"; 1049 error(root, msg); 1050 } 1051 1052 Term arg = root; 1053 Token htok, stok, btok, atok; 1054 1055 if ((arg = arg.firstArg) == null) error(root, "Expected \'<number>\'"); 1056 if ((htok = arg.token) == null || htok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'"); 1057 1058 root = arg; 1059 1060 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'"); 1061 if ((stok = arg.token) == null || stok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'"); 1062 1063 root = arg; 1064 1065 if ((arg = arg.nextArg) == null) error(root, "Expected \'<percent>\'"); 1066 if ((btok = arg.token) == null || btok.getType() != CssLexer.PERCENTAGE) error(arg, "Expected \'<percent>\'"); 1067 1068 root = arg; 1069 1070 if ((arg = arg.nextArg) != null) { 1071 if ((atok = arg.token) == null || atok.getType() != CssLexer.NUMBER) error(arg, "Expected \'<number>\'"); 1072 } else { 1073 atok = null; 1074 } 1075 1076 final Size hval = size(htok); 1077 final Size sval = size(stok); 1078 final Size bval = size(btok); 1079 1080 final double hue = hval.pixels(); // no clamp - hue can be negative 1081 final double saturation = clamp(0.0f, sval.pixels(), 1.0f); 1082 final double brightness = clamp(0.0f, bval.pixels(), 1.0f); 1083 1084 final Size aval = (atok != null) ? size(atok) : null; 1085 final double opacity = (aval != null) ? clamp(0.0f, aval.pixels(), 1.0f) : 1.0; 1086 1087 return new ParsedValueImpl<Color,Color>(Color.hsb(hue, saturation, brightness, opacity), null); 1088 } 1089 1090 // derive(color, pct) 1091 private ParsedValueImpl<ParsedValue[],Color> derive(final Term root) 1092 throws ParseException { 1093 1094 // first term in the chain is the function name... 1095 final String fn = (root.token != null) ? root.token.getText() : null; 1096 if (fn == null || !"derive".regionMatches(true, 0, fn, 0, 6)) { 1097 final String msg = "Expected \'derive\'"; 1098 error(root, msg); 1099 } 1100 1101 Term arg = root; 1102 if ((arg = arg.firstArg) == null) error(root, "Expected \'<color>\'"); 1103 1104 final ParsedValueImpl<?,Color> color = parseColor(arg); 1105 1106 final Term prev = arg; 1107 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<percent\'"); 1108 1109 final ParsedValueImpl<?,Size> brightness = parseSize(arg); 1110 1111 ParsedValueImpl[] values = new ParsedValueImpl[] { color, brightness }; 1112 return new ParsedValueImpl<ParsedValue[],Color>(values, DeriveColorConverter.getInstance()); 1113 } 1114 1115 // 'ladder' color 'stops' stop+ 1116 private ParsedValueImpl<ParsedValue[],Color> ladder(final Term root) throws ParseException { 1117 1118 // first term in the chain is the function name... 1119 final String fn = (root.token != null) ? root.token.getText() : null; 1120 if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) { 1121 final String msg = "Expected \'ladder\'"; 1122 error(root, msg); 1123 } 1124 1125 if (LOGGER.isLoggable(Level.WARNING)) { 1126 LOGGER.warning(formatDeprecatedMessage(root, "ladder")); 1127 } 1128 1129 Term term = root; 1130 1131 if ((term = term.nextInSeries) == null) error(root, "Expected \'<color>\'"); 1132 final ParsedValueImpl<?,Color> color = parse(term); 1133 1134 Term prev = term; 1135 1136 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'"); 1137 if (term.token == null || 1138 term.token.getType() != CssLexer.IDENT || 1139 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'"); 1140 1141 prev = term; 1142 1143 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <color>)\'"); 1144 1145 int nStops = 0; 1146 Term temp = term; 1147 do { 1148 nStops += 1; 1149 // if next token type is IDENT, then we have CycleMethod 1150 } while (((temp = temp.nextInSeries) != null) && 1151 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN))); 1152 1153 ParsedValueImpl[] values = new ParsedValueImpl[nStops+1]; 1154 values[0] = color; 1155 int stopIndex = 1; 1156 do { 1157 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term); 1158 if (stop != null) values[stopIndex++] = stop; 1159 prev = term; 1160 } while(((term = term.nextInSeries) != null) && 1161 (term.token.getType() == CssLexer.LPAREN)); 1162 1163 // if term is not null and the last term was not an lparen, 1164 // then term starts a new series of Paint. Point 1165 // root.nextInSeries to term so the next loop skips over the 1166 // already parsed ladder bits. 1167 if (term != null) { 1168 root.nextInSeries = term; 1169 } 1170 1171 // if term is null, then we are at the end of a series. 1172 // root points to 'ladder', now we want the next term after root 1173 // to be the term after the last stop, which may be another layer 1174 else { 1175 root.nextInSeries = null; 1176 root.nextLayer = prev.nextLayer; 1177 } 1178 1179 return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance()); 1180 } 1181 1182 // <ladder> = ladder(<color>, <color-stop>[, <color-stop>]+ ) 1183 private ParsedValueImpl<ParsedValue[],Color> parseLadder(final Term root) throws ParseException { 1184 1185 // first term in the chain is the function name... 1186 final String fn = (root.token != null) ? root.token.getText() : null; 1187 if (fn == null || !"ladder".regionMatches(true, 0, fn, 0, 6)) { 1188 final String msg = "Expected \'ladder\'"; 1189 error(root, msg); 1190 } 1191 1192 Term term = root; 1193 1194 if ((term = term.firstArg) == null) error(root, "Expected \'<color>\'"); 1195 final ParsedValueImpl<?,Color> color = parse(term); 1196 1197 Term prev = term; 1198 1199 if ((term = term.nextArg) == null) 1200 error(prev, "Expected \'<color-stop>[, <color-stop>]+\'"); 1201 1202 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(term); 1203 1204 ParsedValueImpl[] values = new ParsedValueImpl[stops.length+1]; 1205 values[0] = color; 1206 System.arraycopy(stops, 0, values, 1, stops.length); 1207 return new ParsedValueImpl<ParsedValue[], Color>(values, LadderConverter.getInstance()); 1208 } 1209 1210 // parse (<number>, <color>)+ 1211 // root.token should be a size 1212 // root.token.next should be a color 1213 private ParsedValueImpl<ParsedValue[], Stop> stop(final Term root) 1214 throws ParseException { 1215 1216 // first term in the chain is the function name... 1217 final String fn = (root.token != null) ? root.token.getText() : null; 1218 if (fn == null || !"(".equals(fn)) { 1219 final String msg = "Expected \'(\'"; 1220 error(root, msg); 1221 } 1222 1223 Term arg = null; 1224 1225 if ((arg = root.firstArg) == null) error(root, "Expected \'<number>\'"); 1226 1227 ParsedValueImpl<?,Size> size = parseSize(arg); 1228 1229 Term prev = arg; 1230 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1231 1232 ParsedValueImpl<?,Color> color = parseColor(arg); 1233 1234 ParsedValueImpl[] values = new ParsedValueImpl[] { size, color }; 1235 return new ParsedValueImpl<ParsedValue[],Stop>(values, StopConverter.getInstance()); 1236 1237 } 1238 1239 // http://dev.w3.org/csswg/css3-images/#color-stop-syntax 1240 // <color-stop> = <color> [ <percentage> | <length> ]? 1241 private ParsedValueImpl<ParsedValue[], Stop>[] parseColorStops(final Term root) 1242 throws ParseException { 1243 1244 int nArgs = 1; 1245 Term temp = root; 1246 while(temp != null) { 1247 if (temp.nextArg != null) { 1248 nArgs += 1; 1249 temp = temp.nextArg; 1250 } else if (temp.nextInSeries != null) { 1251 temp = temp.nextInSeries; 1252 } else { 1253 break; 1254 } 1255 } 1256 1257 if (nArgs < 2) { 1258 error(root, "Expected \'<color-stop>\'"); 1259 } 1260 1261 ParsedValueImpl<?,Color>[] colors = new ParsedValueImpl[nArgs]; 1262 Size[] positions = new Size[nArgs]; 1263 java.util.Arrays.fill(positions, null); 1264 1265 Term stop = root; 1266 Term prev = root; 1267 SizeUnits units = null; 1268 for (int n = 0; n<nArgs; n++) { 1269 1270 colors[n] = parseColor(stop); 1271 1272 prev = stop; 1273 Term term = stop.nextInSeries; 1274 if (term != null) { 1275 if (isSize(term.token)) { 1276 positions[n] = size(term.token); 1277 if (units != null) { 1278 if (units != positions[n].getUnits()) { 1279 error(term, "Parser unable to handle mixed \'<percent>\' and \'<length>\'"); 1280 } 1281 } 1282 } else { 1283 error(prev, "Expected \'<percent>\' or \'<length>\'"); 1284 } 1285 prev = term; 1286 stop = term.nextArg; 1287 } else { 1288 prev = stop; 1289 stop = stop.nextArg; 1290 } 1291 1292 } 1293 1294 // 1295 // normalize positions according to 1296 // http://dev.w3.org/csswg/css3-images/#color-stop-syntax 1297 // 1298 // If the first color-stop does not have a position, set its 1299 // position to 0%. If the last color-stop does not have a position, 1300 // set its position to 100%. 1301 if (positions[0] == null) positions[0] = new Size(0, SizeUnits.PERCENT); 1302 if (positions[nArgs-1] == null) positions[nArgs-1] = new Size(100, SizeUnits.PERCENT); 1303 1304 // If a color-stop has a position that is less than the specified 1305 // position of any color-stop before it in the list, set its 1306 // position to be equal to the largest specified position of any 1307 // color-stop before it. 1308 Size max = null; 1309 for (int n = 1 ; n<nArgs; n++) { 1310 Size pos0 = positions[n-1]; 1311 if (pos0 == null) continue; 1312 if (max == null || max.getValue() < pos0.getValue()) { 1313 // TODO: this doesn't work with mixed length and percent 1314 max = pos0; 1315 } 1316 Size pos1 = positions[n]; 1317 if (pos1 == null) continue; 1318 1319 if (pos1.getValue() < max.getValue()) positions[n] = max; 1320 } 1321 1322 // If any color-stop still does not have a position, then, 1323 // for each run of adjacent color-stops without positions, set 1324 // their positions so that they are evenly spaced between the 1325 // preceding and following color-stops with positions. 1326 Size preceding = null; 1327 int withoutIndex = -1; 1328 for (int n = 0 ; n<nArgs; n++) { 1329 Size pos = positions[n]; 1330 if (pos == null) { 1331 if (withoutIndex == -1) withoutIndex = n; 1332 } else { 1333 if (withoutIndex > -1) { 1334 1335 int nWithout = n - withoutIndex; 1336 double precedingValue = preceding.getValue(); 1337 double delta = 1338 (pos.getValue() - precedingValue) / (nWithout + 1); 1339 1340 while(withoutIndex < n) { 1341 precedingValue += delta; 1342 positions[withoutIndex++] = 1343 new Size(precedingValue, pos.getUnits()); 1344 } 1345 withoutIndex = -1; 1346 preceding = pos; 1347 } else { 1348 preceding = pos; 1349 } 1350 } 1351 } 1352 1353 ParsedValueImpl<ParsedValue[],Stop>[] stops = new ParsedValueImpl[nArgs]; 1354 for (int n=0; n<nArgs; n++) { 1355 stops[n] = new ParsedValueImpl<ParsedValue[],Stop>( 1356 new ParsedValueImpl[] { 1357 new ParsedValueImpl<Size,Size>(positions[n], null), 1358 colors[n] 1359 }, 1360 StopConverter.getInstance() 1361 ); 1362 } 1363 1364 return stops; 1365 1366 } 1367 1368 // parse (<number>, <number>) 1369 private ParsedValueImpl[] point(final Term root) throws ParseException { 1370 1371 if (root.token == null || 1372 root.token.getType() != CssLexer.LPAREN) error(root, "Expected \'(<number>, <number>)\'"); 1373 1374 final String fn = root.token.getText(); 1375 if (fn == null || !"(".equalsIgnoreCase(fn)) { 1376 final String msg = "Expected \'(\'"; 1377 error(root, msg); 1378 } 1379 1380 Term arg = null; 1381 1382 // no <number> 1383 if ((arg = root.firstArg) == null) error(root, "Expected \'<number>\'"); 1384 1385 final ParsedValueImpl<?,Size> ptX = parseSize(arg); 1386 1387 final Term prev = arg; 1388 1389 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1390 1391 final ParsedValueImpl<?,Size> ptY = parseSize(arg); 1392 1393 return new ParsedValueImpl[] { ptX, ptY }; 1394 } 1395 1396 private ParsedValueImpl parseFunction(final Term root) throws ParseException { 1397 1398 // Text from parser is function name plus the lparen, e.g., 'derive(' 1399 final String fcn = (root.token != null) ? root.token.getText() : null; 1400 if (fcn == null) { 1401 error(root, "Expected function name"); 1402 } else if ("rgb".regionMatches(true, 0, fcn, 0, 3)) { 1403 return rgb(root); 1404 } else if ("hsb".regionMatches(true, 0, fcn, 0, 3)) { 1405 return hsb(root); 1406 } else if ("derive".regionMatches(true, 0, fcn, 0, 6)) { 1407 return derive(root); 1408 } else if ("innershadow".regionMatches(true, 0, fcn, 0, 11)) { 1409 return innershadow(root); 1410 } else if ("dropshadow".regionMatches(true, 0, fcn, 0, 10)) { 1411 return dropshadow(root); 1412 } else if ("linear-gradient".regionMatches(true, 0, fcn, 0, 15)) { 1413 return parseLinearGradient(root); 1414 } else if ("radial-gradient".regionMatches(true, 0, fcn, 0, 15)) { 1415 return parseRadialGradient(root); 1416 } else if ("image-pattern".regionMatches(true, 0, fcn, 0, 13)) { 1417 return parseImagePattern(root); 1418 } else if ("repeating-image-pattern".regionMatches(true, 0, fcn, 0, 23)) { 1419 return parseRepeatingImagePattern(root); 1420 } else if ("ladder".regionMatches(true, 0, fcn, 0, 6)) { 1421 return parseLadder(root); 1422 } else if ("region".regionMatches(true, 0, fcn, 0, 6)) { 1423 return parseRegion(root); 1424 } else { 1425 error(root, "Unexpected function \'" + fcn + "\'"); 1426 } 1427 return null; 1428 } 1429 1430 private ParsedValueImpl<String,BlurType> blurType(final Term root) throws ParseException { 1431 1432 if (root == null) return null; 1433 if (root.token == null || 1434 root.token.getType() != CssLexer.IDENT || 1435 root.token.getText() == null || 1436 root.token.getText().isEmpty()) { 1437 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'"; 1438 error(root, msg); 1439 } 1440 final String blurStr = root.token.getText().toLowerCase(Locale.ROOT); 1441 BlurType blurType = BlurType.THREE_PASS_BOX; 1442 if ("gaussian".equals(blurStr)) { 1443 blurType = BlurType.GAUSSIAN; 1444 } else if ("one-pass-box".equals(blurStr)) { 1445 blurType = BlurType.ONE_PASS_BOX; 1446 } else if ("two-pass-box".equals(blurStr)) { 1447 blurType = BlurType.TWO_PASS_BOX; 1448 } else if ("three-pass-box".equals(blurStr)) { 1449 blurType = BlurType.THREE_PASS_BOX; 1450 } else { 1451 final String msg = "Expected \'gaussian\', \'one-pass-box\', \'two-pass-box\', or \'three-pass-box\'"; 1452 error(root, msg); 1453 } 1454 return new ParsedValueImpl<String,BlurType>(blurType.name(), new EnumConverter<BlurType>(BlurType.class)); 1455 } 1456 1457 // innershadow <blur-type> <color> <radius> <choke> <offset-x> <offset-y> 1458 private ParsedValueImpl innershadow(final Term root) throws ParseException { 1459 1460 // first term in the chain is the function name... 1461 final String fn = (root.token != null) ? root.token.getText() : null; 1462 if (!"innershadow".regionMatches(true, 0, fn, 0, 11)) { 1463 final String msg = "Expected \'innershadow\'"; 1464 error(root, msg); 1465 } 1466 1467 Term arg; 1468 1469 if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'"); 1470 ParsedValueImpl<String,BlurType> blurVal = blurType(arg); 1471 1472 Term prev = arg; 1473 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1474 1475 ParsedValueImpl<?,Color> colorVal = parseColor(arg); 1476 1477 prev = arg; 1478 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1479 1480 ParsedValueImpl<?,Size> radiusVal = parseSize(arg); 1481 1482 prev = arg; 1483 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1484 1485 ParsedValueImpl<?,Size> chokeVal = parseSize(arg); 1486 1487 prev = arg; 1488 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1489 1490 ParsedValueImpl<?,Size> offsetXVal = parseSize(arg); 1491 1492 prev = arg; 1493 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1494 1495 ParsedValueImpl<?,Size> offsetYVal = parseSize(arg); 1496 1497 ParsedValueImpl[] values = new ParsedValueImpl[] { 1498 blurVal, 1499 colorVal, 1500 radiusVal, 1501 chokeVal, 1502 offsetXVal, 1503 offsetYVal 1504 }; 1505 return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.InnerShadowConverter.getInstance()); 1506 } 1507 1508 // dropshadow <blur-type> <color> <radius> <spread> <offset-x> <offset-y> 1509 private ParsedValueImpl dropshadow(final Term root) throws ParseException { 1510 1511 // first term in the chain is the function name... 1512 final String fn = (root.token != null) ? root.token.getText() : null; 1513 if (!"dropshadow".regionMatches(true, 0, fn, 0, 10)) { 1514 final String msg = "Expected \'dropshadow\'"; 1515 error(root, msg); 1516 } 1517 1518 Term arg; 1519 1520 if ((arg = root.firstArg) == null) error(root, "Expected \'<blur-type>\'"); 1521 ParsedValueImpl<String,BlurType> blurVal = blurType(arg); 1522 1523 Term prev = arg; 1524 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<color>\'"); 1525 1526 ParsedValueImpl<?,Color> colorVal = parseColor(arg); 1527 1528 prev = arg; 1529 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1530 1531 ParsedValueImpl<?,Size> radiusVal = parseSize(arg); 1532 1533 prev = arg; 1534 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1535 1536 ParsedValueImpl<?,Size> spreadVal = parseSize(arg); 1537 1538 prev = arg; 1539 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1540 1541 ParsedValueImpl<?,Size> offsetXVal = parseSize(arg); 1542 1543 prev = arg; 1544 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<number>\'"); 1545 1546 ParsedValueImpl<?,Size> offsetYVal = parseSize(arg); 1547 1548 ParsedValueImpl[] values = new ParsedValueImpl[] { 1549 blurVal, 1550 colorVal, 1551 radiusVal, 1552 spreadVal, 1553 offsetXVal, 1554 offsetYVal 1555 }; 1556 return new ParsedValueImpl<ParsedValue[],Effect>(values, EffectConverter.DropShadowConverter.getInstance()); 1557 } 1558 1559 // returns null if the Term is null or is not a cycle method. 1560 private ParsedValueImpl<String, CycleMethod> cycleMethod(final Term root) { 1561 CycleMethod cycleMethod = null; 1562 if (root != null && root.token.getType() == CssLexer.IDENT) { 1563 1564 final String text = root.token.getText().toLowerCase(Locale.ROOT); 1565 if ("repeat".equals(text)) { 1566 cycleMethod = CycleMethod.REPEAT; 1567 } else if ("reflect".equals(text)) { 1568 cycleMethod = CycleMethod.REFLECT; 1569 } else if ("no-cycle".equals(text)) { 1570 cycleMethod = CycleMethod.NO_CYCLE; 1571 } 1572 } 1573 if (cycleMethod != null) 1574 return new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 1575 else 1576 return null; 1577 } 1578 1579 // linear <point> TO <point> STOPS <stop>+ cycleMethod? 1580 private ParsedValueImpl<ParsedValue[],Paint> linearGradient(final Term root) throws ParseException { 1581 1582 final String fn = (root.token != null) ? root.token.getText() : null; 1583 if (fn == null || !"linear".equalsIgnoreCase(fn)) { 1584 final String msg = "Expected \'linear\'"; 1585 error(root, msg); 1586 } 1587 1588 if (LOGGER.isLoggable(Level.WARNING)) { 1589 LOGGER.warning(formatDeprecatedMessage(root, "linear gradient")); 1590 } 1591 1592 Term term = root; 1593 1594 if ((term = term.nextInSeries) == null) error(root, "Expected \'(<number>, <number>)\'"); 1595 1596 final ParsedValueImpl<?,Size>[] startPt = point(term); 1597 1598 Term prev = term; 1599 if ((term = term.nextInSeries) == null) error(prev, "Expected \'to\'"); 1600 if (term.token == null || 1601 term.token.getType() != CssLexer.IDENT || 1602 !"to".equalsIgnoreCase(term.token.getText())) error(root, "Expected \'to\'"); 1603 1604 prev = term; 1605 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'"); 1606 1607 final ParsedValueImpl<?,Size>[] endPt = point(term); 1608 1609 prev = term; 1610 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\'"); 1611 if (term.token == null || 1612 term.token.getType() != CssLexer.IDENT || 1613 !"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'"); 1614 1615 prev = term; 1616 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'"); 1617 1618 int nStops = 0; 1619 Term temp = term; 1620 do { 1621 nStops += 1; 1622 // if next token type is IDENT, then we have CycleMethod 1623 } while (((temp = temp.nextInSeries) != null) && 1624 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN))); 1625 1626 ParsedValueImpl[] stops = new ParsedValueImpl[nStops]; 1627 int stopIndex = 0; 1628 do { 1629 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term); 1630 if (stop != null) stops[stopIndex++] = stop; 1631 prev = term; 1632 } while(((term = term.nextInSeries) != null) && 1633 (term.token.getType() == CssLexer.LPAREN)); 1634 1635 // term is either null or is a cycle method, or the start of another Paint. 1636 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term); 1637 1638 if (cycleMethod == null) { 1639 1640 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 1641 1642 // if term is not null and the last term was not a cycle method, 1643 // then term starts a new series or layer of Paint 1644 if (term != null) { 1645 root.nextInSeries = term; 1646 } 1647 1648 // if term is null, then we are at the end of a series. 1649 // root points to 'linear', now we want the next term after root 1650 // to be the term after the last stop, which may be another layer 1651 else { 1652 root.nextInSeries = null; 1653 root.nextLayer = prev.nextLayer; 1654 } 1655 1656 1657 } else { 1658 // last term was a CycleMethod, so term is not null. 1659 // root points at 'linear', now we want the next term after root 1660 // to be the term after cyclemethod, which may be another series 1661 // of paint or another layer. 1662 // 1663 root.nextInSeries = term.nextInSeries; 1664 root.nextLayer = term.nextLayer; 1665 } 1666 1667 ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length]; 1668 int index = 0; 1669 values[index++] = (startPt != null) ? startPt[0] : null; 1670 values[index++] = (startPt != null) ? startPt[1] : null; 1671 values[index++] = (endPt != null) ? endPt[0] : null; 1672 values[index++] = (endPt != null) ? endPt[1] : null; 1673 values[index++] = cycleMethod; 1674 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 1675 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance()); 1676 } 1677 1678 // Based off http://dev.w3.org/csswg/css3-images/#linear-gradients 1679 // 1680 // <linear-gradient> = linear-gradient( 1681 // [ [from <point> to <point>] | [ to <side-or-corner> ] ] ,]? [ [ repeat | reflect ] ,]? 1682 // <color-stop>[, <color-stop>]+ 1683 // ) 1684 // 1685 // 1686 // <point> = <percentage> <percentage> | <length> <length> 1687 // <side-or-corner> = [left | right] || [top | bottom] 1688 // 1689 // If neither repeat nor reflect are given, then the CycleMethod defaults "NO_CYCLE". 1690 // If neither [from <point> to <point>] nor [ to <side-or-corner> ] are given, 1691 // then the gradient direction defaults to 'to bottom'. 1692 // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax. 1693 private ParsedValueImpl parseLinearGradient(final Term root) throws ParseException { 1694 1695 // first term in the chain is the function name... 1696 final String fn = (root.token != null) ? root.token.getText() : null; 1697 if (!"linear-gradient".regionMatches(true, 0, fn, 0, 15)) { 1698 final String msg = "Expected \'linear-gradient\'"; 1699 error(root, msg); 1700 } 1701 1702 Term arg; 1703 1704 if ((arg = root.firstArg) == null || 1705 arg.token == null || 1706 arg.token.getText().isEmpty()) { 1707 error(root, 1708 "Expected \'from <point> to <point>\' or \'to <side-or-corner>\' " + 1709 "or \'<cycle-method>\' or \'<color-stop>\'"); 1710 } 1711 1712 Term prev = arg; 1713 // ParsedValueImpl<Size,Size> angleVal = null; 1714 ParsedValueImpl<?,Size>[] startPt = null; 1715 ParsedValueImpl<?,Size>[] endPt = null; 1716 1717 if ("from".equalsIgnoreCase(arg.token.getText())) { 1718 1719 prev = arg; 1720 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 1721 1722 ParsedValueImpl<?,Size> ptX = parseSize(arg); 1723 1724 prev = arg; 1725 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 1726 1727 ParsedValueImpl<?,Size> ptY = parseSize(arg); 1728 1729 startPt = new ParsedValueImpl[] { ptX, ptY }; 1730 1731 prev = arg; 1732 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'to\'"); 1733 if (arg.token == null || 1734 arg.token.getType() != CssLexer.IDENT || 1735 !"to".equalsIgnoreCase(arg.token.getText())) error(prev, "Expected \'to\'"); 1736 1737 prev = arg; 1738 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 1739 1740 ptX = parseSize(arg); 1741 1742 prev = arg; 1743 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 1744 1745 ptY = parseSize(arg); 1746 1747 endPt = new ParsedValueImpl[] { ptX, ptY }; 1748 1749 prev = arg; 1750 arg = arg.nextArg; 1751 1752 } else if("to".equalsIgnoreCase(arg.token.getText())) { 1753 1754 prev = arg; 1755 if ((arg = arg.nextInSeries) == null || 1756 arg.token == null || 1757 arg.token.getType() != CssLexer.IDENT || 1758 arg.token.getText().isEmpty()) { 1759 error (prev, "Expected \'<side-or-corner>\'"); 1760 } 1761 1762 1763 int startX = 0; 1764 int startY = 0; 1765 int endX = 0; 1766 int endY = 0; 1767 1768 String sideOrCorner1 = arg.token.getText().toLowerCase(Locale.ROOT); 1769 // The keywords denote the direction. 1770 if ("top".equals(sideOrCorner1)) { 1771 // going toward the top, then start at the bottom 1772 startY = 100; 1773 endY = 0; 1774 1775 } else if ("bottom".equals(sideOrCorner1)) { 1776 // going toward the bottom, then start at the top 1777 startY = 0; 1778 endY = 100; 1779 1780 } else if ("right".equals(sideOrCorner1)) { 1781 // going toward the right, then start at the left 1782 startX = 0; 1783 endX = 100; 1784 1785 } else if ("left".equals(sideOrCorner1)) { 1786 // going toward the left, then start at the right 1787 startX = 100; 1788 endX = 0; 1789 1790 } else { 1791 error(arg, "Invalid \'<side-or-corner>\'"); 1792 } 1793 1794 prev = arg; 1795 if (arg.nextInSeries != null) { 1796 arg = arg.nextInSeries; 1797 if (arg.token != null && 1798 arg.token.getType() == CssLexer.IDENT && 1799 !arg.token.getText().isEmpty()) { 1800 1801 String sideOrCorner2 = arg.token.getText().toLowerCase(Locale.ROOT); 1802 1803 // if right or left has already been given, 1804 // then either startX or endX will not be zero. 1805 if ("right".equals(sideOrCorner2) && 1806 startX == 0 && endX == 0) { 1807 // start left, end right 1808 startX = 0; 1809 endX = 100; 1810 } else if ("left".equals(sideOrCorner2) && 1811 startX == 0 && endX == 0) { 1812 // start right, end left 1813 startX = 100; 1814 endX = 0; 1815 1816 // if top or bottom has already been given, 1817 // then either startY or endY will not be zero. 1818 } else if("top".equals(sideOrCorner2) && 1819 startY == 0 && endY == 0) { 1820 // start bottom, end top 1821 startY = 100; 1822 endY = 0; 1823 } else if ("bottom".equals(sideOrCorner2) && 1824 startY == 0 && endY == 0) { 1825 // start top, end bottom 1826 startY = 0; 1827 endY = 100; 1828 1829 } else { 1830 error(arg, "Invalid \'<side-or-corner>\'"); 1831 } 1832 1833 } else { 1834 error (prev, "Expected \'<side-or-corner>\'"); 1835 } 1836 } 1837 1838 1839 startPt = new ParsedValueImpl[] { 1840 new ParsedValueImpl<Size,Size>(new Size(startX, SizeUnits.PERCENT), null), 1841 new ParsedValueImpl<Size,Size>(new Size(startY, SizeUnits.PERCENT), null) 1842 }; 1843 1844 endPt = new ParsedValueImpl[] { 1845 new ParsedValueImpl<Size,Size>(new Size(endX, SizeUnits.PERCENT), null), 1846 new ParsedValueImpl<Size,Size>(new Size(endY, SizeUnits.PERCENT), null) 1847 }; 1848 1849 prev = arg; 1850 arg = arg.nextArg; 1851 } 1852 1853 if (startPt == null && endPt == null) { 1854 // spec says defaults to bottom 1855 startPt = new ParsedValueImpl[] { 1856 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null), 1857 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null) 1858 }; 1859 1860 endPt = new ParsedValueImpl[] { 1861 new ParsedValueImpl<Size,Size>(new Size(0, SizeUnits.PERCENT), null), 1862 new ParsedValueImpl<Size,Size>(new Size(100, SizeUnits.PERCENT), null) 1863 }; 1864 } 1865 1866 if (arg == null || 1867 arg.token == null || 1868 arg.token.getText().isEmpty()) { 1869 error(prev, "Expected \'<cycle-method>\' or \'<color-stop>\'"); 1870 } 1871 1872 CycleMethod cycleMethod = CycleMethod.NO_CYCLE; 1873 if ("reflect".equalsIgnoreCase(arg.token.getText())) { 1874 cycleMethod = CycleMethod.REFLECT; 1875 prev = arg; 1876 arg = arg.nextArg; 1877 } else if ("repeat".equalsIgnoreCase(arg.token.getText())) { 1878 cycleMethod = CycleMethod.REFLECT; 1879 prev = arg; 1880 arg = arg.nextArg; 1881 } 1882 1883 if (arg == null || 1884 arg.token == null || 1885 arg.token.getText().isEmpty()) { 1886 error(prev, "Expected \'<color-stop>\'"); 1887 } 1888 1889 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg); 1890 1891 ParsedValueImpl[] values = new ParsedValueImpl[5 + stops.length]; 1892 int index = 0; 1893 values[index++] = (startPt != null) ? startPt[0] : null; 1894 values[index++] = (startPt != null) ? startPt[1] : null; 1895 values[index++] = (endPt != null) ? endPt[0] : null; 1896 values[index++] = (endPt != null) ? endPt[1] : null; 1897 values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 1898 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 1899 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.LinearGradientConverter.getInstance()); 1900 1901 } 1902 1903 // radial [focus-angle <number | percent>]? [focus-distance <size>]? 1904 // [center (<size>,<size>)]? <size> 1905 // stops [ ( <number> , <color> ) ]+ [ repeat | reflect ]? 1906 private ParsedValueImpl<ParsedValue[], Paint> radialGradient(final Term root) throws ParseException { 1907 1908 final String fn = (root.token != null) ? root.token.getText() : null; 1909 if (fn == null || !"radial".equalsIgnoreCase(fn)) { 1910 final String msg = "Expected \'radial\'"; 1911 error(root, msg); 1912 } 1913 1914 if (LOGGER.isLoggable(Level.WARNING)) { 1915 LOGGER.warning(formatDeprecatedMessage(root, "radial gradient")); 1916 } 1917 1918 Term term = root; 1919 Term prev = root; 1920 1921 if ((term = term.nextInSeries) == null) error(root, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'"); 1922 if (term.token == null) error(term, "Expected \'focus-angle <number>\', \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'"); 1923 1924 1925 ParsedValueImpl<?,Size> focusAngle = null; 1926 if (term.token.getType() == CssLexer.IDENT) { 1927 final String keyword = term.token.getText().toLowerCase(Locale.ROOT); 1928 if ("focus-angle".equals(keyword)) { 1929 1930 prev = term; 1931 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'"); 1932 if (term.token == null) error(prev, "Expected \'<number>\'"); 1933 1934 focusAngle = parseSize(term); 1935 1936 prev = term; 1937 if ((term = term.nextInSeries) == null) error(prev, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'"); 1938 if (term.token == null) error(term, "Expected \'focus-distance <number>\', \'center (<number>,<number>)\' or \'<size>\'"); 1939 } 1940 } 1941 1942 ParsedValueImpl<?,Size> focusDistance = null; 1943 if (term.token.getType() == CssLexer.IDENT) { 1944 final String keyword = term.token.getText().toLowerCase(Locale.ROOT); 1945 if ("focus-distance".equals(keyword)) { 1946 1947 prev = term; 1948 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<number>\'"); 1949 if (term.token == null) error(prev, "Expected \'<number>\'"); 1950 1951 focusDistance = parseSize(term); 1952 1953 prev = term; 1954 if ((term = term.nextInSeries) == null) error(prev, "Expected \'center (<number>,<number>)\' or \'<size>\'"); 1955 if (term.token == null) error(term, "Expected \'center (<number>,<number>)\' or \'<size>\'"); 1956 } 1957 } 1958 1959 ParsedValueImpl<?,Size>[] centerPoint = null; 1960 if (term.token.getType() == CssLexer.IDENT) { 1961 final String keyword = term.token.getText().toLowerCase(Locale.ROOT); 1962 if ("center".equals(keyword)) { 1963 1964 prev = term; 1965 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>,<number>)\'"); 1966 if (term.token == null || 1967 term.token.getType() != CssLexer.LPAREN) error(term, "Expected \'(<number>,<number>)\'"); 1968 1969 centerPoint = point(term); 1970 1971 prev = term; 1972 if ((term = term.nextInSeries) == null) error(prev, "Expected \'<size>\'"); 1973 if (term.token == null) error(term, "Expected \'<size>\'"); 1974 } 1975 } 1976 1977 ParsedValueImpl<?,Size> radius = parseSize(term); 1978 1979 prev = term; 1980 if ((term = term.nextInSeries) == null) error(prev, "Expected \'stops\' keyword"); 1981 if (term.token == null || 1982 term.token.getType() != CssLexer.IDENT) error(term, "Expected \'stops\' keyword"); 1983 1984 if (!"stops".equalsIgnoreCase(term.token.getText())) error(term, "Expected \'stops\'"); 1985 1986 prev = term; 1987 if ((term = term.nextInSeries) == null) error(prev, "Expected \'(<number>, <number>)\'"); 1988 1989 int nStops = 0; 1990 Term temp = term; 1991 do { 1992 nStops += 1; 1993 // if next token type is IDENT, then we have CycleMethod 1994 } while (((temp = temp.nextInSeries) != null) && 1995 ((temp.token != null) && (temp.token.getType() == CssLexer.LPAREN))); 1996 1997 ParsedValueImpl[] stops = new ParsedValueImpl[nStops]; 1998 int stopIndex = 0; 1999 do { 2000 ParsedValueImpl<ParsedValue[],Stop> stop = stop(term); 2001 if (stop != null) stops[stopIndex++] = stop; 2002 prev = term; 2003 } while(((term = term.nextInSeries) != null) && 2004 (term.token.getType() == CssLexer.LPAREN)); 2005 2006 // term is either null or is a cycle method, or the start of another Paint. 2007 ParsedValueImpl<String,CycleMethod> cycleMethod = cycleMethod(term); 2008 2009 if (cycleMethod == null) { 2010 2011 cycleMethod = new ParsedValueImpl<String,CycleMethod>(CycleMethod.NO_CYCLE.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 2012 2013 // if term is not null and the last term was not a cycle method, 2014 // then term starts a new series or layer of Paint 2015 if (term != null) { 2016 root.nextInSeries = term; 2017 } 2018 2019 // if term is null, then we are at the end of a series. 2020 // root points to 'linear', now we want the next term after root 2021 // to be the term after the last stop, which may be another layer 2022 else { 2023 root.nextInSeries = null; 2024 root.nextLayer = prev.nextLayer; 2025 } 2026 2027 2028 } else { 2029 // last term was a CycleMethod, so term is not null. 2030 // root points at 'linear', now we want the next term after root 2031 // to be the term after cyclemethod, which may be another series 2032 // of paint or another layer. 2033 // 2034 root.nextInSeries = term.nextInSeries; 2035 root.nextLayer = term.nextLayer; 2036 } 2037 2038 ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length]; 2039 int index = 0; 2040 values[index++] = focusAngle; 2041 values[index++] = focusDistance; 2042 values[index++] = (centerPoint != null) ? centerPoint[0] : null; 2043 values[index++] = (centerPoint != null) ? centerPoint[1] : null; 2044 values[index++] = radius; 2045 values[index++] = cycleMethod; 2046 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 2047 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance()); 2048 } 2049 2050 // Based off http://dev.w3.org/csswg/css3-images/#radial-gradients 2051 // 2052 // <radial-gradient> = radial-gradient( 2053 // [ focus-angle <angle>, ]? 2054 // [ focus-distance <percentage>, ]? 2055 // [ center <point>, ]? 2056 // radius <length>, 2057 // [ [ repeat | reflect ] ,]? 2058 // <color-stop>[, <color-stop>]+ ) 2059 // 2060 // Stops are per http://dev.w3.org/csswg/css3-images/#color-stop-syntax. 2061 private ParsedValueImpl parseRadialGradient(final Term root) throws ParseException { 2062 2063 // first term in the chain is the function name... 2064 final String fn = (root.token != null) ? root.token.getText() : null; 2065 if (!"radial-gradient".regionMatches(true, 0, fn, 0, 15)) { 2066 final String msg = "Expected \'radial-gradient\'"; 2067 error(root, msg); 2068 } 2069 2070 Term arg; 2071 2072 if ((arg = root.firstArg) == null || 2073 arg.token == null || 2074 arg.token.getText().isEmpty()) { 2075 error(root, 2076 "Expected \'focus-angle <angle>\' " + 2077 "or \'focus-distance <percentage>\' " + 2078 "or \'center <point>\' " + 2079 "or \'radius [<length> | <percentage>]\'"); 2080 } 2081 2082 Term prev = arg; 2083 ParsedValueImpl<?,Size>focusAngle = null; 2084 ParsedValueImpl<?,Size>focusDistance = null; 2085 ParsedValueImpl<?,Size>[] centerPoint = null; 2086 ParsedValueImpl<?,Size>radius = null; 2087 2088 if ("focus-angle".equalsIgnoreCase(arg.token.getText())) { 2089 2090 prev = arg; 2091 if ((arg = arg.nextInSeries) == null || 2092 !isSize(arg.token)) error(prev, "Expected \'<angle>\'"); 2093 2094 Size angle = size(arg.token); 2095 switch(angle.getUnits()) { 2096 case DEG: 2097 case RAD: 2098 case GRAD: 2099 case TURN: 2100 case PX: 2101 break; 2102 default: 2103 error(arg, "Expected [deg | rad | grad | turn ]"); 2104 } 2105 focusAngle = new ParsedValueImpl<Size,Size>(angle, null); 2106 2107 prev = arg; 2108 if ((arg = arg.nextArg) == null) 2109 error(prev, "Expected \'focus-distance <percentage>\' " + 2110 "or \'center <point>\' " + 2111 "or \'radius [<length> | <percentage>]\'"); 2112 2113 } 2114 2115 if ("focus-distance".equalsIgnoreCase(arg.token.getText())) { 2116 2117 prev = arg; 2118 if ((arg = arg.nextInSeries) == null || 2119 !isSize(arg.token)) error(prev, "Expected \'<percentage>\'"); 2120 2121 Size distance = size(arg.token); 2122 2123 // "The focus point is always specified relative to the center 2124 // point by an angle and a distance relative to the radius." 2125 switch(distance.getUnits()) { 2126 case PERCENT: 2127 break; 2128 default: 2129 error(arg, "Expected \'%\'"); 2130 } 2131 focusDistance = new ParsedValueImpl<Size,Size>(distance, null); 2132 2133 prev = arg; 2134 if ((arg = arg.nextArg) == null) 2135 error(prev, "Expected \'center <center>\' " + 2136 "or \'radius <length>\'"); 2137 2138 } 2139 2140 if ("center".equalsIgnoreCase(arg.token.getText())) { 2141 2142 prev = arg; 2143 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 2144 2145 ParsedValueImpl<?,Size> ptX = parseSize(arg); 2146 2147 prev = arg; 2148 if ((arg = arg.nextInSeries) == null) error(prev, "Expected \'<point>\'"); 2149 2150 ParsedValueImpl<?,Size> ptY = parseSize(arg); 2151 2152 centerPoint = new ParsedValueImpl[] { ptX, ptY }; 2153 2154 prev = arg; 2155 if ((arg = arg.nextArg) == null) 2156 error(prev, "Expected \'radius [<length> | <percentage>]\'"); 2157 } 2158 2159 if ("radius".equalsIgnoreCase(arg.token.getText())) { 2160 2161 prev = arg; 2162 if ((arg = arg.nextInSeries) == null || 2163 !isSize(arg.token)) error(prev, "Expected \'[<length> | <percentage>]\'"); 2164 2165 radius = parseSize(arg); 2166 2167 prev = arg; 2168 if ((arg = arg.nextArg) == null) 2169 error(prev, "Expected \'radius [<length> | <percentage>]\'"); 2170 } 2171 2172 CycleMethod cycleMethod = CycleMethod.NO_CYCLE; 2173 if ("reflect".equalsIgnoreCase(arg.token.getText())) { 2174 cycleMethod = CycleMethod.REFLECT; 2175 prev = arg; 2176 arg = arg.nextArg; 2177 } else if ("repeat".equalsIgnoreCase(arg.token.getText())) { 2178 cycleMethod = CycleMethod.REFLECT; 2179 prev = arg; 2180 arg = arg.nextArg; 2181 } 2182 2183 if (arg == null || 2184 arg.token == null || 2185 arg.token.getText().isEmpty()) { 2186 error(prev, "Expected \'<color-stop>\'"); 2187 } 2188 2189 ParsedValueImpl<ParsedValue[],Stop>[] stops = parseColorStops(arg); 2190 2191 ParsedValueImpl[] values = new ParsedValueImpl[6 + stops.length]; 2192 int index = 0; 2193 values[index++] = focusAngle; 2194 values[index++] = focusDistance; 2195 values[index++] = (centerPoint != null) ? centerPoint[0] : null; 2196 values[index++] = (centerPoint != null) ? centerPoint[1] : null; 2197 values[index++] = radius; 2198 values[index++] = new ParsedValueImpl<String,CycleMethod>(cycleMethod.name(), new EnumConverter<CycleMethod>(CycleMethod.class)); 2199 for (int n=0; n<stops.length; n++) values[index++] = stops[n]; 2200 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RadialGradientConverter.getInstance()); 2201 2202 } 2203 2204 // Based off ImagePattern constructor 2205 // 2206 // image-pattern(<uri-string>[,<size>,<size>,<size>,<size>[,<boolean>]?]?) 2207 // 2208 private ParsedValueImpl<ParsedValue[], Paint> parseImagePattern(final Term root) throws ParseException { 2209 2210 // first term in the chain is the function name... 2211 final String fn = (root.token != null) ? root.token.getText() : null; 2212 if (!"image-pattern".regionMatches(true, 0, fn, 0, 13)) { 2213 final String msg = "Expected \'image-pattern\'"; 2214 error(root, msg); 2215 } 2216 2217 Term arg; 2218 if ((arg = root.firstArg) == null || 2219 arg.token == null || 2220 arg.token.getText().isEmpty()) { 2221 error(root, 2222 "Expected \'<uri-string>\'"); 2223 } 2224 2225 Term prev = arg; 2226 2227 final String uri = arg.token.getText(); 2228 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 2229 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()), 2230 null // placeholder for Stylesheet URL 2231 }; 2232 ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance()); 2233 2234 // If nextArg is null, then there are no remaining arguments, so we are done. 2235 if (arg.nextArg == null) { 2236 ParsedValueImpl[] values = new ParsedValueImpl[1]; 2237 values[0] = parsedURI; 2238 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2239 } 2240 2241 // There must now be 4 sizes in a row. 2242 Token token; 2243 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2244 ParsedValueImpl<?, Size> x = parseSize(arg); 2245 2246 prev = arg; 2247 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2248 ParsedValueImpl<?, Size> y = parseSize(arg); 2249 2250 prev = arg; 2251 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2252 ParsedValueImpl<?, Size> w = parseSize(arg); 2253 2254 prev = arg; 2255 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<size>\'"); 2256 ParsedValueImpl<?, Size> h = parseSize(arg); 2257 2258 // If there are no more args, then we are done. 2259 if (arg.nextArg == null) { 2260 ParsedValueImpl[] values = new ParsedValueImpl[5]; 2261 values[0] = parsedURI; 2262 values[1] = x; 2263 values[2] = y; 2264 values[3] = w; 2265 values[4] = h; 2266 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2267 } 2268 2269 prev = arg; 2270 if ((arg = arg.nextArg) == null) error(prev, "Expected \'<boolean>\'"); 2271 if ((token = arg.token) == null || token.getText() == null) error(arg, "Expected \'<boolean>\'"); 2272 2273 ParsedValueImpl[] values = new ParsedValueImpl[6]; 2274 values[0] = parsedURI; 2275 values[1] = x; 2276 values[2] = y; 2277 values[3] = w; 2278 values[4] = h; 2279 values[5] = new ParsedValueImpl<Boolean, Boolean>(Boolean.parseBoolean(token.getText()), null); 2280 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.ImagePatternConverter.getInstance()); 2281 } 2282 2283 // For tiling ImagePatterns easily. 2284 // 2285 // repeating-image-pattern(<uri-string>) 2286 // 2287 private ParsedValueImpl<ParsedValue[], Paint> parseRepeatingImagePattern(final Term root) throws ParseException { 2288 // first term in the chain is the function name... 2289 final String fn = (root.token != null) ? root.token.getText() : null; 2290 if (!"repeating-image-pattern".regionMatches(true, 0, fn, 0, 23)) { 2291 final String msg = "Expected \'repeating-image-pattern\'"; 2292 error(root, msg); 2293 } 2294 2295 Term arg; 2296 if ((arg = root.firstArg) == null || 2297 arg.token == null || 2298 arg.token.getText().isEmpty()) { 2299 error(root, 2300 "Expected \'<uri-string>\'"); 2301 } 2302 2303 final String uri = arg.token.getText(); 2304 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 2305 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()), 2306 null // placeholder for Stylesheet URL 2307 }; 2308 ParsedValueImpl parsedURI = new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance()); 2309 ParsedValueImpl[] values = new ParsedValueImpl[1]; 2310 values[0] = parsedURI; 2311 return new ParsedValueImpl<ParsedValue[], Paint>(values, PaintConverter.RepeatingImagePatternConverter.getInstance()); 2312 } 2313 2314 // parse a series of paint values separated by commas. 2315 // i.e., <paint> [, <paint>]* 2316 private ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]> parsePaintLayers(Term root) 2317 throws ParseException { 2318 2319 // how many paints in the series? 2320 int nPaints = numberOfLayers(root); 2321 2322 ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[nPaints]; 2323 2324 Term temp = root; 2325 int paint = 0; 2326 2327 do { 2328 if (temp.token == null || 2329 temp.token.getText() == null || 2330 temp.token.getText().isEmpty()) error(temp, "Expected \'<paint>\'"); 2331 2332 paints[paint++] = (ParsedValueImpl<?,Paint>)parse(temp); 2333 2334 temp = nextLayer(temp); 2335 } while (temp != null); 2336 2337 return new ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>(paints, PaintConverter.SequenceConverter.getInstance()); 2338 2339 } 2340 2341 // An size or a series of four size values 2342 // <size> | <size> <size> <size> <size> 2343 private ParsedValueImpl<?,Size>[] parseSize1to4(final Term root) 2344 throws ParseException { 2345 2346 Term temp = root; 2347 ParsedValueImpl<?,Size>[] sides = new ParsedValueImpl[4]; 2348 int side = 0; 2349 2350 while (side < 4 && temp != null) { 2351 sides[side++] = parseSize(temp); 2352 temp = temp.nextInSeries; 2353 } 2354 2355 if (side < 2) sides[1] = sides[0]; // right = top 2356 if (side < 3) sides[2] = sides[0]; // bottom = top 2357 if (side < 4) sides[3] = sides[1]; // left = right 2358 2359 return sides; 2360 } 2361 2362 // A series of inset or sets of four inset values 2363 // <size> | <size> <size> <size> <size> [ , [ <size> | <size> <size> <size> <size>] ]* 2364 private ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]> parseInsetsLayers(Term root) 2365 throws ParseException { 2366 2367 int nLayers = numberOfLayers(root); 2368 2369 Term temp = root; 2370 int layer = 0; 2371 ParsedValueImpl<ParsedValue[],Insets>[] layers = new ParsedValueImpl[nLayers]; 2372 2373 while(temp != null) { 2374 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2375 layers[layer++] = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 2376 while(temp.nextInSeries != null) { 2377 temp = temp.nextInSeries; 2378 } 2379 temp = nextLayer(temp); 2380 } 2381 2382 return new ParsedValueImpl<ParsedValue<ParsedValue[],Insets>[], Insets[]>(layers, InsetsConverter.SequenceConverter.getInstance()); 2383 } 2384 2385 // A single inset (1, 2, 3, or 4 digits) 2386 // <size> | <size> <size> <size> <size> 2387 private ParsedValueImpl<ParsedValue[],Insets> parseInsetsLayer(Term root) 2388 throws ParseException { 2389 2390 Term temp = root; 2391 ParsedValueImpl<ParsedValue[],Insets> layer = null; 2392 2393 while(temp != null) { 2394 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2395 layer = new ParsedValueImpl<ParsedValue[],Insets>(sides, InsetsConverter.getInstance()); 2396 while(temp.nextInSeries != null) { 2397 temp = temp.nextInSeries; 2398 } 2399 temp = nextLayer(temp); 2400 } 2401 return layer; 2402 } 2403 2404 // <size> | <size> <size> <size> <size> 2405 private ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]> parseMarginsLayers(Term root) 2406 throws ParseException { 2407 2408 int nLayers = numberOfLayers(root); 2409 2410 Term temp = root; 2411 int layer = 0; 2412 ParsedValueImpl<ParsedValue[],Margins>[] layers = new ParsedValueImpl[nLayers]; 2413 2414 while(temp != null) { 2415 ParsedValueImpl<?,Size>[] sides = parseSize1to4(temp); 2416 layers[layer++] = new ParsedValueImpl<ParsedValue[],Margins>(sides, Margins.Converter.getInstance()); 2417 while(temp.nextInSeries != null) { 2418 temp = temp.nextInSeries; 2419 } 2420 temp = nextLayer(temp); 2421 } 2422 2423 return new ParsedValueImpl<ParsedValue<ParsedValue[],Margins>[], Margins[]>(layers, Margins.SequenceConverter.getInstance()); 2424 } 2425 2426 // <size> | <size> <size> <size> <size> 2427 private ParsedValueImpl<Size, Size>[] parseSizeSeries(Term root) 2428 throws ParseException { 2429 2430 if (root.token == null) error(root, "Parse error"); 2431 2432 List<ParsedValueImpl<Size,Size>> sizes = new ArrayList<>(); 2433 2434 Term term = root; 2435 while(term != null) { 2436 Token token = term.token; 2437 final int ttype = token.getType(); 2438 switch (ttype) { 2439 case CssLexer.NUMBER: 2440 case CssLexer.PERCENTAGE: 2441 case CssLexer.EMS: 2442 case CssLexer.EXS: 2443 case CssLexer.PX: 2444 case CssLexer.CM: 2445 case CssLexer.MM: 2446 case CssLexer.IN: 2447 case CssLexer.PT: 2448 case CssLexer.PC: 2449 case CssLexer.DEG: 2450 case CssLexer.GRAD: 2451 case CssLexer.RAD: 2452 case CssLexer.TURN: 2453 ParsedValueImpl sizeValue = new ParsedValueImpl<Size, Size>(size(token), null); 2454 sizes.add(sizeValue); 2455 break; 2456 default: 2457 error (root, "expected series of <size>"); 2458 } 2459 term = term.nextInSeries; 2460 } 2461 return sizes.toArray(new ParsedValueImpl[sizes.size()]); 2462 2463 } 2464 2465 // http://www.w3.org/TR/css3-background/#the-border-radius 2466 // <size>{1,4} [ '/' <size>{1,4}]? [',' <size>{1,4} [ '/' <size>{1,4}]?]? 2467 private ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]> parseCornerRadius(Term root) 2468 throws ParseException { 2469 2470 2471 int nLayers = numberOfLayers(root); 2472 2473 Term term = root; 2474 int layer = 0; 2475 ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>[] layers = new ParsedValueImpl[nLayers]; 2476 2477 while(term != null) { 2478 2479 int nHorizontalTerms = 0; 2480 Term temp = term; 2481 while (temp != null) { 2482 if (temp.token.getType() == CssLexer.SOLIDUS) { 2483 temp = temp.nextInSeries; 2484 break; 2485 } 2486 nHorizontalTerms += 1; 2487 temp = temp.nextInSeries; 2488 }; 2489 2490 int nVerticalTerms = 0; 2491 while (temp != null) { 2492 if (temp.token.getType() == CssLexer.SOLIDUS) { 2493 error(temp, "unexpected SOLIDUS"); 2494 break; 2495 } 2496 nVerticalTerms += 1; 2497 temp = temp.nextInSeries; 2498 } 2499 2500 if ((nHorizontalTerms == 0 || nHorizontalTerms > 4) || nVerticalTerms > 4) { 2501 error(root, "expected [<length>|<percentage>]{1,4} [/ [<length>|<percentage>]{1,4}]?"); 2502 } 2503 2504 // used as index into margins[]. horizontal = 0, vertical = 1 2505 int orientation = 0; 2506 2507 // at most, there should be four radii in the horizontal orientation and four in the vertical. 2508 ParsedValueImpl<?,Size>[][] radii = new ParsedValueImpl[2][4]; 2509 2510 ParsedValueImpl<?,Size> zero = new ParsedValueImpl<Size,Size>(new Size(0,SizeUnits.PX), null); 2511 for (int r=0; r<4; r++) { radii[0][r] = zero; radii[1][r] = zero; } 2512 2513 int hr = 0; 2514 int vr = 0; 2515 2516 Term lastTerm = term; 2517 while ((hr <= 4) && (vr <= 4) && (term != null)) { 2518 2519 if (term.token.getType() == CssLexer.SOLIDUS) { 2520 orientation += 1; 2521 } else { 2522 ParsedValueImpl<?,Size> parsedValue = parseSize(term); 2523 if (orientation == 0) { 2524 radii[orientation][hr++] = parsedValue; 2525 } else { 2526 radii[orientation][vr++] = parsedValue; 2527 } 2528 } 2529 lastTerm = term; 2530 term = term.nextInSeries; 2531 } 2532 2533 // 2534 // http://www.w3.org/TR/css3-background/#the-border-radius 2535 // The four values for each radii are given in the order top-left, top-right, bottom-right, bottom-left. 2536 // If bottom-left is omitted it is the same as top-right. 2537 // If bottom-right is omitted it is the same as top-left. 2538 // If top-right is omitted it is the same as top-left. 2539 // 2540 // If there is no vertical component, then set both equally. 2541 // If either is zero, then both are zero. 2542 // 2543 2544 // if hr == 0, then there were no horizontal radii (which would be an error caught above) 2545 if (hr != 0) { 2546 if (hr < 2) radii[0][1] = radii[0][0]; // top-right = top-left 2547 if (hr < 3) radii[0][2] = radii[0][0]; // bottom-right = top-left 2548 if (hr < 4) radii[0][3] = radii[0][1]; // bottom-left = top-right 2549 } else { 2550 assert(false); 2551 } 2552 2553 // if vr == 0, then there were no vertical radii 2554 if (vr != 0) { 2555 if (vr < 2) radii[1][1] = radii[1][0]; // top-right = top-left 2556 if (vr < 3) radii[1][2] = radii[1][0]; // bottom-right = top-left 2557 if (vr < 4) radii[1][3] = radii[1][1]; // bottom-left = top-right 2558 } else { 2559 // if no vertical, the vertical value is same as horizontal 2560 radii[1][0] = radii[0][0]; 2561 radii[1][1] = radii[0][1]; 2562 radii[1][2] = radii[0][2]; 2563 radii[1][3] = radii[0][3]; 2564 } 2565 2566 // if either is zero, both are zero 2567 if (zero.equals(radii[0][0]) || zero.equals(radii[1][0])) { radii[1][0] = radii[0][0] = zero; } 2568 if (zero.equals(radii[0][1]) || zero.equals(radii[1][1])) { radii[1][1] = radii[0][1] = zero; } 2569 if (zero.equals(radii[0][2]) || zero.equals(radii[1][2])) { radii[1][2] = radii[0][2] = zero; } 2570 if (zero.equals(radii[0][3]) || zero.equals(radii[1][3])) { radii[1][3] = radii[0][3] = zero; } 2571 2572 layers[layer++] = new ParsedValueImpl<ParsedValue<?,Size>[][],CornerRadii>(radii, null); 2573 2574 term = nextLayer(lastTerm); 2575 } 2576 return new ParsedValueImpl<ParsedValue<ParsedValue<?,Size>[][],CornerRadii>[], CornerRadii[]>(layers, CornerRadiiConverter.getInstance()); 2577 } 2578 2579 /* Constant for background position */ 2580 private final static ParsedValueImpl<Size,Size> ZERO_PERCENT = 2581 new ParsedValueImpl<Size,Size>(new Size(0f, SizeUnits.PERCENT), null); 2582 /* Constant for background position */ 2583 private final static ParsedValueImpl<Size,Size> FIFTY_PERCENT = 2584 new ParsedValueImpl<Size,Size>(new Size(50f, SizeUnits.PERCENT), null); 2585 /* Constant for background position */ 2586 private final static ParsedValueImpl<Size,Size> ONE_HUNDRED_PERCENT = 2587 new ParsedValueImpl<Size,Size>(new Size(100f, SizeUnits.PERCENT), null); 2588 2589 private static boolean isPositionKeyWord(String value) { 2590 return "center".equalsIgnoreCase(value) || "top".equalsIgnoreCase(value) || "bottom".equalsIgnoreCase(value) || "left".equalsIgnoreCase(value) || "right".equalsIgnoreCase(value); 2591 } 2592 2593 /* 2594 * http://www.w3.org/TR/css3-background/#the-background-position 2595 * 2596 * <bg-position> = [ 2597 * [ top | bottom ] 2598 * | 2599 * [ <percentage> | <length> | left | center | right ] 2600 * [ <percentage> | <length> | top | center | bottom ]? 2601 * | 2602 * [ center | [ left | right ] [ <percentage> | <length> ]? ] && 2603 * [ center | [ top | bottom ] [ <percentage> | <length> ]? ] 2604 * ] 2605 * 2606 * From the W3 spec: 2607 * 2608 * returned ParsedValueImpl is [size, size, size, size] with the semantics 2609 * [top offset, right offset, bottom offset left offset] 2610 */ 2611 private ParsedValueImpl<ParsedValue[], BackgroundPosition> parseBackgroundPosition(Term term) 2612 throws ParseException { 2613 2614 if (term.token == null || 2615 term.token.getText() == null || 2616 term.token.getText().isEmpty()) error(term, "Expected \'<bg-position>\'"); 2617 2618 Term termOne = term; 2619 Token valueOne = term.token; 2620 2621 Term termTwo = termOne.nextInSeries; 2622 Token valueTwo = (termTwo != null) ? termTwo.token : null; 2623 2624 Term termThree = (termTwo != null) ? termTwo.nextInSeries : null; 2625 Token valueThree = (termThree != null) ? termThree.token : null; 2626 2627 Term termFour = (termThree != null) ? termThree.nextInSeries : null; 2628 Token valueFour = (termFour != null) ? termFour.token : null; 2629 2630 // are the horizontal and vertical exchanged 2631 if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { 2632 // 2 values filled 2633 String v1 = valueOne.getText(); 2634 String v2 = valueTwo.getText(); 2635 if( ("top".equals(v1) || "bottom".equals(v1)) 2636 && ("left".equals(v2) || "right".equals(v2) || "center".equals(v2)) ) { 2637 { 2638 Token tmp = valueTwo; 2639 valueTwo = valueOne; 2640 valueOne = tmp; 2641 } 2642 2643 { 2644 Term tmp = termTwo; 2645 termTwo = termOne; 2646 termOne = tmp; 2647 } 2648 } 2649 } else if( valueOne != null && valueTwo != null && valueThree != null ) { 2650 Term[] termArray = null; 2651 Token[] tokeArray = null; 2652 // 4 values filled 2653 if( valueFour != null ) { 2654 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) 2655 && ("left".equals(valueThree.getText()) || "right".equals(valueThree.getText())) ) { 2656 // e.g. top 50 left 20 2657 termArray = new Term[] { termThree, termFour, termOne, termTwo }; 2658 tokeArray = new Token[] { valueThree, valueFour, valueOne, valueTwo }; 2659 } 2660 } else { 2661 if( ("top".equals(valueOne.getText()) || "bottom".equals(valueOne.getText())) ) { 2662 if( ("left".equals(valueTwo.getText()) || "right".equals(valueTwo.getText())) ) { 2663 // e.g. top left 50 2664 termArray = new Term[] { termTwo, termThree, termOne, null }; 2665 tokeArray = new Token[] { valueTwo, valueThree, valueOne, null }; 2666 } else { 2667 // e.g. top 50 left 2668 termArray = new Term[] { termThree, termOne, termTwo, null }; 2669 tokeArray = new Token[] { valueThree, valueOne, valueTwo, null }; 2670 } 2671 } 2672 } 2673 2674 if( termArray != null ) { 2675 termOne = termArray[0]; 2676 termTwo = termArray[1]; 2677 termThree = termArray[2]; 2678 termFour = termArray[3]; 2679 2680 valueOne = tokeArray[0]; 2681 valueTwo = tokeArray[1]; 2682 valueThree = tokeArray[2]; 2683 valueFour = tokeArray[3]; 2684 } 2685 } 2686 2687 2688 ParsedValueImpl<?,Size> top, right, bottom, left; 2689 top = right = bottom = left = ZERO_PERCENT; 2690 { 2691 if(valueOne == null && valueTwo == null && valueThree == null && valueFour == null) { 2692 error(term, "No value found for background-position"); 2693 } else if( valueOne != null && valueTwo == null && valueThree == null && valueFour == null ) { 2694 // Only one value 2695 String v1 = valueOne.getText(); 2696 2697 if( "center".equals(v1) ) { 2698 left = FIFTY_PERCENT; 2699 right = ZERO_PERCENT; 2700 2701 top = FIFTY_PERCENT; 2702 bottom = ZERO_PERCENT; 2703 2704 } else if("left".equals(v1)) { 2705 left = ZERO_PERCENT; 2706 right = ZERO_PERCENT; 2707 2708 top = FIFTY_PERCENT; 2709 bottom = ZERO_PERCENT; 2710 2711 } else if( "right".equals(v1) ) { 2712 left = ONE_HUNDRED_PERCENT; 2713 right = ZERO_PERCENT; 2714 2715 top = FIFTY_PERCENT; 2716 bottom = ZERO_PERCENT; 2717 2718 } else if( "top".equals(v1) ) { 2719 left = FIFTY_PERCENT; 2720 right = ZERO_PERCENT; 2721 2722 top = ZERO_PERCENT; 2723 bottom = ZERO_PERCENT; 2724 2725 } else if( "bottom".equals(v1) ) { 2726 left = FIFTY_PERCENT; 2727 right = ZERO_PERCENT; 2728 2729 top = ONE_HUNDRED_PERCENT; 2730 bottom = ZERO_PERCENT; 2731 } else { 2732 left = parseSize(termOne); 2733 right = ZERO_PERCENT; 2734 top = FIFTY_PERCENT; 2735 bottom = ZERO_PERCENT; 2736 } 2737 } else if( valueOne != null && valueTwo != null && valueThree == null && valueFour == null ) { 2738 // 2 values 2739 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2740 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2741 2742 if( ! isPositionKeyWord(v1) ) { 2743 left = parseSize(termOne); 2744 right = ZERO_PERCENT; 2745 2746 if( "top".equals(v2) ) { 2747 top = ZERO_PERCENT; 2748 bottom = ZERO_PERCENT; 2749 } else if( "bottom".equals(v2) ) { 2750 top = ONE_HUNDRED_PERCENT; 2751 bottom = ZERO_PERCENT; 2752 } else if( "center".equals(v2) ) { 2753 top = FIFTY_PERCENT; 2754 bottom = ZERO_PERCENT; 2755 } else if( !isPositionKeyWord(v2) ) { 2756 top = parseSize(termTwo); 2757 bottom = ZERO_PERCENT; 2758 } else { 2759 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2760 } 2761 } else if( v1.equals("left") || v1.equals("right") ) { 2762 left = v1.equals("right") ? ONE_HUNDRED_PERCENT : ZERO_PERCENT; 2763 right = ZERO_PERCENT; 2764 2765 if( ! isPositionKeyWord(v2) ) { 2766 top = parseSize(termTwo); 2767 bottom = ZERO_PERCENT; 2768 } else if( v2.equals("top") || v2.equals("bottom") || v2.equals("center") ) { 2769 if( v2.equals("top") ) { 2770 top = ZERO_PERCENT; 2771 bottom = ZERO_PERCENT; 2772 } else if(v2.equals("center")) { 2773 top = FIFTY_PERCENT; 2774 bottom = ZERO_PERCENT; 2775 } else { 2776 top = ONE_HUNDRED_PERCENT; 2777 bottom = ZERO_PERCENT; 2778 } 2779 } else { 2780 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2781 } 2782 } else if( v1.equals("center") ) { 2783 left = FIFTY_PERCENT; 2784 right = ZERO_PERCENT; 2785 2786 if( v2.equals("top") ) { 2787 top = ZERO_PERCENT; 2788 bottom = ZERO_PERCENT; 2789 } else if( v2.equals("bottom") ) { 2790 top = ONE_HUNDRED_PERCENT; 2791 bottom = ZERO_PERCENT; 2792 } else if( v2.equals("center") ) { 2793 top = FIFTY_PERCENT; 2794 bottom = ZERO_PERCENT; 2795 } else if( ! isPositionKeyWord(v2) ) { 2796 top = parseSize(termTwo); 2797 bottom = ZERO_PERCENT; 2798 } else { 2799 error(termTwo,"Expected 'top', 'bottom', 'center' or <size>"); 2800 } 2801 } 2802 } else if( valueOne != null && valueTwo != null && valueThree != null && valueFour == null ) { 2803 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2804 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2805 String v3 = valueThree.getText().toLowerCase(Locale.ROOT); 2806 2807 if( ! isPositionKeyWord(v1) || "center".equals(v1) ) { 2808 // 1 is horizontal 2809 // means 2 & 3 are vertical 2810 if( "center".equals(v1) ) { 2811 left = FIFTY_PERCENT; 2812 } else { 2813 left = parseSize(termOne); 2814 } 2815 right = ZERO_PERCENT; 2816 2817 if( !isPositionKeyWord(v3) ) { 2818 if( "top".equals(v2) ) { 2819 top = parseSize(termThree); 2820 bottom = ZERO_PERCENT; 2821 } else if( "bottom".equals(v2) ) { 2822 top = ZERO_PERCENT; 2823 bottom = parseSize(termThree); 2824 } else { 2825 error(termTwo,"Expected 'top' or 'bottom'"); 2826 } 2827 } else { 2828 error(termThree,"Expected <size>"); 2829 } 2830 } else if( "left".equals(v1) || "right".equals(v1) ) { 2831 if( ! isPositionKeyWord(v2) ) { 2832 // 1 & 2 are horizontal 2833 // 3 is vertical 2834 if( "left".equals(v1) ) { 2835 left = parseSize(termTwo); 2836 right = ZERO_PERCENT; 2837 } else { 2838 left = ZERO_PERCENT; 2839 right = parseSize(termTwo); 2840 } 2841 2842 if( "top".equals(v3) ) { 2843 top = ZERO_PERCENT; 2844 bottom = ZERO_PERCENT; 2845 } else if( "bottom".equals(v3) ) { 2846 top = ONE_HUNDRED_PERCENT; 2847 bottom = ZERO_PERCENT; 2848 } else if( "center".equals(v3) ) { 2849 top = FIFTY_PERCENT; 2850 bottom = ZERO_PERCENT; 2851 } else { 2852 error(termThree,"Expected 'top', 'bottom' or 'center'"); 2853 } 2854 } else { 2855 // 1 is horizontal 2856 // 2 & 3 are vertical 2857 if( "left".equals(v1) ) { 2858 left = ZERO_PERCENT; 2859 right = ZERO_PERCENT; 2860 } else { 2861 left = ONE_HUNDRED_PERCENT; 2862 right = ZERO_PERCENT; 2863 } 2864 2865 if( ! isPositionKeyWord(v3) ) { 2866 if( "top".equals(v2) ) { 2867 top = parseSize(termThree); 2868 bottom = ZERO_PERCENT; 2869 } else if( "bottom".equals(v2) ) { 2870 top = ZERO_PERCENT; 2871 bottom = parseSize(termThree); 2872 } else { 2873 error(termTwo,"Expected 'top' or 'bottom'"); 2874 } 2875 } else { 2876 error(termThree,"Expected <size>"); 2877 } 2878 } 2879 } 2880 } else { 2881 String v1 = valueOne.getText().toLowerCase(Locale.ROOT); 2882 String v2 = valueTwo.getText().toLowerCase(Locale.ROOT); 2883 String v3 = valueThree.getText().toLowerCase(Locale.ROOT); 2884 String v4 = valueFour.getText().toLowerCase(Locale.ROOT); 2885 2886 if( (v1.equals("left") || v1.equals("right")) && (v3.equals("top") || v3.equals("bottom") ) && ! isPositionKeyWord(v2) && ! isPositionKeyWord(v4) ) { 2887 if( v1.equals("left") ) { 2888 left = parseSize(termTwo); 2889 right = ZERO_PERCENT; 2890 } else { 2891 left = ZERO_PERCENT; 2892 right = parseSize(termTwo); 2893 } 2894 2895 if( v3.equals("top") ) { 2896 top = parseSize(termFour); 2897 bottom = ZERO_PERCENT; 2898 } else { 2899 top = ZERO_PERCENT; 2900 bottom = parseSize(termFour); 2901 } 2902 2903 } else { 2904 error(term,"Expected 'left' or 'right' followed by <size> followed by 'top' or 'bottom' followed by <size>"); 2905 } 2906 } 2907 } 2908 2909 ParsedValueImpl<?,Size>[] values = new ParsedValueImpl[] {top, right, bottom, left}; 2910 return new ParsedValueImpl<ParsedValue[], BackgroundPosition>(values, BackgroundPositionConverter.getInstance()); 2911 } 2912 2913 private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]> 2914 parseBackgroundPositionLayers(final Term root) throws ParseException { 2915 2916 int nLayers = numberOfLayers(root); 2917 ParsedValueImpl<ParsedValue[], BackgroundPosition>[] layers = new ParsedValueImpl[nLayers]; 2918 int layer = 0; 2919 Term term = root; 2920 while (term != null) { 2921 layers[layer++] = parseBackgroundPosition(term); 2922 term = nextLayer(term); 2923 } 2924 return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundPosition>[], BackgroundPosition[]>(layers, LayeredBackgroundPositionConverter.getInstance()); 2925 } 2926 2927 /* 2928 http://www.w3.org/TR/css3-background/#the-background-repeat 2929 <repeat-style> = repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2} 2930 */ 2931 private ParsedValueImpl<String, BackgroundRepeat>[] parseRepeatStyle(final Term root) 2932 throws ParseException { 2933 2934 BackgroundRepeat xAxis, yAxis; 2935 xAxis = yAxis = BackgroundRepeat.NO_REPEAT; 2936 2937 Term term = root; 2938 2939 if (term.token == null || 2940 term.token.getType() != CssLexer.IDENT || 2941 term.token.getText() == null || 2942 term.token.getText().isEmpty()) error(term, "Expected \'<repeat-style>\'"); 2943 2944 String text = term.token.getText().toLowerCase(Locale.ROOT); 2945 if ("repeat-x".equals(text)) { 2946 xAxis = BackgroundRepeat.REPEAT; 2947 yAxis = BackgroundRepeat.NO_REPEAT; 2948 } else if ("repeat-y".equals(text)) { 2949 xAxis = BackgroundRepeat.NO_REPEAT; 2950 yAxis = BackgroundRepeat.REPEAT; 2951 } else if ("repeat".equals(text)) { 2952 xAxis = BackgroundRepeat.REPEAT; 2953 yAxis = BackgroundRepeat.REPEAT; 2954 } else if ("space".equals(text)) { 2955 xAxis = BackgroundRepeat.SPACE; 2956 yAxis = BackgroundRepeat.SPACE; 2957 } else if ("round".equals(text)) { 2958 xAxis = BackgroundRepeat.ROUND; 2959 yAxis = BackgroundRepeat.ROUND; 2960 } else if ("no-repeat".equals(text)) { 2961 xAxis = BackgroundRepeat.NO_REPEAT; 2962 yAxis = BackgroundRepeat.NO_REPEAT; 2963 } else if ("stretch".equals(text)) { 2964 xAxis = BackgroundRepeat.NO_REPEAT; 2965 yAxis = BackgroundRepeat.NO_REPEAT; 2966 } else { 2967 error(term, "Expected \'<repeat-style>\' " + text); 2968 } 2969 2970 if ((term = term.nextInSeries) != null && 2971 term.token != null && 2972 term.token.getType() == CssLexer.IDENT && 2973 term.token.getText() != null && 2974 !term.token.getText().isEmpty()) { 2975 2976 text = term.token.getText().toLowerCase(Locale.ROOT); 2977 if ("repeat-x".equals(text)) { 2978 error(term, "Unexpected \'repeat-x\'"); 2979 } else if ("repeat-y".equals(text)) { 2980 error(term, "Unexpected \'repeat-y\'"); 2981 } else if ("repeat".equals(text)) { 2982 yAxis = BackgroundRepeat.REPEAT; 2983 } else if ("space".equals(text)) { 2984 yAxis = BackgroundRepeat.SPACE; 2985 } else if ("round".equals(text)) { 2986 yAxis = BackgroundRepeat.ROUND; 2987 } else if ("no-repeat".equals(text)) { 2988 yAxis = BackgroundRepeat.NO_REPEAT; 2989 } else if ("stretch".equals(text)) { 2990 yAxis = BackgroundRepeat.NO_REPEAT; 2991 } else { 2992 error(term, "Expected \'<repeat-style>\'"); 2993 } 2994 } 2995 2996 return new ParsedValueImpl[] { 2997 new ParsedValueImpl<String,BackgroundRepeat>(xAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class)), 2998 new ParsedValueImpl<String,BackgroundRepeat>(yAxis.name(), new EnumConverter<BackgroundRepeat>(BackgroundRepeat.class)) 2999 }; 3000 } 3001 3002 private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]> 3003 parseBorderImageRepeatStyleLayers(final Term root) throws ParseException { 3004 3005 int nLayers = numberOfLayers(root); 3006 ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][]; 3007 int layer = 0; 3008 Term term = root; 3009 while (term != null) { 3010 layers[layer++] = parseRepeatStyle(term); 3011 term = nextLayer(term); 3012 } 3013 return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][],RepeatStruct[]>(layers, RepeatStructConverter.getInstance()); 3014 } 3015 3016 3017 private ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]> 3018 parseBackgroundRepeatStyleLayers(final Term root) throws ParseException { 3019 3020 int nLayers = numberOfLayers(root); 3021 ParsedValueImpl<String, BackgroundRepeat>[][] layers = new ParsedValueImpl[nLayers][]; 3022 int layer = 0; 3023 Term term = root; 3024 while (term != null) { 3025 layers[layer++] = parseRepeatStyle(term); 3026 term = nextLayer(term); 3027 } 3028 return new ParsedValueImpl<ParsedValue<String, BackgroundRepeat>[][], RepeatStruct[]>(layers, RepeatStructConverter.getInstance()); 3029 } 3030 3031 /* 3032 http://www.w3.org/TR/css3-background/#the-background-size 3033 <bg-size> = [ <length> | <percentage> | auto ]{1,2} | cover | contain 3034 */ 3035 private ParsedValueImpl<ParsedValue[], BackgroundSize> parseBackgroundSize(final Term root) 3036 throws ParseException { 3037 3038 ParsedValueImpl<?,Size> height = null, width = null; 3039 boolean cover = false, contain = false; 3040 3041 Term term = root; 3042 if (term.token == null) error(term, "Expected \'<bg-size>\'"); 3043 3044 if (term.token.getType() == CssLexer.IDENT) { 3045 final String text = 3046 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null; 3047 3048 if ("auto".equals(text)) { 3049 // We don't do anything because width / height are already initialized 3050 } else if ("cover".equals(text)) { 3051 cover = true; 3052 } else if ("contain".equals(text)) { 3053 contain = true; 3054 } else if ("stretch".equals(text)) { 3055 width = ONE_HUNDRED_PERCENT; 3056 height = ONE_HUNDRED_PERCENT; 3057 } else { 3058 error(term, "Expected \'auto\', \'cover\', \'contain\', or \'stretch\'"); 3059 } 3060 } else if (isSize(term.token)) { 3061 width = parseSize(term); 3062 height = null; 3063 } else { 3064 error(term, "Expected \'<bg-size>\'"); 3065 } 3066 3067 if ((term = term.nextInSeries) != null) { 3068 if (cover || contain) error(term, "Unexpected \'<bg-size>\'"); 3069 3070 if (term.token.getType() == CssLexer.IDENT) { 3071 final String text = 3072 (term.token.getText() != null) ? term.token.getText().toLowerCase(Locale.ROOT) : null; 3073 3074 if ("auto".equals(text)) { 3075 height = null; 3076 } else if ("cover".equals(text)) { 3077 error(term, "Unexpected \'cover\'"); 3078 } else if ("contain".equals(text)) { 3079 error(term, "Unexpected \'contain\'"); 3080 } else if ("stretch".equals(text)) { 3081 height = ONE_HUNDRED_PERCENT; 3082 } else { 3083 error(term, "Expected \'auto\' or \'stretch\'"); 3084 } 3085 } else if (isSize(term.token)) { 3086 height = parseSize(term); 3087 } else { 3088 error(term, "Expected \'<bg-size>\'"); 3089 } 3090 3091 } 3092 3093 ParsedValueImpl[] values = new ParsedValueImpl[] { 3094 width, 3095 height, 3096 // TODO: handling of booleans is really bogus 3097 new ParsedValueImpl<String,Boolean>((cover ? "true" : "false"), BooleanConverter.getInstance()), 3098 new ParsedValueImpl<String,Boolean>((contain ? "true" : "false"), BooleanConverter.getInstance()) 3099 }; 3100 return new ParsedValueImpl<ParsedValue[], BackgroundSize>(values, BackgroundSizeConverter.getInstance()); 3101 } 3102 3103 private ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[], BackgroundSize[]> 3104 parseBackgroundSizeLayers(final Term root) throws ParseException { 3105 3106 int nLayers = numberOfLayers(root); 3107 ParsedValueImpl<ParsedValue[], BackgroundSize>[] layers = new ParsedValueImpl[nLayers]; 3108 int layer = 0; 3109 Term term = root; 3110 while (term != null) { 3111 layers[layer++] = parseBackgroundSize(term); 3112 term = nextLayer(term); 3113 } 3114 return new ParsedValueImpl<ParsedValue<ParsedValue[], BackgroundSize>[], BackgroundSize[]>(layers, LayeredBackgroundSizeConverter.getInstance()); 3115 } 3116 3117 private ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]> parseBorderPaint(final Term root) 3118 throws ParseException { 3119 3120 Term term = root; 3121 ParsedValueImpl<?,Paint>[] paints = new ParsedValueImpl[4]; 3122 int paint = 0; 3123 3124 while(term != null) { 3125 if (term.token == null || paints.length <= paint) error(term, "Expected \'<paint>\'"); 3126 paints[paint++] = parse(term); 3127 term = term.nextInSeries; 3128 } 3129 3130 if (paint < 2) paints[1] = paints[0]; // right = top 3131 if (paint < 3) paints[2] = paints[0]; // bottom = top 3132 if (paint < 4) paints[3] = paints[1]; // left = right 3133 3134 return new ParsedValueImpl<ParsedValue<?,Paint>[], Paint[]>(paints, StrokeBorderPaintConverter.getInstance()); 3135 } 3136 3137 private ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]> parseBorderPaintLayers(final Term root) 3138 throws ParseException { 3139 3140 int nLayers = numberOfLayers(root); 3141 ParsedValueImpl<ParsedValue<?,Paint>[],Paint[]>[] layers = new ParsedValueImpl[nLayers]; 3142 int layer = 0; 3143 Term term = root; 3144 while (term != null) { 3145 layers[layer++] = parseBorderPaint(term); 3146 term = nextLayer(term); 3147 } 3148 return new ParsedValueImpl<ParsedValue<ParsedValue<?,Paint>[],Paint[]>[], Paint[][]>(layers, LayeredBorderPaintConverter.getInstance()); 3149 } 3150 3151 // borderStyle (borderStyle (borderStyle borderStyle?)?)? 3152 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]> parseBorderStyleSeries(final Term root) 3153 throws ParseException { 3154 3155 Term term = root; 3156 ParsedValueImpl<ParsedValue[],BorderStrokeStyle>[] borders = new ParsedValueImpl[4]; 3157 int border = 0; 3158 while (term != null) { 3159 borders[border++] = parseBorderStyle(term); 3160 term = term.nextInSeries; 3161 } 3162 3163 if (border < 2) borders[1] = borders[0]; // right = top 3164 if (border < 3) borders[2] = borders[0]; // bottom = top 3165 if (border < 4) borders[3] = borders[1]; // left = right 3166 3167 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>(borders, BorderStrokeStyleSequenceConverter.getInstance()); 3168 } 3169 3170 3171 private ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]> 3172 parseBorderStyleLayers(final Term root) throws ParseException { 3173 3174 int nLayers = numberOfLayers(root); 3175 ParsedValueImpl<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[] layers = new ParsedValueImpl[nLayers]; 3176 int layer = 0; 3177 Term term = root; 3178 while (term != null) { 3179 layers[layer++] = parseBorderStyleSeries(term); 3180 term = nextLayer(term); 3181 } 3182 return new ParsedValueImpl<ParsedValue<ParsedValue<ParsedValue[],BorderStrokeStyle>[],BorderStrokeStyle[]>[], BorderStrokeStyle[][]>(layers, LayeredBorderStyleConverter.getInstance()); 3183 } 3184 3185 // Only meant to be used from parseBorderStyle, but might be useful elsewhere 3186 private String getKeyword(final Term term) { 3187 if (term != null && 3188 term.token != null && 3189 term.token.getType() == CssLexer.IDENT && 3190 term.token.getText() != null && 3191 !term.token.getText().isEmpty()) { 3192 3193 return term.token.getText().toLowerCase(Locale.ROOT); 3194 } 3195 return null; 3196 } 3197 3198 //<border-style> [ , <border-style> ]* 3199 // where <border-style> = 3200 // <dash-style> [centered | inside | outside]? [line-join [miter <number> | bevel | round]]? [line-cap [square | butt | round]]? 3201 // where <dash-style> = 3202 // [ none | solid | dotted | dashed ] 3203 private ParsedValueImpl<ParsedValue[],BorderStrokeStyle> parseBorderStyle(final Term root) 3204 throws ParseException { 3205 3206 3207 ParsedValue<ParsedValue[],Number[]> dashStyle = null; 3208 ParsedValue<ParsedValue<?,Size>,Number> dashPhase = null; 3209 ParsedValue<String,StrokeType> strokeType = null; 3210 ParsedValue<String,StrokeLineJoin> strokeLineJoin = null; 3211 ParsedValue<ParsedValue<?,Size>,Number> strokeMiterLimit = null; 3212 ParsedValue<String,StrokeLineCap> strokeLineCap = null; 3213 3214 Term term = root; 3215 3216 dashStyle = dashStyle(term); 3217 3218 Term prev = term; 3219 term = term.nextInSeries; 3220 String keyword = getKeyword(term); 3221 3222 // dash-style might be followed by 'phase <size>' 3223 if ("phase".equals(keyword)) { 3224 3225 prev = term; 3226 if ((term = term.nextInSeries) == null || 3227 term.token == null || 3228 !isSize(term.token)) error(term, "Expected \'<size>\'"); 3229 3230 ParsedValueImpl<?,Size> sizeVal = parseSize(term); 3231 dashPhase = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance()); 3232 3233 prev = term; 3234 term = term.nextInSeries; 3235 } 3236 3237 // stroke type might be next 3238 strokeType = parseStrokeType(term); 3239 if (strokeType != null) { 3240 prev = term; 3241 term = term.nextInSeries; 3242 } 3243 3244 keyword = getKeyword(term); 3245 3246 if ("line-join".equals(keyword)) { 3247 3248 prev = term; 3249 term = term.nextInSeries; 3250 3251 ParsedValueImpl[] lineJoinValues = parseStrokeLineJoin(term); 3252 if (lineJoinValues != null) { 3253 strokeLineJoin = lineJoinValues[0]; 3254 strokeMiterLimit = lineJoinValues[1]; 3255 } else { 3256 error(term, "Expected \'miter <size>?\', \'bevel\' or \'round\'"); 3257 } 3258 prev = term; 3259 term = term.nextInSeries; 3260 keyword = getKeyword(term); 3261 } 3262 3263 if ("line-cap".equals(keyword)) { 3264 3265 prev = term; 3266 term = term.nextInSeries; 3267 3268 strokeLineCap = parseStrokeLineCap(term); 3269 if (strokeLineCap == null) { 3270 error(term, "Expected \'square\', \'butt\' or \'round\'"); 3271 } 3272 3273 prev = term; 3274 term = term.nextInSeries; 3275 } 3276 3277 if (term != null) { 3278 // if term is not null, then we have gotten to the end of 3279 // one border style and term is start of another border style 3280 root.nextInSeries = term; 3281 } else { 3282 // If term is null, then we have gotten to the end of 3283 // a border style with no more to follow in this series. 3284 // But there may be another layer. 3285 root.nextInSeries = null; 3286 root.nextLayer = prev.nextLayer; 3287 } 3288 3289 final ParsedValue[] values = new ParsedValue[]{ 3290 dashStyle, 3291 dashPhase, 3292 strokeType, 3293 strokeLineJoin, 3294 strokeMiterLimit, 3295 strokeLineCap 3296 }; 3297 3298 return new ParsedValueImpl(values, BorderStyleConverter.getInstance()); 3299 } 3300 3301 // 3302 // segments(<size> [, <size>]+) | <border-style> 3303 // 3304 private ParsedValue<ParsedValue[],Number[]> dashStyle(final Term root) throws ParseException { 3305 3306 if (root.token == null) error(root, "Expected \'<dash-style>\'"); 3307 3308 final int ttype = root.token.getType(); 3309 3310 ParsedValue<ParsedValue[],Number[]> segments = null; 3311 if (ttype == CssLexer.IDENT) { 3312 segments = borderStyle(root); 3313 } else if (ttype == CssLexer.FUNCTION) { 3314 segments = segments(root); 3315 } else { 3316 error(root, "Expected \'<dash-style>\'"); 3317 } 3318 3319 return segments; 3320 } 3321 3322 /* 3323 <border-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset 3324 */ 3325 private ParsedValue<ParsedValue[],Number[]> borderStyle(Term root) 3326 throws ParseException { 3327 3328 if (root.token == null || 3329 root.token.getType() != CssLexer.IDENT || 3330 root.token.getText() == null || 3331 root.token.getText().isEmpty()) error(root, "Expected \'<border-style>\'"); 3332 3333 final String text = root.token.getText().toLowerCase(Locale.ROOT); 3334 3335 if ("none".equals(text)) { 3336 return BorderStyleConverter.NONE; 3337 } else if ("hidden".equals(text)) { 3338 // The "hidden" mode doesn't make sense for FX, because it is the 3339 // same as "none" except for border-collapsed CSS tables 3340 return BorderStyleConverter.NONE; 3341 } else if ("dotted".equals(text)) { 3342 return BorderStyleConverter.DOTTED; 3343 } else if ("dashed".equals(text)) { 3344 return BorderStyleConverter.DASHED; 3345 } else if ("solid".equals(text)) { 3346 return BorderStyleConverter.SOLID; 3347 } else if ("double".equals(text)) { 3348 error(root, "Unsupported <border-style> \'double\'"); 3349 } else if ("groove".equals(text)) { 3350 error(root, "Unsupported <border-style> \'groove\'"); 3351 } else if ("ridge".equals(text)) { 3352 error(root, "Unsupported <border-style> \'ridge\'"); 3353 } else if ("inset".equals(text)) { 3354 error(root, "Unsupported <border-style> \'inset\'"); 3355 } else if ("outset".equals(text)) { 3356 error(root, "Unsupported <border-style> \'outset\'"); 3357 } else { 3358 error(root, "Unsupported <border-style> \'" + text + "\'"); 3359 } 3360 // error throws so we should never get here. 3361 // but the compiler wants a return, so here it is. 3362 return BorderStyleConverter.SOLID; 3363 } 3364 3365 private ParsedValueImpl<ParsedValue[],Number[]> segments(Term root) 3366 throws ParseException { 3367 3368 // first term in the chain is the function name... 3369 final String fn = (root.token != null) ? root.token.getText() : null; 3370 if (!"segments".regionMatches(true, 0, fn, 0, 8)) { 3371 error(root,"Expected \'segments\'"); 3372 } 3373 3374 Term arg = root.firstArg; 3375 if (arg == null) error(null, "Expected \'<size>\'"); 3376 3377 int nArgs = numberOfArgs(root); 3378 ParsedValueImpl<?,Size>[] segments = new ParsedValueImpl[nArgs]; 3379 int segment = 0; 3380 while(arg != null) { 3381 segments[segment++] = parseSize(arg); 3382 arg = arg.nextArg; 3383 } 3384 3385 return new ParsedValueImpl<ParsedValue[],Number[]>(segments,SizeConverter.SequenceConverter.getInstance()); 3386 3387 } 3388 3389 private ParsedValueImpl<String,StrokeType> parseStrokeType(final Term root) 3390 throws ParseException { 3391 3392 final String keyword = getKeyword(root); 3393 3394 3395 if ("centered".equals(keyword) || 3396 "inside".equals(keyword) || 3397 "outside".equals(keyword)) { 3398 3399 return new ParsedValueImpl<String,StrokeType>(keyword, new EnumConverter(StrokeType.class)); 3400 3401 } 3402 return null; 3403 } 3404 3405 // Root term is the term just after the line-join keyword 3406 // Returns an array of two Values or null. 3407 // ParsedValueImpl[0] is ParsedValueImpl<StrokeLineJoin,StrokeLineJoin> 3408 // ParsedValueImpl[1] is ParsedValueImpl<Value<?,Size>,Number> if miter limit is given, null otherwise. 3409 // If the token is not a StrokeLineJoin, then null is returned. 3410 private ParsedValueImpl[] parseStrokeLineJoin(final Term root) 3411 throws ParseException { 3412 3413 final String keyword = getKeyword(root); 3414 3415 if ("miter".equals(keyword) || 3416 "bevel".equals(keyword) || 3417 "round".equals(keyword)) { 3418 3419 ParsedValueImpl<String,StrokeLineJoin> strokeLineJoin = 3420 new ParsedValueImpl<String,StrokeLineJoin>(keyword, new EnumConverter(StrokeLineJoin.class)); 3421 3422 ParsedValueImpl<ParsedValue<?,Size>,Number> strokeMiterLimit = null; 3423 if ("miter".equals(keyword)) { 3424 3425 Term next = root.nextInSeries; 3426 if (next != null && 3427 next.token != null && 3428 isSize(next.token)) { 3429 3430 root.nextInSeries = next.nextInSeries; 3431 ParsedValueImpl<?,Size> sizeVal = parseSize(next); 3432 strokeMiterLimit = new ParsedValueImpl<ParsedValue<?,Size>,Number>(sizeVal,SizeConverter.getInstance()); 3433 } 3434 3435 } 3436 3437 return new ParsedValueImpl[] { strokeLineJoin, strokeMiterLimit }; 3438 } 3439 return null; 3440 } 3441 3442 // Root term is the term just after the line-cap keyword 3443 // If the token is not a StrokeLineCap, then null is returned. 3444 private ParsedValueImpl<String,StrokeLineCap> parseStrokeLineCap(final Term root) 3445 throws ParseException { 3446 3447 final String keyword = getKeyword(root); 3448 3449 if ("square".equals(keyword) || 3450 "butt".equals(keyword) || 3451 "round".equals(keyword)) { 3452 3453 return new ParsedValueImpl<String,StrokeLineCap>(keyword, new EnumConverter(StrokeLineCap.class)); 3454 } 3455 return null; 3456 } 3457 3458 /* 3459 * http://www.w3.org/TR/css3-background/#the-border-image-slice 3460 * [<number> | <percentage>]{1,4} && fill? 3461 */ 3462 private ParsedValueImpl<ParsedValue[],BorderImageSlices> parseBorderImageSlice(final Term root) 3463 throws ParseException { 3464 3465 Term term = root; 3466 if (term.token == null || !isSize(term.token)) 3467 error(term, "Expected \'<size>\'"); 3468 3469 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4]; 3470 Boolean fill = Boolean.FALSE; 3471 3472 int inset = 0; 3473 while (inset < 4 && term != null) { 3474 insets[inset++] = parseSize(term); 3475 3476 if ((term = term.nextInSeries) != null && 3477 term.token != null && 3478 term.token.getType() == CssLexer.IDENT) { 3479 3480 if("fill".equalsIgnoreCase(term.token.getText())) { 3481 fill = Boolean.TRUE; 3482 break; 3483 } 3484 } 3485 } 3486 3487 if (inset < 2) insets[1] = insets[0]; // right = top 3488 if (inset < 3) insets[2] = insets[0]; // bottom = top 3489 if (inset < 4) insets[3] = insets[1]; // left = right 3490 3491 ParsedValueImpl[] values = new ParsedValueImpl[] { 3492 new ParsedValueImpl<ParsedValue[],Insets>(insets, InsetsConverter.getInstance()), 3493 new ParsedValueImpl<Boolean,Boolean>(fill, null) 3494 }; 3495 return new ParsedValueImpl<ParsedValue[], BorderImageSlices>(values, BorderImageSliceConverter.getInstance()); 3496 } 3497 3498 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]> 3499 parseBorderImageSliceLayers(final Term root) throws ParseException { 3500 3501 int nLayers = numberOfLayers(root); 3502 ParsedValueImpl<ParsedValue[], BorderImageSlices>[] layers = new ParsedValueImpl[nLayers]; 3503 int layer = 0; 3504 Term term = root; 3505 while (term != null) { 3506 layers[layer++] = parseBorderImageSlice(term); 3507 term = nextLayer(term); 3508 } 3509 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderImageSlices>[],BorderImageSlices[]> (layers, SliceSequenceConverter.getInstance()); 3510 } 3511 3512 /* 3513 * http://www.w3.org/TR/css3-background/#border-image-width 3514 * [ <length> | <percentage> | <number> | auto ]{1,4} 3515 */ 3516 private ParsedValueImpl<ParsedValue[], BorderWidths> parseBorderImageWidth(final Term root) 3517 throws ParseException { 3518 3519 Term term = root; 3520 if (term.token == null || !isSize(term.token)) 3521 error(term, "Expected \'<size>\'"); 3522 3523 ParsedValueImpl<?,Size>[] insets = new ParsedValueImpl[4]; 3524 3525 int inset = 0; 3526 while (inset < 4 && term != null) { 3527 insets[inset++] = parseSize(term); 3528 3529 if ((term = term.nextInSeries) != null && 3530 term.token != null && 3531 term.token.getType() == CssLexer.IDENT) { 3532 } 3533 } 3534 3535 if (inset < 2) insets[1] = insets[0]; // right = top 3536 if (inset < 3) insets[2] = insets[0]; // bottom = top 3537 if (inset < 4) insets[3] = insets[1]; // left = right 3538 3539 return new ParsedValueImpl<ParsedValue[], BorderWidths>(insets, BorderImageWidthConverter.getInstance()); 3540 } 3541 3542 private ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> 3543 parseBorderImageWidthLayers(final Term root) throws ParseException { 3544 3545 int nLayers = numberOfLayers(root); 3546 ParsedValueImpl<ParsedValue[], BorderWidths>[] layers = new ParsedValueImpl[nLayers]; 3547 int layer = 0; 3548 Term term = root; 3549 while (term != null) { 3550 layers[layer++] = parseBorderImageWidth(term); 3551 term = nextLayer(term); 3552 } 3553 return new ParsedValueImpl<ParsedValue<ParsedValue[],BorderWidths>[],BorderWidths[]> (layers, BorderImageWidthsSequenceConverter.getInstance()); 3554 } 3555 3556 // parse a Region value 3557 // i.e., region(".styleClassForRegion") or region("#idForRegion") 3558 private static final String SPECIAL_REGION_URL_PREFIX = "SPECIAL-REGION-URL:"; 3559 private ParsedValueImpl<String,String> parseRegion(Term root) 3560 throws ParseException { 3561 // first term in the chain is the function name... 3562 final String fn = (root.token != null) ? root.token.getText() : null; 3563 if (!"region".regionMatches(true, 0, fn, 0, 6)) { 3564 error(root,"Expected \'region\'"); 3565 } 3566 3567 Term arg = root.firstArg; 3568 if (arg == null) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'"); 3569 3570 if (arg.token == null || 3571 arg.token.getType() != CssLexer.STRING || 3572 arg.token.getText() == null || 3573 arg.token.getText().isEmpty()) error(root, "Expected \'region(\"<styleclass-or-id-string>\")\'"); 3574 3575 final String styleClassOrId = SPECIAL_REGION_URL_PREFIX+ Utils.stripQuotes(arg.token.getText()); 3576 return new ParsedValueImpl<String,String>(styleClassOrId, StringConverter.getInstance()); 3577 } 3578 3579 // url("<uri>") is tokenized by the lexer, so the root arg should be a URL token. 3580 private ParsedValueImpl<ParsedValue[],String> parseURI(Term root) 3581 throws ParseException { 3582 3583 if (root == null) error(root, "Expected \'url(\"<uri-string>\")\'"); 3584 3585 if (root.token == null || 3586 root.token.getType() != CssLexer.URL || 3587 root.token.getText() == null || 3588 root.token.getText().isEmpty()) error(root, "Expected \'url(\"<uri-string>\")\'"); 3589 3590 final String uri = root.token.getText(); 3591 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 3592 new ParsedValueImpl<String,String>(uri, StringConverter.getInstance()), 3593 null // placeholder for Stylesheet URL 3594 }; 3595 return new ParsedValueImpl<ParsedValue[],String>(uriValues, URLConverter.getInstance()); 3596 } 3597 3598 // parse a series of URI values separated by commas. 3599 // i.e., <uri> [, <uri>]* 3600 private ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]> parseURILayers(Term root) 3601 throws ParseException { 3602 3603 int nLayers = numberOfLayers(root); 3604 3605 Term temp = root; 3606 int layer = 0; 3607 ParsedValueImpl<ParsedValue[],String>[] layers = new ParsedValueImpl[nLayers]; 3608 3609 while(temp != null) { 3610 layers[layer++] = parseURI(temp); 3611 temp = nextLayer(temp); 3612 } 3613 3614 return new ParsedValueImpl<ParsedValue<ParsedValue[],String>[],String[]>(layers, URLConverter.SequenceConverter.getInstance()); 3615 } 3616 3617 //////////////////////////////////////////////////////////////////////////// 3618 // 3619 // http://www.w3.org/TR/css3-fonts 3620 // 3621 //////////////////////////////////////////////////////////////////////////// 3622 3623 /* http://www.w3.org/TR/css3-fonts/#font-size-the-font-size-property */ 3624 private ParsedValueImpl<ParsedValue<?,Size>,Number> parseFontSize(final Term root) throws ParseException { 3625 3626 if (root == null) return null; 3627 final Token token = root.token; 3628 if (token == null || !isSize(token)) error(root, "Expected \'<font-size>\'"); 3629 3630 Size size = null; 3631 if (token.getType() == CssLexer.IDENT) { 3632 final String ident = token.getText().toLowerCase(Locale.ROOT); 3633 double value = -1; 3634 if ("inherit".equals(ident)) { 3635 value = 100; 3636 } else if ("xx-small".equals(ident)) { 3637 value = 60; 3638 } else if ("x-small".equals(ident)) { 3639 value = 75; 3640 } else if ("small".equals(ident)) { 3641 value = 80; 3642 } else if ("medium".equals(ident)) { 3643 value = 100; 3644 } else if ("large".equals(ident)) { 3645 value = 120; 3646 } else if ("x-large".equals(ident)) { 3647 value = 150; 3648 } else if ("xx-large".equals(ident)) { 3649 value = 200; 3650 } else if ("smaller".equals(ident)) { 3651 value = 80; 3652 } else if ("larger".equals(ident)) { 3653 value = 120; 3654 } 3655 3656 if (value > -1) { 3657 size = new Size(value, SizeUnits.PERCENT); 3658 } 3659 } 3660 3661 // if size is null, then size is not one of the keywords above. 3662 if (size == null) { 3663 size = size(token); 3664 } 3665 3666 ParsedValueImpl<?,Size> svalue = new ParsedValueImpl<Size,Size>(size, null); 3667 return new ParsedValueImpl<ParsedValue<?,Size>,Number>(svalue, FontConverter.FontSizeConverter.getInstance()); 3668 } 3669 3670 /* http://www.w3.org/TR/css3-fonts/#font-style-the-font-style-property */ 3671 private ParsedValueImpl<String,FontPosture> parseFontStyle(Term root) throws ParseException { 3672 3673 if (root == null) return null; 3674 final Token token = root.token; 3675 if (token == null || 3676 token.getType() != CssLexer.IDENT || 3677 token.getText() == null || 3678 token.getText().isEmpty()) error(root, "Expected \'<font-style>\'"); 3679 3680 final String ident = token.getText().toLowerCase(Locale.ROOT); 3681 String posture = FontPosture.REGULAR.name(); 3682 3683 if ("normal".equals(ident)) { 3684 posture = FontPosture.REGULAR.name(); 3685 } else if ("italic".equals(ident)) { 3686 posture = FontPosture.ITALIC.name(); 3687 } else if ("oblique".equals(ident)) { 3688 posture = FontPosture.ITALIC.name(); 3689 } else if ("inherit".equals(ident)) { 3690 posture = "inherit"; 3691 } else { 3692 return null; 3693 } 3694 3695 return new ParsedValueImpl<String,FontPosture>(posture, FontConverter.FontStyleConverter.getInstance()); 3696 } 3697 3698 /* http://www.w3.org/TR/css3-fonts/#font-weight-the-font-weight-property */ 3699 private ParsedValueImpl<String, FontWeight> parseFontWeight(Term root) throws ParseException { 3700 3701 if (root == null) return null; 3702 final Token token = root.token; 3703 if (token == null || 3704 token.getText() == null || 3705 token.getText().isEmpty()) error(root, "Expected \'<font-weight>\'"); 3706 3707 final String ident = token.getText().toLowerCase(Locale.ROOT); 3708 String weight = FontWeight.NORMAL.name(); 3709 3710 if ("inherit".equals(ident)) { 3711 weight = FontWeight.NORMAL.name(); 3712 } else if ("normal".equals(ident)) { 3713 weight = FontWeight.NORMAL.name(); 3714 } else if ("bold".equals(ident)) { 3715 weight = FontWeight.BOLD.name(); 3716 } else if ("bolder".equals(ident)) { 3717 weight = FontWeight.BOLD.name(); 3718 } else if ("lighter".equals(ident)) { 3719 weight = FontWeight.LIGHT.name(); 3720 } else if ("100".equals(ident)) { 3721 weight = FontWeight.findByWeight(100).name(); 3722 } else if ("200".equals(ident)) { 3723 weight = FontWeight.findByWeight(200).name(); 3724 } else if ("300".equals(ident)) { 3725 weight = FontWeight.findByWeight(300).name(); 3726 } else if ("400".equals(ident)) { 3727 weight = FontWeight.findByWeight(400).name(); 3728 } else if ("500".equals(ident)) { 3729 weight = FontWeight.findByWeight(500).name(); 3730 } else if ("600".equals(ident)) { 3731 weight = FontWeight.findByWeight(600).name(); 3732 } else if ("700".equals(ident)) { 3733 weight = FontWeight.findByWeight(700).name(); 3734 } else if ("800".equals(ident)) { 3735 weight = FontWeight.findByWeight(800).name(); 3736 } else if ("900".equals(ident)) { 3737 weight = FontWeight.findByWeight(900).name(); 3738 } else { 3739 error(root, "Expected \'<font-weight>\'"); 3740 } 3741 return new ParsedValueImpl<String,FontWeight>(weight, FontConverter.FontWeightConverter.getInstance()); 3742 } 3743 3744 private ParsedValueImpl<String,String> parseFontFamily(Term root) throws ParseException { 3745 3746 if (root == null) return null; 3747 final Token token = root.token; 3748 String text = null; 3749 if (token == null || 3750 (token.getType() != CssLexer.IDENT && 3751 token.getType() != CssLexer.STRING) || 3752 (text = token.getText()) == null || 3753 text.isEmpty()) error(root, "Expected \'<font-family>\'"); 3754 3755 final String fam = stripQuotes(text.toLowerCase(Locale.ROOT)); 3756 if ("inherit".equals(fam)) { 3757 return new ParsedValueImpl<String,String>("inherit", StringConverter.getInstance()); 3758 } else if ("serif".equals(fam) || 3759 "sans-serif".equals(fam) || 3760 "cursive".equals(fam) || 3761 "fantasy".equals(fam) || 3762 "monospace".equals(fam)) { 3763 return new ParsedValueImpl<String,String>(fam, StringConverter.getInstance()); 3764 } else { 3765 return new ParsedValueImpl<String,String>(token.getText(), StringConverter.getInstance()); 3766 } 3767 } 3768 3769 // (fontStyle || fontVariant || fontWeight)* fontSize (SOLIDUS size)? fontFamily 3770 private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseException { 3771 3772 // Because style, variant, weight, size and family can inherit 3773 // AND style, variant and weight are optional, parsing this backwards 3774 // is easier. 3775 Term next = root.nextInSeries; 3776 root.nextInSeries = null; 3777 while (next != null) { 3778 Term temp = next.nextInSeries; 3779 next.nextInSeries = root; 3780 root = next; 3781 next = temp; 3782 } 3783 3784 // Now, root should point to fontFamily 3785 Token token = root.token; 3786 int ttype = token.getType(); 3787 if (ttype != CssLexer.IDENT && 3788 ttype != CssLexer.STRING) error(root, "Expected \'<font-family>\'"); 3789 ParsedValueImpl<String,String> ffamily = parseFontFamily(root); 3790 3791 Term term = root; 3792 if ((term = term.nextInSeries) == null) error(root, "Expected \'<size>\'"); 3793 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'"); 3794 3795 // Now, term could be the font size or it could be the line-height. 3796 // If the next term is a forward slash, then it's line-height. 3797 Term temp; 3798 if (((temp = term.nextInSeries) != null) && 3799 (temp.token != null && temp.token.getType() == CssLexer.SOLIDUS)) { 3800 3801 root = temp; 3802 3803 if ((term = temp.nextInSeries) == null) error(root, "Expected \'<size>\'"); 3804 if (term.token == null || !isSize(term.token)) error(term, "Expected \'<size>\'"); 3805 3806 token = term.token; 3807 } 3808 3809 ParsedValueImpl<ParsedValue<?,Size>,Number> fsize = parseFontSize(term); 3810 if (fsize == null) error(root, "Expected \'<size>\'"); 3811 3812 ParsedValueImpl<String,FontPosture> fstyle = null; 3813 ParsedValueImpl<String,FontWeight> fweight = null; 3814 String fvariant = null; 3815 3816 while ((term = term.nextInSeries) != null) { 3817 3818 if (term.token == null || 3819 term.token.getType() != CssLexer.IDENT || 3820 term.token.getText() == null || 3821 term.token.getText().isEmpty()) 3822 error(term, "Expected \'<font-weight>\', \'<font-style>\' or \'<font-variant>\'"); 3823 3824 if (fstyle == null && ((fstyle = parseFontStyle(term)) != null)) { 3825 ; 3826 } else if (fvariant == null && "small-caps".equalsIgnoreCase(term.token.getText())) { 3827 fvariant = term.token.getText(); 3828 } else if (fweight == null && ((fweight = parseFontWeight(term)) != null)) { 3829 ; 3830 } 3831 } 3832 3833 ParsedValueImpl[] values = new ParsedValueImpl[]{ ffamily, fsize, fweight, fstyle }; 3834 return new ParsedValueImpl<ParsedValue[],Font>(values, FontConverter.getInstance()); 3835 } 3836 3837 // 3838 // Parser state machine 3839 // 3840 Token currentToken = null; 3841 3842 // return the next token that is not whitespace. 3843 private Token nextToken(CssLexer lexer) { 3844 3845 Token token = null; 3846 3847 do { 3848 token = lexer.nextToken(); 3849 } while ((token != null) && 3850 (token.getType() == CssLexer.WS) || 3851 (token.getType() == CssLexer.NL)); 3852 3853 if (LOGGER.isLoggable(Level.FINEST)) { 3854 LOGGER.finest(token.toString()); 3855 } 3856 3857 return token; 3858 3859 } 3860 3861 // keep track of what is in process of being parsed to avoid import loops 3862 private static Stack<String> imports; 3863 3864 private void parse(Stylesheet stylesheet, CssLexer lexer) { 3865 3866 // need to read the first token 3867 currentToken = nextToken(lexer); 3868 3869 while((currentToken != null) && 3870 (currentToken.getType() == CssLexer.AT_KEYWORD)) { 3871 3872 currentToken = nextToken(lexer); 3873 3874 if (currentToken == null || currentToken.getType() != CssLexer.IDENT) { 3875 3876 // just using ParseException for a nice error message, not for throwing the exception. 3877 ParseException parseException = new ParseException("Expected IDENT", currentToken, this); 3878 final String msg = parseException.toString(); 3879 ParseError error = createError(msg); 3880 if (LOGGER.isLoggable(Level.WARNING)) { 3881 LOGGER.warning(error.toString()); 3882 } 3883 reportError(error); 3884 3885 // get past EOL or SEMI 3886 do { 3887 currentToken = lexer.nextToken(); 3888 } while ((currentToken != null) && 3889 (currentToken.getType() == CssLexer.SEMI) || 3890 (currentToken.getType() == CssLexer.WS) || 3891 (currentToken.getType() == CssLexer.NL)); 3892 continue; 3893 } 3894 3895 String keyword = currentToken.getText().toLowerCase(Locale.ROOT); 3896 if ("font-face".equals(keyword)) { 3897 FontFace fontFace = fontFace(lexer); 3898 if (fontFace != null) stylesheet.getFontFaces().add(fontFace); 3899 currentToken = nextToken(lexer); 3900 continue; 3901 3902 } else if ("import".equals(keyword)) { 3903 3904 if (CssParser.imports == null) { 3905 CssParser.imports = new Stack<>(); 3906 } 3907 3908 if (!imports.contains(sourceOfStylesheet)) { 3909 3910 imports.push(sourceOfStylesheet); 3911 3912 Stylesheet importedStylesheet = handleImport(lexer); 3913 3914 if (importedStylesheet != null) { 3915 stylesheet.importStylesheet(importedStylesheet); 3916 } 3917 3918 imports.pop(); 3919 3920 if (CssParser.imports.isEmpty()) { 3921 CssParser.imports = null; 3922 } 3923 3924 } else { 3925 // Import imports import! 3926 final int line = currentToken.getLine(); 3927 final int pos = currentToken.getOffset(); 3928 final String msg = 3929 MessageFormat.format("Recursive @import at {2} [{0,number,#},{1,number,#}]", 3930 line, pos, imports.peek()); 3931 ParseError error = createError(msg); 3932 if (LOGGER.isLoggable(Level.WARNING)) { 3933 LOGGER.warning(error.toString()); 3934 } 3935 reportError(error); 3936 } 3937 3938 // get past EOL or SEMI 3939 do { 3940 currentToken = lexer.nextToken(); 3941 } while ((currentToken != null) && 3942 (currentToken.getType() == CssLexer.SEMI) || 3943 (currentToken.getType() == CssLexer.WS) || 3944 (currentToken.getType() == CssLexer.NL)); 3945 3946 continue; 3947 3948 } 3949 } 3950 3951 while ((currentToken != null) && 3952 (currentToken.getType() != Token.EOF)) { 3953 3954 List<Selector> selectors = selectors(lexer); 3955 if (selectors == null) return; 3956 3957 if ((currentToken == null) || 3958 (currentToken.getType() != CssLexer.LBRACE)) { 3959 final int line = currentToken != null ? currentToken.getLine() : -1; 3960 final int pos = currentToken != null ? currentToken.getOffset() : -1; 3961 final String msg = 3962 MessageFormat.format("Expected LBRACE at [{0,number,#},{1,number,#}]", 3963 line, pos); 3964 ParseError error = createError(msg); 3965 if (LOGGER.isLoggable(Level.WARNING)) { 3966 LOGGER.warning(error.toString()); 3967 } 3968 reportError(error); 3969 currentToken = null; 3970 return; 3971 } 3972 3973 // get past the LBRACE 3974 currentToken = nextToken(lexer); 3975 3976 List<Declaration> declarations = declarations(lexer); 3977 if (declarations == null) return; 3978 3979 if ((currentToken != null) && 3980 (currentToken.getType() != CssLexer.RBRACE)) { 3981 final int line = currentToken.getLine(); 3982 final int pos = currentToken.getOffset(); 3983 final String msg = 3984 MessageFormat.format("Expected RBRACE at [{0,number,#},{1,number,#}]", 3985 line, pos); 3986 ParseError error = createError(msg); 3987 if (LOGGER.isLoggable(Level.WARNING)) { 3988 LOGGER.warning(error.toString()); 3989 } 3990 reportError(error); 3991 currentToken = null; 3992 return; 3993 } 3994 3995 stylesheet.getRules().add(new Rule(selectors, declarations)); 3996 3997 currentToken = nextToken(lexer); 3998 3999 } 4000 currentToken = null; 4001 } 4002 4003 private FontFace fontFace(CssLexer lexer) { 4004 final Map<String,String> descriptors = new HashMap<String,String>(); 4005 final List<FontFaceImpl.FontFaceSrc> sources = new ArrayList<FontFaceImpl.FontFaceSrc>(); 4006 while(true) { 4007 currentToken = nextToken(lexer); 4008 if (currentToken.getType() == CssLexer.IDENT) { 4009 String key = currentToken.getText(); 4010 // ignore the colon that follows 4011 currentToken = nextToken(lexer); 4012 // get the next token after colon 4013 currentToken = nextToken(lexer); 4014 // ignore all but "src" 4015 if ("src".equalsIgnoreCase(key)) { 4016 while(true) { 4017 if((currentToken != null) && 4018 (currentToken.getType() != CssLexer.SEMI) && 4019 (currentToken.getType() != CssLexer.RBRACE) && 4020 (currentToken.getType() != Token.EOF)) { 4021 4022 if (currentToken.getType() == CssLexer.IDENT) { 4023 // simple reference to other font-family 4024 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.REFERENCE,currentToken.getText())); 4025 4026 } else if (currentToken.getType() == CssLexer.URL) { 4027 4028 // let URLConverter do the conversion 4029 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 4030 new ParsedValueImpl<String,String>(currentToken.getText(), StringConverter.getInstance()), 4031 new ParsedValueImpl<String,String>(sourceOfStylesheet, null) 4032 }; 4033 ParsedValue<ParsedValue[], String> parsedValue = 4034 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance()); 4035 String urlStr = parsedValue.convert(null); 4036 4037 URL url = null; 4038 try { 4039 URI fontUri = new URI(urlStr); 4040 url = fontUri.toURL(); 4041 } catch (URISyntaxException | MalformedURLException malf) { 4042 4043 final int line = currentToken.getLine(); 4044 final int pos = currentToken.getOffset(); 4045 final String msg = MessageFormat.format("Could not resolve @font-face url [{2}] at [{0,number,#},{1,number,#}]", line, pos, urlStr); 4046 ParseError error = createError(msg); 4047 if (LOGGER.isLoggable(Level.WARNING)) { 4048 LOGGER.warning(error.toString()); 4049 } 4050 reportError(error); 4051 4052 // skip the rest. 4053 while(currentToken != null) { 4054 int ttype = currentToken.getType(); 4055 if (ttype == CssLexer.RBRACE || 4056 ttype == Token.EOF) { 4057 return null; 4058 } 4059 currentToken = nextToken(lexer); 4060 } 4061 } 4062 4063 String format = null; 4064 while(true) { 4065 currentToken = nextToken(lexer); 4066 final int ttype = (currentToken != null) ? currentToken.getType() : Token.EOF; 4067 if (ttype == CssLexer.FUNCTION) { 4068 if ("format(".equalsIgnoreCase(currentToken.getText())) { 4069 continue; 4070 } else { 4071 break; 4072 } 4073 } else if (ttype == CssLexer.IDENT || 4074 ttype == CssLexer.STRING) { 4075 4076 format = Utils.stripQuotes(currentToken.getText()); 4077 } else if (ttype == CssLexer.RPAREN) { 4078 continue; 4079 } else { 4080 break; 4081 } 4082 } 4083 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.URL,url.toExternalForm(), format)); 4084 4085 } else if (currentToken.getType() == CssLexer.FUNCTION) { 4086 if ("local(".equalsIgnoreCase(currentToken.getText())) { 4087 // consume the function token 4088 currentToken = nextToken(lexer); 4089 // parse function contents 4090 final StringBuilder localSb = new StringBuilder(); 4091 while(true) { 4092 if((currentToken != null) && (currentToken.getType() != CssLexer.RPAREN) && 4093 (currentToken.getType() != Token.EOF)) { 4094 localSb.append(currentToken.getText()); 4095 } else { 4096 break; 4097 } 4098 currentToken = nextToken(lexer); 4099 } 4100 int start = 0, end = localSb.length(); 4101 if (localSb.charAt(start) == '\'' || localSb.charAt(start) == '\"') start ++; 4102 if (localSb.charAt(end-1) == '\'' || localSb.charAt(end-1) == '\"') end --; 4103 final String local = localSb.substring(start,end); 4104 sources.add(new FontFaceImpl.FontFaceSrc(FontFaceImpl.FontFaceSrcType.LOCAL,local)); 4105 } else { 4106 // error unknown fontface src type 4107 final int line = currentToken.getLine(); 4108 final int pos = currentToken.getOffset(); 4109 final String msg = MessageFormat.format("Unknown @font-face src type [" + currentToken.getText() + ")] at [{0,number,#},{1,number,#}]", line, pos); 4110 ParseError error = createError(msg); 4111 if (LOGGER.isLoggable(Level.WARNING)) { 4112 LOGGER.warning(error.toString()); 4113 } 4114 reportError(error); 4115 4116 } 4117 } else if (currentToken.getType() == CssLexer.COMMA) { 4118 // ignore 4119 } else { 4120 // error unexpected token 4121 final int line = currentToken.getLine(); 4122 final int pos = currentToken.getOffset(); 4123 final String msg = MessageFormat.format("Unexpected TOKEN [" + currentToken.getText() + "] at [{0,number,#},{1,number,#}]", line, pos); 4124 ParseError error = createError(msg); 4125 if (LOGGER.isLoggable(Level.WARNING)) { 4126 LOGGER.warning(error.toString()); 4127 } 4128 reportError(error); 4129 } 4130 } else { 4131 break; 4132 } 4133 currentToken = nextToken(lexer); 4134 } 4135 } else { 4136 StringBuilder descriptorVal = new StringBuilder(); 4137 while(true) { 4138 if((currentToken != null) && (currentToken.getType() != CssLexer.SEMI) && 4139 (currentToken.getType() != Token.EOF)) { 4140 descriptorVal.append(currentToken.getText()); 4141 } else { 4142 break; 4143 } 4144 currentToken = nextToken(lexer); 4145 } 4146 descriptors.put(key,descriptorVal.toString()); 4147 } 4148 // continue; 4149 } 4150 4151 if ((currentToken == null) || 4152 (currentToken.getType() == CssLexer.RBRACE) || 4153 (currentToken.getType() == Token.EOF)) { 4154 break; 4155 } 4156 4157 } 4158 return new FontFaceImpl(descriptors, sources); 4159 } 4160 4161 private Stylesheet handleImport(CssLexer lexer) { 4162 currentToken = nextToken(lexer); 4163 4164 if (currentToken == null || currentToken.getType() == Token.EOF) { 4165 return null; 4166 } 4167 4168 int ttype = currentToken.getType(); 4169 4170 String fname = null; 4171 if (ttype == CssLexer.STRING || ttype == CssLexer.URL) { 4172 fname = currentToken.getText(); 4173 } 4174 4175 Stylesheet importedStylesheet = null; 4176 final String _sourceOfStylesheet = sourceOfStylesheet; 4177 4178 if (fname != null) { 4179 // let URLConverter do the conversion 4180 ParsedValueImpl[] uriValues = new ParsedValueImpl[] { 4181 new ParsedValueImpl<String,String>(fname, StringConverter.getInstance()), 4182 new ParsedValueImpl<String,String>(sourceOfStylesheet, null) 4183 }; 4184 ParsedValue<ParsedValue[], String> parsedValue = 4185 new ParsedValueImpl<ParsedValue[], String>(uriValues, URLConverter.getInstance()); 4186 4187 String urlString = parsedValue.convert(null); 4188 importedStylesheet = StyleManager.loadStylesheet(urlString); 4189 4190 // When we load an imported stylesheet, the sourceOfStylesheet field 4191 // gets set to the new stylesheet. Once it is done loading we must reset 4192 // this field back to the previous value, otherwise we will potentially 4193 // run into problems (for example, see RT-40346). 4194 sourceOfStylesheet = _sourceOfStylesheet; 4195 } 4196 if (importedStylesheet == null) { 4197 final String msg = 4198 MessageFormat.format("Could not import {0}", fname); 4199 ParseError error = createError(msg); 4200 if (LOGGER.isLoggable(Level.WARNING)) { 4201 LOGGER.warning(error.toString()); 4202 } 4203 reportError(error); 4204 } 4205 return importedStylesheet; 4206 } 4207 4208 private List<Selector> selectors(CssLexer lexer) { 4209 4210 List<Selector> selectors = new ArrayList<Selector>(); 4211 4212 while(true) { 4213 Selector selector = selector(lexer); 4214 if (selector == null) { 4215 // some error happened, skip the rule... 4216 while ((currentToken != null) && 4217 (currentToken.getType() != CssLexer.RBRACE) && 4218 (currentToken.getType() != Token.EOF)) { 4219 currentToken = nextToken(lexer); 4220 } 4221 4222 // current token is either RBRACE or EOF. Calling 4223 // currentToken will get the next token or EOF. 4224 currentToken = nextToken(lexer); 4225 4226 // skipped the last rule? 4227 if (currentToken == null || currentToken.getType() == Token.EOF) { 4228 currentToken = null; 4229 return null; 4230 } 4231 4232 continue; 4233 } 4234 selectors.add(selector); 4235 4236 if ((currentToken != null) && 4237 (currentToken.getType() == CssLexer.COMMA)) { 4238 // get past the comma 4239 currentToken = nextToken(lexer); 4240 continue; 4241 } 4242 4243 // currentToken was either null or not a comma 4244 // so we are done with selectors. 4245 break; 4246 } 4247 4248 return selectors; 4249 } 4250 4251 private Selector selector(CssLexer lexer) { 4252 4253 List<Combinator> combinators = null; 4254 List<SimpleSelector> sels = null; 4255 4256 SimpleSelector ancestor = simpleSelector(lexer); 4257 if (ancestor == null) return null; 4258 4259 while (true) { 4260 Combinator comb = combinator(lexer); 4261 if (comb != null) { 4262 if (combinators == null) { 4263 combinators = new ArrayList<Combinator>(); 4264 } 4265 combinators.add(comb); 4266 SimpleSelector descendant = simpleSelector(lexer); 4267 if (descendant == null) return null; 4268 if (sels == null) { 4269 sels = new ArrayList<SimpleSelector>(); 4270 sels.add(ancestor); 4271 } 4272 sels.add(descendant); 4273 } else { 4274 break; 4275 } 4276 } 4277 4278 // RT-15473 4279 // We might return from selector with a NL token instead of an 4280 // LBRACE, so skip past the NL here. 4281 if (currentToken != null && currentToken.getType() == CssLexer.NL) { 4282 currentToken = nextToken(lexer); 4283 } 4284 4285 4286 if (sels == null) { 4287 return ancestor; 4288 } else { 4289 return new CompoundSelector(sels,combinators); 4290 } 4291 4292 } 4293 4294 private SimpleSelector simpleSelector(CssLexer lexer) { 4295 4296 String esel = "*"; // element selector. default to universal 4297 String isel = ""; // id selector 4298 List<String> csels = null; // class selector 4299 List<String> pclasses = null; // pseudoclasses 4300 4301 while (true) { 4302 4303 final int ttype = 4304 (currentToken != null) ? currentToken.getType() : Token.INVALID; 4305 4306 switch(ttype) { 4307 // element selector 4308 case CssLexer.STAR: 4309 case CssLexer.IDENT: 4310 esel = currentToken.getText(); 4311 break; 4312 4313 // class selector 4314 case CssLexer.DOT: 4315 currentToken = nextToken(lexer); 4316 if (currentToken != null && 4317 currentToken.getType() == CssLexer.IDENT) { 4318 if (csels == null) { 4319 csels = new ArrayList<String>(); 4320 } 4321 csels.add(currentToken.getText()); 4322 } else { 4323 currentToken = Token.INVALID_TOKEN; 4324 return null; 4325 } 4326 break; 4327 4328 // id selector 4329 case CssLexer.HASH: 4330 isel = currentToken.getText().substring(1); 4331 break; 4332 4333 case CssLexer.COLON: 4334 currentToken = nextToken(lexer); 4335 if (currentToken != null && pclasses == null) { 4336 pclasses = new ArrayList<String>(); 4337 } 4338 4339 if (currentToken.getType() == CssLexer.IDENT) { 4340 pclasses.add(currentToken.getText()); 4341 } else if (currentToken.getType() == CssLexer.FUNCTION){ 4342 String pclass = functionalPseudo(lexer); 4343 pclasses.add(pclass); 4344 } else { 4345 currentToken = Token.INVALID_TOKEN; 4346 } 4347 4348 if (currentToken.getType() == Token.INVALID) { 4349 return null; 4350 } 4351 break; 4352 4353 case CssLexer.NL: 4354 case CssLexer.WS: 4355 case CssLexer.COMMA: 4356 case CssLexer.GREATER: 4357 case CssLexer.LBRACE: 4358 case Token.EOF: 4359 return new SimpleSelector(esel, csels, pclasses, isel); 4360 4361 default: 4362 return null; 4363 4364 4365 } 4366 4367 // get the next token, but don't skip whitespace 4368 // since it may be a combinator 4369 currentToken = lexer.nextToken(); 4370 if (LOGGER.isLoggable(Level.FINEST)) { 4371 LOGGER.finest(currentToken.toString()); 4372 } 4373 } 4374 } 4375 4376 // From http://www.w3.org/TR/selectors/#grammar 4377 // functional_pseudo 4378 // : FUNCTION S* expression ')' 4379 // ; 4380 // expression 4381 // /* In CSS3, the expressions are identifiers, strings, */ 4382 // /* or of the form "an+b" */ 4383 // : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ 4384 // ; 4385 private String functionalPseudo(CssLexer lexer) { 4386 4387 // TODO: This is not how we should handle functional pseudo-classes in the long-run! 4388 4389 StringBuilder pclass = new StringBuilder(currentToken.getText()); 4390 4391 while(true) { 4392 4393 currentToken = nextToken(lexer); 4394 4395 switch(currentToken.getType()) { 4396 4397 // TODO: lexer doesn't really scan right and isn't CSS3, 4398 // so PLUS, '-', NUMBER, etc are all useless at this point. 4399 case CssLexer.STRING: 4400 case CssLexer.IDENT: 4401 pclass.append(currentToken.getText()); 4402 break; 4403 4404 case CssLexer.RPAREN: 4405 pclass.append(')'); 4406 return pclass.toString(); 4407 4408 default: 4409 currentToken = Token.INVALID_TOKEN; 4410 return null; 4411 } 4412 } 4413 4414 } 4415 4416 private Combinator combinator(CssLexer lexer) { 4417 4418 Combinator combinator = null; 4419 4420 while (true) { 4421 4422 final int ttype = 4423 (currentToken != null) ? currentToken.getType() : Token.INVALID; 4424 4425 switch(ttype) { 4426 4427 case CssLexer.WS: 4428 // need to check if combinator is null since child token 4429 // might be surrounded by whitespace. 4430 if (combinator == null && " ".equals(currentToken.getText())) { 4431 combinator = Combinator.DESCENDANT; 4432 } 4433 break; 4434 4435 case CssLexer.GREATER: 4436 // no need to check if combinator is null here 4437 combinator = Combinator.CHILD; 4438 break; 4439 4440 case CssLexer.STAR: 4441 case CssLexer.IDENT: 4442 case CssLexer.DOT: 4443 case CssLexer.HASH: 4444 case CssLexer.COLON: 4445 return combinator; 4446 4447 default: 4448 // only selector is expected 4449 return null; 4450 4451 } 4452 4453 // get the next token, but don't skip whitespace 4454 currentToken = lexer.nextToken(); 4455 if (LOGGER.isLoggable(Level.FINEST)) { 4456 LOGGER.finest(currentToken.toString()); 4457 } 4458 } 4459 } 4460 4461 private List<Declaration> declarations(CssLexer lexer) { 4462 4463 List<Declaration> declarations = new ArrayList<Declaration>(); 4464 4465 while (true) { 4466 4467 Declaration decl = declaration(lexer); 4468 if (decl != null) { 4469 declarations.add(decl); 4470 } else { 4471 // some error happened, skip the decl... 4472 while ((currentToken != null) && 4473 (currentToken.getType() != CssLexer.SEMI) && 4474 (currentToken.getType() != CssLexer.RBRACE) && 4475 (currentToken.getType() != Token.EOF)) { 4476 currentToken = nextToken(lexer); 4477 } 4478 4479 // current token is either SEMI, RBRACE or EOF. 4480 if (currentToken != null && 4481 currentToken.getType() != CssLexer.SEMI) 4482 return declarations; 4483 } 4484 4485 // declaration; declaration; ??? 4486 // RT-17830 - allow declaration;; 4487 while ((currentToken != null) && 4488 (currentToken.getType() == CssLexer.SEMI)) { 4489 currentToken = nextToken(lexer); 4490 } 4491 4492 // if it is delcaration; declaration, then the 4493 // next token should be an IDENT. 4494 if ((currentToken != null) && 4495 (currentToken.getType() == CssLexer.IDENT)) { 4496 continue; 4497 } 4498 4499 break; 4500 } 4501 4502 return declarations; 4503 } 4504 4505 private Declaration declaration(CssLexer lexer) { 4506 4507 final int ttype = 4508 (currentToken != null) ? currentToken.getType() : Token.INVALID; 4509 4510 if ((currentToken == null) || 4511 (currentToken.getType() != CssLexer.IDENT)) { 4512 // 4513 // RT-16547: this warning was misleading because an empty rule 4514 // not invalid. Some people put in empty rules just as placeholders. 4515 // 4516 // if (LOGGER.isLoggable(PlatformLogger.WARNING)) { 4517 // final int line = currentToken != null ? currentToken.getLine() : -1; 4518 // final int pos = currentToken != null ? currentToken.getOffset() : -1; 4519 // final String url = 4520 // (stylesheet != null && stylesheet.getUrl() != null) ? 4521 // stylesheet.getUrl().toExternalForm() : "?"; 4522 // LOGGER.warning("Expected IDENT at {0}[{1,number,#},{2,number,#}]", 4523 // url,line,pos); 4524 // } 4525 return null; 4526 } 4527 4528 String property = currentToken.getText(); 4529 4530 currentToken = nextToken(lexer); 4531 4532 if ((currentToken == null) || 4533 (currentToken.getType() != CssLexer.COLON)) { 4534 final int line = currentToken.getLine(); 4535 final int pos = currentToken.getOffset(); 4536 final String msg = 4537 MessageFormat.format("Expected COLON at [{0,number,#},{1,number,#}]", 4538 line, pos); 4539 ParseError error = createError(msg); 4540 if (LOGGER.isLoggable(Level.WARNING)) { 4541 LOGGER.warning(error.toString()); 4542 } 4543 reportError(error); 4544 return null; 4545 } 4546 4547 currentToken = nextToken(lexer); 4548 4549 Term root = expr(lexer); 4550 ParsedValueImpl value = null; 4551 try { 4552 value = (root != null) ? valueFor(property, root, lexer) : null; 4553 } catch (ParseException re) { 4554 Token badToken = re.tok; 4555 final int line = badToken != null ? badToken.getLine() : -1; 4556 final int pos = badToken != null ? badToken.getOffset() : -1; 4557 final String msg = 4558 MessageFormat.format("{2} while parsing ''{3}'' at [{0,number,#},{1,number,#}]", 4559 line,pos,re.getMessage(),property); 4560 ParseError error = createError(msg); 4561 if (LOGGER.isLoggable(Level.WARNING)) { 4562 LOGGER.warning(error.toString()); 4563 } 4564 reportError(error); 4565 return null; 4566 } 4567 4568 boolean important = currentToken.getType() == CssLexer.IMPORTANT_SYM; 4569 if (important) currentToken = nextToken(lexer); 4570 4571 Declaration decl = (value != null) 4572 ? new Declaration(property.toLowerCase(Locale.ROOT), value, important) : null; 4573 return decl; 4574 } 4575 4576 private Term expr(CssLexer lexer) { 4577 4578 final Term expr = term(lexer); 4579 Term current = expr; 4580 4581 while(true) { 4582 4583 // if current is null, then term returned null 4584 final int ttype = 4585 (current != null && currentToken != null) 4586 ? currentToken.getType() : Token.INVALID; 4587 4588 if (ttype == Token.INVALID) { 4589 skipExpr(lexer); 4590 return null; 4591 } else if (ttype == CssLexer.SEMI || 4592 ttype == CssLexer.IMPORTANT_SYM || 4593 ttype == CssLexer.RBRACE || 4594 ttype == Token.EOF) { 4595 return expr; 4596 } else if (ttype == CssLexer.COMMA) { 4597 // comma breaks up sequences of terms. 4598 // next series of terms chains off the last term in 4599 // the current series. 4600 currentToken = nextToken(lexer); 4601 current = current.nextLayer = term(lexer); 4602 } else { 4603 current = current.nextInSeries = term(lexer); 4604 } 4605 4606 } 4607 } 4608 4609 private void skipExpr(CssLexer lexer) { 4610 4611 while(true) { 4612 4613 currentToken = nextToken(lexer); 4614 4615 final int ttype = 4616 (currentToken != null) ? currentToken.getType() : Token.INVALID; 4617 4618 if (ttype == CssLexer.SEMI || 4619 ttype == CssLexer.RBRACE || 4620 ttype == Token.EOF) { 4621 return; 4622 } 4623 } 4624 } 4625 4626 private Term term(CssLexer lexer) { 4627 4628 final int ttype = 4629 (currentToken != null) ? currentToken.getType() : Token.INVALID; 4630 4631 switch (ttype) { 4632 4633 case CssLexer.NUMBER: 4634 case CssLexer.CM: 4635 case CssLexer.EMS: 4636 case CssLexer.EXS: 4637 case CssLexer.IN: 4638 case CssLexer.MM: 4639 case CssLexer.PC: 4640 case CssLexer.PT: 4641 case CssLexer.PX: 4642 case CssLexer.DEG: 4643 case CssLexer.GRAD: 4644 case CssLexer.RAD: 4645 case CssLexer.TURN: 4646 case CssLexer.PERCENTAGE: 4647 case CssLexer.SECONDS: 4648 case CssLexer.MS: 4649 break; 4650 4651 case CssLexer.STRING: 4652 break; 4653 case CssLexer.IDENT: 4654 break; 4655 4656 case CssLexer.HASH: 4657 break; 4658 4659 case CssLexer.FUNCTION: 4660 case CssLexer.LPAREN: 4661 4662 Term function = new Term(currentToken); 4663 currentToken = nextToken(lexer); 4664 4665 Term arg = term(lexer); 4666 function.firstArg = arg; 4667 4668 while(true) { 4669 4670 final int operator = 4671 currentToken != null ? currentToken.getType() : Token.INVALID; 4672 4673 if (operator == CssLexer.RPAREN) { 4674 currentToken = nextToken(lexer); 4675 return function; 4676 } else if (operator == CssLexer.COMMA) { 4677 // comma breaks up sequences of terms. 4678 // next series of terms chains off the last term in 4679 // the current series. 4680 currentToken = nextToken(lexer); 4681 arg = arg.nextArg = term(lexer); 4682 4683 } else { 4684 arg = arg.nextInSeries = term(lexer); 4685 } 4686 4687 } 4688 4689 case CssLexer.URL: 4690 break; 4691 4692 case CssLexer.SOLIDUS: 4693 break; 4694 4695 default: 4696 final int line = currentToken != null ? currentToken.getLine() : -1; 4697 final int pos = currentToken != null ? currentToken.getOffset() : -1; 4698 final String text = currentToken != null ? currentToken.getText() : ""; 4699 final String msg = 4700 MessageFormat.format("Unexpected token {0}{1}{0} at [{2,number,#},{3,number,#}]", 4701 "\'",text,line,pos); 4702 ParseError error = createError(msg); 4703 if (LOGGER.isLoggable(Level.WARNING)) { 4704 LOGGER.warning(error.toString()); 4705 } 4706 reportError(error); 4707 return null; 4708 // currentToken = nextToken(lexer); 4709 // 4710 // return new Term(Token.INVALID_TOKEN); 4711 } 4712 4713 Term term = new Term(currentToken); 4714 currentToken = nextToken(lexer); 4715 return term; 4716 } 4717 4718 public static ObservableList<ParseError> errorsProperty() { 4719 return StyleManager.errorsProperty(); 4720 } 4721 4722 4723 4724 /** 4725 * Encapsulate information about the source and nature of errors encountered 4726 * while parsing CSS or applying styles to Nodes. 4727 */ 4728 public static class ParseError { 4729 4730 /** @return The error message from the CSS code. */ 4731 public final String getMessage() { 4732 return message; 4733 } 4734 4735 public ParseError(String message) { 4736 this.message = message; 4737 } 4738 4739 final String message; 4740 4741 @Override public String toString() { 4742 return "CSS Error: " + message; 4743 } 4744 4745 /** Encapsulate errors arising from parsing of stylesheet files */ 4746 public final static class StylesheetParsingError extends ParseError { 4747 4748 StylesheetParsingError(String url, String message) { 4749 super(message); 4750 this.url = url; 4751 } 4752 4753 String getURL() { 4754 return url; 4755 } 4756 4757 private final String url; 4758 4759 @Override public String toString() { 4760 final String path = url != null ? url : "?"; 4761 // TBD: i18n 4762 return "CSS Error parsing " + path + ": " + message; 4763 } 4764 4765 } 4766 4767 /** Encapsulate errors arising from parsing of Node's style property */ 4768 public final static class InlineStyleParsingError extends ParseError { 4769 4770 InlineStyleParsingError(Styleable styleable, String message) { 4771 super(message); 4772 this.styleable = styleable; 4773 } 4774 4775 Styleable getStyleable() { 4776 return styleable; 4777 } 4778 4779 private final Styleable styleable; 4780 4781 @Override public String toString() { 4782 final String inlineStyle = styleable.getStyle(); 4783 final String source = styleable.toString(); 4784 // TBD: i18n 4785 return "CSS Error parsing in-line style \'" + inlineStyle + 4786 "\' from " + source + ": " + message; 4787 } 4788 } 4789 4790 /** 4791 * Encapsulate errors arising from parsing when the style is not 4792 * an in-line style nor is the style from a stylesheet. Primarily to 4793 * support unit testing. 4794 */ 4795 public final static class StringParsingError extends ParseError { 4796 private final String style; 4797 4798 StringParsingError(String style, String message) { 4799 super(message); 4800 this.style = style; 4801 } 4802 4803 String getStyle() { 4804 return style; 4805 } 4806 4807 @Override public String toString() { 4808 // TBD: i18n 4809 return "CSS Error parsing \'" + style + ": " + message; 4810 } 4811 } 4812 4813 /** Encapsulates errors arising from applying a style to a Node. */ 4814 public final static class PropertySetError extends ParseError { 4815 private final CssMetaData styleableProperty; 4816 private final Styleable styleable; 4817 4818 public PropertySetError(CssMetaData styleableProperty, 4819 Styleable styleable, String message) { 4820 super(message); 4821 this.styleableProperty = styleableProperty; 4822 this.styleable = styleable; 4823 } 4824 4825 Styleable getStyleable() { 4826 return styleable; 4827 } 4828 4829 CssMetaData getProperty() { 4830 return styleableProperty; 4831 } 4832 4833 @Override public String toString() { 4834 // TBD: i18n 4835 return "CSS Error parsing \'" + styleableProperty + ": " + message; 4836 } 4837 } 4838 } 4839 }