blob: d8766f8a078310065db03ece8b3984b374da603e [file] [log] [blame]
/*
* 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;
for (let i = 0; i < this._sourceText.length; ++i) {
let c = this._sourceText[i];
let testCurrentLine = (regExp) => regExp.test(this._builder.currentLine);
let inComment = false;
let inSelector = () => {
let nextOpenBrace = this._sourceText.indexOf(`{`, i);
if (nextOpenBrace !== -1) {
let nextQuote = Infinity;
for (let quoteType of quoteTypes) {
let quoteIndex = this._sourceText.indexOf(quoteType, i);
if (quoteIndex !== -1 && quoteIndex < nextQuote)
nextQuote = quoteIndex;
}
if (nextOpenBrace < nextQuote) {
let nextSemicolon = this._sourceText.indexOf(`;`, i);
if (nextSemicolon === -1)
nextSemicolon = Infinity;
let nextNewline = this._sourceText.indexOf(`\n`, i);
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 && c === `}`)
this._builder.removeLastNewline();
if (dedentBefore.has(c))
this._builder.dedent();
if (!this._builder.lastTokenWasNewline && newlineBefore.has(c))
this._builder.appendNewline();
if (!this._builder.lastTokenWasWhitespace && addSpaceBefore.has(c)) {
let shouldAddSpaceBefore = () => {
if (c === `(`) {
if (testCurrentLine(inAtSupportsRuleRegExp))
return false;
if (!testCurrentLine(inAtRuleRegExp))
return false;
}
return true;
};
if (shouldAddSpaceBefore())
this._builder.appendSpace();
}
while (this._builder.lastTokenWasWhitespace && removeSpaceBefore.has(c)) {
let shouldRemoveSpaceBefore = () => {
if (c === `:`) {
if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) {
if (!inProperty())
return false;
}
}
if (c === `(`) {
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(c)) {
let shouldRemoveSpaceAfter = () => {
if (c === `(`) {
if (!testCurrentLine(lastTokenWasOpenParenthesisRegExp)) {
if (!testCurrentLine(inAtRuleRegExp)) {
if (!inProperty())
return false;
}
}
}
return true;
};
if (!shouldRemoveSpaceAfter())
break;
this._builder.removeLastWhitespace();
}
if (!this._builder.lastTokenWasWhitespace && addSpaceAfter.has(c)) {
let shouldAddSpaceAfter = () => {
if (c === `:`) {
if (!testCurrentLine(this._builder.currentLine.includes(`(`) ? inAtRuleRegExp : inAtRuleBeforeParenthesisRegExp)) {
if (!inProperty())
return false;
}
}
if (c === `)`) {
if (!testCurrentLine(inAtRuleRegExp)) {
if (!inProperty())
return false;
}
}
return true;
};
if (shouldAddSpaceAfter())
this._builder.appendSpace();
}
if (indentAfter.has(c))
this._builder.indent();
if (newlineAfter.has(c)) {
if (c === `}`)
this._builder.appendMultipleNewlines(2);
else
this._builder.appendNewline();
}
};
let specialSequenceEnd = null;
if (quoteTypes.has(c))
specialSequenceEnd = c;
else if (c === `/` && this._sourceText[i + 1] === `*`) {
inComment = true;
specialSequenceEnd = `*/`;
} else if (c === `u` && this._sourceText[i + 1] === `r` && this._sourceText[i + 2] === `l` && this._sourceText[i + 3] === `(`)
specialSequenceEnd = `)`;
if (specialSequenceEnd) {
let startIndex = i;
let endIndex = i;
do {
endIndex = this._sourceText.indexOf(specialSequenceEnd, endIndex + specialSequenceEnd.length);
} while (endIndex !== -1 && !inComment && this._sourceText[endIndex - 1] === `\\`);
if (endIndex === -1)
endIndex = this._sourceText.length;
endIndex += specialSequenceEnd.length;
let specialSequenceText = this._sourceText.substring(startIndex, endIndex);
let lastSourceNewlineWasMultiple = this._sourceText[startIndex - 1] === `\n` && this._sourceText[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 (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, startIndex);
i = endIndex;
c = this._sourceText[i];
if (inComment) {
if (commentOnOwnLine) {
if (lastAppendNewlineWasMultiple && !lastSourceNewlineWasMultiple)
this._builder.appendMultipleNewlines(2);
else
this._builder.appendNewline();
} else if (!/\s/.test(c)) {
if (!testCurrentLine(inAtRuleRegExp) && !inSelector() && !inProperty())
this._builder.appendNewline();
else
this._builder.appendSpace();
}
}
--i; // Account for the iteration of the for loop.
c = this._sourceText[i];
formatAfter();
continue;
}
if (/\s/.test(c)) {
if (c === `\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 (c === `{`)
++depth;
else if (c === `}`)
--depth;
formatBefore();
this._builder.appendToken(c, i);
formatAfter();
}
this._builder.finish();
}
};