123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- /*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
- };
- }
- 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 isFirstNewLine = true;
- while (whitespaceChar.test(this._input.peek())) {
- this._ch = this._input.next();
- if (allowAtLeastOneNewLine && this._ch === '\n') {
- if (this._options.preserve_newlines || isFirstNewLine) {
- isFirstNewLine = false;
- 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 topCharacter = this._ch;
- 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.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 {
- 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);
- }
- }
- } else if (this._ch === ":") {
- 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 === ';') {
- 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);
- this.eatWhitespace();
- parenLevel++;
- this.indent();
- }
- } else if (this._ch === ')') {
- if (parenLevel) {
- parenLevel--;
- this.outdent();
- }
- this.print_string(this._ch);
- } else if (this._ch === ',') {
- this.print_string(this._ch);
- this.eatWhitespace(true);
- if (this._options.selector_separator_newline && !insidePropertyValue && parenLevel === 0 && !insideAtImport) {
- 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 {
- this.preserveSingleSpace(isAfterSpace);
- this.print_string(this._ch);
- }
- }
- var sweetCode = this._output.get_code(eol);
- return sweetCode;
- };
- module.exports.Beautifier = Beautifier;
|