1 | // Copyright (C) 2009 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.plugin.templates; |
16 | |
17 | import com.google.caja.lexer.FilePosition; |
18 | import com.google.caja.parser.css.CssTree; |
19 | import com.google.caja.parser.html.Namespaces; |
20 | import com.google.caja.parser.html.Nodes; |
21 | import com.google.caja.parser.js.ArrayConstructor; |
22 | import com.google.caja.parser.js.Expression; |
23 | import com.google.caja.parser.js.ExpressionStmt; |
24 | import com.google.caja.parser.js.Statement; |
25 | import com.google.caja.parser.js.StringLiteral; |
26 | import com.google.caja.parser.quasiliteral.QuasiBuilder; |
27 | import com.google.caja.parser.quasiliteral.ReservedNames; |
28 | import com.google.caja.plugin.CssRuleRewriter; |
29 | import com.google.caja.util.Lists; |
30 | import com.google.caja.util.Pair; |
31 | |
32 | import java.util.List; |
33 | |
34 | import org.w3c.dom.Document; |
35 | import org.w3c.dom.Element; |
36 | |
37 | /** |
38 | * Attaches CSS to either the static HTML or the uncajoled JS as appropriate. |
39 | * <p> |
40 | * Depends on <code>emitCss___</code> as defined in DOMita. |
41 | * |
42 | * @author mikesamuel@gmail.com |
43 | */ |
44 | final class SafeCssMaker { |
45 | private final List<CssTree.StyleSheet> validatedStylesheets; |
46 | private final Document doc; |
47 | |
48 | SafeCssMaker(List<CssTree.StyleSheet> validatedStylesheets, Document doc) { |
49 | this.validatedStylesheets = validatedStylesheets; |
50 | this.doc = doc; |
51 | } |
52 | |
53 | Pair<Statement, Element> make() { |
54 | if (validatedStylesheets.isEmpty()) { return Pair.pair(null, null); } |
55 | |
56 | // Accumulates dynamic CSS that will be added to the JS. |
57 | List<Expression> cssParts = Lists.newArrayList(); |
58 | // Accumulate static CSS that can be embedded in the DOM. |
59 | StringBuilder css = new StringBuilder(); |
60 | FilePosition staticPos = null, dynamicPos = null; |
61 | for (CssTree.StyleSheet ss : validatedStylesheets) { |
62 | ArrayConstructor ac = CssRuleRewriter.cssToJs(ss); |
63 | List<? extends Expression> children = ac.children(); |
64 | if (children.isEmpty()) { continue; } |
65 | FilePosition acPos = ac.getFilePosition(); |
66 | Expression child0 = children.get(0); |
67 | // The CssRuleRewriter gets to distinguish between static and dynamic. |
68 | // If the output is a single string, then joining it on the idClass would |
69 | // not add any information, so we can put it in the static HTML. |
70 | if (children.size() == 1 && child0 instanceof StringLiteral) { |
71 | css.append('\n').append(((StringLiteral) child0).getUnquotedValue()); |
72 | staticPos = staticPos == null |
73 | ? acPos : FilePosition.span(staticPos, acPos); |
74 | } else { |
75 | // Don't just push all onto the list since that would create an |
76 | // extra, spurious separator after they're joined. |
77 | // To avoid the spurious separator, we concatenate the last item |
78 | // already on cssParts with child0. |
79 | int n = cssParts.size(); |
80 | if (n == 0) { |
81 | cssParts.addAll(children); |
82 | } else { |
83 | JsConcatenator cat = new JsConcatenator(); |
84 | cat.append(cssParts.get(n - 1)); |
85 | cat.append(FilePosition.startOf(child0.getFilePosition()), "\n"); |
86 | cat.append(child0); |
87 | cssParts.set(n - 1, cat.toExpression(false)); |
88 | cssParts.addAll(children.subList(1, children.size())); |
89 | } |
90 | dynamicPos = dynamicPos == null |
91 | ? acPos : FilePosition.span(dynamicPos, acPos); |
92 | } |
93 | } |
94 | |
95 | ExpressionStmt dynamicCss = null; |
96 | Element staticCss = null; |
97 | // Emit any dynamic CSS. |
98 | if (!cssParts.isEmpty()) { |
99 | // The CSS rule |
100 | // p { color: purple } |
101 | // is converted to the JavaScript |
102 | // IMPORTS___.emitCss___( |
103 | // ['.', ' p { color: purple }'] |
104 | // .join(IMPORTS___.getIdClass___())); |
105 | // |
106 | // If IMPORTS___.getIdClass() returns "g123___", then the resulting |
107 | // .g123___ p { color: purple } |
108 | // will only make purple paragraphs that are under a node with class |
109 | // g123__. |
110 | dynamicCss = new ExpressionStmt( |
111 | dynamicPos, |
112 | (Expression) QuasiBuilder.substV( |
113 | ReservedNames.IMPORTS |
114 | + ".emitCss___(@cssParts./*@synthetic*/join(" |
115 | + ReservedNames.IMPORTS + ".getIdClass___()))", |
116 | "cssParts", new ArrayConstructor(dynamicPos, cssParts))); |
117 | } |
118 | |
119 | // Emit any static CSS. |
120 | if (css.length() != 0) { |
121 | String nsUri = Namespaces.HTML_NAMESPACE_URI; |
122 | Element style = doc.createElementNS(nsUri, "style"); |
123 | style.setAttributeNS(nsUri, "type", "text/css"); |
124 | style.appendChild(doc.createTextNode(css.toString())); |
125 | Nodes.setFilePositionFor(style, dynamicPos); |
126 | staticCss = style; |
127 | } |
128 | |
129 | return Pair.pair((Statement) dynamicCss, staticCss); |
130 | } |
131 | } |