/*jshint node:true */
|
/*
|
|
The MIT License (MIT)
|
|
Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
|
|
Permission is hereby granted, free of charge, to any person
|
obtaining a copy of this software and associated documentation files
|
(the "Software"), to deal in the Software without restriction,
|
including without limitation the rights to use, copy, modify, merge,
|
publish, distribute, sublicense, and/or sell copies of the Software,
|
and to permit persons to whom the Software is furnished to do so,
|
subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be
|
included in all copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
SOFTWARE.
|
*/
|
|
'use strict';
|
|
var Options = require('./options').Options;
|
var Output = require('../core/output').Output;
|
var InputScanner = require('../core/inputscanner').InputScanner;
|
var Directives = require('../core/directives').Directives;
|
|
var directives_core = new Directives(/\/\*/, /\*\//);
|
|
var lineBreak = /\r\n|[\r\n]/;
|
var allLineBreaks = /\r\n|[\r\n]/g;
|
|
// tokenizer
|
var whitespaceChar = /\s/;
|
var whitespacePattern = /(?:\s|\n)+/g;
|
var block_comment_pattern = /\/\*(?:[\s\S]*?)((?:\*\/)|$)/g;
|
var comment_pattern = /\/\/(?:[^\n\r\u2028\u2029]*)/g;
|
|
function Beautifier(source_text, options) {
|
this._source_text = source_text || '';
|
// Allow the setting of language/file-type specific options
|
// with inheritance of overall settings
|
this._options = new Options(options);
|
this._ch = null;
|
this._input = null;
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
|
this.NESTED_AT_RULE = {
|
"@page": true,
|
"@font-face": true,
|
"@keyframes": true,
|
// also in CONDITIONAL_GROUP_RULE below
|
"@media": true,
|
"@supports": true,
|
"@document": true
|
};
|
this.CONDITIONAL_GROUP_RULE = {
|
"@media": true,
|
"@supports": true,
|
"@document": true
|
};
|
this.NON_SEMICOLON_NEWLINE_PROPERTY = [
|
"grid-template"
|
];
|
|
}
|
|
Beautifier.prototype.eatString = function(endChars) {
|
var result = '';
|
this._ch = this._input.next();
|
while (this._ch) {
|
result += this._ch;
|
if (this._ch === "\\") {
|
result += this._input.next();
|
} else if (endChars.indexOf(this._ch) !== -1 || this._ch === "\n") {
|
break;
|
}
|
this._ch = this._input.next();
|
}
|
return result;
|
};
|
|
// Skips any white space in the source text from the current position.
|
// When allowAtLeastOneNewLine is true, will output new lines for each
|
// newline character found; if the user has preserve_newlines off, only
|
// the first newline will be output
|
Beautifier.prototype.eatWhitespace = function(allowAtLeastOneNewLine) {
|
var result = whitespaceChar.test(this._input.peek());
|
var newline_count = 0;
|
while (whitespaceChar.test(this._input.peek())) {
|
this._ch = this._input.next();
|
if (allowAtLeastOneNewLine && this._ch === '\n') {
|
if (newline_count === 0 || newline_count < this._options.max_preserve_newlines) {
|
newline_count++;
|
this._output.add_new_line(true);
|
}
|
}
|
}
|
return result;
|
};
|
|
// Nested pseudo-class if we are insideRule
|
// and the next special character found opens
|
// a new block
|
Beautifier.prototype.foundNestedPseudoClass = function() {
|
var openParen = 0;
|
var i = 1;
|
var ch = this._input.peek(i);
|
while (ch) {
|
if (ch === "{") {
|
return true;
|
} else if (ch === '(') {
|
// pseudoclasses can contain ()
|
openParen += 1;
|
} else if (ch === ')') {
|
if (openParen === 0) {
|
return false;
|
}
|
openParen -= 1;
|
} else if (ch === ";" || ch === "}") {
|
return false;
|
}
|
i++;
|
ch = this._input.peek(i);
|
}
|
return false;
|
};
|
|
Beautifier.prototype.print_string = function(output_string) {
|
this._output.set_indent(this._indentLevel);
|
this._output.non_breaking_space = true;
|
this._output.add_token(output_string);
|
};
|
|
Beautifier.prototype.preserveSingleSpace = function(isAfterSpace) {
|
if (isAfterSpace) {
|
this._output.space_before_token = true;
|
}
|
};
|
|
Beautifier.prototype.indent = function() {
|
this._indentLevel++;
|
};
|
|
Beautifier.prototype.outdent = function() {
|
if (this._indentLevel > 0) {
|
this._indentLevel--;
|
}
|
};
|
|
/*_____________________--------------------_____________________*/
|
|
Beautifier.prototype.beautify = function() {
|
if (this._options.disabled) {
|
return this._source_text;
|
}
|
|
var source_text = this._source_text;
|
var eol = this._options.eol;
|
if (eol === 'auto') {
|
eol = '\n';
|
if (source_text && lineBreak.test(source_text || '')) {
|
eol = source_text.match(lineBreak)[0];
|
}
|
}
|
|
|
// HACK: newline parsing inconsistent. This brute force normalizes the this._input.
|
source_text = source_text.replace(allLineBreaks, '\n');
|
|
// reset
|
var baseIndentString = source_text.match(/^[\t ]*/)[0];
|
|
this._output = new Output(this._options, baseIndentString);
|
this._input = new InputScanner(source_text);
|
this._indentLevel = 0;
|
this._nestedLevel = 0;
|
|
this._ch = null;
|
var parenLevel = 0;
|
|
var insideRule = false;
|
// This is the value side of a property value pair (blue in the following ex)
|
// label { content: blue }
|
var insidePropertyValue = false;
|
var enteringConditionalGroup = false;
|
var insideAtExtend = false;
|
var insideAtImport = false;
|
var insideScssMap = false;
|
var topCharacter = this._ch;
|
var insideNonSemiColonValues = false;
|
var whitespace;
|
var isAfterSpace;
|
var previous_ch;
|
|
while (true) {
|
whitespace = this._input.read(whitespacePattern);
|
isAfterSpace = whitespace !== '';
|
previous_ch = topCharacter;
|
this._ch = this._input.next();
|
if (this._ch === '\\' && this._input.hasNext()) {
|
this._ch += this._input.next();
|
}
|
topCharacter = this._ch;
|
|
if (!this._ch) {
|
break;
|
} else if (this._ch === '/' && this._input.peek() === '*') {
|
// /* css comment */
|
// Always start block comments on a new line.
|
// This handles scenarios where a block comment immediately
|
// follows a property definition on the same line or where
|
// minified code is being beautified.
|
this._output.add_new_line();
|
this._input.back();
|
|
var comment = this._input.read(block_comment_pattern);
|
|
// Handle ignore directive
|
var directives = directives_core.get_directives(comment);
|
if (directives && directives.ignore === 'start') {
|
comment += directives_core.readIgnored(this._input);
|
}
|
|
this.print_string(comment);
|
|
// Ensures any new lines following the comment are preserved
|
this.eatWhitespace(true);
|
|
// Block comments are followed by a new line so they don't
|
// share a line with other properties
|
this._output.add_new_line();
|
} else if (this._ch === '/' && this._input.peek() === '/') {
|
// // single line comment
|
// Preserves the space before a comment
|
// on the same line as a rule
|
this._output.space_before_token = true;
|
this._input.back();
|
this.print_string(this._input.read(comment_pattern));
|
|
// Ensures any new lines following the comment are preserved
|
this.eatWhitespace(true);
|
} else if (this._ch === '@' || this._ch === '$') {
|
this.preserveSingleSpace(isAfterSpace);
|
|
// deal with less propery mixins @{...}
|
if (this._input.peek() === '{') {
|
this.print_string(this._ch + this.eatString('}'));
|
} else {
|
this.print_string(this._ch);
|
|
// strip trailing space, if present, for hash property checks
|
var variableOrRule = this._input.peekUntilAfter(/[: ,;{}()[\]\/='"]/g);
|
|
if (variableOrRule.match(/[ :]$/)) {
|
// we have a variable or pseudo-class, add it and insert one space before continuing
|
variableOrRule = this.eatString(": ").replace(/\s$/, '');
|
this.print_string(variableOrRule);
|
this._output.space_before_token = true;
|
}
|
|
variableOrRule = variableOrRule.replace(/\s$/, '');
|
|
if (variableOrRule === 'extend') {
|
insideAtExtend = true;
|
} else if (variableOrRule === 'import') {
|
insideAtImport = true;
|
}
|
|
// might be a nesting at-rule
|
if (variableOrRule in this.NESTED_AT_RULE) {
|
this._nestedLevel += 1;
|
if (variableOrRule in this.CONDITIONAL_GROUP_RULE) {
|
enteringConditionalGroup = true;
|
}
|
// might be less variable
|
} else if (!insideRule && parenLevel === 0 && variableOrRule.indexOf(':') !== -1) {
|
insidePropertyValue = true;
|
this.indent();
|
}
|
}
|
} else if (this._ch === '#' && this._input.peek() === '{') {
|
this.preserveSingleSpace(isAfterSpace);
|
this.print_string(this._ch + this.eatString('}'));
|
} else if (this._ch === '{') {
|
if (insidePropertyValue) {
|
insidePropertyValue = false;
|
this.outdent();
|
}
|
|
// when entering conditional groups, only rulesets are allowed
|
if (enteringConditionalGroup) {
|
enteringConditionalGroup = false;
|
insideRule = (this._indentLevel >= this._nestedLevel);
|
} else {
|
// otherwise, declarations are also allowed
|
insideRule = (this._indentLevel >= this._nestedLevel - 1);
|
}
|
if (this._options.newline_between_rules && insideRule) {
|
if (this._output.previous_line && this._output.previous_line.item(-1) !== '{') {
|
this._output.ensure_empty_line_above('/', ',');
|
}
|
}
|
|
this._output.space_before_token = true;
|
|
// The difference in print_string and indent order is necessary to indent the '{' correctly
|
if (this._options.brace_style === 'expand') {
|
this._output.add_new_line();
|
this.print_string(this._ch);
|
this.indent();
|
this._output.set_indent(this._indentLevel);
|
} else {
|
// inside mixin and first param is object
|
if (previous_ch === '(') {
|
this._output.space_before_token = false;
|
} else if (previous_ch !== ',') {
|
this.indent();
|
}
|
this.print_string(this._ch);
|
}
|
|
this.eatWhitespace(true);
|
this._output.add_new_line();
|
} else if (this._ch === '}') {
|
this.outdent();
|
this._output.add_new_line();
|
if (previous_ch === '{') {
|
this._output.trim(true);
|
}
|
insideAtImport = false;
|
insideAtExtend = false;
|
if (insidePropertyValue) {
|
this.outdent();
|
insidePropertyValue = false;
|
}
|
this.print_string(this._ch);
|
insideRule = false;
|
if (this._nestedLevel) {
|
this._nestedLevel--;
|
}
|
|
this.eatWhitespace(true);
|
this._output.add_new_line();
|
|
if (this._options.newline_between_rules && !this._output.just_added_blankline()) {
|
if (this._input.peek() !== '}') {
|
this._output.add_new_line(true);
|
}
|
}
|
if (this._input.peek() === ')') {
|
this._output.trim(true);
|
if (this._options.brace_style === "expand") {
|
this._output.add_new_line(true);
|
}
|
}
|
} else if (this._ch === ":") {
|
|
for (var i = 0; i < this.NON_SEMICOLON_NEWLINE_PROPERTY.length; i++) {
|
if (this._input.lookBack(this.NON_SEMICOLON_NEWLINE_PROPERTY[i])) {
|
insideNonSemiColonValues = true;
|
break;
|
}
|
}
|
|
if ((insideRule || enteringConditionalGroup) && !(this._input.lookBack("&") || this.foundNestedPseudoClass()) && !this._input.lookBack("(") && !insideAtExtend && parenLevel === 0) {
|
// 'property: value' delimiter
|
// which could be in a conditional group query
|
this.print_string(':');
|
if (!insidePropertyValue) {
|
insidePropertyValue = true;
|
this._output.space_before_token = true;
|
this.eatWhitespace(true);
|
this.indent();
|
}
|
} else {
|
// sass/less parent reference don't use a space
|
// sass nested pseudo-class don't use a space
|
|
// preserve space before pseudoclasses/pseudoelements, as it means "in any child"
|
if (this._input.lookBack(" ")) {
|
this._output.space_before_token = true;
|
}
|
if (this._input.peek() === ":") {
|
// pseudo-element
|
this._ch = this._input.next();
|
this.print_string("::");
|
} else {
|
// pseudo-class
|
this.print_string(':');
|
}
|
}
|
} else if (this._ch === '"' || this._ch === '\'') {
|
this.preserveSingleSpace(isAfterSpace);
|
this.print_string(this._ch + this.eatString(this._ch));
|
this.eatWhitespace(true);
|
} else if (this._ch === ';') {
|
insideNonSemiColonValues = false;
|
if (parenLevel === 0) {
|
if (insidePropertyValue) {
|
this.outdent();
|
insidePropertyValue = false;
|
}
|
insideAtExtend = false;
|
insideAtImport = false;
|
this.print_string(this._ch);
|
this.eatWhitespace(true);
|
|
// This maintains single line comments on the same
|
// line. Block comments are also affected, but
|
// a new line is always output before one inside
|
// that section
|
if (this._input.peek() !== '/') {
|
this._output.add_new_line();
|
}
|
} else {
|
this.print_string(this._ch);
|
this.eatWhitespace(true);
|
this._output.space_before_token = true;
|
}
|
} else if (this._ch === '(') { // may be a url
|
if (this._input.lookBack("url")) {
|
this.print_string(this._ch);
|
this.eatWhitespace();
|
parenLevel++;
|
this.indent();
|
this._ch = this._input.next();
|
if (this._ch === ')' || this._ch === '"' || this._ch === '\'') {
|
this._input.back();
|
} else if (this._ch) {
|
this.print_string(this._ch + this.eatString(')'));
|
if (parenLevel) {
|
parenLevel--;
|
this.outdent();
|
}
|
}
|
} else {
|
this.preserveSingleSpace(isAfterSpace);
|
this.print_string(this._ch);
|
|
// handle scss/sass map
|
if (insidePropertyValue && previous_ch === "$" && this._options.selector_separator_newline) {
|
this._output.add_new_line();
|
insideScssMap = true;
|
} else {
|
this.eatWhitespace();
|
parenLevel++;
|
this.indent();
|
}
|
}
|
} else if (this._ch === ')') {
|
if (parenLevel) {
|
parenLevel--;
|
this.outdent();
|
}
|
if (insideScssMap && this._input.peek() === ";" && this._options.selector_separator_newline) {
|
insideScssMap = false;
|
this.outdent();
|
this._output.add_new_line();
|
}
|
this.print_string(this._ch);
|
} else if (this._ch === ',') {
|
this.print_string(this._ch);
|
this.eatWhitespace(true);
|
if (this._options.selector_separator_newline && (!insidePropertyValue || insideScssMap) && parenLevel === 0 && !insideAtImport && !insideAtExtend) {
|
this._output.add_new_line();
|
} else {
|
this._output.space_before_token = true;
|
}
|
} else if ((this._ch === '>' || this._ch === '+' || this._ch === '~') && !insidePropertyValue && parenLevel === 0) {
|
//handle combinator spacing
|
if (this._options.space_around_combinator) {
|
this._output.space_before_token = true;
|
this.print_string(this._ch);
|
this._output.space_before_token = true;
|
} else {
|
this.print_string(this._ch);
|
this.eatWhitespace();
|
// squash extra whitespace
|
if (this._ch && whitespaceChar.test(this._ch)) {
|
this._ch = '';
|
}
|
}
|
} else if (this._ch === ']') {
|
this.print_string(this._ch);
|
} else if (this._ch === '[') {
|
this.preserveSingleSpace(isAfterSpace);
|
this.print_string(this._ch);
|
} else if (this._ch === '=') { // no whitespace before or after
|
this.eatWhitespace();
|
this.print_string('=');
|
if (whitespaceChar.test(this._ch)) {
|
this._ch = '';
|
}
|
} else if (this._ch === '!' && !this._input.lookBack("\\")) { // !important
|
this.print_string(' ');
|
this.print_string(this._ch);
|
} else {
|
var preserveAfterSpace = previous_ch === '"' || previous_ch === '\'';
|
this.preserveSingleSpace(preserveAfterSpace || isAfterSpace);
|
this.print_string(this._ch);
|
|
if (!this._output.just_added_newline() && this._input.peek() === '\n' && insideNonSemiColonValues) {
|
this._output.add_new_line();
|
}
|
}
|
}
|
|
var sweetCode = this._output.get_code(eol);
|
|
return sweetCode;
|
};
|
|
module.exports.Beautifier = Beautifier;
|