Home | History | Annotate | Download | only in script_formatter_worker
      1 /*
      2  * Copyright (C) 2013 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @param {string} content
     34  * @param {!FormatterWorker.CSSFormattedContentBuilder} builder
     35  */
     36 FormatterWorker.CSSFormatter = function(content, builder)
     37 {
     38     this._content = content;
     39     this._builder = builder;
     40     this._lastLine = -1;
     41     this._state = {};
     42 }
     43 
     44 FormatterWorker.CSSFormatter.prototype = {
     45     format: function()
     46     {
     47         this._lineEndings = this._lineEndings(this._content);
     48         var tokenize = FormatterWorker.createTokenizer("text/css");
     49         var lines = this._content.split("\n");
     50 
     51         for (var i = 0; i < lines.length; ++i) {
     52             var line = lines[i];
     53             tokenize(line, this._tokenCallback.bind(this, i));
     54         }
     55         this._builder.flushNewLines(true);
     56     },
     57 
     58     /**
     59      * @param {string} text
     60      */
     61     _lineEndings: function(text)
     62     {
     63         var lineEndings = [];
     64         var i = text.indexOf("\n");
     65         while (i !== -1) {
     66             lineEndings.push(i);
     67             i = text.indexOf("\n", i + 1);
     68         }
     69         lineEndings.push(text.length);
     70         return lineEndings;
     71     },
     72 
     73     /**
     74      * @param {number} startLine
     75      * @param {string} token
     76      * @param {?string} type
     77      * @param {number} startColumn
     78      */
     79     _tokenCallback: function(startLine, token, type, startColumn)
     80     {
     81         if (startLine !== this._lastLine)
     82             this._state.eatWhitespace = true;
     83         if (/^property/.test(type) && !this._state.inPropertyValue)
     84             this._state.seenProperty = true;
     85         this._lastLine = startLine;
     86         var isWhitespace = /^\s+$/.test(token);
     87         if (isWhitespace) {
     88             if (!this._state.eatWhitespace)
     89                 this._builder.addSpace();
     90             return;
     91         }
     92         this._state.eatWhitespace = false;
     93         if (token === "\n")
     94             return;
     95 
     96         if (token !== "}") {
     97             if (this._state.afterClosingBrace)
     98                 this._builder.addNewLine();
     99             this._state.afterClosingBrace = false;
    100         }
    101         var startPosition = (startLine ? this._lineEndings[startLine - 1] : 0) + startColumn;
    102         if (token === "}") {
    103             if (this._state.inPropertyValue)
    104                 this._builder.addNewLine();
    105             this._builder.decreaseNestingLevel();
    106             this._state.afterClosingBrace = true;
    107             this._state.inPropertyValue = false;
    108         } else if (token === ":" && !this._state.inPropertyValue && this._state.seenProperty) {
    109             this._builder.addToken(token, startPosition, startLine, startColumn);
    110             this._builder.addSpace();
    111             this._state.eatWhitespace = true;
    112             this._state.inPropertyValue = true;
    113             this._state.seenProperty = false;
    114             return;
    115         } else if (token === "{") {
    116             this._builder.addSpace();
    117             this._builder.addToken(token, startPosition, startLine, startColumn);
    118             this._builder.addNewLine();
    119             this._builder.increaseNestingLevel();
    120             return;
    121         }
    122 
    123         this._builder.addToken(token, startPosition, startLine, startColumn);
    124 
    125         if (type === "comment" && !this._state.inPropertyValue && !this._state.seenProperty)
    126             this._builder.addNewLine();
    127         if (token === ";" && this._state.inPropertyValue) {
    128             this._state.inPropertyValue = false;
    129             this._builder.addNewLine();
    130         } else if (token === "}") {
    131             this._builder.addNewLine();
    132         }
    133     }
    134 }
    135 
    136 /**
    137  * @constructor
    138  * @param {string} content
    139  * @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
    140  * @param {number} originalOffset
    141  * @param {number} formattedOffset
    142  * @param {string} indentString
    143  */
    144 FormatterWorker.CSSFormattedContentBuilder = function(content, mapping, originalOffset, formattedOffset, indentString)
    145 {
    146     this._originalContent = content;
    147     this._originalOffset = originalOffset;
    148     this._lastOriginalPosition = 0;
    149 
    150     this._formattedContent = [];
    151     this._formattedContentLength = 0;
    152     this._formattedOffset = formattedOffset;
    153     this._lastFormattedPosition = 0;
    154 
    155     this._mapping = mapping;
    156 
    157     this._lineNumber = 0;
    158     this._nestingLevel = 0;
    159     this._needNewLines = 0;
    160     this._atLineStart = true;
    161     this._indentString = indentString;
    162     this._cachedIndents = {};
    163 }
    164 
    165 FormatterWorker.CSSFormattedContentBuilder.prototype = {
    166     /**
    167      * @param {string} token
    168      * @param {number} startPosition
    169      * @param {number} startLine
    170      * @param {number} startColumn
    171      */
    172     addToken: function(token, startPosition, startLine, startColumn)
    173     {
    174         if ((this._isWhitespaceRun || this._atLineStart) && /^\s+$/.test(token))
    175             return;
    176 
    177         if (this._isWhitespaceRun && this._lineNumber === startLine && !this._needNewLines)
    178             this._addText(" ");
    179 
    180         this._isWhitespaceRun = false;
    181         this._atLineStart = false;
    182 
    183         while (this._lineNumber < startLine) {
    184             this._addText("\n");
    185             this._addIndent();
    186             this._needNewLines = 0;
    187             this._lineNumber += 1;
    188             this._atLineStart = true;
    189         }
    190 
    191         if (this._needNewLines) {
    192             this.flushNewLines();
    193             this._addIndent();
    194             this._atLineStart = true;
    195         }
    196 
    197         this._addMappingIfNeeded(startPosition);
    198         this._addText(token);
    199         this._lineNumber = startLine;
    200     },
    201 
    202     addSpace: function()
    203     {
    204         if (this._isWhitespaceRun)
    205             return;
    206         this._isWhitespaceRun = true;
    207     },
    208 
    209     addNewLine: function()
    210     {
    211         ++this._needNewLines;
    212     },
    213 
    214     /**
    215      * @param {boolean=} atLeastOne
    216      */
    217     flushNewLines: function(atLeastOne)
    218     {
    219         var newLineCount = atLeastOne && !this._needNewLines ? 1 : this._needNewLines;
    220         if (newLineCount)
    221             this._isWhitespaceRun = false;
    222         for (var i = 0; i < newLineCount; ++i)
    223             this._addText("\n");
    224         this._needNewLines = 0;
    225     },
    226 
    227     increaseNestingLevel: function()
    228     {
    229         this._nestingLevel += 1;
    230     },
    231 
    232     /**
    233      * @param {boolean=} addNewline
    234      */
    235     decreaseNestingLevel: function(addNewline)
    236     {
    237         if (this._nestingLevel)
    238             this._nestingLevel -= 1;
    239         if (addNewline)
    240             this.addNewLine();
    241     },
    242 
    243     /**
    244      * @return {string}
    245      */
    246     content: function()
    247     {
    248         return this._formattedContent.join("");
    249     },
    250 
    251     _addIndent: function()
    252     {
    253         if (this._cachedIndents[this._nestingLevel]) {
    254             this._addText(this._cachedIndents[this._nestingLevel]);
    255             return;
    256         }
    257 
    258         var fullIndent = "";
    259         for (var i = 0; i < this._nestingLevel; ++i)
    260             fullIndent += this._indentString;
    261         this._addText(fullIndent);
    262 
    263         // Cache a maximum of 20 nesting level indents.
    264         if (this._nestingLevel <= 20)
    265             this._cachedIndents[this._nestingLevel] = fullIndent;
    266     },
    267 
    268     /**
    269      * @param {string} text
    270      */
    271     _addText: function(text)
    272     {
    273         if (!text)
    274             return;
    275         this._formattedContent.push(text);
    276         this._formattedContentLength += text.length;
    277     },
    278 
    279     /**
    280      * @param {number} originalPosition
    281      */
    282     _addMappingIfNeeded: function(originalPosition)
    283     {
    284         if (originalPosition - this._lastOriginalPosition === this._formattedContentLength - this._lastFormattedPosition)
    285             return;
    286         this._mapping.original.push(this._originalOffset + originalPosition);
    287         this._lastOriginalPosition = originalPosition;
    288         this._mapping.formatted.push(this._formattedOffset + this._formattedContentLength);
    289         this._lastFormattedPosition = this._formattedContentLength;
    290     }
    291 }
    292