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