1 | // Copyright (C) 2006 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.parser.css; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lexer.FilePosition; |
19 | import com.google.caja.lexer.TokenConsumer; |
20 | import com.google.caja.lexer.escaping.Escaping; |
21 | import com.google.caja.lexer.escaping.UriUtil; |
22 | import com.google.caja.parser.AbstractParseTreeNode; |
23 | import com.google.caja.render.Concatenator; |
24 | import com.google.caja.render.CssPrettyPrinter; |
25 | import com.google.caja.reporting.MessageContext; |
26 | import com.google.caja.reporting.RenderContext; |
27 | import com.google.caja.util.Callback; |
28 | import com.google.caja.util.Name; |
29 | |
30 | import java.io.IOException; |
31 | import java.net.URI; |
32 | import java.net.URISyntaxException; |
33 | import java.util.ArrayList; |
34 | import java.util.Arrays; |
35 | import java.util.Collections; |
36 | import java.util.EnumSet; |
37 | import java.util.List; |
38 | import java.util.Locale; |
39 | import java.util.Set; |
40 | import java.util.regex.Pattern; |
41 | |
42 | /** |
43 | * A node in a CSS parse tree. |
44 | * |
45 | * @author mikesamuel@gmail.com |
46 | */ |
47 | public abstract class CssTree extends AbstractParseTreeNode { |
48 | private CssTree(FilePosition pos, List<? extends CssTree> children) { |
49 | super(pos, CssTree.class); |
50 | createMutation().appendChildren(children).execute(); |
51 | } |
52 | private <T extends CssTree> CssTree( |
53 | FilePosition pos, Class<T> subType, List<? extends T> children) { |
54 | super(pos, subType); |
55 | createMutation().appendChildren(children).execute(); |
56 | } |
57 | |
58 | @Override |
59 | public Object getValue() { |
60 | return null; |
61 | } |
62 | |
63 | @Override |
64 | public List<? extends CssTree> children() { |
65 | return childrenAs(CssTree.class); |
66 | } |
67 | |
68 | @Override |
69 | public String toString() { |
70 | StringBuilder sb = new StringBuilder(); |
71 | try { |
72 | formatSelf(new MessageContext(), 0, sb); |
73 | } catch (IOException ex) { |
74 | throw new SomethingWidgyHappenedError( |
75 | "StringBuilders shouldn't throw IOExceptions"); |
76 | } |
77 | return sb.toString(); |
78 | } |
79 | |
80 | public final TokenConsumer makeRenderer( |
81 | Appendable out, Callback<IOException> exHandler) { |
82 | return new CssPrettyPrinter(new Concatenator(out, exHandler)); |
83 | } |
84 | |
85 | |
86 | /** |
87 | * The top level parsetree node. |
88 | * <pre> |
89 | * stylesheet |
90 | * : [ CHARSET_SYM S* STRING S* ';' ]? |
91 | * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* |
92 | * [ [ ruleset | media | page | font_face ] [S|CDO|CDC]* ]* |
93 | * </pre> |
94 | */ |
95 | public static final class StyleSheet extends CssTree { |
96 | /** @param novalue ignored but required for reflection. */ |
97 | @ReflectiveCtor |
98 | public StyleSheet( |
99 | FilePosition pos, Void novalue, List<? extends CssStatement> rulesets) { |
100 | this(pos, rulesets); |
101 | } |
102 | |
103 | public StyleSheet(FilePosition pos, List<? extends CssStatement> rulesets) { |
104 | super(pos, rulesets); |
105 | } |
106 | |
107 | public void render(RenderContext r) { |
108 | for (CssTree child : children()) { |
109 | child.render(r); |
110 | } |
111 | } |
112 | } |
113 | |
114 | /** |
115 | * A root node with no equivalent in the grammar. |
116 | * This node is like a ruleset, but without the selector, so it works as |
117 | * the root node of CSS parsed from an XHTML <code>style</code> attribute. |
118 | */ |
119 | public static final class DeclarationGroup extends CssTree { |
120 | /** @param novalue ignored but required for reflection. */ |
121 | @ReflectiveCtor |
122 | public DeclarationGroup( |
123 | FilePosition pos, Void novalue, List<? extends Declaration> decls) { |
124 | this(pos, decls); |
125 | } |
126 | |
127 | public DeclarationGroup( |
128 | FilePosition pos, List<? extends Declaration> decls) { |
129 | super(pos, Declaration.class, decls); |
130 | } |
131 | |
132 | @Override |
133 | public List<? extends Declaration> children() { |
134 | return childrenAs(Declaration.class); |
135 | } |
136 | |
137 | @Override |
138 | protected void childrenChanged() { |
139 | super.childrenChanged(); |
140 | for (CssTree child : children()) { |
141 | if (!(child instanceof Declaration)) { |
142 | throw new ClassCastException(child.getClass().getName()); |
143 | } |
144 | } |
145 | } |
146 | |
147 | public void render(RenderContext r) { |
148 | r.getOut().mark(getFilePosition()); |
149 | boolean first = true; |
150 | for (Declaration d : children()) { |
151 | if (!first) { |
152 | r.getOut().consume(";"); |
153 | } else { |
154 | first = false; |
155 | } |
156 | d.render(r); |
157 | } |
158 | } |
159 | } |
160 | |
161 | /** Part of a stylesheet. */ |
162 | public abstract static class CssStatement extends CssTree { |
163 | CssStatement(FilePosition pos, List<? extends CssTree> children) { |
164 | super(pos, children); |
165 | } |
166 | } |
167 | /** |
168 | * <pre> |
169 | * import |
170 | * : IMPORT_SYM S* |
171 | * [STRING|URI] S* [ medium [ ',' S* medium]* ]? ';' S* |
172 | * </pre> |
173 | */ |
174 | public static final class Import extends CssStatement { |
175 | private static <T> List<T> join(List<? extends T> a, List<? extends T> b) { |
176 | List<T> l = new ArrayList<T>(a.size() + b.size()); |
177 | l.addAll(a); |
178 | l.addAll(b); |
179 | return l; |
180 | } |
181 | |
182 | /** @param novalue ignored but required for reflection. */ |
183 | @ReflectiveCtor |
184 | public Import( |
185 | FilePosition pos, Void novalue, List<? extends Medium> media) { |
186 | super(pos, media); |
187 | } |
188 | |
189 | public Import( |
190 | FilePosition pos, UriLiteral uri, List<? extends Medium> media) { |
191 | super(pos, join(Collections.singletonList(uri), media)); |
192 | } |
193 | |
194 | public UriLiteral getUri() { return (UriLiteral) children().get(0); } |
195 | public List<Medium> getMedia() { |
196 | List<Medium> media = new ArrayList<Medium>(children().size() - 1); |
197 | for (CssTree t : children().subList(1, children().size())) { |
198 | media.add((Medium) t); |
199 | } |
200 | return media; |
201 | } |
202 | |
203 | public void render(RenderContext r) { |
204 | TokenConsumer out = r.getOut(); |
205 | out.mark(getFilePosition()); |
206 | out.consume("@"); |
207 | out.consume("import"); |
208 | out.consume(" "); |
209 | getUri().render(r); |
210 | |
211 | List<? extends CssTree> media = getMedia(); |
212 | if (!media.isEmpty()) { |
213 | out.consume(" "); |
214 | renderCommaGroup(media, r); |
215 | } |
216 | out.consume(";"); |
217 | out.consume("\n"); |
218 | } |
219 | } |
220 | |
221 | /** |
222 | * <pre> |
223 | * media |
224 | * : MEDIA_SYM S* medium [ ',' S* medium ]* '{' S* ruleset* '}' S* |
225 | * </pre> |
226 | */ |
227 | public static final class Media extends CssStatement { |
228 | /** @param novalue ignored but required for reflection. */ |
229 | @ReflectiveCtor |
230 | public Media( |
231 | FilePosition pos, Void novalue, |
232 | List<? extends CssTree> mediaAndRuleset) { |
233 | this(pos, mediaAndRuleset); |
234 | } |
235 | public Media(FilePosition pos, List<? extends CssTree> mediaAndRuleset) { |
236 | super(pos, mediaAndRuleset); |
237 | } |
238 | |
239 | public List<Medium> getMedia() { |
240 | List<Medium> media = new ArrayList<Medium>(); |
241 | for (CssTree t : children()) { |
242 | if (!(t instanceof Medium)) { break; } |
243 | media.add((Medium) t); |
244 | } |
245 | return media; |
246 | } |
247 | |
248 | public void render(RenderContext r) { |
249 | TokenConsumer out = r.getOut(); |
250 | out.mark(getFilePosition()); |
251 | out.consume("@"); |
252 | out.consume("media"); |
253 | out.consume(" "); |
254 | List<? extends CssTree> children = children(); |
255 | int i = 0; |
256 | while (i < children.size() && children.get(i) instanceof Medium) { ++i; } |
257 | renderCommaGroup(children.subList(0, i), r); |
258 | out.consume("{"); |
259 | for (CssTree ruleset : children.subList(i, children.size())) { |
260 | ruleset.render(r); |
261 | } |
262 | out.mark(FilePosition.endOfOrNull(getFilePosition())); |
263 | out.consume("}"); |
264 | } |
265 | } |
266 | |
267 | /** |
268 | * <pre> |
269 | * medium |
270 | * : IDENT S* |
271 | * </pre> |
272 | */ |
273 | public static final class Medium extends CssTree { |
274 | final Name ident; |
275 | |
276 | /** @param none ignored but required for reflection. */ |
277 | @ReflectiveCtor |
278 | public Medium(FilePosition pos, Name ident, List<? extends CssTree> none) { |
279 | this(pos, ident); |
280 | } |
281 | public Medium(FilePosition pos, Name ident) { |
282 | super(pos, Collections.<CssTree>emptyList()); |
283 | this.ident = ident; |
284 | } |
285 | |
286 | @Override |
287 | public Name getValue() { return ident; } |
288 | |
289 | public void render(RenderContext r) { |
290 | r.getOut().mark(getFilePosition()); |
291 | renderCssIdent(ident.getCanonicalForm(), r); |
292 | } |
293 | } |
294 | |
295 | /** |
296 | * <pre> |
297 | * page |
298 | * : PAGE_SYM S* IDENT? pseudo_page? S* |
299 | * '{' S* declaration [ ';' S* declaration ]* '}' S* |
300 | * </pre> |
301 | */ |
302 | public static final class Page extends CssStatement { |
303 | final Name ident; |
304 | |
305 | @ReflectiveCtor |
306 | public Page( |
307 | FilePosition pos, Name ident, List<? extends PageElement> decls) { |
308 | super(pos, decls); |
309 | this.ident = ident; |
310 | } |
311 | |
312 | @Override |
313 | public Name getValue() { return ident; } |
314 | |
315 | public void render(RenderContext r) { |
316 | TokenConsumer out = r.getOut(); |
317 | out.mark(getFilePosition()); |
318 | out.consume("@"); |
319 | out.consume("page"); |
320 | if (null != ident) { |
321 | out.consume(" "); |
322 | renderCssIdent(ident.getCanonicalForm(), r); |
323 | } |
324 | List<? extends CssTree> children = children(); |
325 | if (children.get(0) instanceof PseudoPage) { |
326 | children.get(0).render(r); |
327 | children = children.subList(1, children.size()); |
328 | } |
329 | renderStatements(children, getFilePosition(), r); |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * A part of a CSS statement. |
335 | */ |
336 | public abstract static class PageElement extends CssTree { |
337 | PageElement(FilePosition pos, List<? extends CssTree> children) { |
338 | super(pos, children); |
339 | } |
340 | <T extends CssTree> PageElement( |
341 | FilePosition pos, Class<T> childType, List<? extends T> children) { |
342 | super(pos, childType, children); |
343 | } |
344 | } |
345 | |
346 | /** |
347 | * <pre> |
348 | * pseudo_page |
349 | * : ':' IDENT |
350 | * </pre> |
351 | */ |
352 | public static final class PseudoPage extends PageElement { |
353 | final Name ident; |
354 | |
355 | /** @param none ignored but required for reflection. */ |
356 | @ReflectiveCtor |
357 | public PseudoPage( |
358 | FilePosition pos, Name ident, List<? extends CssTree> none) { |
359 | this(pos, ident); |
360 | } |
361 | public PseudoPage(FilePosition pos, Name ident) { |
362 | super(pos, Collections.<CssTree>emptyList()); |
363 | this.ident = ident; |
364 | } |
365 | |
366 | @Override |
367 | public Name getValue() { return ident; } |
368 | |
369 | public void render(RenderContext r) { |
370 | r.getOut().mark(getFilePosition()); |
371 | r.getOut().consume(":"); |
372 | renderCssIdent(ident.getCanonicalForm(), r); |
373 | } |
374 | } |
375 | |
376 | /** |
377 | * <pre> |
378 | * font_face |
379 | * : FONT_FACE_SYM S* |
380 | * '{' S* declaration [ ';' S* declaration ]* '}' S* |
381 | * </pre> |
382 | */ |
383 | public static final class FontFace extends CssStatement { |
384 | /** @param novalue ignored but required for reflection. */ |
385 | @ReflectiveCtor |
386 | public FontFace( |
387 | FilePosition pos, Void novalue, List<? extends Declaration> decls) { |
388 | this(pos, decls); |
389 | } |
390 | |
391 | public FontFace(FilePosition pos, List<? extends Declaration> decls) { |
392 | super(pos, decls); |
393 | } |
394 | |
395 | @Override |
396 | protected void childrenChanged() { |
397 | super.childrenChanged(); |
398 | for (CssTree child : children()) { |
399 | if (!(child instanceof Declaration)) { |
400 | throw new ClassCastException(child.getClass().getName()); |
401 | } |
402 | } |
403 | } |
404 | |
405 | public void render(RenderContext r) { |
406 | r.getOut().mark(getFilePosition()); |
407 | r.getOut().consume("@"); |
408 | r.getOut().consume("font-face"); |
409 | r.getOut().consume(" "); |
410 | renderStatements(children(), getFilePosition(), r); |
411 | } |
412 | } |
413 | |
414 | /** |
415 | * <pre> |
416 | * property |
417 | * : IDENT S* |
418 | * </pre> |
419 | */ |
420 | public static final class Property extends CssTree { |
421 | private final Name ident; |
422 | /** @param none ignored but required for reflection. */ |
423 | @ReflectiveCtor |
424 | public Property( |
425 | FilePosition pos, Name ident, List<? extends CssTree> none) { |
426 | this(pos, ident); |
427 | } |
428 | |
429 | public Property(FilePosition pos, Name ident) { |
430 | super(pos, Collections.<CssTree>emptyList()); |
431 | this.ident = ident; |
432 | } |
433 | |
434 | @Override |
435 | public Name getValue() { return ident; } |
436 | |
437 | public Name getPropertyName() { |
438 | return ident; |
439 | } |
440 | |
441 | public void render(RenderContext r) { |
442 | r.getOut().mark(getFilePosition()); |
443 | renderCssIdent(ident.getCanonicalForm(), r); |
444 | } |
445 | } |
446 | |
447 | /** |
448 | * <pre> |
449 | * ruleset |
450 | * : selector [ ',' S* selector ]* |
451 | * '{' S* declaration [ ';' S* declaration ]* '}' S* |
452 | * </pre> |
453 | */ |
454 | public static final class RuleSet extends CssStatement { |
455 | /** @param novalue ignored but required for reflection. */ |
456 | @ReflectiveCtor |
457 | public RuleSet( |
458 | FilePosition pos, Void novalue, |
459 | List<? extends CssTree> selectorsAndDecls) { |
460 | this(pos, selectorsAndDecls); |
461 | } |
462 | public RuleSet( |
463 | FilePosition pos, List<? extends CssTree> selectorsAndDecls) { |
464 | super(pos, selectorsAndDecls); |
465 | } |
466 | |
467 | public void render(RenderContext r) { |
468 | List<? extends CssTree> children = children(); |
469 | int i = 0; |
470 | while (i < children.size() && !(children.get(i) instanceof Declaration)) { |
471 | ++i; |
472 | } |
473 | renderCommaGroup(children.subList(0, i), r); |
474 | FilePosition selectorEnd = children.get(i - 1).getFilePosition(); |
475 | FilePosition pos = selectorEnd != null && getFilePosition() != null |
476 | ? FilePosition.span(FilePosition.endOf(selectorEnd), |
477 | FilePosition.endOf(getFilePosition())) |
478 | : null; |
479 | renderStatements(children.subList(i, children.size()), pos, r); |
480 | } |
481 | } |
482 | |
483 | /** |
484 | * <pre> |
485 | * selector |
486 | * : simple_selector [ combinator simple_selector ]* |
487 | * </pre> |
488 | */ |
489 | public static final class Selector extends CssTree { |
490 | /** @param novalue ignored but required for reflection. */ |
491 | @ReflectiveCtor |
492 | public Selector( |
493 | FilePosition pos, Void novalue, List<? extends CssTree> children) { |
494 | this(pos, children); |
495 | } |
496 | public Selector(FilePosition pos, List<? extends CssTree> children) { |
497 | super(pos, children); |
498 | } |
499 | public void render(RenderContext r) { |
500 | renderSpaceGroup(children(), r); |
501 | } |
502 | @Override |
503 | protected void childrenChanged() { |
504 | super.childrenChanged(); |
505 | List<? extends CssTree> children = children(); |
506 | int n = children.size(); |
507 | boolean needSelector = true; |
508 | for (CssTree child : children) { |
509 | if (child instanceof SimpleSelector) { |
510 | needSelector = false; |
511 | } else if (!needSelector && child instanceof Combination) { |
512 | needSelector = true; |
513 | } else { |
514 | throw new ClassCastException(child.getClass().getName()); |
515 | } |
516 | } |
517 | if (needSelector && n != 0) { |
518 | throw new IllegalArgumentException(); |
519 | } |
520 | } |
521 | } |
522 | |
523 | /** |
524 | * <pre> |
525 | * simple_selector |
526 | * : element_name? [ HASH | class | attrib | pseudo ]* S* |
527 | * </pre> |
528 | */ |
529 | public static final class SimpleSelector extends CssTree { |
530 | /** @param novalue ignored but required for reflection. */ |
531 | @ReflectiveCtor |
532 | public SimpleSelector( |
533 | FilePosition pos, Void novalue, List<? extends CssTree> children) { |
534 | this(pos, children); |
535 | } |
536 | |
537 | public SimpleSelector(FilePosition pos, List<? extends CssTree> children) { |
538 | super(pos, children); |
539 | } |
540 | |
541 | public String getElementName() { |
542 | CssTree first = children().get(0); |
543 | if (first instanceof IdentLiteral) { |
544 | return ((IdentLiteral) first).getValue(); |
545 | } |
546 | return null; |
547 | } |
548 | |
549 | public void render(RenderContext r) { |
550 | // no spaces between because space is the DESCENDANT operator in Selector |
551 | for (CssTree child : children()) { child.render(r); } |
552 | } |
553 | } |
554 | |
555 | /** |
556 | * <pre> |
557 | * element_name |
558 | * : IDENT | '*' |
559 | * </pre> |
560 | */ |
561 | public static final class WildcardElement extends CssTree { |
562 | /** |
563 | * @param novalue ignored but required for reflection. |
564 | * @param none ignored but required for reflection. |
565 | */ |
566 | @ReflectiveCtor |
567 | public WildcardElement( |
568 | FilePosition pos, Void novalue, List<? extends CssTree> none) { |
569 | this(pos); |
570 | } |
571 | |
572 | public WildcardElement(FilePosition pos) { |
573 | super(pos, Collections.<CssTree>emptyList()); |
574 | } |
575 | |
576 | public void render(RenderContext r) { |
577 | // Start with a space to make sure that rendering couldn't introduce a |
578 | // comment. I know of no parse tree that would otherwise do this, but |
579 | // comments could be used to introduce security holes. |
580 | TokenConsumer out = r.getOut(); |
581 | out.mark(getFilePosition()); |
582 | out.consume(" "); |
583 | out.consume("*"); |
584 | } |
585 | } |
586 | |
587 | /** |
588 | * <pre> |
589 | * attrib |
590 | * : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* |
591 | * [ IDENT | STRING ] S* ]? ']' |
592 | * </pre> |
593 | */ |
594 | public static final class Attrib extends CssTree { |
595 | final String ident; |
596 | |
597 | @ReflectiveCtor |
598 | public Attrib( |
599 | FilePosition pos, String ident, |
600 | List<? extends CssTree> operatorAndValue) { |
601 | super(pos, operatorAndValue); |
602 | this.ident = ident; |
603 | } |
604 | |
605 | public Attrib(FilePosition pos, String ident, |
606 | AttribOperation operator, CssLiteral value) { |
607 | super(pos, null == operator |
608 | ? Collections.<CssTree>emptyList() |
609 | : Collections.unmodifiableList(Arrays.asList(operator, value))); |
610 | this.ident = ident; |
611 | } |
612 | |
613 | @Override |
614 | public String getValue() { return ident; } |
615 | public String getIdent() { return ident; } |
616 | |
617 | public AttribOperation getOperation() { |
618 | return children().isEmpty() ? null : (AttribOperation) children().get(0); |
619 | } |
620 | |
621 | public CssLiteral getRhsValue() { |
622 | return children().isEmpty() ? null : (CssLiteral) children().get(1); |
623 | } |
624 | |
625 | public void render(RenderContext r) { |
626 | TokenConsumer out = r.getOut(); |
627 | out.mark(getFilePosition()); |
628 | out.consume("["); |
629 | renderCssIdent(ident, r); |
630 | List<? extends CssTree> children = children(); |
631 | if (!children.isEmpty()) { |
632 | out.consume(" "); |
633 | renderSpaceGroup(children, r); |
634 | } |
635 | out.mark(FilePosition.endOfOrNull(getFilePosition())); |
636 | out.consume("]"); |
637 | } |
638 | } |
639 | |
640 | /** Operator used in {@link Attrib} */ |
641 | public static enum AttribOperator { |
642 | EQUAL("="), |
643 | INCLUDES("~="), |
644 | DASHMATCH("|="), |
645 | ; |
646 | private final String op; |
647 | AttribOperator(String op) { this.op = op; } |
648 | |
649 | public String getToken() { return op; } |
650 | } |
651 | |
652 | /** <pre>[ '=' | INCLUDES | DASHMATCH ]</pre> */ |
653 | public static final class AttribOperation extends CssTree { |
654 | final AttribOperator op; |
655 | /** @param none ignored but required for reflection. */ |
656 | @ReflectiveCtor |
657 | public AttribOperation( |
658 | FilePosition pos, AttribOperator op, List<? extends CssTree> none) { |
659 | this(pos, op); |
660 | } |
661 | |
662 | public AttribOperation(FilePosition pos, AttribOperator op) { |
663 | super(pos, Collections.<CssTree>emptyList()); |
664 | this.op = op; |
665 | } |
666 | |
667 | @Override |
668 | public AttribOperator getValue() { return op; } |
669 | |
670 | public void render(RenderContext r) { |
671 | r.getOut().mark(getFilePosition()); |
672 | r.getOut().consume(op.getToken()); |
673 | } |
674 | } |
675 | |
676 | /** |
677 | * <pre> |
678 | * pseudo |
679 | * : ':' [ IDENT | FUNCTION S* IDENT S* ')' ] |
680 | * </pre> |
681 | */ |
682 | public static final class Pseudo extends CssTree { |
683 | /** @param novalue ignored but required for reflection. */ |
684 | @ReflectiveCtor |
685 | public Pseudo( |
686 | FilePosition pos, Void novalue, List<? extends CssExprAtom> oneAtom) { |
687 | this(pos, oneAtom.get(0)); |
688 | } |
689 | |
690 | public Pseudo(FilePosition pos, CssExprAtom child) { |
691 | super(pos, Collections.singletonList(child)); |
692 | } |
693 | |
694 | public void render(RenderContext r) { |
695 | r.getOut().mark(getFilePosition()); |
696 | r.getOut().consume(":"); |
697 | children().get(0).render(r); |
698 | } |
699 | } |
700 | |
701 | /** |
702 | * A CSS property name, style value pair. |
703 | * <pre> |
704 | * declaration |
705 | * : property-declaration |
706 | * | empty-declaration |
707 | * | user-agent-hack |
708 | * </pre> |
709 | * The term "declaration" is used in the CSS2 spec to describe both |
710 | * {@link PropertyDeclaration} and {@link EmptyDeclaration}. Neither |
711 | * of those terms appear in the spec, and the <code>user-agent-hack</code> |
712 | * has no analog in the spec since it models a browser hack. |
713 | */ |
714 | public static abstract class Declaration extends PageElement { |
715 | Declaration(FilePosition pos, List<? extends CssTree> children) { |
716 | super(pos, children); |
717 | } |
718 | |
719 | <T extends CssTree> Declaration( |
720 | FilePosition pos, Class<T> childType, List<? extends T> children) { |
721 | super(pos, childType, children); |
722 | } |
723 | } |
724 | |
725 | /** |
726 | * <pre> |
727 | * empty-declaration |
728 | * : <i>empty</i> |
729 | * </pre> |
730 | */ |
731 | public static final class EmptyDeclaration extends Declaration { |
732 | /** |
733 | * @param novalue ignored but required for reflection. |
734 | * @param none ignored but required for reflection. |
735 | */ |
736 | @ReflectiveCtor |
737 | public EmptyDeclaration( |
738 | FilePosition pos, Void novalue, List<? extends CssTree> none) { |
739 | this(pos); |
740 | assert none.isEmpty(); |
741 | } |
742 | |
743 | public EmptyDeclaration(FilePosition pos) { |
744 | super(pos, Collections.<CssTree>emptyList()); |
745 | } |
746 | |
747 | public void render(RenderContext r) { |
748 | r.getOut().mark(getFilePosition()); |
749 | } |
750 | } |
751 | |
752 | /** |
753 | * <pre> |
754 | * property-declaration |
755 | * : property ':' S* expr prio? |
756 | * </pre> |
757 | */ |
758 | public static final class PropertyDeclaration extends Declaration { |
759 | private Property prop; |
760 | private Expr expr; |
761 | private Prio prio; |
762 | |
763 | /** @param novalue ignored but required for reflection. */ |
764 | @ReflectiveCtor |
765 | public PropertyDeclaration( |
766 | FilePosition pos, Void novalue, List<? extends CssTree> children) { |
767 | this(pos, children); |
768 | } |
769 | |
770 | public PropertyDeclaration( |
771 | FilePosition pos, List<? extends CssTree> children) { |
772 | super(pos, children); |
773 | } |
774 | |
775 | @Override |
776 | protected void childrenChanged() { |
777 | super.childrenChanged(); |
778 | List<? extends CssTree> children = children(); |
779 | prop = (Property) children.get(0); |
780 | expr = (Expr) children.get(1); |
781 | prio = children.size() > 2 ? (Prio) children.get(2) : null; |
782 | assert children.size() <= 3 && null != prop && null != expr; |
783 | } |
784 | |
785 | public Property getProperty() { return prop; } |
786 | public Expr getExpr() { return expr; } |
787 | public Prio getPrio() { return prio; } |
788 | |
789 | public void render(RenderContext r) { |
790 | r.getOut().mark(getFilePosition()); |
791 | prop.render(r); |
792 | r.getOut().consume(":"); |
793 | r.getOut().consume(" "); |
794 | expr.render(r); |
795 | if (null != prio) { |
796 | r.getOut().consume(" "); |
797 | prio.render(r); |
798 | } |
799 | } |
800 | } |
801 | |
802 | /** |
803 | * <pre> |
804 | * prio |
805 | * : IMPORTANT_SYM S* |
806 | * </pre> |
807 | */ |
808 | public static final class Prio extends CssTree { |
809 | final String value; |
810 | |
811 | /** @param none ignored but required for reflection. */ |
812 | @ReflectiveCtor |
813 | public Prio(FilePosition pos, String value, List<? extends CssTree> none) { |
814 | this(pos, value); |
815 | } |
816 | |
817 | public Prio(FilePosition pos, String value) { |
818 | super(pos, Collections.<CssTree>emptyList()); |
819 | this.value = value; |
820 | } |
821 | |
822 | @Override |
823 | public String getValue() { return value; } |
824 | |
825 | public void render(RenderContext r) { |
826 | r.getOut().mark(getFilePosition()); |
827 | r.getOut().consume("!"); |
828 | renderCssIdent(getValue().substring(1).toLowerCase(Locale.ENGLISH), r); |
829 | } |
830 | } |
831 | |
832 | /** |
833 | * <pre> |
834 | * expr |
835 | * : term [ operator term ]* |
836 | * </pre> |
837 | */ |
838 | public static final class Expr extends CssTree { |
839 | /** @param novalue ignored but required for reflection. */ |
840 | @ReflectiveCtor |
841 | public Expr( |
842 | FilePosition pos, Void novalue, List<? extends CssTree> children) { |
843 | this(pos, children); |
844 | } |
845 | |
846 | public Expr(FilePosition pos, List<? extends CssTree> children) { |
847 | super(pos, children); |
848 | } |
849 | |
850 | @Override |
851 | protected void childrenChanged() { |
852 | super.childrenChanged(); |
853 | assert 1 == children().size() % 2; |
854 | } |
855 | |
856 | public int getNTerms() { return (children().size() + 1) >> 1; } |
857 | public Term getNthTerm(int n) { return (Term) children().get(n * 2); } |
858 | public Operation getNthOperation(int n) { |
859 | return (Operation) children().get(1 + n * 2); |
860 | } |
861 | public Operator getNthOperator(int n) { |
862 | return ((Operation) children().get(1 + n * 2)).getOperator(); |
863 | } |
864 | |
865 | public void render(RenderContext r) { |
866 | renderSpaceGroup(children(), r); |
867 | } |
868 | } |
869 | |
870 | /** |
871 | * <pre> |
872 | * term |
873 | * : unary_operator? |
874 | * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | |
875 | * TIME S* | FREQ S* | function ] |
876 | * | STRING S* | IDENT S* | URI S* | RGB S* | UNICODERANGE S* | hexcolor |
877 | * </pre> |
878 | */ |
879 | public static final class Term extends CssTree { |
880 | private final UnaryOperator op; |
881 | @ReflectiveCtor |
882 | public Term(FilePosition pos, UnaryOperator op, |
883 | List<? extends CssExprAtom> oneatom) { |
884 | this(pos, op, oneatom.get(0)); |
885 | } |
886 | public Term(FilePosition pos, UnaryOperator op, CssExprAtom expr) { |
887 | super(pos, Collections.singletonList(expr)); |
888 | this.op = op; |
889 | } |
890 | @Override |
891 | public UnaryOperator getValue() { return op; } |
892 | |
893 | public UnaryOperator getOperator() { return op; } |
894 | |
895 | public CssExprAtom getExprAtom() { return (CssExprAtom) children().get(0); } |
896 | |
897 | public void render(RenderContext r) { |
898 | r.getOut().mark(getFilePosition()); |
899 | if (null != op) { |
900 | r.getOut().consume(op.symbol); |
901 | } |
902 | getExprAtom().render(r); |
903 | } |
904 | } |
905 | |
906 | /** |
907 | * A primitive CSS literal expression or function call. It is an atom in the |
908 | * sense that cannot be divided into an operator and an operand. |
909 | * See also http://www.w3.org/TR/REC-CSS2/syndata.html#values |
910 | */ |
911 | public abstract static class CssExprAtom extends CssTree { |
912 | CssExprAtom(FilePosition pos, List<? extends CssTree> children) { |
913 | super(pos, children); |
914 | } |
915 | <T extends CssTree> |
916 | CssExprAtom( |
917 | FilePosition pos, Class<T> childType, List<? extends T> children) { |
918 | super(pos, childType, children); |
919 | } |
920 | } |
921 | |
922 | // these patterns match unescaped values |
923 | private static final Pattern IDLITERAL = Pattern.compile("^#.+$"); |
924 | private static final Pattern CLASSLITERAL = Pattern.compile("^\\..+$"); |
925 | private static final Pattern IDENTLITERAL = Pattern.compile("^.+$"); |
926 | private static final Pattern HASHLITERAL = Pattern.compile( |
927 | // The CSS spec allows for 3 and 6 digit forms where "#ABC" is equivalent |
928 | // to "#AABBCC". IE filters use a non-standard 8 digit RRGGBB form. |
929 | "^#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3}(?:[a-fA-F0-9]{2})?)?$"); |
930 | private static final Pattern QUANTITYLITERAL = Pattern.compile( |
931 | "^(?:\\.\\d+|\\d+(?:\\.\\d+)?)([a-zA-Z]+|%)?$"); |
932 | private static final Pattern UNICODERANGELITERAL = Pattern.compile( |
933 | "^U\\+(?:[0-9a-fA-F]{1,6}-[0-9a-fA-F]{1,6}|[0-9a-fA-F?]{1,6})$", |
934 | Pattern.CASE_INSENSITIVE); |
935 | private static final Pattern SUBSTITUTION = Pattern.compile( |
936 | "^\\$\\{.*\\}(?:%|[a-z]+)?$", Pattern.DOTALL); |
937 | |
938 | /** |
939 | * Abstract base class for a literal value such as an ID, CLASS, URI, String, |
940 | * Color, or keyword value. |
941 | */ |
942 | public abstract static class CssLiteral extends CssExprAtom { |
943 | private String value; |
944 | /** |
945 | * @param inputValue the unescaped inputValue. Any unicode escapes have |
946 | * been converted to the corresponding character. |
947 | */ |
948 | CssLiteral(FilePosition pos, String inputValue) { |
949 | super(pos, Collections.<Expr>emptyList()); |
950 | setValue(inputValue); |
951 | } |
952 | @Override |
953 | public String getValue() { return value; } |
954 | /** |
955 | * @param newValue the unescaped value. Any unicode escapes have been |
956 | * converted to the corresponding character. |
957 | */ |
958 | public void setValue(String newValue) { |
959 | if (!checkValue(newValue)) { |
960 | throw new IllegalArgumentException(newValue); |
961 | } |
962 | this.value = newValue; |
963 | } |
964 | protected abstract boolean checkValue(String value); |
965 | } |
966 | |
967 | /** |
968 | * An ID in a selector, like {@code #foo}. |
969 | */ |
970 | public static final class IdLiteral extends CssLiteral { |
971 | /** @param none ignored but required for reflection. */ |
972 | @ReflectiveCtor |
973 | public IdLiteral( |
974 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
975 | this(pos, inputValue); |
976 | } |
977 | public IdLiteral(FilePosition pos, String value) { super(pos, value); } |
978 | @Override |
979 | protected boolean checkValue(String value) { |
980 | return IDLITERAL.matcher(value).matches(); |
981 | } |
982 | public void render(RenderContext r) { |
983 | r.getOut().mark(getFilePosition()); |
984 | r.getOut().consume("#"); |
985 | renderCssIdent(getValue().substring(1), r); |
986 | } |
987 | } |
988 | |
989 | /** |
990 | * A class name in a selector like {@code .foo}. |
991 | */ |
992 | public static final class ClassLiteral extends CssLiteral { |
993 | /** @param none ignored but required for reflection. */ |
994 | @ReflectiveCtor |
995 | public ClassLiteral( |
996 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
997 | this(pos, inputValue); |
998 | } |
999 | public ClassLiteral(FilePosition pos, String value) { super(pos, value); } |
1000 | @Override |
1001 | protected boolean checkValue(String value) { |
1002 | return CLASSLITERAL.matcher(value).matches(); |
1003 | } |
1004 | public void render(RenderContext r) { |
1005 | r.getOut().mark(getFilePosition()); |
1006 | r.getOut().consume("."); |
1007 | renderCssIdent(getValue().substring(1), r); |
1008 | } |
1009 | } |
1010 | |
1011 | /** |
1012 | * A string literal in a property value like {@code 'foo'}. |
1013 | */ |
1014 | public static final class StringLiteral extends CssLiteral { |
1015 | /** @param none ignored but required for reflection. */ |
1016 | @ReflectiveCtor |
1017 | public StringLiteral( |
1018 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
1019 | this(pos, inputValue); |
1020 | } |
1021 | public StringLiteral(FilePosition pos, String value) { super(pos, value); } |
1022 | @Override |
1023 | protected boolean checkValue(String value) { |
1024 | return value != null; |
1025 | } |
1026 | public void render(RenderContext r) { |
1027 | r.getOut().mark(getFilePosition()); |
1028 | renderCssString(getValue(), r); |
1029 | } |
1030 | } |
1031 | |
1032 | /** |
1033 | * A color value in a property value like {@code #AABBCC}. |
1034 | */ |
1035 | public static final class HashLiteral extends CssLiteral { |
1036 | /** @param none ignored but required for reflection. */ |
1037 | @ReflectiveCtor |
1038 | public HashLiteral( |
1039 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
1040 | this(pos, inputValue); |
1041 | } |
1042 | public HashLiteral(FilePosition pos, String value) { super(pos, value); } |
1043 | public static HashLiteral hex(FilePosition pos, int n, int digits) { |
1044 | StringBuilder sb = new StringBuilder(digits + 1); |
1045 | sb.append('#'); |
1046 | while (--digits >= 0) { |
1047 | sb.append("0123456789abcdef".charAt((n >>> (digits * 4)) & 0xf)); |
1048 | } |
1049 | return new HashLiteral(pos, sb.toString()); |
1050 | } |
1051 | @Override |
1052 | protected boolean checkValue(String value) { |
1053 | return HASHLITERAL.matcher(value).matches(); |
1054 | } |
1055 | public void render(RenderContext r) { |
1056 | r.getOut().mark(getFilePosition()); |
1057 | r.getOut().consume(getValue()); |
1058 | } |
1059 | } |
1060 | |
1061 | /** |
1062 | * A numeric quantity like {@code 5cm}, {@code 100%}, or {@code 0}. |
1063 | */ |
1064 | public static final class QuantityLiteral extends CssLiteral { |
1065 | /** @param none ignored but required for reflection. */ |
1066 | @ReflectiveCtor |
1067 | public QuantityLiteral( |
1068 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
1069 | this(pos, inputValue); |
1070 | } |
1071 | public QuantityLiteral(FilePosition pos, String value) { |
1072 | super(pos, value); |
1073 | } |
1074 | @Override |
1075 | protected boolean checkValue(String value) { |
1076 | return QUANTITYLITERAL.matcher(value).matches(); |
1077 | } |
1078 | public void render(RenderContext r) { |
1079 | r.getOut().mark(getFilePosition()); |
1080 | r.getOut().consume(getValue()); |
1081 | } |
1082 | } |
1083 | |
1084 | /** |
1085 | * A range of unicode code-points. |
1086 | */ |
1087 | public static final class UnicodeRangeLiteral extends CssLiteral { |
1088 | /** @param none ignored but required for reflection. */ |
1089 | @ReflectiveCtor |
1090 | public UnicodeRangeLiteral( |
1091 | FilePosition pos, String inputValue, List<? extends CssTree> none) { |
1092 | this(pos, inputValue); |
1093 | } |
1094 | public UnicodeRangeLiteral(FilePosition pos, String value) { |
1095 | super(pos, value); |
1096 | } |
1097 | @Override |
1098 | protected boolean checkValue(String value) { |
1099 | return UNICODERANGELITERAL.matcher(value).matches(); |
1100 | } |
1101 | public void render(RenderContext r) { |
1102 | r.getOut().mark(getFilePosition()); |
1103 | r.getOut().consume(getValue()); |
1104 | } |
1105 | } |
1106 | |
1107 | /** |
1108 | * A uri literal like {@code url('foo/bar.css')}. |
1109 | */ |
1110 | public static final class UriLiteral extends CssLiteral { |
1111 | /** @param none ignored but required for reflection. */ |
1112 | @ReflectiveCtor |
1113 | public UriLiteral( |
1114 | FilePosition pos, String value, List<? extends CssTree> none) { |
1115 | this(pos, URI.create(value)); |
1116 | } |
1117 | public UriLiteral(FilePosition pos, URI value) { |
1118 | super(pos, value.toString()); |
1119 | } |
1120 | @Override |
1121 | protected boolean checkValue(String value) { |
1122 | try { |
1123 | URI.create(value); |
1124 | return true; |
1125 | } catch (IllegalArgumentException ex) { |
1126 | return false; |
1127 | } |
1128 | } |
1129 | public void render(RenderContext r) { |
1130 | TokenConsumer out = r.getOut(); |
1131 | out.mark(getFilePosition()); |
1132 | out.consume("url"); |
1133 | out.consume("("); |
1134 | String url; |
1135 | try { |
1136 | url = UriUtil.normalizeUri(getValue()); |
1137 | } catch (URISyntaxException ex) { |
1138 | url = "data:,"; |
1139 | } |
1140 | renderCssString(url, r); |
1141 | out.mark(FilePosition.endOfOrNull(getFilePosition())); |
1142 | out.consume(")"); |
1143 | } |
1144 | } |
1145 | |
1146 | /** |
1147 | * An identifier in a selector like {@code div} or a keyword in a property |
1148 | * value like {@code auto}. |
1149 | */ |
1150 | public static final class IdentLiteral extends CssLiteral { |
1151 | /** @param none ignored but required for reflection. */ |
1152 | @ReflectiveCtor |
1153 | public IdentLiteral( |
1154 | FilePosition pos, String value, List<? extends CssTree> none) { |
1155 | this(pos, value); |
1156 | } |
1157 | public IdentLiteral(FilePosition pos, String value) { |
1158 | super(pos, value); |
1159 | } |
1160 | @Override |
1161 | protected boolean checkValue(String value) { |
1162 | return IDENTLITERAL.matcher(value).matches(); |
1163 | } |
1164 | public void render(RenderContext r) { |
1165 | r.getOut().mark(getFilePosition()); |
1166 | renderCssIdent(getValue(), r); |
1167 | } |
1168 | } |
1169 | |
1170 | /** |
1171 | * <pre> |
1172 | * function |
1173 | * : FUNCTION S* expr ')' S* |
1174 | * </pre> |
1175 | */ |
1176 | public static final class FunctionCall extends CssExprAtom { |
1177 | private final Name name; |
1178 | @ReflectiveCtor |
1179 | public FunctionCall( |
1180 | FilePosition pos, Name name, List<? extends Expr> expr) { |
1181 | this(pos, name, expr.get(0)); |
1182 | } |
1183 | public FunctionCall(FilePosition pos, Name name, Expr expr) { |
1184 | super(pos, Collections.singletonList(expr)); |
1185 | this.name = name; |
1186 | } |
1187 | @Override |
1188 | public Name getValue() { return name; } |
1189 | public Name getName() { return name; } |
1190 | public Expr getArguments() { return (Expr) children().get(0); } |
1191 | |
1192 | @Override |
1193 | protected void childrenChanged() { |
1194 | super.childrenChanged(); |
1195 | assert 1 == children().size() && (children().get(0) instanceof Expr); |
1196 | } |
1197 | public void render(RenderContext r) { |
1198 | TokenConsumer out = r.getOut(); |
1199 | out.mark(getFilePosition()); |
1200 | renderCssIdent(name.getCanonicalForm(), r); |
1201 | out.consume("("); |
1202 | children().get(0).render(r); |
1203 | out.mark(FilePosition.endOfOrNull(getFilePosition())); |
1204 | out.consume(")"); |
1205 | } |
1206 | } |
1207 | |
1208 | /** |
1209 | * An IE extension used in the filter property as described at |
1210 | * http://msdn.microsoft.com/en-us/library/ms532847(VS.85).aspx. |
1211 | */ |
1212 | public static final class ProgId extends CssExprAtom { |
1213 | private final Name name; |
1214 | |
1215 | @ReflectiveCtor |
1216 | public ProgId( |
1217 | FilePosition pos, Name name, List<? extends ProgIdAttribute> attrs) { |
1218 | super(pos, ProgIdAttribute.class, attrs); |
1219 | this.name = name; |
1220 | } |
1221 | |
1222 | @Override |
1223 | public Name getValue() { return name; } |
1224 | public Name getName() { return name; } |
1225 | @Override |
1226 | public List<? extends ProgIdAttribute> children() { |
1227 | return childrenAs(ProgIdAttribute.class); |
1228 | } |
1229 | |
1230 | public void render(RenderContext r) { |
1231 | TokenConsumer tc = r.getOut(); |
1232 | tc.mark(getFilePosition()); |
1233 | tc.consume("progid"); |
1234 | tc.consume(":"); |
1235 | boolean dot = false; |
1236 | for (String part : name.getCanonicalForm().split("\\.")) { |
1237 | if (dot) { tc.consume("."); } |
1238 | dot = true; |
1239 | renderCssIdent(part, r); |
1240 | } |
1241 | tc.consume("("); |
1242 | renderCommaGroup(children(), r); |
1243 | tc.consume(")"); |
1244 | } |
1245 | } |
1246 | |
1247 | public static final class ProgIdAttribute extends CssTree { |
1248 | private final Name name; |
1249 | |
1250 | @ReflectiveCtor |
1251 | public ProgIdAttribute( |
1252 | FilePosition pos, Name name, List<? extends Term> value) { |
1253 | super(pos, Term.class, value); |
1254 | this.name = name; |
1255 | } |
1256 | |
1257 | @Override |
1258 | public Name getValue() { return name; } |
1259 | public Name getName() { return name; } |
1260 | @Override |
1261 | public List<? extends Term> children() { |
1262 | return childrenAs(Term.class); |
1263 | } |
1264 | public Term getPropertyValue() { return children().get(0); } |
1265 | @Override |
1266 | protected void childrenChanged() { |
1267 | super.childrenChanged(); |
1268 | List<? extends Term> terms = children(); |
1269 | if (terms.size() != 1) { throw new IllegalStateException(); } |
1270 | CssExprAtom atom = terms.get(0).getExprAtom(); |
1271 | if (!(atom instanceof CssLiteral)) { |
1272 | throw new ClassCastException(atom.getClass().getName()); |
1273 | } |
1274 | } |
1275 | |
1276 | public void render(RenderContext r) { |
1277 | TokenConsumer tc = r.getOut(); |
1278 | tc.mark(getFilePosition()); |
1279 | renderCssIdent(name.getCanonicalForm(), r); |
1280 | tc.consume("="); |
1281 | getPropertyValue().render(r); |
1282 | } |
1283 | } |
1284 | |
1285 | /** |
1286 | * A template substitution in a CSS stylesheet. This is not part of the |
1287 | * CSS language, and will only be produced if |
1288 | * {@link com.google.caja.lexer.CssLexer#allowSubstitutions} |
1289 | * is set. |
1290 | */ |
1291 | public static final class Substitution extends CssLiteral { |
1292 | /** @param none ignored but required for reflection. */ |
1293 | public Substitution( |
1294 | FilePosition pos, String value, List<? extends CssTree> none) { |
1295 | this(pos, value); |
1296 | } |
1297 | public Substitution(FilePosition pos, String value) { |
1298 | super(pos, value); |
1299 | } |
1300 | |
1301 | public String getBody() { |
1302 | String value = getValue(); |
1303 | // Produce a string of the same length, so that the file position makes |
1304 | // sense. |
1305 | StringBuilder sb = new StringBuilder(" "); // skip ${ |
1306 | int end = value.lastIndexOf('}'); // until } |
1307 | sb.append(value, 2, end); |
1308 | while (sb.length() < value.length()) { sb.append(' '); } |
1309 | return sb.toString(); |
1310 | } |
1311 | |
1312 | public String getSuffix() { |
1313 | String value = getValue(); |
1314 | return value.substring(value.lastIndexOf('}') + 1); |
1315 | } |
1316 | |
1317 | @Override |
1318 | public boolean checkValue(String value) { |
1319 | // TODO(mikesamuel): maybe enforce the convention that there are matched |
1320 | // parentheses outside C-style strings. |
1321 | return SUBSTITUTION.matcher(value).matches(); |
1322 | } |
1323 | |
1324 | public void render(RenderContext r) { |
1325 | r.getOut().mark(getFilePosition()); |
1326 | r.getOut().consume(getValue()); |
1327 | } |
1328 | } |
1329 | |
1330 | /** See http://www.w3.org/TR/REC-CSS2/selector.html#q2 */ |
1331 | public static final class Combination extends CssTree { |
1332 | final Combinator comb; |
1333 | |
1334 | /** @param none ignored but required for reflection. */ |
1335 | @ReflectiveCtor |
1336 | public Combination( |
1337 | FilePosition pos, Combinator comb, List<? extends CssTree> none) { |
1338 | this(pos, comb); |
1339 | } |
1340 | public Combination(FilePosition pos, Combinator comb) { |
1341 | super(pos, Collections.<CssTree>emptyList()); |
1342 | this.comb = comb; |
1343 | } |
1344 | |
1345 | @Override |
1346 | public Combinator getValue() { return comb; } |
1347 | public Combinator getCombinator() { return comb; } |
1348 | public void render(RenderContext r) { |
1349 | r.getOut().mark(getFilePosition()); |
1350 | if (null != comb.symbol) { |
1351 | r.getOut().consume(comb.symbol); |
1352 | } |
1353 | } |
1354 | } |
1355 | |
1356 | /** See http://www.w3.org/TR/REC-CSS2/selector.html#q2 */ |
1357 | public static final class Operation extends CssTree { |
1358 | final Operator op; |
1359 | |
1360 | /** @param none ignored but required for reflection. */ |
1361 | @ReflectiveCtor |
1362 | public Operation( |
1363 | FilePosition pos, Operator op, List<? extends CssTree> none) { |
1364 | this(pos, op); |
1365 | } |
1366 | public Operation(FilePosition pos, Operator op) { |
1367 | super(pos, Collections.<CssTree>emptyList()); |
1368 | this.op = op; |
1369 | } |
1370 | |
1371 | @Override |
1372 | public Operator getValue() { return op; } |
1373 | |
1374 | public Operator getOperator() { return op; } |
1375 | |
1376 | public void render(RenderContext r) { |
1377 | r.getOut().mark(getFilePosition()); |
1378 | if (null != op.symbol) { |
1379 | r.getOut().consume(op.symbol); |
1380 | } |
1381 | } |
1382 | } |
1383 | |
1384 | /** |
1385 | * <pre> |
1386 | * operator |
1387 | * : '/' S* | ',' S* | <i>empty</i> |
1388 | * </pre> |
1389 | */ |
1390 | public static enum Operator { |
1391 | DIV("/"), |
1392 | COMMA(","), |
1393 | EQUAL("="), |
1394 | NONE(null), |
1395 | ; |
1396 | private final String symbol; |
1397 | Operator(String symbol) { this.symbol = symbol; } |
1398 | public String getSymbol() { return symbol; } |
1399 | } |
1400 | |
1401 | /** |
1402 | * <pre> |
1403 | * unary_operator |
1404 | * : '-' | '+' |
1405 | * </pre> |
1406 | */ |
1407 | public static enum UnaryOperator { |
1408 | NEGATION("-"), |
1409 | IDENTITY("+"), |
1410 | ; |
1411 | private final String symbol; |
1412 | UnaryOperator(String symbol) { this.symbol = symbol; } |
1413 | public String getSymbol() { return symbol; } |
1414 | } |
1415 | |
1416 | /** |
1417 | * <pre> |
1418 | * combinator |
1419 | * : '+' S* | '>' S* | <i>empty</i> |
1420 | * </pre> |
1421 | */ |
1422 | public static enum Combinator { |
1423 | SIBLING("+"), |
1424 | CHILD(">"), |
1425 | DESCENDANT(null), |
1426 | ; |
1427 | private final String symbol; |
1428 | Combinator(String symbol) { this.symbol = symbol; } |
1429 | public String getSymbol() { return symbol; } |
1430 | } |
1431 | |
1432 | /** |
1433 | * A hack that uses syntactically invalid CSS to make a rule visible on some |
1434 | * user agents but invisible on others. |
1435 | * <pre> |
1436 | * user-agent-hack |
1437 | * : '*' declaration |
1438 | * </pre> |
1439 | */ |
1440 | public static final class UserAgentHack extends Declaration { |
1441 | private final EnumSet<UserAgent> enabledOn; |
1442 | |
1443 | @ReflectiveCtor |
1444 | public UserAgentHack( |
1445 | FilePosition pos, Set<UserAgent> enabledOn, |
1446 | List<? extends PropertyDeclaration> decl) { |
1447 | super(pos, PropertyDeclaration.class, decl); |
1448 | this.enabledOn = EnumSet.copyOf(enabledOn); |
1449 | } |
1450 | |
1451 | @Override |
1452 | public EnumSet<UserAgent> getValue() { return EnumSet.copyOf(enabledOn); } |
1453 | |
1454 | @Override |
1455 | protected void childrenChanged() { |
1456 | super.childrenChanged(); |
1457 | List<? extends CssTree> children = children(); |
1458 | if (children.size() != 1) { throw new IllegalStateException(); } |
1459 | if (!(children.get(0) instanceof PropertyDeclaration)) { |
1460 | throw new ClassCastException(children.get(0).getClass().getName()); |
1461 | } |
1462 | } |
1463 | |
1464 | public PropertyDeclaration getDeclaration() { |
1465 | return (PropertyDeclaration) children().get(0); |
1466 | } |
1467 | |
1468 | public void render(RenderContext r) { |
1469 | TokenConsumer out = r.getOut(); |
1470 | out.mark(getFilePosition()); |
1471 | out.consume("*"); |
1472 | getDeclaration().render(r); |
1473 | } |
1474 | } |
1475 | |
1476 | /** An identifier for a version of a supported browser. */ |
1477 | public static enum UserAgent { |
1478 | IE6, |
1479 | IE7, |
1480 | IE8, |
1481 | ; |
1482 | |
1483 | public static EnumSet<UserAgent> ie7OrOlder() { |
1484 | return EnumSet.of(IE6, IE7); |
1485 | } |
1486 | } |
1487 | |
1488 | private static void renderStatements( |
1489 | List<? extends CssTree> children, FilePosition pos, RenderContext r) { |
1490 | TokenConsumer out = r.getOut(); |
1491 | out.mark(pos); |
1492 | out.consume("{"); |
1493 | CssTree last = null; |
1494 | for (CssTree decl : children) { |
1495 | if (last != null) { |
1496 | out.consume(";"); |
1497 | } |
1498 | decl.render(r); |
1499 | last = decl; |
1500 | } |
1501 | out.mark(FilePosition.endOfOrNull(pos)); |
1502 | out.consume("}"); |
1503 | } |
1504 | |
1505 | private static void renderCommaGroup( |
1506 | List<? extends CssTree> children, RenderContext r) { |
1507 | boolean first = true; |
1508 | for (CssTree child : children) { |
1509 | if (!first) { |
1510 | r.getOut().consume(","); |
1511 | } else { |
1512 | first = false; |
1513 | } |
1514 | child.render(r); |
1515 | } |
1516 | } |
1517 | |
1518 | private static void renderSpaceGroup( |
1519 | List<? extends CssTree> children, RenderContext r) { |
1520 | boolean needSpace = false; |
1521 | for (CssTree child : children) { |
1522 | if (needSpace) { |
1523 | r.getOut().consume(" "); |
1524 | } else { |
1525 | needSpace = true; |
1526 | } |
1527 | child.render(r); |
1528 | } |
1529 | } |
1530 | |
1531 | private static void renderCssIdent(String ident, RenderContext r) { |
1532 | StringBuilder sb = new StringBuilder(); |
1533 | Escaping.escapeCssIdent(ident, sb); |
1534 | r.getOut().consume(sb.toString()); |
1535 | } |
1536 | |
1537 | private static void renderCssString(String s, RenderContext r) { |
1538 | StringBuilder sb = new StringBuilder(); |
1539 | sb.append('\''); |
1540 | Escaping.escapeCssString(s, sb); |
1541 | sb.append('\''); |
1542 | r.getOut().consume(sb.toString()); |
1543 | } |
1544 | } |