stylelint
Version:
Modern CSS linter
363 lines (305 loc) • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.messages = exports.ruleName = undefined;
exports.default = function (expectation, options) {
return function (root, result) {
var validOptions = (0, _utils.validateOptions)(result, ruleName, {
actual: expectation,
possible: validatePrimaryOption
}, {
actual: options,
possible: {
unspecified: ["top", "bottom", "ignore", "bottomAlphabetical"]
},
optional: true
});
if (!validOptions) {
return;
}
var alphabetical = expectation === "alphabetical";
var expectedOrder = alphabetical ? null : createExpectedOrder(expectation);
// By default, ignore unspecified properties
var unspecified = _lodash2.default.get(options, ["unspecified"], "ignore");
// Shallow loop
root.each(function (node) {
if (node.type === "rule" || node.type === "atrule") {
checkNode(node);
}
});
function checkNode(node) {
var allPropData = [];
var lastKnownSeparatedGroup = 1;
node.each(function (child) {
// If the child has nested nodes with child
// (e.g. a rule nested within a rule), make
// sure to check the children
if (child.nodes && child.nodes.length) {
checkNode(child);
}
if (child.type !== "decl") {
return;
}
var prop = child.prop;
if (!(0, _utils.isStandardSyntaxProperty)(prop)) {
return;
}
if ((0, _utils.isCustomProperty)(prop)) {
return;
}
var unprefixedPropName = _postcss.vendor.unprefixed(prop);
// Hack to allow -moz-osx-font-smoothing to be understood
// just like -webkit-font-smoothing
if (unprefixedPropName.indexOf("osx-") === 0) {
unprefixedPropName = unprefixedPropName.slice(4);
}
var propData = {
name: prop,
unprefixedName: unprefixedPropName,
orderData: alphabetical ? null : getOrderData(expectedOrder, unprefixedPropName),
before: child.raw("before"),
index: allPropData.length,
node: child
};
var previousPropData = _lodash2.default.last(allPropData);
allPropData.push(propData);
// Skip first decl
if (!previousPropData) {
return;
}
var isCorrectOrder = alphabetical ? checkAlpabeticalOrder(previousPropData, propData) : checkOrder(previousPropData, propData);
if (isCorrectOrder) {
return;
}
complain({
message: messages.expected(propData.name, previousPropData.name),
node: child
});
});
function checkOrder(firstPropData, secondPropData) {
// If the unprefixed property names are the same, resort to alphabetical ordering
if (firstPropData.unprefixedName === secondPropData.unprefixedName) {
return firstPropData.name <= secondPropData.name;
}
var firstPropIsUnspecified = !firstPropData.orderData;
var secondPropIsUnspecified = !secondPropData.orderData;
// Now check newlines between ...
var firstPropSeparatedGroup = !firstPropIsUnspecified ? firstPropData.orderData.separatedGroup : lastKnownSeparatedGroup;
var secondPropSeparatedGroup = !secondPropIsUnspecified ? secondPropData.orderData.separatedGroup : lastKnownSeparatedGroup;
if (firstPropSeparatedGroup !== secondPropSeparatedGroup && !secondPropIsUnspecified) {
// Get an array of just the property groups, remove any solo properties
var groups = _lodash2.default.reject(expectation, _lodash2.default.isString);
// secondProp seperatedGroups start at 2 so we minus 2 to get the 1st item
// from our groups array
var emptyLineBefore = _lodash2.default.get(groups[secondPropSeparatedGroup - 2], "emptyLineBefore");
if (emptyLineBefore) {
result.warn("The 'emptyLineBefore' option for 'declaration-block-properties-order' has been deprecated, " + "and will be removed in '7.0'. If you use this option please consider " + "creating a plugin for the community.", {
stylelintType: "deprecation",
stylelintReference: "http://stylelint.io/user-guide/release-planning/"
});
}
if (!hasEmptyLineBefore(secondPropData.node) && emptyLineBefore === "always") {
complain({
message: messages.expectedEmptyLineBetween(secondPropData.name, firstPropData.name),
node: secondPropData.node
});
} else if (hasEmptyLineBefore(secondPropData.node) && emptyLineBefore === "never") {
complain({
message: messages.rejectedEmptyLineBetween(secondPropData.name, firstPropData.name),
node: secondPropData.node
});
}
}
lastKnownSeparatedGroup = secondPropSeparatedGroup;
// Now check actual known properties ...
if (!firstPropIsUnspecified && !secondPropIsUnspecified) {
return firstPropData.orderData.expectedPosition <= secondPropData.orderData.expectedPosition;
}
if (firstPropIsUnspecified && !secondPropIsUnspecified) {
// If first prop is unspecified, look for a specified prop before it to
// compare to the current prop
var priorSpecifiedPropData = _lodash2.default.findLast(allPropData.slice(0, -1), function (d) {
return !!d.orderData;
});
if (priorSpecifiedPropData && priorSpecifiedPropData.orderData && priorSpecifiedPropData.orderData.expectedPosition > secondPropData.orderData.expectedPosition) {
complain({
message: messages.expected(secondPropData.name, priorSpecifiedPropData.name),
node: secondPropData.node
});
return true; // avoid logging another warning
}
}
// Now deal with unspecified props ...
// Starting with bottomAlphabetical as it requires more specific conditionals
if (unspecified === "bottomAlphabetical" && !firstPropIsUnspecified && secondPropIsUnspecified) {
return true;
}
if (unspecified === "bottomAlphabetical" && secondPropIsUnspecified && firstPropIsUnspecified) {
if (checkAlpabeticalOrder(firstPropData, secondPropData)) {
return true;
} else {
return false;
}
}
if (unspecified === "bottomAlphabetical" && firstPropIsUnspecified) {
return false;
}
if (firstPropIsUnspecified && secondPropIsUnspecified) {
return true;
}
if (unspecified === "ignore" && (firstPropIsUnspecified || secondPropIsUnspecified)) {
return true;
}
if (unspecified === "top" && firstPropIsUnspecified) {
return true;
}
if (unspecified === "top" && secondPropIsUnspecified) {
return false;
}
if (unspecified === "bottom" && secondPropIsUnspecified) {
return true;
}
if (unspecified === "bottom" && firstPropIsUnspecified) {
return false;
}
}
}
function complain(_ref) {
var message = _ref.message;
var node = _ref.node;
(0, _utils.report)({
message: message,
node: node,
result: result,
ruleName: ruleName
});
}
};
};
var _lodash = require("lodash");
var _lodash2 = _interopRequireDefault(_lodash);
var _postcss = require("postcss");
var _utils = require("../../utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var ruleName = exports.ruleName = "declaration-block-properties-order";
var messages = exports.messages = (0, _utils.ruleMessages)(ruleName, {
expected: function expected(first, second) {
return "Expected \"" + first + "\" to come before \"" + second + "\"";
},
expectedEmptyLineBetween: function expectedEmptyLineBetween(first, second) {
return "Expected an empty line between property \"" + first + "\" and property \"" + second + "\"";
},
rejectedEmptyLineBetween: function rejectedEmptyLineBetween(first, second) {
return "Unexpected empty line between property \"" + first + " and property \"" + second + "\"";
}
});
function createExpectedOrder(input) {
var order = {};
var separatedGroup = 1;
var expectedPosition = 0;
appendGroup(input, 1);
function appendGroup(items) {
items.forEach(function (item) {
return appendItem(item, false);
});
}
function appendItem(item, inFlexibleGroup) {
if (_lodash2.default.isString(item)) {
// In flexible groups, the expectedPosition does not ascend
// to make that flexibility work;
// otherwise, it will always ascend
if (!inFlexibleGroup) {
expectedPosition += 1;
}
order[item] = { separatedGroup: separatedGroup, expectedPosition: expectedPosition };
return;
}
// If item is not a string, it's a group ...
if (item.emptyLineBefore) {
separatedGroup += 1;
}
if (!item.order || item.order === "strict") {
appendGroup(item.properties);
return;
} else if (item.order === "flexible") {
expectedPosition += 1;
item.properties.forEach(function (property) {
appendItem(property, true);
});
}
}
return order;
}
function getOrderData(expectedOrder, propName) {
var orderData = expectedOrder[propName];
// If prop was not specified but has a hyphen
// (e.g. `padding-top`), try looking for the segment preceding the hyphen
// and use that index
if (!orderData && propName.lastIndexOf("-") !== -1) {
var propNamePreHyphen = propName.slice(0, propName.lastIndexOf("-"));
orderData = getOrderData(expectedOrder, propNamePreHyphen);
}
return orderData;
}
function hasEmptyLineBefore(decl) {
if (/\r?\n\s*\r?\n/.test(decl.raw("before"))) {
return true;
}
var prevNode = decl.prev();
if (!prevNode) {
return false;
}
if (prevNode.type !== "comment") {
return false;
}
if (/\r?\n\s*\r?\n/.test(prevNode.raw("before"))) {
return true;
}
return false;
}
function checkAlpabeticalOrder(firstPropData, secondPropData) {
// If unprefixed prop names are the same, compare the prefixed versions
if (firstPropData.unprefixedName === secondPropData.unprefixedName) {
return firstPropData.name <= secondPropData.name;
}
return firstPropData.unprefixedName < secondPropData.unprefixedName;
}
function validatePrimaryOption(actualOptions) {
if (actualOptions === "alphabetical") {
return true;
}
if (!Array.isArray(actualOptions)) {
return false;
}
// Every item in the array must be a string or an object
// with a "properties" property
if (actualOptions.every(function (item) {
if (_lodash2.default.isString(item)) {
return true;
}
return _lodash2.default.isPlainObject(item) && !_lodash2.default.isUndefined(item.properties);
})) {
return true;
}
var objectItems = actualOptions.filter(_lodash2.default.isPlainObject);
// Every object-item's "emptyLineBefore" must be "always" or "never"
if (objectItems.every(function (item) {
if (_lodash2.default.isUndefined(item.emptyLineBefore)) {
return true;
}
return _lodash2.default.includes(["always", "never"], item.emptyLineBefore);
})) {
return true;
}
// Every object-item's "type" property must be "strict" or "flexible"
if (objectItems.every(function (item) {
if (_lodash2.default.isUndefined(item.type)) {
return true;
}
return _lodash2.default.includes(["string", "flexible"], item.type);
})) {
return true;
}
return false;
}