/**
 * Helper class to format the CSS code.
 * Based on code initially developed by: http://jsbeautifier.org/
 *
 * Usage:
        css_beautify(source_text);
        css_beautify(source_text, options);

    The options are (default in brackets):
        indent_size (4)                   - indentation size,
        indent_char (space)               - character to indent with,
        selector_separator_newline (true) - separate selectors with newline or
                                            not (e.g. "a,\nbr" or "a, br")
        end_with_newline (false)          - end with a newline

    e.g:
    css_beautify(css_source_text, {
      'indent_size': 1,
      'indent_char': '\t',
      'selector_separator': ' ',
      'end_with_newline': false,
    });
 */ export class CSSFormatter { private ch : any ; private pos : number = - 1 ; private source_text : string ; private whiteRe : RegExp = /^\s+$/ ; private indentSize : any ; private indentString : string ; private singleIndent : string ; private indentLevel = 0 ; private nestedLevel = 0 ; private output : string [ ] = [ ] ; private print : string [ ] = [ ] ; private cssBeautifyNestedAtRule = { "@page" : true , "@font-face" : true , "@keyframes" : true , "@media" : true , "@supports" : true , "@document" : true } ; private cssBeautifyConditionalGroupRule = { "@media" : true , "@supports" : true , "@document" : true } ; private next ( ) { this . ch = this . source_text . charAt ( ++ this . pos ) ; return this . ch ; } private peek ( ) { return this . source_text . charAt ( this . pos + 1 ) ; } private eatString ( endChar ) { let start = this . pos ; while ( this . next ( ) ) { if ( this . ch === "\\" ) { this . next ( ) ; this . next ( ) ; } else if ( this . ch === endChar ) { break ; } else if ( this . ch === "\n" ) { break ; } } return this . source_text . substring ( start , this . pos + 1 ) ; } private eatWhitespace ( ) { let start = this . pos ; while ( this . whiteRe . test ( this . peek ( ) ) ) { this . pos ++ ; } return this . pos !== start ; } private skipWhitespace ( ) { let start = this . pos ; do { } while ( this . whiteRe . test ( this . next ( ) ) ) ; return this . pos !== start + 1 ; } private eatComment ( singleLine ) { let start = this . pos ; this . next ( ) ; while ( this . next ( ) ) { if ( this . ch === "*" && this . peek ( ) === "/" ) { this . pos ++ ; break ; } else if ( singleLine && this . ch === "\n" ) { break ; } } return this . source_text . substring ( start , this . pos + 1 ) ; } private lookBack ( str ) { return this . source_text . substring ( this . pos - str . length , this . pos ) . toLowerCase ( ) === str ; } private isCommentOnLine ( ) { let endOfLine = this . source_text . indexOf ( '\n' , this . pos ) ; if ( endOfLine === - 1 ) { return false ; } let restOfLine = this . source_text . substring ( this . pos , endOfLine ) ; return restOfLine . indexOf ( '//' ) !== - 1 ; } private indent ( ) { this . indentLevel ++ ; this . indentString += this . singleIndent ; } private outdent ( ) { this . indentLevel -- ; this . indentString = this . indentString . slice ( 0 , - this . indentSize ) ; } private lastCharWhitespace ( ) { return this . whiteRe . test ( this . output [ this . output . length - 1 ] ) ; } private newLine ( keepWhitespace ) { if ( ! keepWhitespace ) { while ( this . lastCharWhitespace ( ) ) { this . output . pop ( ) ; } } if ( this . output . length ) { this . output . push ( '\n' ) ; } if ( this . indentString ) { this . output . push ( this . indentString ) ; } } private singleSpace ( ) { if ( this . output . length && ! this . lastCharWhitespace ( ) ) { this . output . push ( ' ' ) ; } } public css_beautify ( sourceText , options ) { options = options || { } ; this . source_text = sourceText ; let indentSize = options . indent_size || 4 ; let indentCharacter = options . indent_char || ' ' ; let selectorSeparatorNewline = ( options . selector_separator_newline === undefined ) ? true : options . selector_separator_newline ; let endWithNewline = ( options . end_with_newline === undefined ) ? false : options . end_with_newline ; if ( typeof indentSize === "string" ) { indentSize = parseInt ( indentSize , 10 ) ; } this . indentString = sourceText . match ( /^[\r\n]*[\t ]*/ ) [ 0 ] ; this . singleIndent = new Array ( indentSize + 1 ) . join ( indentCharacter ) ; this . print [ "{" ] = ( chOpenBrace ) : void => { this . singleSpace ( ) ; this . output . push ( chOpenBrace ) ; this . newLine ( false ) ; } ; this . print [ "}" ] = ( chCloseBrace ) : void => { this . newLine ( false ) ; this . output . push ( chCloseBrace ) ; this . newLine ( false ) ; } ; if ( this . indentString ) { this . output . push ( this . indentString ) ; } let insideRule = false ; let enteringConditionalGroup = false ; while ( true ) { let isAfterSpace = this . skipWhitespace ( ) ; if ( ! this . ch ) { break ; } else if ( this . ch === '/' && this . peek ( ) === '*' ) { this . newLine ( false ) ; this . output . push ( this . eatComment ( true ) , "\n" , this . indentString ) ; let header = this . lookBack ( "" ) ; if ( header ) { this . newLine ( false ) ; } } else if ( this . ch === '/' && this . peek ( ) === '/' ) { this . output . push ( this . eatComment ( true ) , this . indentString ) ; } else if ( this . ch === '@' ) { let atRule = this . eatString ( " " ) . replace ( / $/ , '' ) ; this . output . push ( atRule , this . ch ) ; if ( atRule in this . cssBeautifyNestedAtRule ) { this . nestedLevel += 1 ; if ( atRule in this . cssBeautifyConditionalGroupRule ) { enteringConditionalGroup = true ; } } } else if ( this . ch === '{' ) { this . eatWhitespace ( ) ; if ( this . peek ( ) === '}' ) { this . next ( ) ; this . output . push ( " {}" ) ; } else { this . indent ( ) ; this . print [ "{" ] ( this . ch ) ; if ( enteringConditionalGroup ) { enteringConditionalGroup = false ; insideRule = ( this . indentLevel > this . nestedLevel ) ; } else { insideRule = ( this . indentLevel >= this . nestedLevel ) ; } } } else if ( this . ch === '}' ) { this . outdent ( ) ; this . print [ "}" ] ( this . ch ) ; insideRule = false ; if ( this . nestedLevel ) { this . nestedLevel -- ; } } else if ( this . ch === ":" ) { this . eatWhitespace ( ) ; if ( insideRule || enteringConditionalGroup ) { this . output . push ( this . ch , " " ) ; } else { if ( this . peek ( ) === ":" ) { this . next ( ) ; this . output . push ( "::" ) ; } else { this . output . push ( this . ch ) ; } } } else if ( this . ch === '"' || this . ch === '\'' ) { this . output . push ( this . eatString ( this . ch ) ) ; } else if ( this . ch === ';' ) { if ( this . isCommentOnLine ( ) ) { let beforeComment = this . eatString ( '/' ) ; let comment = this . eatComment ( true ) ; this . output . push ( beforeComment , comment . substring ( 1 , comment . length - 1 ) , '\n' , this . indentString ) ; } else { this . output . push ( this . ch , '\n' , this . indentString ) ; } } else if ( this . ch === '(' ) { if ( this . lookBack ( "url" ) ) { this . output . push ( this . ch ) ; this . eatWhitespace ( ) ; if ( this . next ( ) ) { if ( this . ch !== ')' && this . ch !== '"' && this . ch !== '\'' ) { this . output . push ( this . eatString ( ')' ) ) ; } else { this . pos -- ; } } } else { if ( isAfterSpace ) { this . singleSpace ( ) ; } this . output . push ( this . ch ) ; this . eatWhitespace ( ) ; } } else if ( this . ch === ')' ) { this . output . push ( this . ch ) ; } else if ( this . ch === ',' ) { this . eatWhitespace ( ) ; this . output . push ( this . ch ) ; if ( ! insideRule && selectorSeparatorNewline ) { this . newLine ( false ) ; } else { this . singleSpace ( ) ; } } else if ( this . ch === ']' ) { this . output . push ( this . ch ) ; } else if ( this . ch === '[' || this . ch === '=' ) { this . eatWhitespace ( ) ; this . output . push ( this . ch ) ; } else { if ( isAfterSpace ) { this . singleSpace ( ) ; } this . output . push ( this . ch ) ; } } let sweetCode = this . output . join ( '' ) . replace ( /[\n ]+$/ , '' ) ; let should = endWithNewline ; let actually = /\n$/ . test ( sweetCode ) ; if ( should && ! actually ) { sweetCode += "\n" ; } else if ( ! should && actually ) { sweetCode = sweetCode . slice ( 0 , - 1 ) ; } return sweetCode ; } }