346 lines
14 KiB
JavaScript
346 lines
14 KiB
JavaScript
/*
|
|
* 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();
|
|
}
|
|
};
|