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