001 /*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 *
014 * See the NOTICE file distributed with this work for additional
015 * information regarding copyright ownership.
016 */
017
018 package com.osbcp.cssparser;
019
020 import java.util.ArrayList;
021 import java.util.LinkedHashMap;
022 import java.util.List;
023 import java.util.Map;
024 import java.util.Map.Entry;
025
026 import com.osbcp.cssparser.IncorrectFormatException.ErrorCode;
027
028 /**
029 * Main logic for the CSS parser.
030 *
031 * @author <a href="mailto:christoffer@christoffer.me">Christoffer Pettersson</a>
032 */
033
034 public final class CSSParser {
035
036 /**
037 * Reads CSS as a String and returns back a list of Rules.
038 *
039 * @param css A String representation of CSS.
040 * @return A list of Rules
041 * @throws Exception If any errors occur.
042 */
043
044 public static List<Rule> parse(final String css) throws Exception {
045
046 CSSParser parser = new CSSParser();
047
048 List<Rule> rules = new ArrayList<Rule>();
049
050 if (css == null || css.trim().isEmpty()) {
051 return rules;
052 }
053
054 for (int i = 0; i < css.length(); i++) {
055
056 char c = css.charAt(i);
057
058 if (i < css.length() - 1) {
059
060 char nextC = css.charAt(i + 1);
061 parser.parse(rules, c, nextC);
062
063 } else {
064
065 parser.parse(rules, c, null);
066
067 }
068
069 }
070
071 return rules;
072 }
073
074 private List<String> selectorNames;
075 private String selectorName;
076 private String propertyName;
077 private String valueName;
078 private Map<String, String> map;
079 private State state;
080 private Character previousChar;
081 private State beforeCommentMode;
082
083 /**
084 * Creates a new parser.
085 */
086
087 private CSSParser() {
088 this.selectorName = "";
089 this.propertyName = "";
090 this.valueName = "";
091 this.map = new LinkedHashMap<String, String>();
092 this.state = State.INSIDE_SELECTOR;
093 this.previousChar = null;
094 this.beforeCommentMode = null;
095 this.selectorNames = new ArrayList<String>();
096 }
097
098 /**
099 * Main parse logic.
100 *
101 * @param rules The list of rules.
102 * @param c The current currency.
103 * @param nextC The next currency (or null).
104 * @throws Exception If any errors occurs.
105 */
106
107 private void parse(final List<Rule> rules, final Character c, final Character nextC) throws Exception {
108
109 // Special case if we find a comment
110 if (Chars.SLASH.equals(c) && Chars.STAR.equals(nextC)) {
111 beforeCommentMode = state;
112 state = State.INSIDE_COMMENT;
113 }
114
115 switch (state) {
116
117 case INSIDE_SELECTOR: {
118 parseSelector(c);
119 break;
120 }
121 case INSIDE_COMMENT: {
122 parseComment(c);
123 break;
124 }
125 case INSIDE_PROPERTY_NAME: {
126 parsePropertyName(rules, c);
127 break;
128 }
129 case INSIDE_VALUE: {
130 parseValue(c);
131 break;
132 }
133 case INSIDE_VALUE_ROUND_BRACKET: {
134 parseValueInsideRoundBrackets(c);
135 break;
136 }
137
138 }
139
140 // Save the previous character
141 previousChar = c;
142
143 }
144
145 /**
146 * Parse a value.
147 *
148 * @param c The current character.
149 * @throws IncorrectFormatException If any errors occur.
150 */
151
152 private void parseValue(final Character c) throws IncorrectFormatException {
153
154 // Special case if the value is a data uri, the value can contain a ;
155 // boolean valueHasDataURI = valueName.toLowerCase().indexOf("data:") != -1;
156
157 if (Chars.SEMI_COLON.equals(c)) {
158
159 // Store it in the values map
160 map.put(propertyName.trim(), valueName.trim());
161 propertyName = "";
162 valueName = "";
163
164 state = State.INSIDE_PROPERTY_NAME;
165 return;
166
167 } else if (Chars.ROUND_BRACKET_BEG.equals(c)) {
168
169 valueName += Chars.ROUND_BRACKET_BEG;
170
171 state = State.INSIDE_VALUE_ROUND_BRACKET;
172 return;
173
174 } else if (Chars.BRACKET_END.equals(c)) {
175
176 throw new IncorrectFormatException(ErrorCode.FOUND_END_BRACKET_BEFORE_SEMICOLON, "The value '" + valueName.trim() + "' for property '" + propertyName.trim() + "' in the selector '" + selectorName.trim() + "' should end with an ';', not with '}'.");
177
178 } else {
179
180 valueName += c;
181 return;
182
183 }
184
185 }
186
187 /**
188 * Parse value inside a round bracket (
189 *
190 * @param c The current character.
191 * @throws IncorrectFormatException If any error occurs.
192 */
193
194 private void parseValueInsideRoundBrackets(final Character c) throws IncorrectFormatException {
195
196 if (Chars.ROUND_BRACKET_END.equals(c)) {
197
198 valueName += Chars.ROUND_BRACKET_END;
199 state = State.INSIDE_VALUE;
200 return;
201
202 } else {
203
204 valueName += c;
205 return;
206
207 }
208
209 }
210
211 /**
212 * Parse property name.
213 *
214 * @param rules The list of rules.
215 * @param c The current character.
216 * @throws IncorrectFormatException If any error occurs
217 */
218
219 private void parsePropertyName(final List<Rule> rules, final Character c) throws IncorrectFormatException {
220
221 if (Chars.COLON.equals(c)) {
222
223 state = State.INSIDE_VALUE;
224 return;
225
226 } else if (Chars.SEMI_COLON.equals(c)) {
227
228 throw new IncorrectFormatException(ErrorCode.FOUND_SEMICOLON_WHEN_READING_PROPERTY_NAME, "Unexpected character '" + c + "' for property '" + propertyName.trim() + "' in the selector '" + selectorName.trim() + "' should end with an ';', not with '}'.");
229
230 } else if (Chars.BRACKET_END.equals(c)) {
231
232 Rule rule = new Rule();
233
234 /*
235 * Huge logic to create a new rule
236 */
237
238 for (String s : selectorNames) {
239 Selector selector = new Selector(s.trim());
240 rule.addSelector(selector);
241 }
242 selectorNames.clear();
243
244 Selector selector = new Selector(selectorName.trim());
245 selectorName = "";
246 rule.addSelector(selector);
247
248 for (Entry<String, String> entry : map.entrySet()) {
249
250 String property = entry.getKey();
251 String value = entry.getValue();
252
253 PropertyValue propertyValue = new PropertyValue(property, value);
254 rule.addPropertyValue(propertyValue);
255
256 }
257
258 map.clear();
259
260 if (!rule.getPropertyValues().isEmpty()) {
261 rules.add(rule);
262 }
263
264 state = State.INSIDE_SELECTOR;
265
266 } else {
267
268 propertyName += c;
269 return;
270
271 }
272
273 }
274
275 /**
276 * Parse a selector.
277 *
278 * @param c The current character.
279 */
280
281 private void parseComment(final Character c) {
282
283 if (Chars.STAR.equals(previousChar) && Chars.SLASH.equals(c)) {
284
285 state = beforeCommentMode;
286 return;
287
288 }
289
290 }
291
292 /**
293 * Parse a selector.
294 *
295 * @param c The current character.
296 * @throws IncorrectFormatException If an error occurs.
297 */
298
299 private void parseSelector(final Character c) throws IncorrectFormatException {
300
301 if (Chars.BRACKET_BEG.equals(c)) {
302
303 state = State.INSIDE_PROPERTY_NAME;
304 return;
305
306 } else if (Chars.COMMA.equals(c)) {
307
308 if (selectorName.trim().isEmpty()) {
309 throw new IncorrectFormatException(ErrorCode.FOUND_COLON_WHEN_READING_SELECTOR_NAME, "Found an ',' in a selector name without any actual name before it.");
310 }
311
312 selectorNames.add(selectorName.trim());
313 selectorName = "";
314
315 } else {
316
317 selectorName += c;
318 return;
319
320 }
321
322 }
323
324 }