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 }