All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.inet.lib.less.CssFormatter Maven / Gradle / Ivy

There is a newer version: 1.12
Show newest version
/**
 * MIT License (MIT)
 *
 * Copyright (c) 2014 - 2019 Volker Berlin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * UT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author Volker Berlin
 * @license: The MIT license 
 */
package com.inet.lib.less;

import java.net.URL;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * A formatter for the CSS output. Hold some formating states.
 */
class CssFormatter implements Cloneable {

    /**
     * The scope of a single stack element.
     */
    private static class Scope {
        private Rule mixin;
        private Map parameters;
        private Map variables;
        private final Map returns = new HashMap<>();

        /**
         * Get a variable expression from this scope
         *
         * @param name
         *            the name of the variable starting with @
         * @return the expression or null if not found
         */
        Expression getVariable( String name ) {
            if( parameters != null ) {
                Expression variable = parameters.get( name );
                if( variable != null ) {
                    return variable;
                }
            }
            if( variables != null ) {
                Expression variable = variables.get( name );
                if( variable != null ) {
                    return variable;
                }
            }
            if( returns != null ) {
                Expression variable = returns.get( name );
                if( variable != null ) {
                    return variable;
                }
            }
            return null;
        }
    }

    /**
     * The global state of the sequential formatting.
     */
    private static class SharedState {
        private final StringBuilderPool                      pool             = new StringBuilderPool();

        private URL                                          baseURL;

        private final ArrayList                       stack = new ArrayList<>();

        private int                                          stackIdx;

        private int                                          rulesStackModCount;

        private final List                        results          = new ArrayList<>();

        private boolean                                      charsetDirective;

        private CssFormatter                                 header;

        private boolean                                      isReference;

        private int                                          importantCount;

        private LessExtendMap                                lessExtends = new LessExtendMap();
    }

    private final SharedState               state = new SharedState();

    private LessExtendMap                   lessExtends = state.lessExtends;

    private ReaderFactory                   readerFactory;

    private Map             options;

    private int                             rewriteUrl;

    private CssOutput                       currentOutput;

    private final static char[]             DIGITS    = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    private final ArrayDeque outputs   = new ArrayDeque<>();

    private StringBuilder                   output;

    private StringBuilder                   insets    = new StringBuilder();

    private boolean                         inlineMode;

    private final DecimalFormat             decFormat = new DecimalFormat( "#.########", DecimalFormatSymbols.getInstance( Locale.ENGLISH ) );

    private int                             blockDeep;

    private boolean                         isGuard;

    private boolean                         guardDefault;

    private boolean                         wasDefaultFunction;

    /**
     * Create a initial instance.
     */
    CssFormatter() {
        state.header = copy( null );
        state.results.add( currentOutput = new CssPlainOutput( state.header.output ) ); // header
    }

    /**
     * Create a new formatter for a single rule with optional output.
     *
     * @param output
     *            optional target
     * @return a new formatter
     * @throws LessException
     *             if any error occur.
     */
    private CssFormatter copy( @Nullable StringBuilder output ) {
        try {
            CssFormatter formatter = (CssFormatter)clone();
            formatter.output = output == null ? state.pool.get() : output;
            formatter.insets = state.pool.get();
            formatter.insets.append( insets );
            return formatter;
        } catch( CloneNotSupportedException ex ) {
            throw new LessException( ex );
        }
    }

    /**
     * Format the a parsed less file.
     *
     * @param parser
     *            the parser result
     * @param baseURL
     *            the URL of the less file
     * @param readerFactory
     *            A factory for the readers for imports.
     * @param target
     *            the output of the resulting string
     * @param options
     *            some optional options, see constants for details
     */
    void format( LessParser parser, URL baseURL, ReaderFactory readerFactory, StringBuilder target,  @Nonnull Map options ) {
        state.baseURL = baseURL;
        this.readerFactory = readerFactory;
        this.options = options;
        this.rewriteUrl = parseRewriteUrl();
        addVariables( parser.getVariables() );
        state.isReference = false;

        for( Formattable rule : parser.getRules() ) {
            rule.prepare( this );
        }

        for( Formattable rule : parser.getRules() ) {
            switch( rule.getType() ) {
                case Formattable.REFERENCE_INFO:
                    state.isReference = ((ReferenceInfo)rule).isReference();
                    continue;
                case Formattable.MIXIN:
                    ((Mixin)rule).appendSubRules( null, this );
                    break;
                case Formattable.CSS_AT_RULE:
                    if( state.isReference ) {
                        continue;
                    }
                    //$FALL-THROUGH$
                default:
                    rule.appendTo( this );
            }
        }
        removeVariables( parser.getVariables() );

        output = target;
        for( CssOutput result : state.results ) {
            result.appendTo( target, lessExtends, this );
        }
    }

    /**
     * Parse the parameter rewrite URL
     * 
     * @return 0, 1 or 2
     */
    private int parseRewriteUrl() {
        String rewrite = options.get( Less.REWRITE_URLS );
        if( rewrite != null ) {
            switch( rewrite.toLowerCase() ) {
                case "off":
                    return 0;
                case "local":
                    return 1;
                case "all":
                    return 2;
            }
        }
        return 0;
    }

    /**
     * Get the formatter for CSS directives.
     * @return the header formatter
     */
    CssFormatter getHeader() {
        return state.header;//results.get( 0 );
    }

    /**
     * If already was write a character directive. Such directive can write only once.
     *
     * @return true, if already write.
     */
    boolean isCharsetDirective(){
        return state.charsetDirective;
    }

    /**
     * Mark that there was write a character directive.
     */
    void setCharsetDirective() {
        state.charsetDirective = true;
    }

    /**
     * Add a new output buffer to the formatter.
     */
    void addOutput() {
        if( output != null ) {
            outputs.addLast( output );
        }
        output = state.pool.get();
    }

    /**
     * Release an output and delete it.
     */
    void freeOutput() {
        state.pool.free( output );
        output = outputs.size() > 0 ? outputs.removeLast() : null;
    }

    /**
     * Release an output buffer, return the content and restore the previous output.
     * @return the content of the current output
     */
    String releaseOutput() {
        if( output == null ) { // occur in Rule.toString()
            return "";
        }
        String str = output.toString();
        freeOutput();
        return str;
    }

    /**
     * Release an output buffer, restore the previous output and add the content of the previous output.
     */
    void flushOutput() {
        StringBuilder current = output;
        freeOutput();
        output.append( current );
    }

    /**
     * Get the size of the current content in the current output.
     * @return the size
     */
    int getOutputSize() {
        return output == null ? -1 : output.length();
    }

    /**
     * Reset the output to a previous marker.
     * @param size the marker position
     */
    void setOutputSize( int size ) {
        output.setLength( size );
    }

    /**
     * Add a processed extend
     * @param lessExtend the extend
     */
    void add( LessExtend lessExtend ) {
        lessExtends.add( lessExtend, this.currentOutput.getSelectors() );
    }

    /**
     * Get the URL of the top less file.
     * @return the URL
     */
    URL getBaseURL() {
        return state.baseURL;
    }

    /**
     * If the URL should be rewrite. This depends on the option "rewrite-urls"
     * 
     * @param url the URL that should be rewrite
     * @return true, if rewrite
     */
    boolean isRewriteUrl( String url ) {
        switch( rewriteUrl ) {
            default:
            case 0:
                return false;
            case 1:
                return url.startsWith( "." );
            case 2:
                return true;
        }
    }

    /**
     * Get the reader factory.
     * @return the factory
     */
    ReaderFactory getReaderFactory() {
        return readerFactory;
    }

    /**
     * Get a variable expression from the current stack
     *
     * @param name
     *            the name of the variable starting with @
     * @return the expression or null if not found
     */
    Expression getVariable( String name ) {
        for( int i = state.stackIdx - 1; i >= 0; i-- ) {
            Expression variable = state.stack.get( i ).getVariable( name );
            if( variable != null ) {
                return variable;
            }
        }
        if( name.equals( "@arguments" ) ) {
            for( int i = state.stackIdx - 1; i >= 0; i-- ) {
                Scope scope = state.stack.get( i );
                if( scope.parameters != null ) {
                    Operation params = new Operation( scope.mixin, ' ' );
                    for( Expression expr : scope.parameters.values() ) {
                        if( expr.getClass() == Operation.class && scope.parameters.size() == 1 ) {
                            return expr;
                        }
                        params.addOperand( expr );
                    }
                    return params;
                }
            }
        }
        return null;
    }

    /**
     * Add the scope of a mixin to the stack.
     * @param mixin the mixin
     * @param parameters the calling parameters
     * @param variables the variables of the mixin
     */
    void addMixin( Rule mixin, Map parameters, Map variables ) {
        int idx = state.stackIdx++;
        Scope scope;
        if( state.stack.size() <= idx ) {
            scope = new Scope();
            state.stack.add( scope );
        } else {
            scope = state.stack.get( idx );
            scope.returns.clear();
        }
        scope.mixin = mixin;
        scope.parameters = parameters;
        scope.variables = variables;
    }

    /**
     * Remove the scope of a mixin.
     */
    void removeMixin() {
        int idx = state.stackIdx - 1;
        Scope current = state.stack.get( idx );
        if( idx > 0 ) {
            Scope previous = state.stack.get( idx - 1 );
            Map currentReturn = previous.returns;
            Map vars = current.variables;
            if( vars != null ) {
                for( Entry entry : vars.entrySet() ) {
                    if( previous.getVariable( entry.getKey() ) == null ) {
                        currentReturn.put( entry.getKey(), ValueExpression.eval( this, entry.getValue() ) );
                    }
                }
            }
            vars = current.returns;
            if( vars != null ) {
                for( Entry entry : vars.entrySet() ) {
                    if( previous.getVariable( entry.getKey() ) == null ) {
                        currentReturn.put( entry.getKey(), ValueExpression.eval( this, entry.getValue() ) );
                    }
                }
            }
        }
        state.stackIdx--;
        state.rulesStackModCount++;
    }

    /**
     * Add rule variables to the stack.
     *
     * @param variables
     *            the variables, can be null if the current rule has no parameters.
     */
    void addVariables( HashMap variables ) {
        addMixin( null, null, variables );
    }

    /**
     * Remove rule variables from the stack.
     *
     * @param variables
     *            the variables, can be null if the current rule has no parameters.
     */
    void removeVariables( Map variables ) {
        removeMixin();
    }

    /**
     * Add the parameters of a guard
     *
     * @param parameters the parameters
     * @param isDefault if the default case will be evaluated, in this case the expression "default" in guard is true.
     */
    void addGuardParameters( Map parameters, boolean isDefault ) {
        isGuard = true;
        wasDefaultFunction = false;
        guardDefault = isDefault;
        if( parameters != null ) {
            addMixin( null, parameters, null );
        }
    }

    /**
     * remove the parameters of a guard
     * @param parameters the parameters
     */
    void removeGuardParameters( Map parameters ) {
        if( parameters != null ) {
            removeMixin();
        }
        isGuard = false;
    }

    /**
     * if we are inside of a guard
     * @return is guard
     */
    boolean isGuard() {
        return isGuard;
    }

    /**
     * Get the current value of "default" in a guard.
     *
     * @return true if the default case will be evaluate.
     */
    boolean getGuardDefault() {
        wasDefaultFunction = true;
        return guardDefault;
    }

    /**
     * If the state of "default" was requested since the last guard parameter was added.
     *
     * @return true, if default was called
     */
    boolean wasDefaultFunction() {
        return wasDefaultFunction;
    }


    /**
     * A mixin inline never it self.
     *
     * @param rule the rule
     * @return true, if the mixin is currently formatting
     */
    boolean containsRule( Rule rule ) {
        for( int i = state.stackIdx - 1; i >= 0; i-- ) {
            if( rule == state.stack.get( i ).mixin ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get a nested mixin of a parent rule.
     *
     * @param name
     *            the name of the mixin
     * @return the mixin or null
     */
    List getMixin( String name ) {
        for( int i = state.stackIdx - 1; i >= 0; i-- ) {
            Rule mixin = state.stack.get( i ).mixin;
            if( mixin != null ) {
                List rules = mixin.getMixin( name );
                if( rules != null ) {
                    for( int r = 0; r < rules.size(); r++ ) {
                        if( !containsRule( rules.get( r ) ) ) {
                            return rules;
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * The counter of the variable stack changes. This is an optimizing that some expression does not need reevaluate if this ID has not changed.
     * @return the id
     */
    int stackID() {
        return state.rulesStackModCount;
    }

    /**
     * Get the current output of the formatter.
     *
     * @return the output.
     */
    StringBuilder getOutput() {
        if( output == null ) {
            CssFormatter block = copy( null );
            state.results.add( new CssPlainOutput( block.output ) );
            output = block.output;
        }
        return output;
    }

    /**
     * Set the inline mode. In the inline mode:
     * 
  • quotes are removed from strings *
  • colors are written ever as full color RGB value * @param mode the new mode. */ void setInlineMode( boolean mode ) { inlineMode = mode; } /** * Current inline mode * @return the mode */ boolean inlineMode() { return inlineMode; } /** * Append a string to the output. In inline mode quotes are removed. * @param str the string * @return this */ CssFormatter append( String str ) { if( inlineMode ) { str = UrlUtils.removeQuote( str ); } output.append( str ); return this; } /** * Append a color. In inline mode it is ever a 6 digit RGB value. * @param color the color value * @param hint the original spelling of the color if not calculated * @return this */ CssFormatter appendColor( double color, @Nullable String hint ) { if( !inlineMode && hint != null ) { output.append( hint ); } else { int argb = ColorUtils.argb( color ); output.append( '#' ); appendHex( argb, 6 ); } return this; } /** * Append an hex value to the output. * * @param value the value * @param digits the digits to write. */ void appendHex( int value, int digits ) { if( digits > 1 ) { appendHex( value >>> 4, digits-1 ); } output.append( DIGITS[ value & 0xF ] ); } /** * Append a single character to the output. * * @param ch the character * @return a reference to this object */ CssFormatter append( char ch ) { output.append( ch ); return this; } /** * Append a decimal number to the output. * * @param value the number * @return a reference to this object */ CssFormatter append( double value ) { if( value == (int)value ) { output.append( Integer.toString( (int)value ) ); } else { output.append( decFormat.format( value ) ); } return this; } /** * Append a value with a unit. In compress mode not all units are written. * * @param value the value. * @param unit the unit * @return a reference to this object */ CssFormatter appendValue( double value, String unit ) { append( value ); append( unit ); return this; } /** * Increment the insets of the output. */ void incInsets() { insets.append( " " ); } /** * Decrement the insets of the output. */ void decInsets() { insets.setLength( insets.length() - 2 ); } /** * Start a new block with a list of selectors. * @param selectors the selectors * @return this */ CssFormatter startBlock( String[] selectors ) { final List results = state.results; if( blockDeep == 0 ) { output = null; CssOutput nextOutput = null; if( results.size() > 0 && !"@font-face".equals( selectors[0] ) ) { CssOutput cssOutput = results.get( results.size() - 1 ); if( Arrays.equals( selectors, cssOutput.getSelectors() ) ) { nextOutput = cssOutput; } } CssFormatter block; if( nextOutput == null ) { block = copy( null ); if( selectors[0].startsWith( "@media" ) ) { block.lessExtends = new LessExtendMap( state.lessExtends ); nextOutput = new CssMediaOutput( selectors, block.output, state.isReference, block.lessExtends ); } else { nextOutput = new CssRuleOutput( selectors, block.output, state.isReference ); } results.add( nextOutput ); } else { block = copy( nextOutput.getOutput() ); } block.currentOutput = nextOutput; block.incInsets(); block.blockDeep++; return block; } else { if( selectors[0].startsWith( "@media" ) ) { CssFormatter block = copy( null ); block.lessExtends = new LessExtendMap( state.lessExtends ); String[] sel = new String[]{ this.currentOutput.getSelectors()[0] + " and " + selectors[0].substring( 6 ).trim() }; block.currentOutput = new CssMediaOutput( sel, block.output, state.isReference, block.lessExtends ); results.add( block.currentOutput ); block.insets.setLength( 2 ); block.blockDeep = 1; return block; } else { if( blockDeep == 1 && this.currentOutput.getClass() == CssMediaOutput.class ) { CssFormatter block = copy( null ); block.incInsets(); block.currentOutput = this.currentOutput; ((CssMediaOutput)this.currentOutput).startBlock( selectors, block.output ); block.blockDeep++; return block; } else { blockDeep++; startBlockImpl( selectors ); return this; } } } } /** * Output a new block and increment the insets. * * @param selectors the selectors of the block. */ void startBlockImpl( String[] selectors ) { for( int i=0; i 0 ) { output.append( ',' ); newline(); } insets(); append( selectors[i] ); } space(); output.append( '{' ); newline(); incInsets(); } /** * Terminate a CSS block. * * @return a reference to this object */ CssFormatter endBlock() { blockDeep--; if( blockDeep == 0 ) { state.pool.free( insets ); insets = null; inlineMode = false; } else { if( blockDeep == 1 && currentOutput.getClass() == CssMediaOutput.class ) { state.pool.free( insets ); insets = null; inlineMode = false; } else { endBlockImpl(); } } return this; } /** * Decrement the insets and output the end block. */ void endBlockImpl() { decInsets(); insets(); output.append( '}' ); newline(); } /** * Append a property to the output like: name: value; * * @param name * the name * @param value * the value * @throws LessException * if write properties in the root */ void appendProperty( @Nonnull String name, @Nonnull Expression value ) { if( output == null ) { throw new LessException( "Properties must be inside selector blocks, they cannot be in the root." ); } insets(); name = SelectorUtils.replacePlaceHolder( this, name, value ); output.append( name ).append( ':' ); space(); value.appendTo( this ); if( state.importantCount > 0 || value.isImportant() ) { output.append( " !important" ); } semicolon(); newline(); } /** * Increment the important flag. */ void incImportant() { state.importantCount++; } /** * Decrement the important flag. */ void decImportant() { state.importantCount--; } /** * Write a single space. The compress formatter do nothing. * * @return a reference to this object */ CssFormatter space() { output.append( ' ' ); return this; } /** * Write a newline. The compress formatter do nothing. * * @return a reference to this object */ CssFormatter newline() { output.append( '\n' ); return this; } /** * Write a semicolon. The compress formatter do nothing before an end block. */ void semicolon() { output.append( ';' ); } /** * Write the current insets. The compress formatter do nothing. */ void insets() { output.append( insets ); } /** * Write a comment. The compress formatter do nothing. * @param msg the comment including the comment markers * @return a reference to this object */ CssFormatter comment( String msg ) { getOutput().append( insets ).append( msg ).append( '\n' ); return this; } /** * Get a shared decimal format for parsing numbers with units. * * @return the format */ DecimalFormat getFormat() { return decFormat; } }




  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy