/*
* Copyright 2011 Eric F. Savage, code@efsavage.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ajah.css;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.java.Log;
import com.ajah.css.util.RuleComparator;
import com.ajah.util.AjahUtils;
import com.ajah.util.StringUtils;
import com.ajah.util.io.file.FileUtils;
/**
* Parses a raw CSS file into a {@link CssDocument}. Very much alpha quality!
*
* @author <a href="http://efsavage.com">Eric F. Savage</a>, <a
* href="mailto:code@efsavage.com">code@efsavage.com</a>.
*
*/
@Log
public class CssParser {
private static final Pattern propertyPattern = Pattern.compile("\\*{0,1}([-a-z]+)\\s*:\\s*(.*?);$");
private static final CssParser INSTANCE = new CssParser();
/**
* Return singleton instance of parser.
*
* @return Singleton instance of parser.
*/
public static CssParser getInstance() {
return INSTANCE;
}
private static List<String> getLines(final String rawCss) {
final List<String> lines = new ArrayList<>();
StringBuffer line = new StringBuffer();
for (final char c : rawCss.toCharArray()) {
switch (c) {
case '{':
if (!StringUtils.isBlank(line.toString())) {
lines.add(line.toString().trim());
line = new StringBuffer();
}
lines.add("{");
break;
case '}':
if (!StringUtils.isBlank(line.toString())) {
lines.add(line.toString().trim());
line = new StringBuffer();
}
lines.add("}");
break;
case ';':
line.append(';');
final String strLine = line.toString().trim();
if (strLine.startsWith("base64")) {
final String previousLine = lines.get(lines.size() - 1);
lines.remove(previousLine);
lines.add(previousLine + strLine);
} else {
lines.add(strLine);
}
line = new StringBuffer();
break;
case '\r':
break;
case '_':
break;
case '\n':
break;
default:
line.append(c);
}
}
return lines;
}
private static CssRule getRule(final String line, final CssRule parent) {
return new CssRule(line.replaceAll("\\s\\{", ""), parent);
}
/**
* Process a file
*
* @param args
* The name of the file to process.
* @throws IOException
* If the file could not be read.
*/
public static void main(final String[] args) throws IOException {
final String rawCss = FileUtils.readFile(args[0]);
final CssDocument doc = parse(rawCss);
for (final CssRule rule : doc.getRules()) {
log.fine(rule.toString());
for (final CssSelector selector : rule.getSelectors()) {
log.fine(selector.getRaw() + ": " + selector.getType());
}
}
}
/**
* Parses an {@link InputStream} of raw CSS.
*
* @param css
* The raw CSS.
* @return The resulting CssDocument.
*/
public static CssDocument parse(final InputStream css) {
AjahUtils.requireParam(css, "css");
try (final Scanner scanner = new Scanner(css)) {
scanner.useDelimiter("\\A");
if (scanner.hasNext()) {
return parse(scanner.next());
}
}
return null;
}
private static CssDocument parse(final String rawCss) {
final List<CssRule> rules = new ArrayList<>();
final List<String> lines = getLines(rawCss);
log.finer(lines.size() + " lines");
for (final String line : lines) {
log.finest("Line: " + line);
}
CssRule currentRule = null;
boolean inComment = false;
for (int i = 0; i < lines.size(); i++) {
final String trimmed = trim(lines.get(i));
if (StringUtils.isBlank(trimmed)) {
// Blank line
} else if (trimmed.startsWith("//")) {
// Comment
log.finest("Comment: " + trimmed.substring(2));
} else if (trimmed.startsWith("/*")) {
inComment = true;
} else if (inComment && trimmed.endsWith("*/")) {
inComment = false;
} else if (currentRule == null) {
currentRule = getRule(trimmed, currentRule);
} else if (trimmed.equals("}")) {
if (currentRule.getParent() == null) {
rules.add(currentRule);
currentRule = null;
} else {
currentRule = currentRule.getParent();
}
} else if (trimmed.equals("{")) {
// We're in a rule, this line is extraneous
} else if (propertyPattern.matcher(trimmed).matches()) {
final Matcher matcher = propertyPattern.matcher(trimmed);
matcher.find();
final String property = matcher.group(1);
final CssProperty cssProperty = CssProperty.get(property);
if (cssProperty != null) {
final CssDeclaration declaration = new CssDeclaration(currentRule, cssProperty, matcher.group(2));
log.finest("Declaration: " + declaration.toString());
log.finest("\tValue: " + matcher.group(2));
} else {
log.warning("Unknown property: " + property);
System.err.println("/** " + property + " */");
System.err.println(property.toUpperCase().replaceAll("-", "_") + "(\"" + property + "\"),");
}
} else {
currentRule = getRule(trimmed, currentRule);
}
}
int i = 0;
for (final CssRule rule : rules) {
rule.add(parseSelector(rule.getRaw(), i++));
}
Collections.sort(rules, RuleComparator.INSTANCE);
return new CssDocument().addAll(rules);
}
private static CssSelector parseSelector(final String raw, final int position) {
final CssSelector selector = new CssSelector(raw, position);
if (raw.matches("[a-z0-9]+")) {
// Simple element-level selector
selector.setType(CssSelectorType.ELEMENT);
} else if (raw.matches("(([a-z0-9]+) *)+")) {
// Simple element-level selector
selector.setType(CssSelectorType.ELEMENT_DESCENDENT);
} else if (raw.matches("\\.[-a-zA-Z0-9]+")) {
// Simple element-level selector
selector.setType(CssSelectorType.SIMPLE_CLASS);
} else if (raw.matches("\\#[-a-zA-Z0-9]+")) {
// Simple element-level selector
selector.setType(CssSelectorType.SIMPLE_ID);
} else if (raw.matches("[a-z0-9]+\\.[-a-zA-Z0-9]+")) {
// Simple element-level selector
selector.setType(CssSelectorType.ELEMENT_CLASS);
} else if (raw.matches("[a-z0-9]+\\#[-a-zA-Z0-9]+")) {
// Simple element-level selector
selector.setType(CssSelectorType.ELEMENT_ID);
} else {
selector.setType(CssSelectorType.UNKNOWN);
}
return selector;
}
private static String trim(final String line) {
return line.replaceAll("/\\*.*\\*/", "").replaceAll("/\\/{2,}/.*", "").trim();
}
private CssParser() {
// Private Constructor
}
}