1 | // Copyright (C) 2008 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.render; |
16 | |
17 | import com.google.caja.lexer.FilePosition; |
18 | import com.google.caja.lexer.TokenConsumer; |
19 | |
20 | import java.util.ArrayList; |
21 | import java.util.List; |
22 | |
23 | /** |
24 | * A formatter that indents code for CSS paying careful attention to where |
25 | * adding whitespace between tokens would change tokenization. |
26 | * |
27 | * @author mikesamuel@gmail.com |
28 | */ |
29 | public final class CssPrettyPrinter extends AbstractRenderer { |
30 | /** |
31 | * Stack of indentation positions. |
32 | * Curly brackets indent to two past the last stack position and |
33 | * parenthetical blocks indent to the open parenthesis. |
34 | * |
35 | * A non-negative number indicates a curly bracket indentation and a negative |
36 | * number a parenthetical indentation. |
37 | */ |
38 | private List<Integer> indentStack = new ArrayList<Integer>(); |
39 | /** Number of characters written to out since the last line-break. */ |
40 | private int charInLine; |
41 | |
42 | /** True if the last token needs a following space. */ |
43 | private char pendingSpace = '\0'; |
44 | |
45 | /** |
46 | * @param out receives the rendered text. Typically a {@link Concatenator}. |
47 | */ |
48 | public CssPrettyPrinter(TokenConsumer out) { |
49 | super(out); |
50 | } |
51 | |
52 | public void mark(FilePosition pos) { out.mark(pos); } |
53 | |
54 | @Override |
55 | public void consume(String text) { |
56 | TokenClassification tClass = TokenClassification.classify(text); |
57 | if (tClass == null) { return; } |
58 | switch (tClass) { |
59 | case LINEBREAK: |
60 | // Allow external code to force line-breaks. |
61 | // This allows us to create a composite-renderer that renders |
62 | // original source code next to translated source code. |
63 | if (pendingSpace == '\n') { newLine(); } |
64 | pendingSpace = '\n'; |
65 | return; |
66 | case SPACE: |
67 | if (pendingSpace != '\n') { pendingSpace = ' '; } |
68 | return; |
69 | case COMMENT: |
70 | emit(text); |
71 | return; |
72 | default: break; |
73 | } |
74 | |
75 | char spaceBefore = pendingSpace; |
76 | pendingSpace = '\0'; |
77 | char spaceAfter = '\0'; |
78 | |
79 | Integer nextIndent = null; |
80 | if (text.length() == 1) { |
81 | char ch0 = text.charAt(0); |
82 | switch (ch0) { |
83 | case '{': |
84 | if (spaceBefore != '\n') { |
85 | spaceBefore = ' '; |
86 | } |
87 | nextIndent = getIndentation() + 2; |
88 | spaceAfter = '\n'; |
89 | break; |
90 | case '}': |
91 | spaceAfter = spaceBefore = '\n'; |
92 | popIndentStack(); |
93 | break; |
94 | case ',': |
95 | spaceBefore = '\0'; |
96 | spaceAfter = ' '; |
97 | break; |
98 | case ';': |
99 | spaceBefore = '\0'; |
100 | // If we're rendering a declaration group, e.g. inside an HTML style |
101 | // attribute, separate them with spaces, but if we're pretty printing |
102 | // a stylesheet, put newlines between declarations. |
103 | spaceAfter = indentStack.isEmpty() ? ' ' : '\n'; |
104 | break; |
105 | } |
106 | } |
107 | |
108 | switch (spaceBefore) { |
109 | case '\n': |
110 | newLine(); |
111 | break; |
112 | case ' ': |
113 | space(); |
114 | break; |
115 | } |
116 | |
117 | indent(); |
118 | emit(text); |
119 | if (nextIndent != null) { |
120 | pushIndent(nextIndent); |
121 | } |
122 | |
123 | pendingSpace = spaceAfter; |
124 | } |
125 | |
126 | private int getIndentation() { |
127 | return indentStack.isEmpty() ? 0 : indentStack.get(indentStack.size() - 1); |
128 | } |
129 | |
130 | private void pushIndent(int indent) { |
131 | indentStack.add(indent); |
132 | } |
133 | |
134 | private void popIndentStack() { |
135 | if (!indentStack.isEmpty()) { |
136 | indentStack.remove(indentStack.size() - 1); |
137 | } |
138 | } |
139 | |
140 | private void indent() { |
141 | if (charInLine != 0) { return; } |
142 | int indent = getIndentation(); |
143 | |
144 | charInLine += indent; |
145 | String spaces = " "; |
146 | while (indent >= spaces.length()) { |
147 | out.consume(spaces); |
148 | indent -= spaces.length(); |
149 | } |
150 | if (indent != 0) { |
151 | out.consume(spaces.substring(0, indent)); |
152 | } |
153 | } |
154 | |
155 | private void newLine() { |
156 | if (charInLine == 0) { return; } |
157 | charInLine = 0; |
158 | out.consume("\n"); |
159 | } |
160 | |
161 | private void space() { |
162 | if (charInLine != 0) { |
163 | out.consume(" "); |
164 | ++charInLine; |
165 | } |
166 | } |
167 | |
168 | private void emit(String s) { |
169 | out.consume(s); |
170 | int n = s.length(); |
171 | for (int i = n; --i >= 0;) { |
172 | char ch = s.charAt(i); |
173 | if (ch == '\r' || ch == '\n') { |
174 | charInLine = n - i; |
175 | break; |
176 | } |
177 | } |
178 | charInLine += n; |
179 | } |
180 | } |