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