/*
* Bibliothek - DockingFrames
* Library built on Java/Swing, allows the user to "drag and drop"
* panels containing any Swing-Component the developer likes to add.
*
* Copyright (C) 2012 Benjamin Sigg
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Benjamin Sigg
* benjamin_sigg@gmx.ch
* CH - Switzerland
*/
package bibliothek.gui.dock.extension.css.intern;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import bibliothek.gui.dock.extension.css.CssPropertyKey;
import bibliothek.gui.dock.extension.css.CssRule;
import bibliothek.gui.dock.extension.css.CssSelector;
import bibliothek.gui.dock.extension.css.CssDeclarationValue;
/**
* The {@link CssParser} takes some text and creates {@link CssRule}s from
* that text.
* @author Benjamin Sigg
*/
public class CssParser {
public List<CssRule> parse( String text ) throws IOException{
return parse( new StringReader( text ) );
}
public List<CssRule> parse( Reader text ) throws IOException{
Collector collector = new Collector();
parse( new CommentReader( text ), collector );
return collector.getRules();
}
private void parse( Reader text, Collector collector ) throws IOException{
StringBuilder builder = new StringBuilder();
int read;
boolean inRule = false;
boolean inString = false;
boolean inCharacter = false;
int line = 1;
while( (read = text.read()) != -1 ){
char c = (char)read;
if( c == '\n'){
line++;
}
boolean storeCharacter = true;
switch( c ){
case '{':
if( !inString && !inCharacter ){
if( inRule ){
throw new IOException( "Line " + line + ": found { inside a rule" );
}
inRule = true;
collector.selectorRead( line, builder.toString().trim() );
builder.setLength( 0 );
storeCharacter = false;
}
break;
case '}':
if( !inString && !inCharacter ){
if( !inRule ){
throw new IOException( "Line " + line + ": found } not ending a rule" );
}
inRule = false;
parseProperty( line, builder.toString(), collector );
builder.setLength( 0 );
storeCharacter = false;
}
break;
case ';':
if( !inString && !inCharacter ){
if( inRule ){
parseProperty( line, builder.toString(), collector );
builder.setLength( 0 );
storeCharacter = false;
}
}
break;
case '\'':
if( !inString ){
inCharacter = !inCharacter;
}
break;
case '"':
if( !inCharacter ){
inString = !inString;
}
break;
}
if( storeCharacter ){
builder.append( c );
}
}
}
private void parseProperty( int line, String property, Collector collector ) throws IOException{
property = property.trim();
if( property.length() > 0 ){
int assignment = property.indexOf( ':' );
if( assignment >= 0 ){
String key = property.substring( 0, assignment ).trim();
String value = property.substring( assignment+1 ).trim();
if( (value.startsWith( "'" ) && value.endsWith( "'" )) || (value.startsWith( "\"" ) && value.endsWith( "\"" ))){
value = value.substring( 1, value.length()-1 );
}
collector.propertyRead( key, value );
}
else{
throw new IOException( "Line " + line + ": cannot read property '" + property + "'" );
}
}
}
private CssSelector[] toSelectors( int line, String selector ) throws IOException{
List<CssSelector> result = new ArrayList<CssSelector>();
boolean inString = false;
boolean inCharacter = false;
boolean inAttribute = false;
int offset = 0;
for( int i = 0, n = selector.length(); i<n; i++ ){
char c = selector.charAt( i );
switch( c ){
case '\'':
if( !inString ){
inCharacter = !inCharacter;
}
break;
case '"':
if( !inCharacter ){
inString = !inString;
}
break;
case '[':
if( !inString && !inCharacter ){
if( inAttribute ){
throw new IOException( "Line " + line + ": found [ in attribute" );
}
inAttribute = true;
}
break;
case ']':
if( !inString && !inCharacter ){
if( !inAttribute ){
throw new IOException( "Line " + line + ": found ] outside attribute" );
}
inAttribute = false;
}
break;
case ',':
if( !inString && !inCharacter ){
result.add( toSelector( line, selector.substring( offset, i ).trim() ));
offset = i+1;
}
break;
}
}
String remainder = selector.substring( offset ).trim();
if( remainder.length() > 0 ){
result.add( toSelector( line, remainder ) );
}
return result.toArray( new CssSelector[ result.size() ] );
}
private CssSelector toSelector( int line, String selector ){
SelectorParser parser = new SelectorParser();
for( int i = 0, n = selector.length(); i<n; i++ ){
char c = selector.charAt( i );
parser.push( line, c );
}
if( parser.string.length() > 0 ){
parser.endCurrent( line );
}
return parser.builder.build();
}
private class SelectorParser{
private DefaultCssSelector.Builder builder = DefaultCssSelector.selector();
private StringBuilder string = new StringBuilder();
private boolean inString = false;
private boolean inCharacter = false;
private boolean inAttribute = false;
private boolean nextIsPseudoClass = false;
private boolean nextIsClass = false;
private boolean nextIsIdentifier = false;
private boolean nextIsChild = false;
private boolean nextIsSibling = false;
private boolean first = true;
private void endCurrent( int line ){
String next = string.toString().trim();
string.setLength( 0 );
if( next.length() > 0 ){
if( nextIsChild ){
if( first ){
builder.any();
first = false;
}
if( !nextIsPseudoClass && !nextIsIdentifier && !nextIsClass ){
builder.child( next );
}
else{
builder.any();
}
}
if( nextIsSibling ){
throw new IllegalArgumentException( "Line " + line + ": siblings are not supported" );
}
if( nextIsPseudoClass ){
if( first ){
builder.any();
}
builder.pseudo( next );
}
if( nextIsClass ){
if( first ){
builder.any();
}
builder.clazz( next );
}
if( nextIsIdentifier ){
if( first ){
builder.any();
}
builder.identifier( next );
}
if( !nextIsPseudoClass && !nextIsClass && !nextIsSibling && !nextIsChild && !nextIsSibling && !nextIsIdentifier ){
if( "*".equals( next )){
builder.any();
}
else{
builder.element( next );
}
}
first = false;
nextIsPseudoClass = false;
nextIsIdentifier = false;
nextIsChild = false;
nextIsSibling = false;
nextIsClass = false;
}
}
public void push( int line, char c ){
switch( c ){
case '"':
if( !inCharacter ){
inString = !inString;
}
else{
string.append( c );
}
break;
case '\'':
if( !inString ){
inCharacter = !inCharacter;
}
else{
string.append( c );
}
break;
case '[':
if( !inCharacter && !inString ){
endCurrent( line );
if( inAttribute ){
throw new IllegalArgumentException( "Line " + line + ": found [ in attribute" );
}
inAttribute = true;
}
else{
string.append( c );
}
break;
case ']':
if( !inCharacter && !inString ){
if( !inAttribute ){
throw new IllegalArgumentException( "Line " + line + ": found ] without attribute" );
}
inAttribute = false;
int assignment = string.indexOf( "=" );
if( assignment == -1 ){
builder.attribute( string.toString().trim() );
}
else{
String key = string.substring( 0, assignment ).trim();
String value = string.substring( assignment+1 ).trim();
builder.attribute( key, value );
}
string.setLength( 0 );
}
else{
string.append( c );
}
break;
case ':':
if( !inCharacter && !inString && !inAttribute ){
endCurrent( line );
nextIsPseudoClass = true;
}
else{
string.append( c );
}
break;
case '#':
if( !inCharacter && !inString && !inAttribute ){
endCurrent( line );
nextIsIdentifier = true;
}
else{
string.append( c );
}
break;
case '.':
if( !inCharacter && !inString && !inAttribute ){
endCurrent( line );
nextIsClass = true;
}
else{
string.append( c );
}
break;
case '>':
if( !inCharacter && !inString && !inAttribute ){
endCurrent( line );
nextIsChild = true;
}
else{
string.append( c );
}
break;
case '+':
if( !inCharacter && !inString && !inAttribute ){
endCurrent( line );
nextIsSibling = true;
}
else{
string.append( c );
}
break;
default:
if( !inCharacter && !inString && !inAttribute && Character.isWhitespace( c )){
if( string.length() > 0 ){
endCurrent( line );
}
}
else{
string.append( c );
}
}
}
}
private class Collector{
private List<CssRule> rules = new ArrayList<CssRule>();
private DefaultCssRule[] currentRules = new DefaultCssRule[]{};
public void selectorRead( int line, String selector ) throws IOException{
CssSelector[] selectors = toSelectors( line, selector );
if( currentRules.length != selectors.length ){
currentRules = new DefaultCssRule[ selectors.length ];
}
for( int i = 0; i < selectors.length; i++ ){
DefaultCssRule rule = new DefaultCssRule( selectors[i] );
currentRules[i] = rule;
rules.add( rule );
}
}
public void propertyRead( String key, String value ){
for( DefaultCssRule rule : currentRules ){
if( "null".equals( value ) || value == null ){
rule.setProperty( CssPropertyKey.parse( key ), null );
}
else{
rule.setProperty( CssPropertyKey.parse( key ), new CssDeclarationValue( value ));
}
}
}
public List<CssRule> getRules(){
return rules;
}
}
private static class CommentReader extends Reader{
private Reader reader;
private boolean inComment = false;
private int previous = -1;
private int next = -1;
public CommentReader( Reader reader ){
this.reader = reader;
}
@Override
public int read( char[] cbuf, int off, int len ) throws IOException{
int start = off;
while( off < len ){
if( inComment ){
int read = previous;
previous = -1;
if( read == -1 ){
read = reader.read();
}
if( read == '*' ){
read = reader.read();
if( read == '/' ){
inComment = false;
}
else{
previous = read;
}
}
if( read == -1 ){
break;
}
}
else if( previous != -1 ){
cbuf[off++] = (char)previous;
previous = -1;
}
else if( next != -1 ){
cbuf[off++] = (char)next;
next = -1;
}
else{
int read = reader.read();
if( read == '/' ){
previous = read;
read = reader.read();
if( read == '*' ){
inComment = true;
previous = -1;
}
else{
next = read;
}
}
else{
if( read == -1 ){
break;
}
cbuf[off++] = (char)read;
}
}
}
if( start == off ){
return -1;
}
else{
return off - start;
}
}
@Override
public void close() throws IOException{
reader.close();
}
}
}