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    }