• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
36FormatterWorker.CSSFormatter = function(content, builder)
37{
38    this._content = content;
39    this._builder = builder;
40    this._lastLine = -1;
41    this._state = {};
42}
43
44FormatterWorker.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 */
144FormatterWorker.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
165FormatterWorker.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