postcss-sorting
Advanced tools
Comparing version 1.7.0 to 2.0.0
@@ -5,2 +5,35 @@ # Change Log | ||
## 2.0.0 | ||
This release completely incompatible with the previous API. There is a lot new options. Please read the documentation. | ||
[A migration guide](https://github.com/hudochenkov/postcss-sorting#migration-from-1x) is available. | ||
### Added | ||
* `sort-order` split into `order` and `properties-order`. | ||
* Alphabetical order. | ||
* At-rules can be checked if they have a block. E.g., `@include icon;` has no block. | ||
* Custom properties and $-variables can be grouped separately. | ||
* Empty lines for different node types: | ||
* `rule-nested-empty-line-before` | ||
* `at-rule-nested-empty-line-before` | ||
* `declaration-empty-line-before` | ||
* `custom-property-empty-line-before` | ||
* `dollar-variable-empty-line-before` | ||
* `comment-empty-line-before` | ||
* `clean-empty-lines`: Remove all empty lines. | ||
### Changed | ||
* By default all options are disabled, and the plugin does nothing. | ||
* Empty lines don't delete anymore if only “order” options are enabled. | ||
* Droped support for Node <4. | ||
### Removed | ||
* Predefined configs. | ||
* Command comments `/* postcss-sorting: on/off */` | ||
* `preserve-empty-lines-between-children-rules` | ||
* `empty-lines-between-children-rules` | ||
* `empty-lines-between-media-rules` | ||
* `empty-lines-before-comment` | ||
* `empty-lines-after-comment` | ||
## 1.7.0 | ||
@@ -7,0 +40,0 @@ * Added `smacss` and `alphabetical` predefined configs. |
990
index.js
@@ -1,6 +0,31 @@ | ||
var postcss = require('postcss'); | ||
var objectAssign = require('object-assign'); | ||
var path = require('path'); | ||
var fs = require('fs'); | ||
'use strict'; | ||
const postcss = require('postcss'); | ||
const _ = require('lodash'); | ||
const isStandardSyntaxProperty = require('./lib/isStandardSyntaxProperty'); | ||
const isStandardSyntaxDeclaration = require('./lib/isStandardSyntaxDeclaration'); | ||
const isCustomProperty = require('./lib/isCustomProperty'); | ||
const isDollarVariable = require('./lib/isDollarVariable'); | ||
const isRuleWithNodes = require('./lib/isRuleWithNodes'); | ||
const validateOptions = require('./lib/validateOptions'); | ||
const createExpectedOrder = require('./lib/createExpectedOrder'); | ||
const createExpectedPropertiesOrder = require('./lib/createExpectedPropertiesOrder'); | ||
const processMostNodes = require('./lib/processMostNodes'); | ||
const processLastComments = require('./lib/processLastComments'); | ||
const getPropertiesOrderData = require('./lib/getPropertiesOrderData'); | ||
const sorting = require('./lib/sorting'); | ||
const getComments = require('./lib/getComments'); | ||
const cleanEmptyLines = require('./lib/cleanEmptyLines'); | ||
const emptyLineBeforeGroup = require('./lib/emptyLineBeforeGroup'); | ||
const isSingleLineBlock = require('./lib/isSingleLineBlock'); | ||
const isSingleLineString = require('./lib/isSingleLineString'); | ||
const hasEmptyLine = require('./lib/hasEmptyLine'); | ||
const createEmptyLines = require('./lib/createEmptyLines'); | ||
const isStandardSyntaxRule = require('./lib/isStandardSyntaxRule'); | ||
const hasBlock = require('./lib/hasBlock'); | ||
const hasNonSharedLineCommentBefore = require('./lib/hasNonSharedLineCommentBefore'); | ||
const hasSharedLineCommentBefore = require('./lib/hasSharedLineCommentBefore'); | ||
module.exports = postcss.plugin('postcss-sorting', function (opts) { | ||
@@ -13,410 +38,757 @@ return function (css) { | ||
function plugin(css, opts) { | ||
// Verify options and use defaults if not specified | ||
opts = verifyOptions(opts); | ||
const validatedOptions = validateOptions(opts); | ||
var enableSorting = true; | ||
if (validatedOptions !== true) { | ||
if (console && console.warn && _.isString(validatedOptions)) { // eslint-disable-line no-console | ||
console.warn(validatedOptions); // eslint-disable-line no-console | ||
} | ||
css.walk(function (node) { | ||
if (node.type === 'comment' && node.parent.type === 'root') { | ||
if (node.text === 'postcss-sorting: on') { | ||
enableSorting = true; | ||
} else if (node.text === 'postcss-sorting: off') { | ||
enableSorting = false; | ||
return; | ||
} | ||
// Having this option before `properties-order`, because later one can add empty lines by `emptyLineBefore` | ||
if (opts['clean-empty-lines']) { | ||
css.walk(function (node) { | ||
if (isRuleWithNodes(node)) { | ||
// Remove empty lines before every node | ||
node.each(function (childNode) { | ||
if (childNode.raws.before) { | ||
childNode.raws.before = cleanEmptyLines(childNode.raws.before); | ||
} | ||
}); | ||
// Remove empty lines after the last node | ||
if (node.raws.after) { | ||
node.raws.after = cleanEmptyLines(node.raws.after); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
if (!enableSorting) { | ||
return; | ||
} | ||
if (opts.order) { | ||
const expectedOrder = createExpectedOrder(opts.order); | ||
// Process only rules and atrules with nodes | ||
if ((node.type === 'rule' || node.type === 'atrule') && node.nodes && node.nodes.length) { | ||
// Nodes for sorting | ||
var processed = []; | ||
css.walk(function (node) { | ||
// Process only rules and atrules with nodes | ||
if (isRuleWithNodes(node)) { | ||
// Nodes for sorting | ||
let processed = []; | ||
// Add indexes to nodes | ||
node.each(function (childNode, index) { | ||
processed = processMostNodes(childNode, index, opts, processed); | ||
}); | ||
// Add indexes to nodes | ||
node.each(function (childNode, index) { | ||
processed = processMostNodes(childNode, index, expectedOrder, processed); | ||
}); | ||
// Add last comments in the rule. Need this because last comments are not belonging to anything | ||
node.each(function (childNode, index) { | ||
processed = processLastComments(childNode, index, processed); | ||
}); | ||
// Add last comments in the rule. Need this because last comments are not belonging to anything | ||
node.each(function (childNode, index) { | ||
processed = processLastComments(childNode, index, processed); | ||
}); | ||
// Sort declarations saved for sorting | ||
processed.sort(sortByIndexes); | ||
// Sort declarations saved for sorting | ||
processed.sort(sorting.sortByIndexes); | ||
// Replace rule content with sorted one | ||
if (processed.length) { | ||
// Enforce semicolon for the last node | ||
node.raws.semicolon = true; | ||
// Replace rule content with sorted one | ||
node.removeAll(); | ||
node.append(processed); | ||
} | ||
}); | ||
} | ||
// Taking care of empty lines | ||
node.each(function (childNode) { | ||
formatNodes(childNode, opts); | ||
}); | ||
} | ||
}); | ||
} | ||
if (opts['properties-order']) { | ||
const isAlphabetical = opts['properties-order'] === 'alphabetical'; | ||
const expectedOrder = isAlphabetical ? null : createExpectedPropertiesOrder(opts['properties-order']); | ||
const unspecifiedPropertiesPosition = _.get(opts, ['unspecified-properties-position'], 'bottom'); | ||
function verifyOptions(options) { | ||
if (options === null || typeof options !== 'object') { | ||
options = {}; | ||
} | ||
css.walk(function (node) { | ||
// Process only rules and atrules with nodes | ||
if (isRuleWithNodes(node)) { | ||
const allRuleNodes = []; | ||
let declarations = []; | ||
var defaultOptions = { | ||
'sort-order': 'default', | ||
'empty-lines-between-children-rules': 0, | ||
'empty-lines-between-media-rules': 0, | ||
'preserve-empty-lines-between-children-rules': false, | ||
'empty-lines-before-comment': 0, | ||
'empty-lines-after-comment': 0, | ||
}; | ||
node.each(function (childNode, index) { | ||
if ( | ||
childNode.type === 'decl' && | ||
isStandardSyntaxProperty(childNode.prop) && | ||
!isCustomProperty(childNode.prop) | ||
) { | ||
const unprefixedPropName = postcss.vendor.unprefixed(childNode.prop); | ||
return objectAssign({}, defaultOptions, options); | ||
} | ||
const propData = { | ||
name: childNode.prop, | ||
unprefixedName: unprefixedPropName, | ||
orderData: isAlphabetical ? null : getPropertiesOrderData(expectedOrder, unprefixedPropName), | ||
node: childNode, | ||
initialIndex: index, | ||
unspecifiedPropertiesPosition, | ||
}; | ||
function getSortOrderFromOptions(options) { | ||
var sortOrder; | ||
// add a marker | ||
childNode.sortProperty = true; | ||
if (Array.isArray(options['sort-order'])) { | ||
sortOrder = options['sort-order']; | ||
} else if (typeof options['sort-order'] === 'string') { | ||
var configPath = path.join(__dirname, './configs/', options['sort-order']) + '.json'; | ||
// If comment on separate line before node, use node's indexes for comment | ||
const commentsBefore = getComments.beforeDeclaration([], childNode.prev(), propData); | ||
try { | ||
sortOrder = fs.readFileSync(configPath); | ||
sortOrder = JSON.parse(sortOrder); | ||
sortOrder = sortOrder['sort-order']; | ||
} catch (error) { | ||
return {}; | ||
} | ||
} else { | ||
return {}; | ||
} | ||
// If comment on same line with the node and node, use node's indexes for comment | ||
const commentsAfter = getComments.afterDeclaration([], childNode.next(), propData); | ||
// Add sorting indexes to order | ||
var order = {}; | ||
declarations = declarations.concat(commentsBefore, propData, commentsAfter); | ||
} | ||
}); | ||
(typeof sortOrder[0] === 'string' ? [sortOrder] : sortOrder) | ||
.forEach(function (group, groupIndex) { | ||
group.forEach(function (prop, propIndex) { | ||
order[prop] = { | ||
group: groupIndex, | ||
prop: propIndex | ||
}; | ||
}); | ||
}); | ||
if (isAlphabetical) { | ||
declarations.sort(sorting.sortDeclarationsAlphabetically); | ||
} else { | ||
declarations.sort(sorting.sortDeclarations); | ||
} | ||
return order; | ||
} | ||
// Process empty line before group | ||
declarations.forEach(emptyLineBeforeGroup); | ||
function getLinesBetweenRulesFromOptions(name, options) { | ||
var lines = options['empty-lines-between-' + name + '-rules']; | ||
let foundDeclarations = false; | ||
if (typeof lines !== 'number' || isNaN(lines) || !isFinite(lines) || lines < 0 || Math.floor(lines) !== lines) { | ||
throw new Error('Type of "empty-lines-between-' + name + '-rules" option must be integer with positive value.'); | ||
} | ||
node.each(function (childNode) { | ||
if (childNode.sortProperty) { | ||
if (!foundDeclarations) { | ||
foundDeclarations = true; | ||
return lines; | ||
} | ||
declarations.forEach((item) => { | ||
allRuleNodes.push(item.node); | ||
}); | ||
} | ||
} else { | ||
allRuleNodes.push(childNode); | ||
} | ||
}); | ||
// Replace multiple line breaks with one | ||
function cleanLineBreaks(node) { | ||
if (node.raws.before) { | ||
node.raws.before = node.raws.before.replace(/\r\n\s*\r\n/g, '\r\n').replace(/\n\s*\n/g, '\n'); | ||
node.removeAll(); | ||
node.append(allRuleNodes); | ||
} | ||
}); | ||
} | ||
return node; | ||
} | ||
if (!_.isUndefined(opts['custom-property-empty-line-before'])) { | ||
let customPropertyEmptyLineBefore = opts['custom-property-empty-line-before']; | ||
function createLineBreaks(lineBreaksCount) { | ||
return new Array(lineBreaksCount + 1).join('\n'); | ||
} | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(customPropertyEmptyLineBefore)) { | ||
customPropertyEmptyLineBefore = [customPropertyEmptyLineBefore]; | ||
} | ||
function getAtruleSortName(node, order) { | ||
var atruleName = '@' + node.name; | ||
var sortNameExtended; | ||
var atruleParameter; | ||
const optionName = 'custom-property-empty-line-before'; | ||
// If atRule has a parameter like `@mixin name` or `@include name`, sort by this parameter | ||
if (node.params) { | ||
atruleParameter = node.params; | ||
sortNameExtended = atruleName + ' ' + atruleParameter; | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
// check if there is a whole parameter in the config, e. g. `media("<=desk")` | ||
if (order[sortNameExtended]) { | ||
return sortNameExtended; | ||
} | ||
if (!isStandardSyntaxDeclaration(decl) || !isCustomProperty(prop)) { | ||
return; | ||
} | ||
// check if there is a part of parameter in the config, e. g. `media` from `media("<=desk")` | ||
atruleParameter = (/^[\w-]+/).exec(atruleParameter); | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
if (atruleParameter && atruleParameter.length) { | ||
sortNameExtended = atruleName + ' ' + atruleParameter[0]; | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
if (order[sortNameExtended]) { | ||
return sortNameExtended; | ||
let expectEmptyLineBefore = customPropertyEmptyLineBefore[0]; | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
} | ||
} | ||
// If atrule with name is in order use the name | ||
if (order[atruleName]) { | ||
return atruleName; | ||
} | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
return '@atrule'; | ||
} | ||
// Optionally reverse the expectation if a custom property precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-custom-property') | ||
&& decl.prev() | ||
&& ( | ||
( | ||
decl.prev().prop | ||
&& isCustomProperty(decl.prev().prop) | ||
) | ||
|| ( | ||
hasSharedLineCommentBefore(decl) | ||
&& decl.prev().prev() | ||
&& decl.prev().prev().prop | ||
&& isCustomProperty(decl.prev().prev().prop) | ||
) | ||
) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
function getSortName(node, order) { | ||
switch (node.type) { | ||
case 'decl': | ||
return (/^(\$|--)[\w-]+/).test(node.prop) ? '$variable' : node.prop; | ||
const hasEmptyLineBefore = hasEmptyLine(decl.raws.before); | ||
case 'atrule': | ||
return getAtruleSortName(node, order); | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
case 'rule': | ||
return '>child'; | ||
if (expectEmptyLineBefore) { | ||
if (decl.raws.before.indexOf('\n') === -1) { | ||
decl.raws.before = `\n${decl.raws.before}`; | ||
} | ||
default: | ||
return null; | ||
decl.raws.before = createEmptyLines(1) + decl.raws.before; | ||
} else { | ||
decl.raws.before = cleanEmptyLines(decl.raws.before); | ||
} | ||
}); | ||
} | ||
} | ||
function getOrderProperty(node, order) { | ||
var sortName = getSortName(node, order); | ||
if (!_.isUndefined(opts['dollar-variable-empty-line-before'])) { | ||
let dollarVariableEmptyLineBefore = opts['dollar-variable-empty-line-before']; | ||
// Trying to get property indexes from order's list | ||
var orderProperty = order[sortName]; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(dollarVariableEmptyLineBefore)) { | ||
dollarVariableEmptyLineBefore = [dollarVariableEmptyLineBefore]; | ||
} | ||
// If no property in the list and this property is prefixed then trying to get parameters for unprefixed property | ||
if (!orderProperty && postcss.vendor.prefix(sortName)) { | ||
sortName = postcss.vendor.unprefixed(sortName); | ||
orderProperty = order[sortName]; | ||
} | ||
const optionName = 'dollar-variable-empty-line-before'; | ||
return orderProperty; | ||
} | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
function addIndexesToNode(node, index, order) { | ||
// Index to place the nodes that shouldn't be sorted | ||
var lastGroupIndex = order['...'] ? order['...'].group : Infinity; | ||
var lastPropertyIndex = order['...'] ? order['...'].prop : Infinity; | ||
if (!isDollarVariable(prop)) { | ||
return; | ||
} | ||
var orderProperty = getOrderProperty(node, order); | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
// If the declaration's property is in order's list, save its | ||
// group and property indexes. Otherwise set them to 10000, so | ||
// declaration appears at the bottom of a sorted list: | ||
node.groupIndex = orderProperty && orderProperty.group > -1 ? orderProperty.group : lastGroupIndex; | ||
node.propertyIndex = orderProperty && orderProperty.prop > -1 ? orderProperty.prop : lastPropertyIndex; | ||
node.initialIndex = index; | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
return node; | ||
} | ||
let expectEmptyLineBefore = dollarVariableEmptyLineBefore[0]; | ||
function fetchAllCommentsBeforeNode(comments, previousNode, node, currentInitialIndex) { | ||
if (!previousNode || previousNode.type !== 'comment') { | ||
return comments; | ||
} | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
if (!previousNode.raws.before || previousNode.raws.before.indexOf('\n') === -1) { | ||
return comments; | ||
} | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
currentInitialIndex = currentInitialIndex || node.initialIndex; | ||
// Optionally reverse the expectation if a dollar variable precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-dollar-variable') | ||
&& decl.prev() | ||
&& ( | ||
( | ||
decl.prev().prop | ||
&& isDollarVariable(decl.prev().prop) | ||
) | ||
|| ( | ||
hasSharedLineCommentBefore(decl) | ||
&& decl.prev().prev() | ||
&& decl.prev().prev().prop | ||
&& isDollarVariable(decl.prev().prev().prop) | ||
) | ||
) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
previousNode.groupIndex = node.groupIndex; | ||
previousNode.propertyIndex = node.propertyIndex; | ||
previousNode.initialIndex = currentInitialIndex - 0.0001; | ||
const hasEmptyLineBefore = hasEmptyLine(decl.raws.before); | ||
var newComments = [previousNode].concat(comments); | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
return fetchAllCommentsBeforeNode(newComments, previousNode.prev(), node, previousNode.initialIndex); | ||
} | ||
if (expectEmptyLineBefore) { | ||
if (decl.raws.before.indexOf('\n') === -1) { | ||
decl.raws.before = `\n${decl.raws.before}`; | ||
} | ||
function fetchAllCommentsAfterNode(comments, nextNode, node, currentInitialIndex) { | ||
if (!nextNode || nextNode.type !== 'comment') { | ||
return comments; | ||
decl.raws.before = createEmptyLines(1) + decl.raws.before; | ||
} else { | ||
decl.raws.before = cleanEmptyLines(decl.raws.before); | ||
} | ||
}); | ||
} | ||
if (!nextNode.raws.before || nextNode.raws.before.indexOf('\n') >= 0) { | ||
return comments; | ||
} | ||
if (!_.isUndefined(opts['declaration-empty-line-before'])) { | ||
let declarationEmptyLineBefore = opts['declaration-empty-line-before']; | ||
currentInitialIndex = currentInitialIndex || node.initialIndex; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(declarationEmptyLineBefore)) { | ||
declarationEmptyLineBefore = [declarationEmptyLineBefore]; | ||
} | ||
nextNode.groupIndex = node.groupIndex; | ||
nextNode.propertyIndex = node.propertyIndex; | ||
nextNode.initialIndex = currentInitialIndex + 0.0001; | ||
const optionName = 'declaration-empty-line-before'; | ||
return fetchAllCommentsAfterNode(comments.concat(nextNode), nextNode.next(), node, nextNode.initialIndex); | ||
} | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
function getApplicableNode(lookFor, node) { | ||
// find if there any rules before, and skip the comments | ||
var prevNode = node.prev(); | ||
if (!isStandardSyntaxDeclaration(decl)) { | ||
return; | ||
} | ||
if (prevNode) { | ||
if (prevNode.type === lookFor) { | ||
return node; | ||
} | ||
if (isCustomProperty(prop)) { | ||
return; | ||
} | ||
if (prevNode.type === 'comment') { | ||
return getApplicableNode(lookFor, prevNode); | ||
} | ||
} | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
return false; | ||
} | ||
// Optionally ignore the node if a declaration precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-declaration') | ||
&& decl.prev() | ||
&& ( | ||
isDeclarationBefore(decl.prev()) | ||
|| ( | ||
hasSharedLineCommentBefore(decl) | ||
&& isDeclarationBefore(decl.prev().prev()) | ||
) | ||
) | ||
) { | ||
return; | ||
} | ||
function countEmptyLines(str) { | ||
var lineBreaks = (str.match(/\n/g) || []).length; | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
if (lineBreaks > 0) { | ||
lineBreaks -= 1; | ||
} | ||
let expectEmptyLineBefore = declarationEmptyLineBefore[0]; | ||
return lineBreaks; | ||
} | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
function processMostNodes(node, index, opts, processedNodes) { | ||
if (node.type === 'comment') { | ||
if (index === 0 && node.raws.before.indexOf('\n') === -1) { | ||
node.ruleComment = true; // need this flag to not append this comment twice | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
return processedNodes.concat(node); | ||
} | ||
// Optionally reverse the expectation if a declaration precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-declaration') | ||
&& decl.prev() | ||
&& ( | ||
isDeclarationBefore(decl.prev()) | ||
|| ( | ||
hasSharedLineCommentBefore(decl) | ||
&& isDeclarationBefore(decl.prev().prev()) | ||
) | ||
) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
return processedNodes; | ||
const hasEmptyLineBefore = hasEmptyLine(decl.raws.before); | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
if (expectEmptyLineBefore) { | ||
if (decl.raws.before.indexOf('\n') === -1) { | ||
decl.raws.before = `\n${decl.raws.before}`; | ||
} | ||
decl.raws.before = createEmptyLines(1) + decl.raws.before; | ||
} else { | ||
decl.raws.before = cleanEmptyLines(decl.raws.before); | ||
} | ||
function isDeclarationBefore(targetDeclaration) { | ||
return targetDeclaration | ||
&& targetDeclaration.prop | ||
&& isStandardSyntaxDeclaration(targetDeclaration) | ||
&& !isCustomProperty(targetDeclaration.prop); | ||
} | ||
}); | ||
} | ||
var order = getSortOrderFromOptions(opts); | ||
if (!_.isUndefined(opts['rule-nested-empty-line-before'])) { | ||
let ruleNestedEmptyLineBefore = opts['rule-nested-empty-line-before']; | ||
node = addIndexesToNode(node, index, order); | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(ruleNestedEmptyLineBefore)) { | ||
ruleNestedEmptyLineBefore = [ruleNestedEmptyLineBefore]; | ||
} | ||
// If comment on separate line before node, use node's indexes for comment | ||
var commentsBefore = fetchAllCommentsBeforeNode([], node.prev(), node); | ||
const optionName = 'rule-nested-empty-line-before'; | ||
// If comment on same line with the node and node, use node's indexes for comment | ||
var commentsAfter = fetchAllCommentsAfterNode([], node.next(), node); | ||
css.walkRules(function (rule) { | ||
if (!isStandardSyntaxRule(rule)) { | ||
return; | ||
} | ||
return processedNodes.concat(commentsBefore, node, commentsAfter); | ||
} | ||
// Only attend to nested rule sets | ||
if (rule.parent === css) { | ||
return; | ||
} | ||
function processLastComments(node, index, processedNodes) { | ||
if (node.type === 'comment' && !node.hasOwnProperty('groupIndex') && !node.ruleComment) { | ||
node.groupIndex = Infinity; | ||
node.propertyIndex = Infinity; | ||
node.initialIndex = index; | ||
// Optionally ignore the expectation if a non-shared-line comment precedes this node | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(rule) | ||
) { | ||
return; | ||
} | ||
return processedNodes.concat(node); | ||
} | ||
// Ignore if the expectation is for multiple and the rule is single-line | ||
if ( | ||
( | ||
_.isString(ruleNestedEmptyLineBefore[0]) | ||
&& ruleNestedEmptyLineBefore[0].indexOf('multi-line') !== -1 | ||
) | ||
&& isSingleLineString(rule.toString()) | ||
) { | ||
return; | ||
} | ||
return processedNodes; | ||
} | ||
let expectEmptyLineBefore = false; | ||
function sortByIndexes(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 ( | ||
( | ||
_.isString(ruleNestedEmptyLineBefore[0]) | ||
&& ruleNestedEmptyLineBefore[0].indexOf('always') !== -1 | ||
) | ||
|| ruleNestedEmptyLineBefore[0] === true | ||
) { | ||
expectEmptyLineBefore = true; | ||
} | ||
// 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; | ||
} | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& rule === rule.parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// 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.initialIndex - b.initialIndex; | ||
} | ||
// Optionally reverse the expectation if a rule precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-rule') | ||
&& rule.prev() | ||
&& ( | ||
rule.prev().type === 'rule' | ||
|| ( | ||
hasSharedLineCommentBefore(rule) | ||
&& rule.prev().prev() | ||
&& rule.prev().prev().type === 'rule' | ||
) | ||
) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
function formatNodes(node, opts) { | ||
var linesBetweenChildrenRules = getLinesBetweenRulesFromOptions('children', opts); | ||
var linesBetweenMediaRules = getLinesBetweenRulesFromOptions('media', opts); | ||
var preserveLinesBetweenChildren = opts['preserve-empty-lines-between-children-rules']; | ||
var linesBeforeComment = opts['empty-lines-before-comment']; | ||
var linesAfterComment = opts['empty-lines-after-comment']; | ||
const hasEmptyLineBefore = hasEmptyLine(rule.raws.before); | ||
// don't remove empty lines if they are should be preserved | ||
if ( | ||
!( | ||
preserveLinesBetweenChildren && | ||
(node.type === 'rule' || node.type === 'comment') && | ||
node.prev() && | ||
getApplicableNode('rule', node) | ||
) | ||
) { | ||
node = cleanLineBreaks(node); | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
if (expectEmptyLineBefore) { | ||
if (rule.raws.before.indexOf('\n') === -1) { | ||
rule.raws.before = `\n${rule.raws.before}`; | ||
} | ||
rule.raws.before = createEmptyLines(1) + rule.raws.before; | ||
} else { | ||
rule.raws.before = cleanEmptyLines(rule.raws.before); | ||
} | ||
}); | ||
} | ||
var prevNode = node.prev(); | ||
if (!_.isUndefined(opts['at-rule-nested-empty-line-before'])) { | ||
let atRuleNestedEmptyLineBefore = opts['at-rule-nested-empty-line-before']; | ||
if (prevNode && node.raws.before) { | ||
if (node.groupIndex > prevNode.groupIndex) { | ||
node.raws.before = createLineBreaks(1) + node.raws.before; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(atRuleNestedEmptyLineBefore)) { | ||
atRuleNestedEmptyLineBefore = [atRuleNestedEmptyLineBefore]; | ||
} | ||
var applicableNode; | ||
const optionName = 'at-rule-nested-empty-line-before'; | ||
// Insert empty lines between children classes | ||
if (node.type === 'rule' && linesBetweenChildrenRules > 0) { | ||
// between rules can be comments, so empty lines should be added to first comment between rules, rather than to rule | ||
applicableNode = getApplicableNode('rule', node); | ||
css.walkAtRules(function (atRule) { | ||
// Only attend to nested at-rules | ||
if (atRule.parent === css) { | ||
return; | ||
} | ||
if (applicableNode) { | ||
// add lines only if source empty lines not preserved, or if there are less empty lines then should be | ||
if ( | ||
!preserveLinesBetweenChildren || | ||
( | ||
preserveLinesBetweenChildren && | ||
countEmptyLines(applicableNode.raws.before) < linesBetweenChildrenRules | ||
) | ||
) { | ||
applicableNode.raws.before = createLineBreaks(linesBetweenChildrenRules - countEmptyLines(applicableNode.raws.before)) + applicableNode.raws.before; | ||
// Return early if at-rule is to be ignored | ||
if (checkOption(optionName, 'ignoreAtRules', atRule.name)) { | ||
return; | ||
} | ||
// Optionally ignore the expectation if the node is blockless | ||
if ( | ||
checkOption(optionName, 'ignore', 'blockless-after-blockless') | ||
&& isBlocklessAfterBlockless() | ||
) { | ||
return; | ||
} | ||
const previousNode = atRule.prev(); | ||
// Optionally ignore the expection if the node is blockless | ||
// and following another blockless at-rule with the same name | ||
if ( | ||
checkOption(optionName, 'ignore', 'blockless-after-same-name-blockless') | ||
&& isBlocklessAfterSameNameBlockless() | ||
) { | ||
return; | ||
} | ||
// Optionally ignore the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(atRule) | ||
) { | ||
return; | ||
} | ||
let expectEmptyLineBefore = atRuleNestedEmptyLineBefore[0]; | ||
// Optionally reverse the expectation if any exceptions apply | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& isFirstNested() | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
if ( | ||
checkOption(optionName, 'except', 'blockless-after-blockless') | ||
&& isBlocklessAfterBlockless() | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
if ( | ||
checkOption(optionName, 'except', 'blockless-after-same-name-blockless') | ||
&& isBlocklessAfterSameNameBlockless() | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
if ( | ||
checkOption(optionName, 'except', 'after-same-name') | ||
&& isAfterSameName() | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
const hasEmptyLineBefore = hasEmptyLine(atRule.raws.before); | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
if (expectEmptyLineBefore) { | ||
if (atRule.raws.before.indexOf('\n') === -1) { | ||
atRule.raws.before = `\n${atRule.raws.before}`; | ||
} | ||
atRule.raws.before = createEmptyLines(1) + atRule.raws.before; | ||
} else { | ||
atRule.raws.before = cleanEmptyLines(atRule.raws.before); | ||
} | ||
} | ||
// Insert empty lines between media rules | ||
if (node.type === 'atrule' && node.name === 'media' && linesBetweenMediaRules > 0) { | ||
// between rules can be comments, so empty lines should be added to first comment between rules, rather than to rule | ||
applicableNode = getApplicableNode('atrule', node); | ||
function isBlocklessAfterBlockless() { | ||
return !hasBlock(atRule) | ||
&& atRule.prev() | ||
&& ( | ||
( | ||
atRule.prev().type === 'atrule' | ||
&& !hasBlock(atRule.prev()) | ||
&& !hasBlock(atRule) | ||
) | ||
|| ( | ||
hasSharedLineCommentBefore(atRule) | ||
&& atRule.prev().prev() | ||
&& atRule.prev().prev().type === 'atrule' | ||
&& !hasBlock(atRule.prev().prev()) | ||
) | ||
); | ||
} | ||
if (applicableNode) { | ||
applicableNode.raws.before = createLineBreaks(linesBetweenMediaRules - countEmptyLines(applicableNode.raws.before)) + applicableNode.raws.before; | ||
function isBlocklessAfterSameNameBlockless() { | ||
return !hasBlock(atRule) | ||
&& previousNode | ||
&& ( | ||
( | ||
previousNode.type === 'atrule' | ||
&& previousNode.name === atRule.name | ||
&& !hasBlock(previousNode) | ||
) | ||
|| ( | ||
hasSharedLineCommentBefore(atRule) | ||
&& previousNode.prev() | ||
&& previousNode.prev().type === 'atrule' | ||
&& previousNode.prev().name === atRule.name | ||
&& !hasBlock(previousNode.prev()) | ||
) | ||
); | ||
} | ||
} | ||
// Insert empty lines before comment | ||
if ( | ||
linesBeforeComment && | ||
node.type === 'comment' && | ||
(prevNode.type !== 'comment' || prevNode.raws.before.indexOf('\n') === -1) && // prevNode it's not a comment or it's an inline comment | ||
node.raws.before.indexOf('\n') >= 0 && // this isn't an inline comment | ||
countEmptyLines(node.raws.before) < linesBeforeComment | ||
) { | ||
node.raws.before = createLineBreaks(linesBeforeComment - countEmptyLines(node.raws.before)) + node.raws.before; | ||
function isAfterSameName() { | ||
return previousNode | ||
&& ( | ||
( | ||
previousNode.type === 'atrule' | ||
&& previousNode.name === atRule.name | ||
) | ||
|| ( | ||
hasSharedLineCommentBefore(atRule) | ||
&& previousNode.prev() | ||
&& previousNode.prev().type === 'atrule' | ||
&& previousNode.prev().name === atRule.name | ||
) | ||
); | ||
} | ||
function isFirstNested() { | ||
return atRule === atRule.parent.first; | ||
} | ||
}); | ||
} | ||
if (!_.isUndefined(opts['comment-empty-line-before'])) { | ||
let commentEmptyLineBefore = opts['comment-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(commentEmptyLineBefore)) { | ||
commentEmptyLineBefore = [commentEmptyLineBefore]; | ||
} | ||
// Insert empty lines after comment | ||
if ( | ||
linesAfterComment && | ||
node.type !== 'comment' && | ||
prevNode.type === 'comment' && | ||
prevNode.raws.before.indexOf('\n') >= 0 && // this isn't an inline comment | ||
countEmptyLines(node.raws.before) < linesAfterComment | ||
) { | ||
node.raws.before = createLineBreaks(linesAfterComment - countEmptyLines(node.raws.before)) + node.raws.before; | ||
} | ||
const optionName = 'comment-empty-line-before'; | ||
css.walk(function (node) { | ||
// Process only rules and atrules with nodes | ||
if (isRuleWithNodes(node)) { | ||
node.walkComments((comment) => { | ||
// Optionally ignore stylelint commands | ||
if ( | ||
comment.text.indexOf('stylelint-') === 0 | ||
&& checkOption(optionName, 'ignore', 'stylelint-command') | ||
) { | ||
return; | ||
} | ||
// Optionally ignore newlines between comments | ||
const prev = comment.prev(); | ||
if ( | ||
prev | ||
&& prev.type === 'comment' | ||
&& checkOption(optionName, 'ignore', 'after-comment') | ||
) { | ||
return; | ||
} | ||
if ( | ||
comment.raws.inline | ||
|| comment.inline | ||
) { | ||
return; | ||
} | ||
const before = comment.raws.before || ''; | ||
// Ignore shared-line comments | ||
if (before.indexOf('\n') === -1) { | ||
return; | ||
} | ||
const hasEmptyLineBefore = hasEmptyLine(before); | ||
let expectEmptyLineBefore = commentEmptyLineBefore[0]; | ||
// Optionally reverse the expectation if any exceptions apply | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& comment === comment.parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// Return if the expectation is met | ||
if (expectEmptyLineBefore === hasEmptyLineBefore) { | ||
return; | ||
} | ||
if (expectEmptyLineBefore) { | ||
comment.raws.before = createEmptyLines(1) + comment.raws.before; | ||
} else { | ||
comment.raws.before = cleanEmptyLines(comment.raws.before); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
function checkOption(primaryOption, secondaryOption, value) { | ||
const secondaryOptionValues = _.get(opts[primaryOption][1], secondaryOption); | ||
return _.includes(secondaryOptionValues, value); | ||
} | ||
} |
The MIT License (MIT) | ||
Copyright 2016 Aleks Hudochenkov <aleks@hudochenkov.com> | ||
Copyright 2017 Aleks Hudochenkov <aleks@hudochenkov.com> | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of |
{ | ||
"name": "postcss-sorting", | ||
"version": "1.7.0", | ||
"description": "PostCSS plugin to sort rules content with specified order.", | ||
"version": "2.0.0", | ||
"description": "PostCSS plugin to keep rules and at-rules content in order.", | ||
"keywords": [ | ||
@@ -20,19 +20,22 @@ "postcss", | ||
"files": [ | ||
"configs", | ||
"docs", | ||
"lib", | ||
"index.js" | ||
], | ||
"engines": { | ||
"node": ">=4.0.0" | ||
}, | ||
"dependencies": { | ||
"object-assign": "^4.1.0", | ||
"postcss": "^5.1.2" | ||
"lodash": "^4.17.4", | ||
"postcss": "^5.2.10" | ||
}, | ||
"devDependencies": { | ||
"ava": "^0.16.0", | ||
"eslint": "^3.4.0", | ||
"postcss-scss": "^0.2.1" | ||
"ava": "^0.17.0", | ||
"eslint": "^3.13.1" | ||
}, | ||
"scripts": { | ||
"test": "ava && eslint index.js test/test.js", | ||
"test2": "ava", | ||
"lint": "eslint index.js test/test.js" | ||
"test": "ava && eslint index.js lib/*.js test/*.js", | ||
"ava": "ava", | ||
"lint": "eslint index.js lib/*.js test/*.js" | ||
} | ||
} |
614
README.md
# PostCSS Sorting [![Build Status][ci-img]][ci] | ||
[PostCSS] plugin to sort rules content with specified order. Heavily inspired by [CSSComb]. | ||
[PostCSS] plugin to keep rules and at-rules content in order. | ||
Also available as [Sublime Text plugin], [Atom plugin], and [VS Code plugin]. | ||
Lint style sheets order with [stylelint-order]. | ||
## Features | ||
* Plugin is sorting content for rules and at-rules. | ||
* Sorting nested rules. | ||
* Sorting at-rules, also by at-rule name and parameter. | ||
* Sorting variables. | ||
* Grouping content. | ||
* Support CSS, SCSS (if [postcss-scss] parser is used), [PreCSS] and most likely any other syntax added by other PostCSS plugins. | ||
* Sorts rules and at-rules content. | ||
* Sorts properties. | ||
* Sorts at-rules by different options. | ||
* Groups properties, custom properties, dollar variables, nested rules, nested at-rules. | ||
* Adds empty lines before different types of nodes. | ||
* Supports CSS, SCSS (if [postcss-scss] parser is used), [PreCSS] and most likely any other syntax added by other PostCSS plugins. | ||
## Table of Contents | ||
* [Installation](#installation) | ||
* [Options](#options) | ||
* [Default options](#default-options) | ||
* [`sort-order`](#sort-order) | ||
* [Declarations](#declarations) | ||
* [Prefixed properties](#prefixed-properties) | ||
* [Grouping](#grouping) | ||
* [@at-rules](#at-rules) | ||
* [Nested rules](#nested-rules) | ||
* [Variables](#variables) | ||
* [Leftovers](#leftovers) | ||
* [Predefined configs](#predefined-configs) | ||
* [`empty-lines-between-children-rules`](#empty-lines-between-children-rules) | ||
* [`empty-lines-between-media-rules`](#empty-lines-between-media-rules) | ||
* [`preserve-empty-lines-between-children-rules`](#preserve-empty-lines-between-children-rules) | ||
* [`empty-lines-before-comment`](#empty-lines-before-comment) | ||
* [`empty-lines-after-comment`](#empty-lines-after-comment) | ||
* [Disabling in style sheet](#disabling-in-style-sheet) | ||
* [Migration from CSSComb](#migration-from-csscomb) | ||
* [Usage](#usage) | ||
* [Text editor](#text-editor) | ||
* [Gulp](#gulp) | ||
* [Grunt](#grunt) | ||
* [Related tools](#related-tools) | ||
## Installation | ||
@@ -51,212 +26,105 @@ | ||
### Default options | ||
The plugin has no default options. Everything is disabled by default. | ||
```json | ||
{ | ||
"sort-order": "default", | ||
"empty-lines-between-children-rules": 0, | ||
"empty-lines-between-media-rules": 0, | ||
"preserve-empty-lines-between-children-rules": false | ||
} | ||
``` | ||
### Order | ||
### `sort-order` | ||
- [`order`](./docs/order.md): Specify the order of content within declaration blocks. | ||
- [`properties-order`](./docs/properties-order.md): Specify the order of properties within declaration blocks. Can specify empty line before property groups. | ||
- [`unspecified-properties-position`](./docs/unspecified-properties-position.md): Specify position for properties not specified in `properties-order`. | ||
Set sort order. If no order is set, the plugin uses default config. | ||
### Empty lines | ||
**Note**: Use one of [predefined configs] as an example. | ||
- [`clean-empty-lines`](./docs/clean-empty-lines.md): Remove all empty lines. Runs before all other rules. | ||
- [`rule-nested-empty-line-before`](./docs/rule-nested-empty-line-before.md): Specify an empty line before nested rules. | ||
- [`at-rule-nested-empty-line-before`](./docs/at-rule-nested-empty-line-before.md): Specify an empty line before nested at-rules. | ||
- [`declaration-empty-line-before`](./docs/declaration-empty-line-before.md): Specify an empty line before declarations. | ||
- [`custom-property-empty-line-before`](./docs/custom-property-empty-line-before.md): Specify an empty line before custom properties. | ||
- [`dollar-variable-empty-line-before`](./docs/dollar-variable-empty-line-before.md): Specify an empty line before `$`-variable declarations. | ||
- [`comment-empty-line-before`](./docs/comment-empty-line-before.md): Specify an empty line before comments. | ||
Acceptable values: | ||
## Handling comments | ||
* `{Array}` of rules; | ||
* `{Array}` of arrays of rules for groups separation; | ||
* `{String}` with the name of predefined config. | ||
Shared-line comments are comments which are located after a node and on the same line as a node. | ||
#### Declarations | ||
Example: `{ "sort-order": [ "margin", "padding" ] }` | ||
```css | ||
/* before */ | ||
p { | ||
padding: 0; | ||
margin: 0; | ||
a { | ||
/* regular comment */ | ||
color: pink; /* shared-line comment */ | ||
} | ||
/* after */ | ||
p { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
``` | ||
##### Prefixed properties | ||
Shared-line comments are always ignored in all “empty lines before” options. The plugin always looks “through” these comments. For example: | ||
Prefixed properties may not be in sort order. Plugin will look for unprefixed property and if it find one it will use that property order for the prefixed property. It would be better not to write prefixed properties in CSS at all and delegate this job to [Autoprefixer]. | ||
Example: `{ "sort-order": [ "position", "-webkit-box-sizing", "box-sizing", "width" ] }` | ||
```css | ||
/* before */ | ||
div { | ||
-moz-box-sizing: border-box; | ||
width: 100%; | ||
box-sizing: border-box; | ||
position: absolute; | ||
-webkit-box-sizing: border-box; | ||
```js | ||
{ | ||
"declaration-empty-line-before": [true, { | ||
except: "after-declaration" | ||
}] | ||
} | ||
/* after */ | ||
div { | ||
position: absolute; | ||
-webkit-box-sizing: border-box; | ||
-moz-box-sizing: border-box; | ||
box-sizing: border-box; | ||
width: 100%; | ||
} | ||
``` | ||
#### Grouping | ||
Technically there is a comment before `bottom`. But it's a shared line comment, so plugin looks before this comment and sees `top`: | ||
Using an array of arrays for `sort-order` separate content into groups by an empty line. | ||
Example: `{ "sort-order": [ [ "margin", "padding" ], [ "border", "background" ] ] }` | ||
```css | ||
/* before */ | ||
p { | ||
background: none; | ||
border: 0; | ||
margin: 0; | ||
padding: 0; | ||
} | ||
a { | ||
--prop: pink; | ||
/* after */ | ||
p { | ||
margin: 0; | ||
padding: 0; | ||
border: 0; | ||
background: none; | ||
top: 5px; /* shared-line comment */ | ||
bottom: 15px; | ||
} | ||
``` | ||
#### @at-rules | ||
For “order” options comments that are before node and on a separate line linked to that node. Shared-line comments are also linked to that node. | ||
Any @at-rule inside another rule can be sorted. There is some keywords: | ||
* `@atrule` — any at-rule. | ||
* `@atrulename` — any at-rule with a specific name. Ex., `@media` or `@mixin`. | ||
* `@atrulename parameter` — any at-rule with specific name and parameter. Ex., `@mixin clearfix`. | ||
Example: `{ "sort-order": ["@atrule", "@mixin", "border", "@some-rule hello", "@mixin clearfix"] }` | ||
```scss | ||
/* before */ | ||
.block { | ||
@some-rule hello; | ||
border: none; | ||
@mixin clearfix; | ||
@media (min-width: 100px) { | ||
display: none; | ||
} | ||
@mixin island; | ||
```css | ||
a { | ||
top: 5px; /* shared-line comment belongs to `top` */ | ||
/* comment belongs to `bottom` */ | ||
/* comment belongs to `bottom` */ | ||
bottom: 15px; /* shared-line comment belongs to `bottom` */ | ||
} | ||
/* after */ | ||
.block { | ||
@media (min-width: 100px) { | ||
display: none; | ||
} | ||
@mixin island; | ||
border: none; | ||
@some-rule hello; | ||
@mixin clearfix; | ||
} | ||
``` | ||
#### Nested rules | ||
## Migration from `1.x` | ||
`>child` keyword for nested rules. | ||
If you have been using [predefined configs], you can look at [migrated predefined configs]. | ||
Example: `{ "sort-order": [ ["position", "top", "width"], ['>child'] ] }` | ||
`sort-order` was split into [`order`](./docs/order.md) and [`properties-order`](./docs/properties-order.md). | ||
```scss | ||
/* before */ | ||
.block { | ||
position: absolute; | ||
`properties-order` now uses an array of objects for grouping. | ||
span { | ||
display: inline-block; | ||
} | ||
`sort-order` keywords to new config conversion: | ||
width: 50%; | ||
| `1.x` | `2.x` | | ||
| --- | --- | | ||
| `@atrule` | `{ order: ["at-rules"] }` or `{ order: [{ type: "at-rule" }] }` | | ||
| `@atrulename` | `{ order: [{ type: "at-rule", name: "atrulename" }] }` | | ||
| `@atrulename parameter` | `{ order: [{ type: "at-rule", name: "atrulename", parameter: "parameter" }] }` | | ||
| `>child` | `{ order: ["rules"] }` | | ||
| `$variable` | `{ order: ["custom-properties", "dollar-variables"] }` | | ||
| “leftovers” token `...` | `{ "unspecified-properties-position": "bottom" }` | | ||
&__element { | ||
display: none; | ||
} | ||
Config for `1.x`: | ||
top: 0; | ||
} | ||
/* after */ | ||
.block { | ||
position: absolute; | ||
top: 0; | ||
width: 50%; | ||
span { | ||
display: inline-block; | ||
} | ||
&__element { | ||
display: none; | ||
} | ||
} | ||
``` | ||
#### Variables | ||
`$variable` keyword is using to sort variables like `$size`. | ||
Example: `{ "sort-order": [ ["$variable"], ["position", "top", "width", "height"] ] }` | ||
```scss | ||
/* before */ | ||
.block { | ||
position: absolute; | ||
$width: 10px; | ||
top: 0; | ||
$height: 20px; | ||
height: $height; | ||
width: $width; | ||
} | ||
/* after */ | ||
.block { | ||
$width: 10px; | ||
$height: 20px; | ||
position: absolute; | ||
top: 0; | ||
width: $width; | ||
height: $height; | ||
} | ||
``` | ||
#### Leftovers | ||
When there are properties that are not mentioned in the `sort-order` option, they are inserted after all the sorted properties in the new group in the same order they were in the source stylesheet. | ||
You can override this by using a “leftovers” token: `...` — just place it either in its own group or near other properties in any other group and CSSComb would place all the properties that were not sorted where the `...` was in `sort-order`. | ||
So, with this value: | ||
``` json | ||
```js | ||
{ | ||
"sort-order": [ | ||
["$variable"], | ||
["position"], | ||
["...", "border"], | ||
["@mixin"], | ||
["font"] | ||
[ | ||
"$variable" | ||
], | ||
[ | ||
"margin", | ||
"padding" | ||
], | ||
[ | ||
"border", | ||
"background" | ||
], | ||
[ | ||
'...', | ||
"at-rule", | ||
"@include", | ||
"@include media", | ||
">child" | ||
] | ||
] | ||
@@ -266,275 +134,42 @@ } | ||
everything would go into five groups: variables, then group with `position`, then group containing all the leftovers plus the `border`, then group with all mixins and then the `font`. | ||
Config for `2.x`: | ||
#### Predefined configs | ||
[PostCSS Sorting] have [predefined configs]: | ||
* `default` | ||
* `alphabetical` | ||
* `zen` | ||
* `csscomb` | ||
* `yandex` | ||
* `smacss` | ||
Example: `{ "sort-order": "zen" }` | ||
### `empty-lines-between-children-rules` | ||
Set a number of empty lines between nested children rules. By default there is no empty lines between `>child` rules. | ||
Acceptable value: `{Number}` of empty lines | ||
Example: `{ "empty-lines-between-children-rules": 1, "sort-order": [ ["..."], [">child"] ] }` | ||
```scss | ||
/* before */ | ||
.block { | ||
position: absolute; | ||
span { | ||
display: inline-block; | ||
} | ||
&__element { | ||
display: none; | ||
} | ||
&:hover { | ||
top: 0; | ||
} | ||
```js | ||
{ | ||
"order": [ | ||
"custom-properties", | ||
"dollar-variables", | ||
"declarations", | ||
"at-rules", | ||
{ | ||
"type": "at-rule", | ||
"name": "include" | ||
}, | ||
{ | ||
"type": "at-rule", | ||
"name": "include", | ||
"parameter": "icon" | ||
}, | ||
"rules" | ||
], | ||
"properties-order": [ | ||
{ | ||
"emptyLineBefore": true, | ||
"properties": [ | ||
"margin", | ||
"padding" | ||
] | ||
}, | ||
{ | ||
"emptyLineBefore": true, | ||
"properties": [ | ||
"border", | ||
"background" | ||
] | ||
} | ||
], | ||
"unspecified-properties-position": "bottom" | ||
} | ||
/* after */ | ||
.block { | ||
position: absolute; | ||
span { | ||
display: inline-block; | ||
} | ||
&__element { | ||
display: none; | ||
} | ||
&:hover { | ||
top: 0; | ||
} | ||
} | ||
``` | ||
### `empty-lines-between-media-rules` | ||
Set a number of empty lines between nested media rules. By default there is no empty lines between `@media` rules. | ||
Acceptable value: `{Number}` of empty lines | ||
Example: `{ "empty-lines-between-media-rules": 1, "sort-order": ["@media"] }` | ||
```scss | ||
/* before */ | ||
.block { | ||
@media (min-width: 1px) {} | ||
@media (min-width: 2px) {} | ||
@media (min-width: 3px) {} | ||
} | ||
/* after */ | ||
.block { | ||
@media (min-width: 1px) {} | ||
@media (min-width: 2px) {} | ||
@media (min-width: 3px) {} | ||
} | ||
``` | ||
### `preserve-empty-lines-between-children-rules` | ||
Preserve empty lines between children rules and preserve empty lines for comments between children rules. | ||
Acceptable value: `true` | ||
Example: `{ "preserve-empty-lines-between-children-rules": true }` | ||
```scss | ||
/* before */ | ||
.block { | ||
&:before {} | ||
&:after {} | ||
.element {} | ||
/* comment */ | ||
.child {} | ||
} | ||
/* after (nothing changed) */ | ||
.block { | ||
&:before {} | ||
&:after {} | ||
.element {} | ||
/* comment */ | ||
.child {} | ||
} | ||
``` | ||
### `empty-lines-before-comment` | ||
Set a number of empty lines before comment or comments group, which on separate lines. By default, there are no empty lines before comment. | ||
Acceptable value: `{Number}` of empty lines | ||
Example: `{ "empty-lines-before-comment": 2, "sort-order": [ "..." ] }` | ||
```scss | ||
/* before */ | ||
.hello { | ||
display: inline-block; | ||
/* upline comment 1 */ | ||
/* upline comment 2 */ | ||
font-style: italic; | ||
border-bottom: 1px solid red; /* trololo 1 */ /* trololo 2 */ | ||
/* arrow */ | ||
&:before { | ||
/* yeah */ | ||
content: ""; | ||
} | ||
/* thing */ | ||
&:after { | ||
/* joy */ | ||
display: none; | ||
} | ||
&__element { | ||
/* sdfsf */ | ||
} | ||
} | ||
/* after */ | ||
.hello { | ||
display: inline-block; | ||
/* upline comment 1 */ | ||
/* upline comment 2 */ | ||
font-style: italic; | ||
border-bottom: 1px solid red; /* trololo 1 */ /* trololo 2 */ | ||
/* arrow */ | ||
&:before { | ||
/* yeah */ | ||
content: ""; | ||
} | ||
/* thing */ | ||
&:after { | ||
/* joy */ | ||
display: none; | ||
} | ||
&__element { | ||
/* sdfsf */ | ||
} | ||
} | ||
``` | ||
### `empty-lines-after-comment` | ||
Set a number of empty lines after comment or comments group, which on separate lines. By default, there are no empty lines after comment. | ||
Acceptable value: `{Number}` of empty lines | ||
Example: `{ "empty-lines-after-comment": 2, "sort-order": [ "..." ] }` | ||
```scss | ||
/* before */ | ||
.hello { | ||
display: inline-block; | ||
/* upline comment 1 */ | ||
/* upline comment 2 */ | ||
font-style: italic; | ||
border-bottom: 1px solid red; /* trololo 1 */ /* trololo 2 */ | ||
/* arrow */ | ||
&:before { | ||
/* yeah */ | ||
content: ""; | ||
} | ||
/* thing */ | ||
&:after { | ||
/* joy */ | ||
display: none; | ||
} | ||
&__element { | ||
/* sdfsf */ | ||
} | ||
} | ||
/* after */ | ||
.hello { | ||
display: inline-block; | ||
/* upline comment 1 */ | ||
/* upline comment 2 */ | ||
font-style: italic; | ||
border-bottom: 1px solid red; /* trololo 1 */ /* trololo 2 */ | ||
/* arrow */ | ||
&:before { | ||
/* yeah */ | ||
content: ""; | ||
} | ||
/* thing */ | ||
&:after { | ||
/* joy */ | ||
display: none; | ||
} | ||
&__element { | ||
/* sdfsf */ | ||
} | ||
} | ||
``` | ||
### Disabling in style sheet | ||
The plugin can be temporarily turned off by using special comments. | ||
```css | ||
/* postcss-sorting: off */ | ||
.block1 { | ||
width: 50px; | ||
display: inline-block; | ||
} | ||
/* postcss-sorting: on */ | ||
``` | ||
Due to plugin nature only comments in the root of stylesheet will affect plugin processing. In this case comments will be treated like regular comments: | ||
```css | ||
.block5 { | ||
/* postcss-sorting: off */ | ||
width: 20px; | ||
display: inline-block; | ||
/* postcss-sorting: on */ | ||
} | ||
``` | ||
### Migration from CSSComb | ||
If you used to use custom sorting order in [CSSComb] you can easily use this sorting order in PostCSS Sorting. `sort-order` option in this plugin is compatible with `sort-order` in CSSComb. Just copy `sort-order` value from CSSComb config to PostCSS Sorting config. | ||
## Usage | ||
@@ -544,7 +179,7 @@ | ||
#### Text editor | ||
### Text editor | ||
This plugin available as [Sublime Text plugin], [Atom plugin], and [VS Code plugin]. | ||
#### Gulp | ||
### Gulp | ||
@@ -569,3 +204,3 @@ Add [Gulp PostCSS] and PostCSS Sorting to your build tool: | ||
).pipe( | ||
gulp.dest('./css') | ||
gulp.dest('./css/src') | ||
); | ||
@@ -575,3 +210,3 @@ }); | ||
#### Grunt | ||
### Grunt | ||
@@ -605,9 +240,9 @@ Add [Grunt PostCSS] and PostCSS Sorting to your build tool: | ||
If you want format stylesheets, use [perfectionist] or [stylefmt], also a PostCSS-based tool. | ||
[stylelint] and [stylelint-order] help lint style sheets and let know if style sheet order is correct. | ||
Don't forget to lint stylesheets with [stylelint]! | ||
If you want format style sheets, use [perfectionist] or [stylefmt], also a PostCSS-based tools. | ||
## Thanks | ||
This plugin is heavily inspired by [CSSComb]. Some code logic, tests, and documentation parts are taken from this tool. | ||
This plugin is heavily inspired by [stylelint]. Some code logic, tests, and documentation parts are taken from this tool. | ||
@@ -617,9 +252,8 @@ [PostCSS]: https://github.com/postcss/postcss | ||
[ci]: https://travis-ci.org/hudochenkov/postcss-sorting | ||
[PostCSS Sorting]: https://github.com/hudochenkov/postcss-sorting | ||
[predefined configs]: https://github.com/hudochenkov/postcss-sorting/tree/master/configs | ||
[Sublime Text plugin]: https://github.com/hudochenkov/sublime-postcss-sorting | ||
[Atom plugin]: https://github.com/lysyi3m/atom-postcss-sorting | ||
[VS Code plugin]: https://github.com/mrmlnc/vscode-postcss-sorting | ||
[predefined configs]: https://github.com/hudochenkov/postcss-sorting/tree/ee71c3b61eea8fa11bc3aa2d26dd99a832df6d54/configs | ||
[migrated predefined configs]: https://gist.github.com/hudochenkov/b7127590d3013a5982ed90ad63a85306 | ||
[CSSComb]: https://github.com/csscomb/csscomb.js | ||
[Gulp PostCSS]: https://github.com/postcss/gulp-postcss | ||
@@ -629,5 +263,5 @@ [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss | ||
[postcss-scss]: https://github.com/postcss/postcss-scss | ||
[Autoprefixer]: https://github.com/postcss/autoprefixer | ||
[perfectionist]: https://github.com/ben-eb/perfectionist | ||
[stylefmt]: https://github.com/morishitter/stylefmt | ||
[stylelint]: http://stylelint.io/ | ||
[stylelint-order]: https://github.com/hudochenkov/stylelint-order |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
87147
2
41
1
1593
260
1
+ Addedlodash@^4.17.4
+ Addedlodash@4.17.21(transitive)
- Removedobject-assign@^4.1.0
- Removedobject-assign@4.1.1(transitive)
Updatedpostcss@^5.2.10