install gulp

This commit is contained in:
2018-05-28 13:26:25 +02:00
parent 2de92c908b
commit 04fab6ba46
8032 changed files with 709336 additions and 3100 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* Command line implementation for CSSComb
*
* Usage example:
* ./node_modules/.bin/csscomb [options] file1 [dir1 [fileN [dirN]]]
*/
var fs = require('fs');
var path = require('path');
var program = require('commander');
var vow = require('vow');
var Comb = require('./csscomb');
program
.version(require('../package.json').version)
.usage('[options] <file ...>')
.option('-v, --verbose', 'verbose mode')
.option('-c, --config [path]', 'configuration file path')
.option('-d, --detect', 'detect mode (would return detected options)')
.option('-l, --lint', 'in case some fixes needed returns an error')
.parse(process.argv);
if (!program.args.length) {
console.log('No input paths specified');
program.help();
}
var comb = new Comb();
if (program.detect) {
console.log(JSON.stringify(Comb.detectInFile(program.args[0]), false, 4));
process.exit(0);
}
var config;
var configPath = program.config &&
path.resolve(process.cwd(), program.config) ||
Comb.getCustomConfigPath();
if (!fs.existsSync(configPath)) {
config = require('../config/csscomb.json');
} else if (configPath.match(/\.css$/)) {
config = Comb.detectInFile(configPath);
} else {
config = Comb.getCustomConfig(configPath);
}
if (!config) {
console.log('Configuration file ' + configPath + ' was not found.');
process.exit(1);
}
if (config.template) {
if (fs.existsSync(config.template)) {
var templateConfig = Comb.detectInFile(config.template);
for (var attrname in templateConfig) {
if (!config[attrname]) {
config[attrname] = templateConfig[attrname];
}
}
} else {
console.log('Template configuration file ' + config.template + ' was not found.');
process.exit(1);
}
}
console.time('spent');
config.verbose = program.verbose === true || config.verbose;
config.lint = program.lint;
comb.configure(config);
vow.all(program.args.map(comb.processPath.bind(comb)))
.then(function(changedFiles) {
changedFiles = [].concat.apply([], changedFiles)
.filter(function(isChanged) {
return isChanged !== undefined;
});
for (var i = changedFiles.length, tbchanged = 0; i--;) {
tbchanged += changedFiles[i];
}
var changed = config.lint ? 0 : tbchanged;
if (config.verbose) {
console.log('');
console.log(changedFiles.length + ' file' + (changedFiles.length === 1 ? '' : 's') + ' processed');
console.log(changed + ' file' + (changed === 1 ? '' : 's') + ' fixed');
console.timeEnd('spent');
}
if (config.lint && tbchanged) {
process.exit(1);
}
})
.fail(function(e) {
console.log('stack: ', e.stack);
process.exit(1);
});
+321
View File
@@ -0,0 +1,321 @@
var Comb = require('csscomb-core');
var gonzales = require('gonzales-pe');
var fs = require('fs');
var path = require('path');
/**
* Converts CSS string to AST.
*
* @param {String} text CSS string
* @param {String} [syntax] Syntax name (e.g., `scss`)
* @param {String} [filename]
* @returns {Array} AST
*/
function cssToAST(text, syntax, filename) {
var string = JSON.stringify;
var fileInfo = filename ? ' at ' + filename : '';
var tree;
try {
tree = gonzales.parse(text, { syntax: syntax });
} catch (e) {
throw new Error('Parsing error' + fileInfo + ': ' + e.message);
}
// TODO: When can tree be undefined? <tg>
if (typeof tree === 'undefined') {
throw new Error('Undefined tree' + fileInfo + ': ' + string(text) + ' => ' + string(tree));
}
return tree;
}
/**
* Gets option's data needed for detection
*
* @param {String} optionName
* @returns {Object} Object with option's name, link to `detect()` method
* and default value for the case when nothing can be detected
*/
function getHandler(optionName) {
var option = require('./options/' + optionName);
if (!option.detect) throw new Error('Option does not have `detect()` method.');
return {
name: option.name,
detect: option.detect,
detectDefault: option.detectDefault
};
}
/**
* Processes tree node and detects options.
*
* @param {Array} node Tree node
* @param {Number} level Indent level
* @param {Object} handler Object with option's data
* @param {Object} detectedOptions
*/
function detectInNode(node, level, handler, detectedOptions) {
node.map(function(tree) {
var detected = handler.detect(tree);
var variants = detectedOptions[handler.name];
if (typeof detected === 'object') {
variants.push.apply(variants, detected);
} else if (typeof detected !== 'undefined') {
variants.push(detected);
}
//if (nodeType === 'atrulers' || nodeType === 'block') level++;
});
}
/**
* Processes tree and detects options.
*
* @param {Array} tree
* @param {Array} handlers List of options that we should look for
* @returns {Object} Map with detected options and all variants of possible
* values
*/
function detectInTree(tree, handlers) {
var detectedOptions = {};
// We walk across complete tree for each handler,
// because we need strictly maintain order in which handlers work,
// despite fact that handlers work on different level of the tree.
handlers.forEach(function(handler) {
detectedOptions[handler.name] = [];
// TODO: Pass all parameters as one object? <tg>
detectInNode(tree, 0, handler, detectedOptions);
});
return detectedOptions;
}
/**
* Gets the detected options.
*
* @param {Object} detected
* @param {Array} handlers
* @returns {Object}
*/
function getDetectedOptions(detected, handlers) {
var options = {};
Object.keys(detected).forEach(function(option) {
// List of all the detected variants from the stylesheet for the given option:
var values = detected[option];
var i;
if (!values.length) {
// If there are no values for the option, check if there is a default one:
for (i = handlers.length; i--;) {
if (handlers[i].name === option &&
handlers[i].detectDefault !== undefined) {
options[option] = handlers[i].detectDefault;
break;
}
}
} else if (values.length === 1) {
options[option] = values[0];
} else {
// If there are more than one value for the option, find the most popular one;
// `variants` would be populated with the popularity for different values.
var variants = {};
var bestGuess = null;
var maximum = 0;
for (i = values.length; i--;) {
var currentValue = values[i];
// Count the current value:
if (variants[currentValue]) {
variants[currentValue]++;
} else {
variants[currentValue] = 1;
}
// If the current variant is the most popular one, treat
// it as the best guess:
if (variants[currentValue] >= maximum) {
maximum = variants[currentValue];
bestGuess = currentValue;
}
}
if (bestGuess !== null) {
options[option] = bestGuess;
}
}
});
return options;
}
/**
* Starts Code Style processing process.
*
* @param {String|Object} config
* @constructor
* @name CSScomb
*/
var CSScomb = function(config) {
var options = fs.readdirSync(__dirname + '/options').map(function(option) {
return require('./options/' + option);
});
var comb = new Comb(options, 'css', 'less', 'scss', 'sass');
// If config was passed, configure:
if (typeof config === 'string') {
config = CSScomb.getConfig(config);
}
if (typeof config === 'object') {
comb.configure(config);
}
return comb;
};
/**
* STATIC METHODS
* Methods that can be called without creating an instance:
* - getConfig;
* - getCustomConfig;
* - getCustomConfigPath;
* - detectInFile;
* - detectInString.
* For example: `CSScomb.getConfig('zen')`
*/
/**
* Gets one of configuration files from configs' directory.
*
* @param {String} name Config's name, e.g. 'yandex'
* @returns {Object} Configuration object
*/
CSScomb.getConfig = function getConfig(name) {
var DEFAULT_CONFIG_NAME = 'csscomb';
name = name || DEFAULT_CONFIG_NAME;
if (typeof name !== 'string') {
throw new Error('Config name must be a string.');
}
var CONFIG_DIR_PATH = '../config';
var availableConfigsNames = fs.readdirSync(__dirname + '/' + CONFIG_DIR_PATH)
.map(function(configFileName) {
return configFileName.split('.')[0]; // strip file extension(s)
});
if (availableConfigsNames.indexOf(name) < 0) {
var configsNamesAsString = availableConfigsNames
.map(function(configName) {
return '\'' + configName + '\'';
})
.join(', ');
throw new Error('"' + name + '" is not a valid config name. Try one of ' +
'the following: ' + configsNamesAsString + '.');
}
return require(CONFIG_DIR_PATH + '/' + name + '.json');
};
/**
* Gets configuration from provided config path or looks for it in common
* places.
*
* @param {String} [configPath]
* @returns {Object|null}
*/
CSScomb.getCustomConfig = function getCustomConfig(configPath) {
var config;
configPath = configPath || CSScomb.getCustomConfigPath();
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch (e) {
config = null;
}
return config;
};
/**
* Looks for a config file: recursively from current (process) directory
* up to $HOME dir
* If no custom config file is found, return `null`.
*
* @param {String} [configPath]
* @returns {String | null}
*/
CSScomb.getCustomConfigPath = function getCustomConfigPath(configPath) {
var HOME = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
configPath = configPath || path.join(process.cwd(), '.csscomb.json');
// If we've finally found a config, return its path:
if (fs.existsSync(configPath)) return fs.realpathSync(configPath);
// If we are in HOME dir already and yet no config file, return a default
// one from our package.
// If project is located not under HOME, compare to root instead.
// Since there appears to be no good way to get root path in
// Windows, assume that if current dir has no parent dir, we're in
// root.
var dirname = path.dirname(configPath);
var parentDirname = path.dirname(dirname);
if (dirname === HOME || dirname === parentDirname) return null;
// If there is no config in this directory, go one level up and look for
// a config there:
configPath = path.join(parentDirname, '.csscomb.json');
return CSScomb.getCustomConfigPath(configPath);
};
/**
* Detects the options in the given file
*
* @param {String} path Path to the stylesheet
* @param {Array} options List of options to detect
* @returns {Object} Detected options
*/
CSScomb.detectInFile = function detectInFile(path, options) {
var stylesheet = fs.readFileSync(path, 'utf8');
return CSScomb.detectInString(stylesheet, options);
};
/**
* Detects the options in the given string
*
* @param {String} text Stylesheet
* @param {Array} options List of options to detect
* @returns {Object} Detected options
*/
CSScomb.detectInString = function detectInString(text, options) {
var result;
var handlers = [];
if (!text) return text;
var optionNames = fs.readdirSync(__dirname + '/options');
optionNames.forEach(function(option) {
option = option.slice(0, -3);
if (options && options.indexOf(option) < 0) return;
try {
handlers.push(getHandler(option));
} catch (e) {
console.warn('\nFailed to load "%s" option:\n%s', option, e.message);
}
});
var tree = cssToAST(text);
var detectedOptions = detectInTree(tree, handlers);
result = getDetectedOptions(detectedOptions, handlers);
// Handle conflicting options with spaces around braces:
var blockIndent = result['block-indent'];
var spaceAfterOpeningBrace = result['space-after-opening-brace'];
if (typeof blockIndent === 'string' &&
spaceAfterOpeningBrace &&
spaceAfterOpeningBrace.indexOf('\n') > -1) {
result['space-after-opening-brace'] = '\n';
}
return result;
};
module.exports = CSScomb;
+5
View File
@@ -0,0 +1,5 @@
'use strict';
module.exports = function (string) {
return string.replace(/\n\s+/gm, ' ');
};
+4
View File
@@ -0,0 +1,4 @@
// jscs:disable
'use strict';
module.exports = require('../node_modules/csscomb-core/node_modules/gonzales-pe');
@@ -0,0 +1,73 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'always-semicolon',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true] },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
var nodeWithoutSemicolon;
if (!node.is('block')) return;
mainLoop:
for (var i = node.length; i--;) {
var currentNode = node.get(i);
// Skip nodes that already have `;` at the end:
if (currentNode.is('declarationDelimiter')) break;
// Add semicolon only after declarations and includes.
// If current node is include, insert semicolon right into it.
// If it's declaration, look for value node:
if (currentNode.is('include')) {
nodeWithoutSemicolon = currentNode;
} else if (currentNode.is('declaration')) {
nodeWithoutSemicolon = currentNode.last('value');
} else {
continue;
}
// Check if there are spaces and comments at the end of the node:
for (var j = nodeWithoutSemicolon.length; j--; ) {
var lastNode = nodeWithoutSemicolon.get(j);
// If the node's last child is block, do not add semicolon:
// TODO: Add syntax check and run the code only for scss
if (lastNode.is('block')) {
break mainLoop;
} else if (!lastNode.is('space') &&
!lastNode.is('multilineComment') &&
!lastNode.is('singlelineComment')) {
j++;
break;
}
}
var declDelim = gonzales.createNode({ type: 'declarationDelimiter', content: ';' });
nodeWithoutSemicolon.insert(j, declDelim);
break;
}
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('block')) return;
for (var i = node.length; i--;) {
var nodeItem = node.get(i);
if (nodeItem.is('declarationDelimiter')) return true;
if (nodeItem.is('declaration')) return false;
}
}
};
@@ -0,0 +1,135 @@
module.exports = (function() {
var syntax;
var value;
function processNode(node, level) {
level = level || 0;
// XXX: Hack for braces
if (node.is('braces') || node.is('id')) return;
for (var i = 0; i < node.length; i++) {
var n = node.get(i);
if (!n) continue;
if (syntax === 'sass' && n.is('block')) {
processSassBlock(n, level, value);
}
// Continue only with space nodes inside {...}:
if (syntax !== 'sass' && level !== 0 && n.is('space')) {
processSpaceNode(n, level, value);
}
if (n.is('block') || n.is('atrulers')) level++;
processNode(n, level);
}
}
function processSassBlock(node, level, value) {
var spaces;
var whitespaceNode;
var i;
for (i = node.length; i--;) {
whitespaceNode = node.get(i);
if (!whitespaceNode.is('space')) continue;
if (whitespaceNode.content === '\n') continue;
spaces = whitespaceNode.content.replace(/[ \t]/gm, '');
spaces += new Array(level + 2).join(value);
whitespaceNode.content = spaces;
}
}
function processSpaceNode(node, level, value) {
var spaces;
// Remove all whitespaces and tabs, leave only new lines:
spaces = node.content.replace(/[ \t]/gm, '');
if (!spaces) return;
spaces += new Array(level + 1).join(value);
node.content = spaces;
}
return {
name: 'block-indent',
runBefore: 'sort-order',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function process(node) {
var spaces;
var whitespaceNode;
var i;
if (!node.is('stylesheet')) return;
syntax = this.getSyntax();
value = this.getValue('block-indent');
for (i = node.length; i--;) {
whitespaceNode = node.get(i);
if (!whitespaceNode.is('space')) continue;
spaces = whitespaceNode.content.replace(/\n[ \t]+/gm, '\n');
if (spaces === '') {
node.remove(i);
} else {
whitespaceNode.content = spaces;
}
}
processNode(node);
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
var result = [];
// Continue only with non-empty {...} blocks:
if (!node.is('atrulers') && !node.is('block') || !node.length)
return;
for (var i = node.length; i--;) {
var whitespaceNode = node.get(i);
if (!whitespaceNode.is('space')) continue;
var spaces = whitespaceNode.content;
var lastIndex = spaces.lastIndexOf('\n');
// Do not continue if there is no line break:
if (lastIndex < 0) continue;
// Number of spaces from beginning of line:
var spacesLength = spaces.slice(lastIndex + 1).length + 1;
result.push(new Array(spacesLength).join(' '));
}
return result;
}
};
})();
@@ -0,0 +1,34 @@
module.exports = {
name: 'color-case',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { string: /^lower|upper$/ },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('color')) return;
node.content = this.getValue('color-case') === 'lower' ?
node.content.toLowerCase() :
node.content.toUpperCase();
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('color')) return;
if (node.content.match(/^[^A-F]*[a-f][^A-F]*$/)) {
return 'lower';
} else if (node.content.match(/^[^a-f]*[A-F][^a-f]*$/)) {
return 'upper';
}
}
};
@@ -0,0 +1,34 @@
module.exports = {
name: 'color-shorthand',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true, false] },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('color')) return;
node.content = this.getValue('color-shorthand') ?
node.content.replace(/(\w)\1(\w)\2(\w)\3/i, '$1$2$3') :
node.content.replace(/^(\w)(\w)(\w)$/, '$1$1$2$2$3$3');
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('color')) return;
if (node.content.match(/^\w{3}$/)) {
return true;
} else if (node.content.match(/^(\w)\1(\w)\2(\w)\3$/)) {
return false;
}
}
};
@@ -0,0 +1,49 @@
module.exports = {
name: 'element-case',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { string: /^lower|upper$/ },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('selector') &&
!node.is('arguments')) return;
var value = this.getValue('element-case');
node.forEach('simpleSelector', function(selector) {
selector.forEach('ident', function(ident) {
ident.content = value === 'lower' ?
ident.content.toLowerCase() :
ident.content.toUpperCase();
});
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('selector') &&
!node.is('arguments')) return;
var variants = [];
node.forEach('simpleSelector', function(selector) {
selector.forEach('ident', function(ident) {
if (ident.content.match(/^[a-z]+$/)) {
variants.push('lower');
} else if (ident.content.match(/^[A-Z]+$/)) {
variants.push('upper');
}
});
});
return variants;
}
};
@@ -0,0 +1,41 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'eof-newline',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true, false] },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('stylesheet')) return;
var lastChild = node.last();
if (!lastChild.is('space')) {
lastChild = gonzales.createNode({ type: 'space', content: '' });
node.content.push(lastChild);
}
lastChild.content = lastChild.content.replace(/\n$/, '');
if (this.getValue('eof-newline')) lastChild.content += '\n';
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('stylesheet')) return;
var lastChild = node.last();
if (lastChild.is('space') && lastChild.content.indexOf('\n') !== -1) {
return true;
} else {
return false;
}
}
};
@@ -0,0 +1,37 @@
module.exports = {
name: 'leading-zero',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true, false] },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('number')) return;
if (this.getValue('leading-zero')) {
if (node.content[0] === '.')
node.content = '0' + node.content;
} else {
node.content = node.content.replace(/^0+(?=\.)/, '');
}
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('number')) return;
if (node.content.match(/^\.[0-9]+/)) {
return false;
} else if (node.content.match(/^0\.[0-9]+/)) {
return true;
}
}
};
+45
View File
@@ -0,0 +1,45 @@
module.exports = {
name: 'quotes',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { string: /^single|double$/ },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (!node.is('string')) return;
var value = this.getValue('quotes');
if (node.content[0] === '"' && value === 'single') {
node.content = node.content
.replace(/\\"/g, '"') // unescape all escaped double quotes
.replace(/([^\\])'/g, '$1\\\'') // escape all the single quotes
.replace(/^"|"$/g, '\''); // replace the first and the last quote
} else if (node.content[0] === '\'' && value === 'double') {
node.content = node.content
.replace(/\\'/g, '\'') // unescape all escaped single quotes
.replace(/([^\\])"/g, '$1\\\"') // escape all the double quotes
.replace(/^'|'$/g, '"'); // replace the first and the last quote
}
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('string')) return;
if (node.content[0] === '"') {
return 'double';
} else if (node.content[0] === '\'') {
return 'single';
}
}
};
@@ -0,0 +1,81 @@
module.exports = (function() {
function processNode(node) {
removeEmptyRulesets(node);
mergeAdjacentWhitespace(node);
}
function removeEmptyRulesets(stylesheet) {
stylesheet.forEach('ruleset', function(ruleset, i) {
var block = ruleset.first('block');
processNode(block);
if (isEmptyBlock(block)) stylesheet.remove(i);
});
}
/**
* Removing ruleset nodes from tree may result in two adjacent whitespace
* nodes which is not correct AST:
* [space, ruleset, space] => [space, space]
* To ensure correctness of further processing we should merge such nodes
* into one:
* [space, space] => [space]
*/
function mergeAdjacentWhitespace(node) {
var i = node.content.length - 1;
while (i-- > 0) {
if (node.get(i).is('space') && node.get(i + 1).is('space')) {
node.get(i).content += node.get(i + 1).content;
node.remove(i + 1);
}
}
}
/**
* Block is considered empty when it has nothing but spaces.
*/
function isEmptyBlock(node) {
if (!node.length) return true;
return !node.content.some(function(node) {
return !node.is('space');
});
}
return {
name: 'remove-empty-rulesets',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true] },
/**
* Remove rulesets with no declarations.
*
* @param {String} node
*/
process: function(node) {
if (!node.is('stylesheet')) return;
processNode(node);
},
detectDefault: true,
/**
* Detects the value of an option at the tree node.
* This option is treated as `true` by default, but any trailing space would invalidate it.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('atrulers') && !node.is('block')) return;
if (node.length === 0 ||
(node.length === 1 && node.first().is('space'))) {
return false;
}
}
};
})();
@@ -0,0 +1,9 @@
module.exports = {
name: 'sort-order-fallback',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { string: /^abc$/ },
process: function() {}
};
@@ -0,0 +1,388 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'sort-order',
runBefore: 'space-before-closing-brace',
syntax: ['css', 'less', 'sass', 'scss'],
/**
* Sets handler value.
*
* @param {Array} value Option value
* @returns {Array}
*/
setValue: function(value) {
if (!Array.isArray(value)) throw new Error('The option accepts only array of properties.');
var order = {};
if (typeof value[0] === 'string') {
value.forEach(function(prop, propIndex) {
order[prop] = { group: 0, prop: propIndex };
});
} else {
value.forEach(function(group, groupIndex) {
group.forEach(function(prop, propIndex) {
order[prop] = { group: groupIndex, prop: propIndex };
});
});
}
return order;
},
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
var _this = this;
// Types of nodes that can be sorted:
var NODES = ['atruleb', 'atruler', 'atrules', 'multilineComment', 'singlelineComment',
'declaration', 'space', 'include'];
// Spaces and comments:
var SC = ['multilineComment', 'singlelineComment', 'space'];
var currentNode;
// Sort order of properties:
var order = this.getValue('sort-order');
var syntax = this.getSyntax();
// List of declarations that should be sorted:
var sorted = [];
// list of nodes that should be removed from parent node:
var deleted = [];
// List of spaces and comments that go before declaration/@-rule:
var sc0 = [];
// Value to search in sort order: either a declaration's property name
// (e.g. `color`), or @-rule's special keyword (e.g. `$import`):
var propertyName;
// Index to place the nodes that shouldn't be sorted
var lastGroupIndex = order['...'] ? order['...'].group : Infinity;
var lastPropertyIndex = order['...'] ? order['...'].prop : Infinity;
// Counters for loops:
var i;
var l;
var j;
var nl;
/**
* Remove empty lines in space node.
* @param {node} node Space node.
*/
var removeEmptyLines = function(node) {
node.content = node.content.replace(/\n[\s\t\n\r]*\n/, '\n');
};
/**
* Check if there are any comments or spaces before
* the declaration/@-rule.
* @returns {Array} List of nodes with spaces and comments
*/
var checkSC0 = function() {
// List of nodes with spaces and comments:
var sc = [];
// List of nodes that can be later deleted from parent node:
var d = [];
for (; i < l; i++) {
currentNode = node.get(i);
// If there is no node left,
// stop and do nothing with previously found spaces/comments:
if (!currentNode) {
return false;
}
// If the node is declaration or @-rule, stop and return all
// found nodes with spaces and comments (if there are any):
if (SC.indexOf(currentNode.type) === -1) break;
sc.push(currentNode);
d.push(i);
}
deleted = deleted.concat(d);
return sc;
};
/**
* Check if there are any comments or spaces after
* the declaration/@-rule.
* @returns {Array} List of nodes with spaces and comments
* @private
*/
var checkSC1 = function() {
// List of nodes with spaces and comments:
var sc = [];
// List of nodes that can be later deleted from parent node:
var d = [];
// Position of `\n` symbol inside a node with spaces:
var lbIndex;
// Check every next node:
for (; i < l; i++) {
currentNode = node.get(i + 1);
// If there is no node, or it is nor spaces neither comment, stop:
if (!currentNode || SC.indexOf(currentNode.type) === -1) break;
if (currentNode.is('multilineComment') || currentNode.is('singlelineComment')) {
sc.push(currentNode);
d.push(i + 1);
continue;
}
lbIndex = currentNode.content.indexOf('\n');
// If there are any line breaks in a node with spaces, stop and
// split the node into two: one with spaces before line break
// and one with `\n` symbol and everything that goes after.
// Combine the first one with declaration/@-rule's node:
if (lbIndex > -1) {
// TODO: Don't push an empty array
var s = currentNode.content.substring(0, lbIndex);
var space = gonzales.createNode({ type: 's', content: s });
sc.push(space);
currentNode.content = currentNode.content.substring(lbIndex);
break;
}
sc.push(currentNode);
d.push(i + 1);
}
deleted = deleted.concat(d);
return sc;
};
/**
* Combine declaration/@-rule's node with other relevant information:
* property index, semicolon, spaces and comments.
* @returns {Object} Extended node
*/
var extendNode = function() {
currentNode = node.get(i);
var nextNode = node.get(i + 1);
// Object containing current node, all corresponding spaces,
// comments and other information:
var extendedNode;
// Check if current node's property name is in sort order.
// If it is, save information about its indices:
var orderProperty = order[propertyName];
extendedNode = {
i: i,
node: currentNode,
sc0: sc0,
sc1: [],
sc2: [],
delim: []
};
// If the declaration's property is in order's list, save its
// group and property indices. Otherwise set them to 10000, so
// declaration appears at the bottom of a sorted list:
extendedNode.groupIndex = orderProperty && orderProperty.group > -1 ?
orderProperty.group : lastGroupIndex;
extendedNode.propertyIndex = orderProperty && orderProperty.prop > -1 ?
orderProperty.prop : lastPropertyIndex;
// Mark current node to remove it later from parent node:
deleted.push(i);
extendedNode.sc1 = checkSC1();
if (extendedNode.sc1.length) {
currentNode = node.get(i);
nextNode = node.get(i + 1);
}
// If there is `;` right after the declaration, save it with the
// declaration and mark it for removing from parent node:
if (currentNode && nextNode && nextNode.is('declarationDelimiter')) {
extendedNode.delim.push(nextNode);
deleted.push(i + 1);
i++;
if (syntax === 'sass') return extendedNode;
// Save spaces and comments which follow right after the declaration
// and mark them for removing from parent node:
extendedNode.sc2 = checkSC1();
}
return extendedNode;
};
/**
* Sorts properties alphabetically.
*
* @param {Object} a First extended node
* @param {Object} b Second extended node
* @returns {Number} `-1` if properties should go in order `a, b`. `1`
* if properties should go in order `b, a`.
*/
var sortLeftovers = function(a, b) {
var prefixes = ['-webkit-', '-moz-', '-ms-', '-o-', ''];
var prefixesRegExp = /^(-webkit-|-moz-|-ms-|-o-)(.*)$/;
// Get property name (i.e. `color`, `-o-animation`):
a = a.node.get(0).get(0).content;
b = b.node.get(0).get(0).content;
// Get prefix and unprefixed part. For example:
// ['-o-animation', '-o-', 'animation']
// ['color', '', 'color']
a = a.match(prefixesRegExp) || [a, '', a];
b = b.match(prefixesRegExp) || [b, '', b];
if (a[2] !== b[2]) {
// If unprefixed parts are different (i.e. `border` and
// `color`), compare them:
return a[2] < b[2] ? -1 : 1;
} else {
// If unprefixed parts are identical (i.e. `border` in
// `-moz-border` and `-o-border`), compare prefixes (they
// should go in the same order they are set in `prefixes` array):
return prefixes.indexOf(a[1]) < prefixes.indexOf(b[1]) ? -1 : 1;
}
};
// TODO: Think it through!
// Sort properties only inside blocks:
if (!node.is('block')) return;
// Check every child node.
// If it is declaration (property-value pair, e.g. `color: tomato`),
// or @-rule (e.g. `@include nani`),
// combine it with spaces, semicolon and comments and move them from
// current node to a separate list for further sorting:
for (i = 0, l = node.length; i < l; i++) {
if (NODES.indexOf(node.get(i).type) === -1) continue;
// Save preceding spaces and comments, if there are any, and mark
// them for removing from parent node:
sc0 = checkSC0();
if (!sc0) continue;
// If spaces/comments are the last nodes, stop and go to sorting:
if (!node.get(i)) {
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
break;
}
// Check if the node needs to be sorted:
// it should be a special @-rule (e.g. `@include`) or a declaration
// with a valid property (e.g. `color` or `$width`).
// If not, proceed with the next node:
propertyName = null;
// Look for includes:
if (node.get(i).is('include')) {
propertyName = '$include';
} else {
for (j = 0, nl = node.get(i).length; j < nl; j++) {
currentNode = node.get(i).get(j);
if (!currentNode) continue;
if (currentNode.is('property')) {
propertyName = currentNode.get(0).is('variable') ?
'$variable' : currentNode.get(0).content;
break;
} else if (currentNode.is('atkeyword') &&
currentNode.get(0).content === 'import') { // Look for imports
propertyName = '$import';
break;
}
}
}
// If current node is not property-value pair or import or include,
// skip it and continue with the next node:
if (!propertyName) {
deleted.splice(deleted.length - sc0.length, deleted.length + 1);
continue;
}
// Make an extended node and move it to a separate list for further
// sorting:
sorted.push(extendNode());
}
// Remove all nodes, that were moved to a `sorted` list, from parent node:
for (i = deleted.length - 1; i > -1; i--) {
node.content.splice(deleted[i], 1);
}
// Sort declarations saved for sorting:
sorted.sort(function(a, b) {
// If a's group index is higher than b's group index, in a sorted
// list a appears after b:
if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
// If a and b belong to leftovers and `sort-order-fallback` option
// is set to `abc`, sort properties alphabetically:
if (a.groupIndex === lastGroupIndex &&
_this.getValue('sort-order-fallback')) {
return sortLeftovers(a, b);
}
// If a and b have the same group index, and a's property index is
// higher than b's property index, in a sorted list a appears after
// b:
if (a.propertyIndex !== b.propertyIndex) return a.propertyIndex - b.propertyIndex;
// If a and b have the same group index and the same property index,
// in a sorted list they appear in the same order they were in
// original array:
return a.i - b.i;
});
// Build all nodes back together. First go sorted declarations, then
// everything else:
if (sorted.length > 0) {
for (i = sorted.length - 1, l = -1; i > l; i--) {
currentNode = sorted[i];
var prevNode = sorted[i - 1];
sc0 = currentNode.sc0;
var sc1 = currentNode.sc1;
var sc2 = currentNode.sc2;
sc0.reverse().map(removeEmptyLines);
sc1.reverse().map(removeEmptyLines);
sc2.reverse().map(removeEmptyLines);
// Divide declarations from different groups with an empty line:
if (prevNode && currentNode.groupIndex > prevNode.groupIndex) {
if (sc0[0] && sc0[0].is('space') &&
(this.syntax === 'sass' ||
sc0[0].content.match(/\n/g) &&
sc0[0].content.match(/\n/g).length < 2)) {
sc0[0].content = '\n' + sc0[0].content;
}
}
for (j = 0, nl = sc2.length; j < nl; j++) {
node.content.unshift(sc2[j]);
}
if (currentNode.delim.length > 0) {
var delim = this.syntax === 'sass' ? '\n' : ';';
var declDelim = gonzales.createNode({ type: 'declarationDelimiter', content: delim });
node.content.unshift(declDelim);
}
for (j = 0, nl = sc1.length; j < nl; j++) {
node.content.unshift(sc1[j]);
}
node.content.unshift(currentNode.node);
for (j = 0, nl = sc0.length; j < nl; j++) {
node.content.unshift(sc0[j]);
}
}
}
}
};
@@ -0,0 +1,58 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-after-colon',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('declaration')) return;
var value = this.getValue('space-after-colon');
for (var i = node.length; i--;) {
if (!node.get(i).is('propertyDelimiter')) continue;
if (this.getSyntax() === 'sass' && !node.get(i - 1)) break;
// Remove any spaces after colon:
if (node.get(i + 1).is('space')) node.remove(i + 1);
// If the value set in config is not empty, add spaces:
var space = gonzales.createNode({ type: 'space', content: value });
if (value !== '') node.insert(i + 1, space);
break;
}
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('declaration')) return;
for (var i = node.length; i--;) {
if (!node.get(i).is('propertyDelimiter')) continue;
if (node.get(i + 1).is('space')) {
return node.get(i + 1).content;
} else {
return '';
}
}
}
};
@@ -0,0 +1,60 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-after-combinator',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('selector')) return;
var value = this.getValue('space-after-combinator');
node.forEach('simpleSelector', function(simpleSelector) {
simpleSelector.forEach('combinator', function(combinator, i) {
if (simpleSelector.get(i + 1).is('space')) {
simpleSelector.get(i + 1).content = value;
} else {
var space = gonzales.createNode({ type: 'space', content: value });
simpleSelector.insert(i + 1, space);
}
});
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('selector')) return;
var variants = [];
node.forEach('simpleSelector', function(simpleSelector) {
simpleSelector.forEach('combinator', function(combinator, i) {
if (simpleSelector.get(i + 1).is('space')) {
variants.push(simpleSelector.get(i + 1).content);
} else {
variants.push('');
}
});
});
return variants;
}
};
@@ -0,0 +1,49 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-after-opening-brace',
runBefore: 'block-indent',
syntax: ['css', 'less', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
// If found block node stop at the next one for space check
if (!node.is('block') && !node.is('atrulers')) return;
var value = this.getValue('space-after-opening-brace');
if (node.first() &&
node.first().is('space')) {
node.first().content = value;
} else if (value !== '') {
var space = gonzales.createNode({ type: 'space', content: value });
node.insert(0, space);
}
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('block') && !node.is('atrulers')) return;
if (node.first().is('space')) {
return node.first().content;
} else {
return '';
}
}
};
@@ -0,0 +1,62 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-after-selector-delimiter',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('selector')) return;
var value = this.getValue('space-after-selector-delimiter');
node.forEach('delimiter', function(delimiter, i) {
var nextNode = node.get(i + 1);
if (nextNode.is('space')) {
nextNode.content = value;
} else if (nextNode.first().is('space')) {
nextNode.first().content = value;
} else {
var space = gonzales.createNode({ type: 'space', content: value });
nextNode.insert(0, space);
}
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('selector')) return;
var variants = [];
node.forEach('delimiter', function(delimiter, i) {
var nextNode = node.get(i + 1);
if (nextNode && nextNode.is('space')) {
variants.push(nextNode.content);
} else if (nextNode.first() && nextNode.first().is('space')) {
variants.push(nextNode.first().content);
} else {
variants.push('');
}
});
return variants;
}
};
@@ -0,0 +1,103 @@
var gonzales = require('gonzales-pe');
module.exports = (function() {
var valueFromSettings;
var blockIndent;
function getLastWhitespaceNode(node) {
var lastNode = node.last();
if (!lastNode || !lastNode.content) return null;
if (lastNode.is('block')) return null;
if (lastNode.is('space')) return lastNode;
return getLastWhitespaceNode(lastNode);
}
function processBlock(x, level) {
level = level || 0;
// XXX: Hack for braces
if (x.is('braces') || x.is('id')) return;
x.forEach(function(node) {
if (!node.is('block') &&
!node.is('atrulers')) return processBlock(node, level);
level++;
var value = valueFromSettings;
if (value.indexOf('\n') > -1) {
// TODO: Check that it works for '' block indent value <tg>
if (blockIndent) {
value += new Array(level).join(blockIndent);
}
}
// If found block node stop at the next one for space check
// For the pre-block node, find its last (the deepest) child
var whitespaceNode = getLastWhitespaceNode(node);
// If it's spaces, modify this node
// If it's something different from spaces, add a space node to the end
if (whitespaceNode) {
whitespaceNode.content = value;
} else if (value !== '') {
var space = gonzales.createNode({ type: 'space', content: value });
node.content.push(space);
}
processBlock(node, level);
});
}
return {
name: 'space-before-closing-brace',
runBefore: 'tab-size',
syntax: ['css', 'less', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
valueFromSettings = this.getValue('space-before-closing-brace');
blockIndent = this.getValue('block-indent');
if (!node.is('stylesheet')) return;
processBlock(node);
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('block') && !node.is('atrulers')) return;
var variants = [];
// For the block node, find its last (the deepest) child
var whitespaceNode = getLastWhitespaceNode(node);
if (whitespaceNode) {
variants.push(whitespaceNode.content);
} else {
variants.push('');
}
return variants;
}
};
})();
@@ -0,0 +1,60 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-before-colon',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('declaration')) return;
var value = this.getValue('space-before-colon');
var syntax = this.getSyntax();
node.forEach('propertyDelimiter', function(delimiter, i) {
if (syntax === 'sass' && !node.get(i - 1)) return;
// Remove any spaces before colon:
if (node.get(i - 1).is('space')) {
node.remove(--i);
}
// If the value set in config is not empty, add spaces:
var space = gonzales.createNode({ type: 'space', content: value });
if (value !== '') node.insert(i, space);
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('declaration')) return;
var result;
node.forEach('propertyDelimiter', function(delimiter, i) {
if (node.get(i - 1).is('space')) {
result = node.get(i - 1).content;
} else {
result = '';
}
});
return result;
}
};
@@ -0,0 +1,68 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-before-combinator',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('selector')) return;
var value = this.getValue('space-before-combinator');
node.forEach(function(simpleSelector) {
var notFirst = false;
simpleSelector.forEach(function(n, i) {
if (!n.is('space') && !n.is('combinator')) notFirst = true;
// If combinator is the first thing in selector,
// do not add extra spaces:
if (!n.is('combinator') || !notFirst) return;
if (simpleSelector.get(i - 1).is('space')) {
simpleSelector.get(i - 1).content = value;
} else {
var space = gonzales.createNode({ type: 'space', content: value });
simpleSelector.insert(i, space);
}
});
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('selector')) return;
var variants = [];
node.forEach(function(simpleSelector) {
simpleSelector.forEach('combinator', function(combinator, i) {
if (simpleSelector.get(i - 1).is('space')) {
variants.push(simpleSelector.get(i - 1).content);
} else {
variants.push('');
}
});
});
return variants;
}
};
@@ -0,0 +1,99 @@
var gonzales = require('gonzales-pe');
module.exports = (function() {
/**
* Gets the last (the deepest) whitespace node.
*
* @param {node} node
* @returns {node|undefined} If no whitespace node is found, returns
* `undefined`
*/
function getLastWhitespaceNode(node) {
if (typeof node !== 'object') return;
if (node.is('space')) return node;
return getLastWhitespaceNode(node.last());
}
return {
name: 'space-before-opening-brace',
runBefore: 'block-indent',
syntax: ['css', 'less', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
var value = this.getValue('space-before-opening-brace');
// XXX: Hack for braces
if (node.is('braces') || node.is('id')) return;
node.forEach(function(block, i) {
// If found block node stop at the next one for space check:
if (!block.is('block') && !block.is('atrulers')) return;
// For the pre-block node, find its last (the deepest) child:
// TODO: Exclude nodes with braces (for example, arguments)
var previousNode = node.get(i - 1);
var whitespaceNode = getLastWhitespaceNode(previousNode);
// If it's spaces, modify this node.
// If it's something different from spaces, add a space node to
// the end:
if (whitespaceNode) {
whitespaceNode.content = value;
} else if (value !== '') {
var space = gonzales.createNode({ type: 'space', content: value });
if (previousNode && previousNode.is('atrulerq')) {
previousNode.content.push(space);
} else {
node.insert(i, space);
}
}
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
var variants = [];
// XXX: Hack for braces
if (node.is('braces') || node.is('id')) return [];
node.forEach(function(block, i) {
// If found block node stop at the next one for space check:
if (!block.is('block') && !block.is('atrulers')) return;
// For the pre-block node, find its last (the deepest) child:
// TODO: Exclude nodes with braces (for example, arguments)
var previousNode = node.get(i - 1);
var whitespaceNode = getLastWhitespaceNode(previousNode);
// If it's spaces, modify this node.
// If it's something different from spaces, add a space node to
// the end:
if (whitespaceNode) {
variants.push(whitespaceNode.content);
} else {
variants.push('');
}
});
return variants;
}
};
})();
@@ -0,0 +1,57 @@
var gonzales = require('gonzales-pe');
module.exports = {
name: 'space-before-selector-delimiter',
runBefore: 'block-indent',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('selector')) return;
var value = this.getValue('space-before-selector-delimiter');
node.forEach('delimiter', function(delim, i) {
var previousNode = node.get(i - 1);
if (previousNode.last().is('space')) {
previousNode.last().content = value;
} else {
var space = gonzales.createNode({ type: 'space', content: value });
previousNode.content.push(space);
}
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('selector')) return;
var variants = [];
node.forEach('delimiter', function(delim, i) {
var previousNode = node.get(i - 1);
if (previousNode.last().is('space')) {
variants.push(previousNode.last().content);
} else {
variants.push('');
}
});
return variants;
}
};
@@ -0,0 +1,97 @@
var gonzales = require('gonzales-pe');
module.exports = (function() {
function getDeclarationEnd(node, i) {
for (;i < node.length; i++) {
if (!node.get(i + 1) || typeof node.get(i + 1) === 'string') {
return 0;
} else if (node.get(i + 1).is('space')) {
if (node.get(i + 1).content.indexOf('\n') > -1) {
if (node.get(i + 2) && node.get(i + 2).is('declaration')) {
return i;
} else {
return 0;
}
} else if (node.get(i + 2) && node.get(i + 2).is('multilineComment')) {
if (node.get(i + 3) && node.get(i + 3).is('declaration')) {
return i + 2;
} else if (node.get(i + 3) && node.get(i + 3).is('space')) {
if (node.get(i + 4) && node.get(i + 4).is('declaration')) {
return i + 2;
} else {
return 0;
}
} else {
return 0;
}
} else if (node.get(i + 2) && node.get(i + 2).is('declaration')) {
return i;
}
} else if (node.get(i + 1).is('declaration')) {
return i;
} else if (node.get(i + 1).is('multilineComment')) {
if (node.get(i + 2) && node.get(i + 2).is('declaration')) {
return i + 1;
} else if (node.get(i + 2) && node.get(i + 2).is('space')) {
if (node.get(i + 3) && node.get(i + 3).is('declaration')) {
return i + 1;
}
} else {
return 0;
}
} else {
return 0;
}
}
}
return {
name: 'space-between-declarations',
runBefore: 'block-indent',
syntax: ['css', 'less', 'scss'],
accepts: {
number: true,
string: /^[ \t\n]*$/
},
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
var value = this.getValue('space-between-declarations');
// TODO: Limit nodes to blocks, stylesheet, etc.
// XXX: Hack for braces
if (node.is('braces') || node.is('id')) return;
for (var i = 0, l = node.length; i < l; i++) {
if (!node.get(i) || !node.get(i).is('declarationDelimiter')) continue;
// Grom user's point of view "declaration" includes semicolons
// and comments placed on the same line.
// So group those things together:
var declarationEnd = getDeclarationEnd(node, i);
if (!declarationEnd) {
continue;
} else {
i = declarationEnd;
}
var nextNode = node.get(i + 1);
if (nextNode.is('space')) {
nextNode.content = value;
} else {
i++;
l++;
var space = gonzales.createNode({ type: 'space', content: value });
node.insert(i, space);
}
}
}
};
})();
@@ -0,0 +1,58 @@
module.exports = (function() {
/**
* Trim trailing spaces on each line.
* @private
* @param {String} string Spaceful string
* @returns {String}
*/
function trim(string) {
return string.replace(/[ \t]+\n/g, '\n');
}
return {
name: 'strip-spaces',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true] },
/**
* Processes tree node.
* @param {node} node
*/
process: function(node) {
if (node.is('space')) {
node.content = trim(node.content);
} else if (node.is('stylesheet')) {
var lastChild = node.last();
if (lastChild.is('space')) {
lastChild.content = trim(lastChild.content)
.replace(/[ \t]+$/, '')
.replace(/[\n]+/g, '\n');
}
}
},
detectDefault: true,
/**
* Detects the value of an option at the tree node.
* This option is treated as `true` by default, but any trailing space would invalidate it.
*
* @param {node} node
*/
detect: function(node) {
if (node.is('space') &&
node.content.match(/[ \t]\n/)) {
return false;
} else if (node.is('stylesheet')) {
var lastChild = node.last();
if (lastChild.is('space') &&
lastChild.content !== '\n' &&
lastChild.content.match(/^[ \n\t]+$/)) {
return false;
}
}
}
};
})();
@@ -0,0 +1,19 @@
module.exports = {
name: 'tab-size',
runBefore: 'vendor-prefix-align',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { number: true },
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('space')) return;
node.content = node.content.replace(/\t/, this.getValue('tab-size'));
}
};
@@ -0,0 +1,82 @@
module.exports = {
name: 'unitless-zero',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true] },
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
var UNITS = ['cm', 'em', 'ex', 'pt', 'px'];
if (!node.is('value') && !node.is('braces')) return;
node.forEach(function(value) {
if (typeof value === 'string') return;
if (value.is('dimension')) {
var unit = value.first('ident').content;
if (value.first('number').content === '0' &&
UNITS.indexOf(unit) !== -1) {
value.remove(1);
}
} else if (value.is('percentage')) {
// XXX(tonyganch): There is a bug in Gonzales when in Less,
// percentage's content is not wrapped as an array but actually
// type of node's content is object. This bug has already been
// fixed in newer versions of Gonzales so the issue should be
// gone after update of dependencies and csscomb@4.0 release.
// This hack is here as a hotfix for csscomb@3.1 and must be
// removed once csscom@4.0 is released. See #389.
var number;
if (!Array.isArray(value.content) &&
value.content.is('number')) {
number = value.content;
} else {
number = value.first('number').content;
}
if (number === '0') {
value.type = 'number';
value.content = number;
}
}
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
var result;
// If we see a zero with unit and it is not degree, then we dont have an option
if (node.is('percentage') && node.first('number').content[1] === '0') {
result = false;
} else if (node.is('dimension') &&
node.first('number').content === '0' &&
node.first('ident').content !== 'deg') {
result = false;
}
// If we see a zero and previous node is not percentage or dimension, then we have an option
if (node.is('number') &&
node.content === '0' &&
this._prev !== 'percentage' &&
this._prev !== 'dimension') {
result = true;
}
// Store the previous nodeType
this._prev = node.type;
return result;
}
};
@@ -0,0 +1,436 @@
var gonzales = require('gonzales-pe');
module.exports = (function() {
// Vendor prefixes list:
var PREFIXES = [
'webkit',
'khtml',
'moz',
'ms',
'o'
];
var oneline;
/**
* Makes namespace from property name.
*
* @param {String} propertyName
* @returns {String|undefined}
*/
function makeNamespace(propertyName) {
var info = getPrefixInfo(propertyName);
return info && info.baseName;
}
/**
* Creates object which contains info about vendor prefix used in propertyName.
*
* @param {String} propertyName property name
* @param {String} [namespace=''] namespace name
* @param {Number} [extraSymbols=0] extra symbols count
* @returns {Object|undefined}
*/
function getPrefixInfo(propertyName, namespace, extraSymbols) {
var baseName = propertyName;
var prefixLength = 0;
namespace = namespace || '';
extraSymbols = extraSymbols || 0;
if (!propertyName) return;
PREFIXES.some(function(prefix) {
prefix = '-' + prefix + '-';
if (propertyName.indexOf(prefix) !== 0) return;
baseName = baseName.substr(prefix.length);
prefixLength = prefix.length;
return true;
});
return {
id: namespace + baseName,
baseName: baseName,
prefixLength: prefixLength,
extra: extraSymbols
};
}
/**
* Returns extra indent for item in arguments
*
* @param {Array} nodes nodes to process
* @returns {Number|undefined}
*/
function extraIndent(nodes) {
if (!nodes || !nodes.length) return;
var node;
var crPos;
var tabPos;
var result = 0;
for (var i = nodes.length; i--;) {
node = nodes[i];
if (!node.content) {
crPos = -1;
} else {
crPos = node.content.lastIndexOf('\n');
tabPos = node.content.lastIndexOf('\t');
if (tabPos > crPos) crPos = tabPos;
}
if (crPos !== -1)
oneline = false;
if (node.is('space')) {
result += node.content.length - crPos - 1;
if (crPos !== -1)
break;
}
if (node.is('multilineComment')) {
if (crPos === -1) {
result += node.content.length + 4 /* comment symbols length */ ;
} else {
result += node.content.length - crPos + 1 /* only last comment symbols length - 1(not count \n)*/;
break;
}
}
}
return result;
}
/**
* Wrapper for extra indent function for `property` node.
*
* @param {Array} nodes all nodes
* @param {Number} i position in nodes array
*/
function extraIndentProperty(nodes, i) {
var subset = [];
while (i--) {
if (!nodes.get(i) || nodes.get(i).is('declarationDelimiter'))
break;
subset.unshift(nodes.get(i));
}
return extraIndent(subset);
}
/**
* Wrapper for extra indent function for val-node.
*
* @param {Array} nodes all nodes
* @param {Number} i position in nodes array
*/
function extraIndentVal(nodes, i) {
var subset = [];
var declaration = nodes.get(i);
if (!declaration.is('declaration')) return;
for (var x = declaration.length; x--;) {
if (!declaration.get(x).is('value')) continue;
x--;
while (!declaration.get(x).is('propertyDelimiter')) {
subset.push(declaration.get(x));
x--;
}
break;
}
return extraIndent(subset);
}
/**
* Walks across nodes, and call payload for every node that pass selector check.
*
* @param {Object} args arguments in form of:
* {
* node: {object} current node,
* selector: {function} propertyName selector
* payload: {function} work to do with gathered info
* namespaceSelector: {function} selector for namespace
* getExtraSymbols: {Number} extra symbols count
* }
*/
function walk(args) {
args.node.forEach(function(item, i) {
var name = args.selector(item);
var namespace = args.namespaceSelector && makeNamespace(args.namespaceSelector(item));
var extraSymbols = args.getExtraSymbols(args.node, i);
var info = name && getPrefixInfo(name, namespace, extraSymbols);
if (!info) return;
args.payload(info, i);
});
}
/**
* Returns property name.
* e.g.
* for: 'color: #fff'
* returns string: 'color'
*
* @param {node} node
* @returns {String|undefined}
*/
function getPropertyName(node) {
if (!node.is('declaration')) return;
// TODO: Check that it's not a variable
return node.get(0).get(0).content;
}
/**
* Returns property value name.
* e.g.
* for: '-webkit-transition: -webkit-transform 150ms linear'
* returns string: '-webkit-transform', and
* for: 'background: -webkit-linear-gradient(...)'
* returns string: '-webkit-linear-gradient'
*
* @param {node} node
* @returns {String|undefined}
*/
function getValName(node) {
if (!node.is('declaration')) return;
var value = node.first('value');
if (value.get(0).is('ident')) return value.get(0).content;
if (value.get(0).is('function')) return value.get(0).get(0).content;
}
/**
* Updates dict which contains info about items align.
*
* @param {Object} info,
* @param {Object} dict,
*/
function updateDict(info, dict) {
if (info.prefixLength === 0 && info.extra === 0) return;
var indent = dict[info.id] || { prefixLength: 0, extra: 0 };
dict[info.id] = indent.prefixLength + indent.extra > info.prefixLength + info.extra ?
indent :
{
prefixLength: info.prefixLength,
extra: info.extra,
};
}
/**
* Returns string with correct number of spaces for info.baseName property.
*
* @param {Object} info,
* @param {Object} dict,
* @param {String} whitespaceNode
* @returns {String}
*/
function updateIndent(info, dict, whitespaceNode) {
var item = dict[info.id];
if (!item)
return whitespaceNode;
var crPos = whitespaceNode.lastIndexOf('\n');
var tabPos = whitespaceNode.lastIndexOf('\t');
if (tabPos > crPos) crPos = tabPos;
var firstPart = whitespaceNode.substr(0, crPos + 1 );
var extraIndent = new Array(
(item.prefixLength - info.prefixLength) +
(item.extra - info.extra) +
whitespaceNode.length - firstPart.length +
1).join(' ');
return firstPart.concat(extraIndent);
}
return {
name: 'vendor-prefix-align',
syntax: ['css', 'less', 'sass', 'scss'],
accepts: { boolean: [true] },
/**
* Processes tree node.
*
* @param {node} node
*/
process: function(node) {
if (!node.is('block')) return;
oneline = true;
var dict = {};
// Gathering Info
walk({
node: node,
selector: getPropertyName,
getExtraSymbols: extraIndentProperty,
payload: function(info) {
updateDict(info, dict);
}
});
walk({
node: node,
selector: getValName,
namespaceSelector: getPropertyName,
getExtraSymbols: extraIndentVal,
payload: function(info) {
updateDict(info, dict);
}
});
if (oneline && this.getSyntax() !== 'sass') return;
// Update nodes
walk({
node: node,
selector: getValName,
namespaceSelector: getPropertyName,
getExtraSymbols: extraIndentVal,
payload: function(info, i) {
for (var x = node.get(i).length; x--;) {
if (node.get(i).get(x).is('value')) break;
}
if (!node.get(i).get(x - 1).is('space')) {
var space = gonzales.createNode({ type: 'space', content: '' });
node.get(i).insert(x, space);
++x;
}
node.get(i).get(x - 1).content = updateIndent(info, dict, node.get(i).get(x - 1).content);
}
});
if (this.getSyntax() === 'sass') return;
walk({
node: node,
selector: getPropertyName,
getExtraSymbols: extraIndentProperty,
payload: function(info, i) {
// `node.get(i - 1)` can be either space or comment:
var whitespaceNode = node.get(i - 1);
if (!whitespaceNode) return;
// If it's a comment, insert an empty space node:
if (!whitespaceNode.is('space')) {
whitespaceNode = gonzales.createNode({ type: 'space', content: '' });
node.insert(i - 1, whitespaceNode);
}
whitespaceNode.content = updateIndent(info, dict, whitespaceNode.content);
}
});
},
/**
* Detects the value of an option at the tree node.
*
* @param {node} node
*/
detect: function(node) {
if (!node.is('block')) return;
var result = {
true: 0,
false: 0
};
var maybePrefix = false;
var prevPrefixLength = false;
var prevProp;
var prevSum;
var partialResult = null;
var getResult = function(node, sum, info, i) {
var prop = info.baseName;
// If this is the last item in a row and we have a result, then catch it
if (prop !== prevProp && partialResult !== null) {
if (partialResult) {
result.true++;
} else {
result.false++;
}
partialResult = null;
}
if (prop === prevProp && info.prefixLength !== prevPrefixLength) {
maybePrefix = true;
} else {
maybePrefix = false;
}
if (maybePrefix && partialResult !== false) {
// If there is prefixed prop, check if the prefixes are aligned,
// but only if we hadn't already catched that it is false
if (sum === prevSum) {
partialResult = true;
} else {
partialResult = false;
}
}
if (node.length === i + 3 && partialResult !== null) {
// If we're at the last property and have a result, catch it
if (partialResult) {
result.true++;
} else {
result.false++;
}
}
prevPrefixLength = info.prefixLength;
prevProp = prop;
prevSum = sum;
};
// Gathering Info
walk({
node: node,
selector: getPropertyName,
getExtraSymbols: extraIndentProperty,
payload: function(info, i) {
if (node.get(i - 1) && node.get(i - 1).content) {
var sum = node.get(i - 1).content.
replace(/^[ \t]*\n+/, '').length + info.prefixLength;
getResult(node, sum, info, i);
}
}
});
walk({
node: node,
selector: getValName,
getExtraSymbols: extraIndentVal,
payload: function(info, i) {
for (var x = node.get(i).length; x--;) {
if (node.get(i).get(x).is('value')) break;
}
if (node.get(i).get(x - 1)) {
var sum = node.get(i).get(x - 1).content
.replace(/^[ \t]*\n+/, '').length + info.prefixLength;
getResult(node, sum, info, i);
}
}
});
if (result.true > 0 || result.false > 0) {
if (result.true >= result.false) {
return true;
} else {
return false;
}
}
}
};
})();