mirror of
https://github.com/thangisme/notes.git
synced 2024-11-01 08:17:34 -04:00
300 lines
8.7 KiB
JavaScript
300 lines
8.7 KiB
JavaScript
"use strict"
|
|
|
|
const isCustomProperty = require("../../utils/isCustomProperty")
|
|
const isStandardSyntaxProperty = require("../../utils/isStandardSyntaxProperty")
|
|
const report = require("../../utils/report")
|
|
const ruleMessages = require("../../utils/ruleMessages")
|
|
const validateOptions = require("../../utils/validateOptions")
|
|
const _ = require("lodash")
|
|
const postcss = require("postcss")
|
|
|
|
const ruleName = "declaration-block-properties-order"
|
|
|
|
const messages = ruleMessages(ruleName, {
|
|
expected: (first, second) => `Expected "${first}" to come before "${second}"`,
|
|
})
|
|
|
|
const rule = function (expectation, options) {
|
|
return (root, result) => {
|
|
const validOptions = validateOptions(result, ruleName, {
|
|
actual: expectation,
|
|
possible: validatePrimaryOption,
|
|
}, {
|
|
actual: options,
|
|
possible: {
|
|
unspecified: [
|
|
"top",
|
|
"bottom",
|
|
"ignore",
|
|
"bottomAlphabetical",
|
|
],
|
|
},
|
|
optional: true,
|
|
})
|
|
if (!validOptions) {
|
|
return
|
|
}
|
|
|
|
result.warn((
|
|
"'declaration-block-properties-order'has been deprecated and in 8.0 will be removed. " +
|
|
"Instead use the community 'stylelint-order' plugin pack."
|
|
), {
|
|
stylelintType: "deprecation",
|
|
stylelintReference: "https://stylelint.io/user-guide/rules/declaration-block-properties-order/",
|
|
})
|
|
|
|
const alphabetical = expectation === "alphabetical"
|
|
const expectedOrder = alphabetical ? null : createExpectedOrder(expectation)
|
|
// By default, ignore unspecified properties
|
|
const unspecified = _.get(options, ["unspecified"], "ignore")
|
|
|
|
// Shallow loop
|
|
root.each(node => {
|
|
if (node.type === "rule" || node.type === "atrule") {
|
|
checkNode(node)
|
|
}
|
|
})
|
|
|
|
function checkNode(node) {
|
|
const allPropData = []
|
|
|
|
node.each(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
|
|
}
|
|
|
|
const prop = child.prop
|
|
|
|
if (!isStandardSyntaxProperty(prop)) {
|
|
return
|
|
}
|
|
if (isCustomProperty(prop)) {
|
|
return
|
|
}
|
|
|
|
let 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)
|
|
}
|
|
|
|
const propData = {
|
|
name: prop,
|
|
unprefixedName: unprefixedPropName,
|
|
orderData: alphabetical ? null : getOrderData(expectedOrder, unprefixedPropName),
|
|
before: child.raws.before,
|
|
index: allPropData.length,
|
|
node: child,
|
|
}
|
|
|
|
const previousPropData = _.last(allPropData)
|
|
allPropData.push(propData)
|
|
|
|
// Skip first decl
|
|
if (!previousPropData) {
|
|
return
|
|
}
|
|
|
|
const 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
|
|
}
|
|
|
|
const firstPropIsUnspecified = !firstPropData.orderData
|
|
const secondPropIsUnspecified = !secondPropData.orderData
|
|
|
|
// 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
|
|
const priorSpecifiedPropData = _.findLast(allPropData.slice(0, -1), d => !!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(opts) {
|
|
report({
|
|
message: opts.message,
|
|
node: opts.node,
|
|
result,
|
|
ruleName,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
rule.primaryOptionArray = true
|
|
|
|
rule.ruleName = ruleName
|
|
rule.messages = messages
|
|
module.exports = rule
|
|
|
|
function createExpectedOrder(input) {
|
|
const order = {}
|
|
let expectedPosition = 0
|
|
|
|
appendGroup(input)
|
|
|
|
function appendGroup(items) {
|
|
items.forEach(item => appendItem(item, false))
|
|
}
|
|
|
|
function appendItem(item, inFlexibleGroup) {
|
|
if (_.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] = { expectedPosition }
|
|
return
|
|
}
|
|
|
|
if (!item.order || item.order === "strict") {
|
|
appendGroup(item.properties)
|
|
return
|
|
} else if (item.order === "flexible") {
|
|
expectedPosition += 1
|
|
item.properties.forEach(property => {
|
|
appendItem(property, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
function getOrderData(expectedOrder, propName) {
|
|
let 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) {
|
|
const propNamePreHyphen = propName.slice(0, propName.lastIndexOf("-"))
|
|
orderData = getOrderData(expectedOrder, propNamePreHyphen)
|
|
}
|
|
return orderData
|
|
}
|
|
|
|
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) {
|
|
// Return true early if alphabetical
|
|
if (actualOptions === "alphabetical") {
|
|
return true
|
|
}
|
|
|
|
// Otherwise, begin checking array options
|
|
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(item => {
|
|
if (_.isString(item)) {
|
|
return true
|
|
}
|
|
return _.isPlainObject(item) && !_.isUndefined(item.properties)
|
|
})) {
|
|
return false
|
|
}
|
|
|
|
const objectItems = actualOptions.filter(_.isPlainObject)
|
|
|
|
// Every object-item's "order" property must be "strict" or "flexible"
|
|
if (!objectItems.every(item => {
|
|
if (_.isUndefined(item.order)) {
|
|
return true
|
|
}
|
|
return _.includes([
|
|
"strict",
|
|
"flexible",
|
|
], item.order)
|
|
})) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|