postcss-sorting
Advanced tools
Comparing version 2.0.1 to 2.1.0
@@ -5,2 +5,7 @@ # Change Log | ||
## 2.1.0 | ||
* Added: `order` supports new `rule` extended object, which has new `selector` option. Rules in `order` can be specified by their selector. | ||
* Fixed: Inconsistency with shared line comments. | ||
* Fixed: Incorrect behaviour if `hasBlock` was set to `false` in extended at-rule object in `order`. | ||
## 2.0.1 | ||
@@ -7,0 +12,0 @@ * Accept `null` for all options. |
@@ -317,16 +317,17 @@ # at-rule-nested-empty-line-before | ||
```js | ||
[true, { ignoreAtRules: ["import"] }] | ||
[true, { ignoreAtRules: ["else"] }] | ||
``` | ||
Before: | ||
```css | ||
a { | ||
@if (true) { | ||
} @else { | ||
} | ||
@else { | ||
} | ||
} | ||
``` | ||
After: | ||
```css | ||
@@ -333,0 +334,0 @@ a { |
@@ -64,3 +64,3 @@ # comment-empty-line-before | ||
except: ["first-nested"], | ||
ignore: ["after-comment", "inside-single-line-block"] | ||
ignore: ["after-comment", "stylelint-command"] | ||
} | ||
@@ -67,0 +67,0 @@ ] |
@@ -5,3 +5,3 @@ # order | ||
`array`: `["array", "of", "keywords", "or", "expanded", "at-rule", "objects"]` | ||
`array`: `["array", "of", "keywords", "or", "expanded", "at-rule", "or", "rule" "objects"]` | ||
@@ -27,2 +27,15 @@ Within an order array, you can include: | ||
- extended rule objects: | ||
```js | ||
{ | ||
type: 'rule', | ||
selector: 'div' | ||
} | ||
``` | ||
**Unlisted elements will be placed after all listed elements.** So if you specify an array and do not include `declarations`, that means that all declarations will be placed after any other element. | ||
## Extended at-rule objects | ||
Extended at-rule objects have different parameters and variations. | ||
@@ -100,4 +113,39 @@ | ||
**Unlisted elements will be placed after all listed elements.** So if you specify an array and do not include `declarations`, that means that all declarations will be placed after any other element. | ||
## Extended rule objects | ||
Object parameters: | ||
* `type`: always `"rule"` | ||
* `selector`: `string`|`regex`. Selector pattern. A string will be translated into a RegExp — `new RegExp(yourString)` — so _be sure to escape properly_. Examples: | ||
* `selector: /^&:[\w-]+$/` matches simple pseudo-classes. E. g., `&:hover`, `&:first-child`. Doesn't match complex pseudo-classes, e. g. `&:not(.is-visible)`. | ||
* `selector: /^&::[\w-]+$/` matches pseudo-elements. E. g. `&::before`, `&::placeholder`. | ||
Matches all rules: | ||
```js | ||
{ | ||
type: 'rule' | ||
} | ||
``` | ||
Or keyword `rules`. | ||
Matches all rules with selector matching pattern: | ||
```js | ||
{ | ||
type: 'rule', | ||
selector: 'div' | ||
} | ||
``` | ||
```js | ||
{ | ||
type: 'rule', | ||
selector: /^&:\w+$/ | ||
} | ||
``` | ||
If more than one pattern can be applied to a node, and these patterns have equal “power” than the first matched pattern will be applied. | ||
## Examples | ||
@@ -309,2 +357,128 @@ | ||
[ | ||
{ | ||
type: 'rule', | ||
selector: '^a' | ||
}, | ||
{ | ||
type: 'rule', | ||
selector: /^&/ | ||
}, | ||
'rules' | ||
] | ||
``` | ||
Before: | ||
```scss | ||
a { | ||
a {} | ||
&:hover {} | ||
abbr {} | ||
span {} | ||
} | ||
a { | ||
span {} | ||
&:hover {} | ||
} | ||
a { | ||
span {} | ||
abbr {} | ||
} | ||
``` | ||
After: | ||
```scss | ||
a { | ||
a {} | ||
abbr {} | ||
&:hover {} | ||
span {} | ||
} | ||
a { | ||
&:hover {} | ||
span {} | ||
} | ||
a { | ||
abbr {} | ||
span {} | ||
} | ||
``` | ||
--- | ||
Given: | ||
```js | ||
[ | ||
{ | ||
type: 'rule', | ||
selector: /^&/ | ||
}, | ||
{ | ||
type: 'rule', | ||
selector: /^&:\w/ | ||
} | ||
] | ||
``` | ||
This code won't change, because selector patterns have equal “power” for `&:hover` selector, so the first matching is used (`/^&/`): | ||
```scss | ||
a { | ||
&:hover {} | ||
& b {} | ||
} | ||
a { | ||
& b {} | ||
&:hover {} | ||
} | ||
``` | ||
--- | ||
Given: | ||
```js | ||
[ | ||
{ | ||
type: 'rule', | ||
selector: /^&:\w/ | ||
}, | ||
{ | ||
type: 'rule', | ||
selector: /^&/ | ||
} | ||
] | ||
``` | ||
Before: | ||
```scss | ||
a { | ||
& b {} | ||
&:hover {} | ||
} | ||
``` | ||
After: | ||
```scss | ||
a { | ||
&:hover {} | ||
& b {} | ||
} | ||
``` | ||
--- | ||
Given: | ||
```js | ||
[ | ||
'custom-properties', | ||
@@ -311,0 +485,0 @@ { |
805
index.js
@@ -6,26 +6,6 @@ 'use strict'; | ||
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 features = require('./lib/features'); | ||
const isSet = require('./lib/isSet'); | ||
const normalizeOptions = require('./lib/normalizeOptions'); | ||
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'); | ||
@@ -49,778 +29,9 @@ module.exports = postcss.plugin('postcss-sorting', function (opts) { | ||
// Having this option before `properties-order`, because later one can add empty lines by `emptyLineBefore` | ||
if (opts['clean-empty-lines'] === true) { | ||
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); | ||
} | ||
}); | ||
opts = normalizeOptions(opts); | ||
// Remove empty lines after the last node | ||
if (node.raws.after) { | ||
node.raws.after = cleanEmptyLines(node.raws.after); | ||
} | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts.order) | ||
&& !_.isNull(opts.order) | ||
) { | ||
const expectedOrder = createExpectedOrder(opts.order); | ||
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, 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); | ||
}); | ||
// Sort declarations saved for sorting | ||
processed.sort(sorting.sortByIndexes); | ||
// Enforce semicolon for the last node | ||
node.raws.semicolon = true; | ||
// Replace rule content with sorted one | ||
node.removeAll(); | ||
node.append(processed); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['properties-order']) | ||
&& !_.isNull(opts['properties-order']) | ||
) { | ||
const isAlphabetical = opts['properties-order'] === 'alphabetical'; | ||
const expectedOrder = isAlphabetical ? null : createExpectedPropertiesOrder(opts['properties-order']); | ||
let unspecifiedPropertiesPosition = opts['unspecified-properties-position']; | ||
if ( | ||
_.isUndefined(unspecifiedPropertiesPosition) | ||
|| _.isNull(unspecifiedPropertiesPosition) | ||
) { | ||
unspecifiedPropertiesPosition = 'bottom'; | ||
Object.keys(features).forEach(function (featureName) { | ||
if (isSet(opts[featureName])) { | ||
features[featureName](css, opts); | ||
} | ||
css.walk(function (node) { | ||
// Process only rules and atrules with nodes | ||
if (isRuleWithNodes(node)) { | ||
const allRuleNodes = []; | ||
let declarations = []; | ||
node.each(function (childNode, index) { | ||
if ( | ||
childNode.type === 'decl' && | ||
isStandardSyntaxProperty(childNode.prop) && | ||
!isCustomProperty(childNode.prop) | ||
) { | ||
const unprefixedPropName = postcss.vendor.unprefixed(childNode.prop); | ||
const propData = { | ||
name: childNode.prop, | ||
unprefixedName: unprefixedPropName, | ||
orderData: isAlphabetical ? null : getPropertiesOrderData(expectedOrder, unprefixedPropName), | ||
node: childNode, | ||
initialIndex: index, | ||
unspecifiedPropertiesPosition, | ||
}; | ||
// add a marker | ||
childNode.sortProperty = true; | ||
// If comment on separate line before node, use node's indexes for comment | ||
const commentsBefore = getComments.beforeDeclaration([], childNode.prev(), propData); | ||
// If comment on same line with the node and node, use node's indexes for comment | ||
const commentsAfter = getComments.afterDeclaration([], childNode.next(), propData); | ||
declarations = declarations.concat(commentsBefore, propData, commentsAfter); | ||
} | ||
}); | ||
if (isAlphabetical) { | ||
declarations.sort(sorting.sortDeclarationsAlphabetically); | ||
} else { | ||
declarations.sort(sorting.sortDeclarations); | ||
} | ||
// Process empty line before group | ||
declarations.forEach(emptyLineBeforeGroup); | ||
let foundDeclarations = false; | ||
node.each(function (childNode) { | ||
if (childNode.sortProperty) { | ||
if (!foundDeclarations) { | ||
foundDeclarations = true; | ||
declarations.forEach((item) => { | ||
allRuleNodes.push(item.node); | ||
}); | ||
} | ||
} else { | ||
allRuleNodes.push(childNode); | ||
} | ||
}); | ||
node.removeAll(); | ||
node.append(allRuleNodes); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['custom-property-empty-line-before']) | ||
&& !_.isNull(opts['custom-property-empty-line-before']) | ||
) { | ||
let customPropertyEmptyLineBefore = opts['custom-property-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(customPropertyEmptyLineBefore)) { | ||
customPropertyEmptyLineBefore = [customPropertyEmptyLineBefore]; | ||
} | ||
const optionName = 'custom-property-empty-line-before'; | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
if (!isStandardSyntaxDeclaration(decl) || !isCustomProperty(prop)) { | ||
return; | ||
} | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
let expectEmptyLineBefore = customPropertyEmptyLineBefore[0]; | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// 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; | ||
} | ||
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); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['dollar-variable-empty-line-before']) | ||
&& !_.isNull(opts['dollar-variable-empty-line-before']) | ||
) { | ||
let dollarVariableEmptyLineBefore = opts['dollar-variable-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(dollarVariableEmptyLineBefore)) { | ||
dollarVariableEmptyLineBefore = [dollarVariableEmptyLineBefore]; | ||
} | ||
const optionName = 'dollar-variable-empty-line-before'; | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
if (!isDollarVariable(prop)) { | ||
return; | ||
} | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
let expectEmptyLineBefore = dollarVariableEmptyLineBefore[0]; | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// 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; | ||
} | ||
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); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['declaration-empty-line-before']) | ||
&& !_.isNull(opts['declaration-empty-line-before']) | ||
) { | ||
let declarationEmptyLineBefore = opts['declaration-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(declarationEmptyLineBefore)) { | ||
declarationEmptyLineBefore = [declarationEmptyLineBefore]; | ||
} | ||
const optionName = 'declaration-empty-line-before'; | ||
css.walkDecls(function (decl) { | ||
const prop = decl.prop; | ||
const parent = decl.parent; | ||
if (!isStandardSyntaxDeclaration(decl)) { | ||
return; | ||
} | ||
if (isCustomProperty(prop)) { | ||
return; | ||
} | ||
// Optionally ignore the node if a comment precedes it | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
return; | ||
} | ||
// 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; | ||
} | ||
// Optionally ignore nodes inside single-line blocks | ||
if ( | ||
checkOption(optionName, 'ignore', 'inside-single-line-block') | ||
&& isSingleLineBlock(parent) | ||
) { | ||
return; | ||
} | ||
let expectEmptyLineBefore = declarationEmptyLineBefore[0]; | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& decl === parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// Optionally reverse the expectation if a comment precedes this node | ||
if ( | ||
checkOption(optionName, 'except', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(decl) | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// 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; | ||
} | ||
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); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['rule-nested-empty-line-before']) | ||
&& !_.isNull(opts['rule-nested-empty-line-before']) | ||
) { | ||
let ruleNestedEmptyLineBefore = opts['rule-nested-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(ruleNestedEmptyLineBefore)) { | ||
ruleNestedEmptyLineBefore = [ruleNestedEmptyLineBefore]; | ||
} | ||
const optionName = 'rule-nested-empty-line-before'; | ||
css.walkRules(function (rule) { | ||
if (!isStandardSyntaxRule(rule)) { | ||
return; | ||
} | ||
// Only attend to nested rule sets | ||
if (rule.parent === css) { | ||
return; | ||
} | ||
// Optionally ignore the expectation if a non-shared-line comment precedes this node | ||
if ( | ||
checkOption(optionName, 'ignore', 'after-comment') | ||
&& hasNonSharedLineCommentBefore(rule) | ||
) { | ||
return; | ||
} | ||
// 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; | ||
} | ||
let expectEmptyLineBefore = false; | ||
if ( | ||
( | ||
_.isString(ruleNestedEmptyLineBefore[0]) | ||
&& ruleNestedEmptyLineBefore[0].indexOf('always') !== -1 | ||
) | ||
|| ruleNestedEmptyLineBefore[0] === true | ||
) { | ||
expectEmptyLineBefore = true; | ||
} | ||
// Optionally reverse the expectation for the first nested node | ||
if ( | ||
checkOption(optionName, 'except', 'first-nested') | ||
&& rule === rule.parent.first | ||
) { | ||
expectEmptyLineBefore = !expectEmptyLineBefore; | ||
} | ||
// 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; | ||
} | ||
const hasEmptyLineBefore = hasEmptyLine(rule.raws.before); | ||
// 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); | ||
} | ||
}); | ||
} | ||
if ( | ||
!_.isUndefined(opts['at-rule-nested-empty-line-before']) | ||
&& !_.isNull(opts['at-rule-nested-empty-line-before']) | ||
) { | ||
let atRuleNestedEmptyLineBefore = opts['at-rule-nested-empty-line-before']; | ||
// Convert to common options format, e. g. `true` → `[true]` | ||
if (!_.isArray(atRuleNestedEmptyLineBefore)) { | ||
atRuleNestedEmptyLineBefore = [atRuleNestedEmptyLineBefore]; | ||
} | ||
const optionName = 'at-rule-nested-empty-line-before'; | ||
css.walkAtRules(function (atRule) { | ||
// Only attend to nested at-rules | ||
if (atRule.parent === css) { | ||
return; | ||
} | ||
// 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); | ||
} | ||
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()) | ||
) | ||
); | ||
} | ||
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()) | ||
) | ||
); | ||
} | ||
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']) | ||
&& !_.isNull(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]; | ||
} | ||
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); | ||
} | ||
}); | ||
} |
@@ -12,10 +12,47 @@ 'use strict'; | ||
if (_.isString(item) && item !== 'at-rules') { | ||
if ( | ||
_.isString(item) | ||
&& item !== 'at-rules' | ||
&& item !== 'rules' | ||
) { | ||
order[item] = { | ||
position, | ||
}; | ||
} else { | ||
// If it's an object | ||
// Currently 'at-rules' only | ||
} | ||
if ( | ||
item === 'rules' | ||
|| item.type === 'rule' | ||
) { | ||
// Convert 'rules' into extended pattern | ||
if (item === 'rules') { | ||
item = { | ||
type: 'rule', | ||
}; | ||
} | ||
// It there are no nodes like that create array for them | ||
if (!order[item.type]) { | ||
order[item.type] = []; | ||
} | ||
const nodeData = { | ||
position, | ||
}; | ||
if (item.selector) { | ||
nodeData.selector = item.selector; | ||
if (_.isString(item.selector)) { | ||
nodeData.selector = new RegExp(item.selector); | ||
} | ||
} | ||
order[item.type].push(nodeData); | ||
} | ||
if ( | ||
item === 'at-rules' | ||
|| item.type === 'at-rule' | ||
) { | ||
// Convert 'at-rules' into extended pattern | ||
@@ -49,3 +86,3 @@ if (item === 'at-rules') { | ||
if (item.hasBlock) { | ||
if (!_.isUndefined(item.hasBlock)) { | ||
nodeData.hasBlock = item.hasBlock; | ||
@@ -52,0 +89,0 @@ } |
@@ -6,3 +6,4 @@ 'use strict'; | ||
const isDollarVariable = require('./isDollarVariable'); | ||
const calcPatternPriority = require('./calcPatternPriority'); | ||
const calcAtRulePatternPriority = require('./calcAtRulePatternPriority'); | ||
const calcRulePatternPriority = require('./calcRulePatternPriority'); | ||
@@ -21,3 +22,27 @@ module.exports = function getOrderData(expectedOrder, node) { | ||
} else if (node.type === 'rule') { | ||
nodeType = 'rules'; | ||
nodeType = { | ||
type: 'rule', | ||
selector: node.selector, | ||
}; | ||
const rules = expectedOrder.rule; | ||
// Looking for most specified pattern, because it can match many patterns | ||
if (rules && rules.length) { | ||
let prioritizedPattern; | ||
let max = 0; | ||
rules.forEach(function (pattern) { | ||
const priority = calcRulePatternPriority(pattern, nodeType); | ||
if (priority > max) { | ||
max = priority; | ||
prioritizedPattern = pattern; | ||
} | ||
}); | ||
if (max) { | ||
return prioritizedPattern; | ||
} | ||
} | ||
} else if (node.type === 'atrule') { | ||
@@ -46,3 +71,3 @@ nodeType = { | ||
atRules.forEach(function (pattern) { | ||
const priority = calcPatternPriority(pattern, nodeType); | ||
const priority = calcAtRulePatternPriority(pattern, nodeType); | ||
@@ -49,0 +74,0 @@ if (priority > max) { |
@@ -171,21 +171,29 @@ 'use strict'; | ||
if (item.type !== 'at-rule') { | ||
if (item.type !== 'at-rule' && item.type !== 'rule') { | ||
return false; | ||
} | ||
// if parameter is specified, name should be specified also | ||
if (!_.isUndefined(item.parameter) && _.isUndefined(item.name)) { | ||
return false; | ||
} | ||
if (item.type === 'at-rule') { | ||
// if parameter is specified, name should be specified also | ||
if (!_.isUndefined(item.parameter) && _.isUndefined(item.name)) { | ||
return false; | ||
} | ||
if (!_.isUndefined(item.hasBlock)) { | ||
result = item.hasBlock === true || item.hasBlock === false; | ||
} | ||
if (!_.isUndefined(item.hasBlock)) { | ||
result = item.hasBlock === true || item.hasBlock === false; | ||
} | ||
if (!_.isUndefined(item.name)) { | ||
result = _.isString(item.name) && item.name.length; | ||
if (!_.isUndefined(item.name)) { | ||
result = _.isString(item.name) && item.name.length; | ||
} | ||
if (!_.isUndefined(item.parameter)) { | ||
result = (_.isString(item.parameter) && item.parameter.length) || _.isRegExp(item.parameter); | ||
} | ||
} | ||
if (!_.isUndefined(item.parameter)) { | ||
result = (_.isString(item.parameter) && item.parameter.length) || _.isRegExp(item.parameter); | ||
if (item.type === 'rule') { | ||
if (!_.isUndefined(item.selector)) { | ||
result = (_.isString(item.selector) && item.selector.length) || _.isRegExp(item.selector); | ||
} | ||
} | ||
@@ -192,0 +200,0 @@ |
{ | ||
"name": "postcss-sorting", | ||
"version": "2.0.1", | ||
"version": "2.1.0", | ||
"description": "PostCSS plugin to keep rules and at-rules content in order.", | ||
@@ -29,13 +29,23 @@ "keywords": [ | ||
"lodash": "^4.17.4", | ||
"postcss": "^5.2.10" | ||
"postcss": "^5.2.17" | ||
}, | ||
"devDependencies": { | ||
"ava": "^0.17.0", | ||
"eslint": "^3.13.1" | ||
"eslint": "^3.19.0", | ||
"eslint-config-hudochenkov": "^1.3.0", | ||
"jest": "^20.0.4" | ||
}, | ||
"scripts": { | ||
"test": "ava && eslint index.js lib/*.js test/*.js", | ||
"ava": "ava", | ||
"lint": "eslint index.js lib/*.js test/*.js" | ||
"test": "jest && npm run lint", | ||
"jest": "jest", | ||
"watch": "jest --watch", | ||
"coverage": "jest --coverage", | ||
"lint": "eslint index.js lib/*.js __tests__/*.js" | ||
}, | ||
"jest": { | ||
"setupFiles": [ | ||
"./jest-setup.js" | ||
], | ||
"testEnvironment": "node", | ||
"testRegex": "__tests__/.*\\.js$" | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
# PostCSS Sorting [![Build Status][ci-img]][ci] | ||
# PostCSS Sorting [![Build Status][ci-img]][ci] [![npm version][npm-version-img]][npm] [![npm downloads last month][npm-downloads-img]][npm] [![Dependency status][dependencies-img]][dependencies-status] | ||
@@ -243,5 +243,11 @@ [PostCSS] plugin to keep rules and at-rules content in order. | ||
[PostCSS]: https://github.com/postcss/postcss | ||
[ci-img]: https://travis-ci.org/hudochenkov/postcss-sorting.svg | ||
[ci]: https://travis-ci.org/hudochenkov/postcss-sorting | ||
[npm-version-img]: https://img.shields.io/npm/v/postcss-sorting.svg | ||
[npm-downloads-img]: https://img.shields.io/npm/dm/postcss-sorting.svg | ||
[dependencies-img]: https://img.shields.io/gemnasium/hudochenkov/postcss-sorting.svg | ||
[dependencies-status]: https://gemnasium.com/github.com/hudochenkov/postcss-sorting | ||
[npm]: https://www.npmjs.com/package/postcss-sorting | ||
[PostCSS]: https://github.com/postcss/postcss | ||
[Sublime Text plugin]: https://github.com/hudochenkov/sublime-postcss-sorting | ||
@@ -248,0 +254,0 @@ [Atom plugin]: https://github.com/lysyi3m/atom-postcss-sorting |
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
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
97134
63
1777
266
3
Updatedpostcss@^5.2.17