canonical
Version:
Canonical code style linter and formatter for JavaScript, SCSS and CSS.
281 lines (221 loc) • 7.06 kB
JavaScript
;
var helpers,
yaml,
fs,
path,
sortBy,
clone,
propertyCheckList,
orderPresets,
getOrderConfig,
sortProperties,
trimList,
removeMultipleConsequentValue,
validateSeparation,
GROUP_SEPARATOR;
GROUP_SEPARATOR = '(group separator)';
helpers = require('../helpers');
yaml = require('js-yaml');
fs = require('fs');
path = require('path');
sortBy = require('lodash.sortby');
clone = require('lodash.clone');
propertyCheckList = yaml.safeLoad(fs.readFileSync(path.join(__dirname, '../../data', 'properties.yml'), 'utf8')).split(' ');
orderPresets = {
'recess': 'recess.yml',
'smacss': 'smacss.yml',
'concentric': 'concentric.yml'
};
/**
* Gets named order preset.
*
* @param {string} orderName Order name.
* @returns {Object} Order configuration object.
*/
getOrderConfig = function (orderName) {
var filename,
orderConfig;
if (!orderPresets.hasOwnProperty(orderName)) {
throw new Error('Property sort order "' + orderName + '" preset does not exist.');
}
filename = orderPresets[orderName];
orderConfig = helpers.loadConfigFile('property-sort-orders/' + filename);
return orderConfig.order;
};
/**
* Sorts properties using alphabetical order or based on order array properties.
*
* @param {Object[]} properties List of CSS properties.
* @param {string|string[]} order List of CSS property names.
* @returns {Object[]} A copy of the properties that is sorted based on the order.
*/
sortProperties = function (properties, order) {
var sorted;
if (typeof order === 'string') {
if (order === 'alphabetical') {
return sortBy(properties, 'name');
}
throw new Error('Unexpected sorting order.');
}
else {
sorted = sortBy(properties, 'name');
// @see http://jsfiddle.net/vsn32xp3/
sorted = sortBy(sorted, function (property) {
var matchIndex;
matchIndex = order.indexOf(property.name);
return matchIndex === -1 ? properties.length : matchIndex;
});
return sorted;
}
};
/**
* Removes value removeValue from the beginning and end of a list.
*
* @param {string[]} list A subject list.
* @param {string} removeValue A value to be removed from the beginning and end of the subject list.
* @returns {string[]} A shallow clone of the subject list with removeValue removed from the beginning and end.
*/
trimList = function (list, removeValue) {
if (!list.length) {
return list;
}
list = clone(list);
while (list[0] === removeValue) {
list.shift();
}
while (list[list.length - 1] === GROUP_SEPARATOR) {
list.pop();
}
return list;
};
removeMultipleConsequentValue = function (list, removeValue) {
return list.filter(function (x, i, xs) {
if (x !== removeValue) {
return true;
}
return x !== xs[i - 1];
});
};
validateSeparation = function (parser, block, expectedPropertyOrder) {
var blockProperties;
blockProperties = [];
block.forEach(function (dec) {
var prop,
name;
if (dec.type === 'declaration') {
prop = dec.first('property');
name = prop.first('ident');
if (!name) {
return;
}
if (parser.options['ignore-custom-properties'] && propertyCheckList.indexOf(name.content) === -1) {
return;
}
blockProperties.push(name.content);
}
if (dec.type === 'space' && dec.content.indexOf('\n\n') === 0) {
blockProperties.push(GROUP_SEPARATOR);
}
});
// First remove expectedPropertyOrder properties that do not appear in blockProperties.
expectedPropertyOrder = expectedPropertyOrder.filter(function (propertyName) {
if (propertyName === GROUP_SEPARATOR) {
return true;
}
return blockProperties.indexOf(propertyName) !== -1;
});
// console.log('expectedPropertyOrder (in)', expectedPropertyOrder);
expectedPropertyOrder = trimList(expectedPropertyOrder, GROUP_SEPARATOR);
// console.log('expectedPropertyOrder (mid)', expectedPropertyOrder);
expectedPropertyOrder = removeMultipleConsequentValue(expectedPropertyOrder, GROUP_SEPARATOR);
// console.log('expectedPropertyOrder (out)', expectedPropertyOrder);
// Then do the inverse with blockProperties.
blockProperties = blockProperties.filter(function (propertyName) {
if (propertyName === GROUP_SEPARATOR) {
return true;
}
return expectedPropertyOrder.indexOf(propertyName) !== -1;
});
blockProperties = trimList(blockProperties, GROUP_SEPARATOR);
blockProperties = removeMultipleConsequentValue(blockProperties, GROUP_SEPARATOR);
blockProperties = blockProperties.join(', ');
expectedPropertyOrder = expectedPropertyOrder.join(', ');
if (blockProperties !== expectedPropertyOrder) {
return {
'ruleId': parser.rule.name,
'line': block.start.line,
'column': block.start.column,
'message': 'Expected "' + expectedPropertyOrder + '", found "' + blockProperties + '"',
'severity': parser.severity
};
}
return null;
};
module.exports = {
'name': 'property-sort-order',
'defaults': {
'order': 'alphabetical',
'ignore-custom-properties': false
},
'detect': function (ast, parser) {
var result,
validateSeparationResult,
order;
result = [];
if (typeof parser.options.order === 'string' && parser.options.order !== 'alphabetical') {
order = getOrderConfig(parser.options.order);
}
else {
order = parser.options.order;
}
ast.traverseByType('block', function (block) {
var properties = [],
sorted,
expectedOrder,
currentOrder;
if (!block) {
return;
}
block.forEach('declaration', function (dec) {
var prop = dec.first('property'),
name = prop.first('ident');
if (!name) {
return;
}
if (parser.options['ignore-custom-properties'] && propertyCheckList.indexOf(name.content) === -1) {
return;
}
properties.push({
name: name.content,
line: prop.start.line,
column: prop.start.column
});
});
sorted = sortProperties(properties, order);
expectedOrder = sorted.map(function (prop) {
return prop.name;
}).join(', ');
currentOrder = properties.map(function (prop) {
return prop.name;
}).join(', ');
if (expectedOrder !== currentOrder) {
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': block.start.line,
'column': block.start.column,
'message': 'Expected "' + expectedOrder + '", found "' + currentOrder + '"',
'severity': parser.severity
});
}
else {
if (typeof order === 'object') {
validateSeparationResult = validateSeparation(parser, block, order);
if (validateSeparationResult) {
result = helpers.addUnique(result, validateSeparationResult);
}
}
}
});
return result;
}
};