/* * Copyright (C) 2019 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ CSSFormatter = class CSSFormatter { constructor(sourceText, builder, indentString = " ") { this._success = false; this._sourceText = sourceText; this._builder = builder; if (!this._builder) { this._builder = new FormatterContentBuilder(indentString); this._builder.setOriginalLineEndings(this._sourceText.lineEndings()); } this._format(); this._success = true; } // Public get success() { return this._success; } get formattedText() { if (!this._success) return null; return this._builder.formattedContent; } get sourceMapData() { if (!this._success) return null; return this._builder.sourceMapData; } // Private _format() { const quoteTypes = new Set([`"`, `'`]); const dedentBefore = new Set([`}`]); const newlineBefore = new Set([`}`]); const newlineAfter = new Set([`{`, `}`, `;`]); const indentAfter = new Set([`{`]); const addSpaceBefore = new Set([`+`, `*`, `~`, `>`, `(`, `{`, `!`]); const addSpaceAfter = new Set([`,`, `+`, `*`, `~`, `>`, `)`, `:`]); const removeSpaceBefore = new Set([`,`, `(`, `)`, `}`, `:`, `;`]); const removeSpaceAfter = new Set([`(`, `{`, `}`, `,`, `!`, `;`]); const whitespaceOnlyRegExp = /^\s*$/; const inAtRuleRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+/; const inAtRuleBeforeParenthesisRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+$/; const inAtRuleAfterParenthesisRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+[^("':]*\([^"':]*:/; const inAtSupportsRuleRegExp = /^\s*@[a-zA-Z][-a-zA-Z]+[^"':]*:/; const lineStartCouldBePropertyRegExp = /^\s+[-_a-zA-Z][-_a-zA-Z0-9]*/; const lastTokenWasOpenParenthesisRegExp = /\(\s*$/; let depth = 0; let specialSequenceStack = []; let index = 0; let current = null; let testCurrentLine = (regExp) => regExp.test(this._builder.currentLine); let inSelector = () => { let nextOpenBrace = this._sourceText.indexOf(`{`, index); if (nextOpenBrace !== -1) { let nextQuote = Infinity; for (let quoteType of quoteTypes) { let quoteIndex = this._sourceText.indexOf(quoteType, index); if (quoteIndex !== -1 && quoteIndex < nextQuote) nextQuote = quoteIndex; } if (nextOpenBrace < nextQuote) { let nextSemicolon = this._sourceText.indexOf(`;`, index); if (nextSemicolon === -1) nextSemicolon = Infinity; let nextNewline = this._sourceText.indexOf(`\n`, index); if (nextNewline === -1) nextNewline = Infinity; if (nextOpenBrace < Math.min(nextSemicolon, nextNewline)) return true; } } if (testCurrentLine(lineStartCouldBePropertyRegExp)) return false; return true; }; let inProperty = () => { if (!depth) return false; return !testCurrentLine(inAtRuleRegExp) && !inSelector(); }; let formatBefore = () => { if (this._builder.lastNewlineAppendWasMultiple && current === `}`) this._builder.removeLastNewline(); if (dedentBefore.has(current)) this._builder.dedent(); if (!this._builder.lastTokenWasNewline && newlineBefore.has(current)) this._builder.appendNewline(); if (!this._builder.lastTokenWasWhitespace && addSpaceBefore.has(current)) { let shouldAddSpaceBefore = () => { if (current === `(`) { if (testCurrentLine(inAtSupportsRuleRegExp)) return false; if (!testCurrentLine(inAtRuleRegExp)) return false; } return true; }; if (shouldAddSpaceBefore()) this._builder.appendSpace(); } while (this._builder.lastTokenWasWhitespace && removeSpaceBefore.has(current)) { let shouldRemoveSpaceBefore = () => { if (current === `:`) { if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) { if (!inProperty()) return false; } } if (current === `(`) { if (!testCurrentLine(lastTokenWasOpenParenthesisRegExp)) { if (testCurrentLine(inAtRuleRegExp) && !testCurrentLine(inAtRuleAfterParenthesisRegExp)) return false; } } return true; }; if (!shouldRemoveSpaceBefore()) break; this._builder.removeLastWhitespace(); } }; let formatAfter = () => { while (this._builder.lastTokenWasWhitespace && removeSpaceAfter.has(current)) { let shouldRemoveSpaceAfter = () => { if (current === `(`) { if (!testCurrentLine(lastTokenWasOpenParenthesisRegExp)) { if (!testCurrentLine(inAtRuleRegExp)) { if (!inProperty()) return false; } } } return true; }; if (!shouldRemoveSpaceAfter()) break; this._builder.removeLastWhitespace(); } if (!this._builder.lastTokenWasWhitespace && addSpaceAfter.has(current)) { let shouldAddSpaceAfter = () => { if (current === `:`) { if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) { if (!inProperty()) return false; } } if (current === `)`) { if (!testCurrentLine(inAtRuleRegExp)) { if (!inProperty()) return false; } } return true; }; if (shouldAddSpaceAfter()) this._builder.appendSpace(); } if (indentAfter.has(current)) this._builder.indent(); if (newlineAfter.has(current)) { if (current === `}`) this._builder.appendMultipleNewlines(2); else this._builder.appendNewline(); } }; for (; index < this._sourceText.length; ++index) { current = this._sourceText[index]; let possibleSpecialSequence = null; if (quoteTypes.has(current)) possibleSpecialSequence = {type: "quote", startIndex: index, endString: current}; else if (current === `/` && this._sourceText[index + 1] === `*`) possibleSpecialSequence = {type: "comment", startIndex: index, endString: `*/`}; else if (current === `u` && this._sourceText[index + 1] === `r` && this._sourceText[index + 2] === `l` && this._sourceText[index + 3] === `(`) possibleSpecialSequence = {type: "url", startIndex: index, endString: `)`}; if (possibleSpecialSequence || specialSequenceStack.length) { let currentSpecialSequence = specialSequenceStack.lastValue; if (currentSpecialSequence?.type !== "comment") { if (possibleSpecialSequence?.type !== "comment") { let escapeCount = 0; while (this._sourceText[index - 1 - escapeCount] === "\\") ++escapeCount; if (escapeCount % 2) continue; } if (possibleSpecialSequence && (!currentSpecialSequence || currentSpecialSequence.type !== possibleSpecialSequence.type || currentSpecialSequence.endString !== possibleSpecialSequence.endString)) { specialSequenceStack.push(possibleSpecialSequence); continue; } } if (Array.from(currentSpecialSequence.endString).some((item, i) => index + i < this._sourceText.length && this._sourceText[index - currentSpecialSequence.endString.length + i + 1] !== item)) continue; let inComment = specialSequenceStack.some((item) => item.type === "comment"); specialSequenceStack.pop(); if (specialSequenceStack.length) continue; let specialSequenceText = this._sourceText.substring(currentSpecialSequence.startIndex, index + 1); let lastSourceNewlineWasMultiple = this._sourceText[currentSpecialSequence.startIndex - 1] === `\n` && this._sourceText[currentSpecialSequence.startIndex - 2] === `\n`; let lastAppendNewlineWasMultiple = this._builder.lastNewlineAppendWasMultiple; let commentOnOwnLine = false; if (inComment) { commentOnOwnLine = testCurrentLine(whitespaceOnlyRegExp); if (!commentOnOwnLine || lastAppendNewlineWasMultiple) { while (this._builder.lastTokenWasNewline) this._builder.removeLastNewline(); } if (commentOnOwnLine) { if (currentSpecialSequence.startIndex > 0 && !this._builder.indented) this._builder.appendNewline(); } else if (this._builder.currentLine.length && !this._builder.lastTokenWasWhitespace) this._builder.appendSpace(); if (this._builder.lastTokenWasNewline && lastSourceNewlineWasMultiple) this._builder.appendNewline(true); } this._builder.appendStringWithPossibleNewlines(specialSequenceText, currentSpecialSequence.startIndex); if (inComment) { if (commentOnOwnLine) { if (lastAppendNewlineWasMultiple && !lastSourceNewlineWasMultiple) this._builder.appendMultipleNewlines(2); else this._builder.appendNewline(); } else if (!/\s/.test(current)) { if (!testCurrentLine(inAtRuleRegExp) && !inSelector() && !inProperty()) this._builder.appendNewline(); else this._builder.appendSpace(); } } formatAfter(); continue; } if (/\s/.test(current)) { if (current === `\n`) { if (!this._builder.lastTokenWasNewline) { while (this._builder.lastTokenWasWhitespace) this._builder.removeLastWhitespace(); if (!removeSpaceAfter.has(this._builder.lastToken)) this._builder.appendNewline(); else this._builder.appendSpace(); } } else if (!this._builder.lastTokenWasWhitespace && !removeSpaceAfter.has(this._builder.lastToken)) this._builder.appendSpace(); continue; } if (current === `{`) ++depth; else if (current === `}`) --depth; formatBefore(); this._builder.appendToken(current, index); formatAfter(); } if (specialSequenceStack.length) { let firstSpecialSequence = specialSequenceStack[0]; this._builder.appendStringWithPossibleNewlines(this._sourceText.substring(firstSpecialSequence.startIndex), firstSpecialSequence.startIndex); } this._builder.finish(); } };