1
0
mirror of https://github.com/thangisme/notes.git synced 2024-11-01 04:27:17 -04:00
notes/node_modules/stylelint/lib/rules/indentation/index.js
Patrick Marsceill b7b0d0d7bf
Initial commit
2017-03-09 13:16:08 -05:00

276 lines
9.2 KiB
JavaScript

"use strict"
const beforeBlockString = require("../../utils/beforeBlockString")
const hasBlock = require("../../utils/hasBlock")
const optionsMatches = require("../../utils/optionsMatches")
const report = require("../../utils/report")
const ruleMessages = require("../../utils/ruleMessages")
const validateOptions = require("../../utils/validateOptions")
const _ = require("lodash")
const styleSearch = require("style-search")
const ruleName = "indentation"
const messages = ruleMessages(ruleName, {
expected: x => `Expected indentation of ${x}`,
})
/**
* @param {number|"tab"} space - Number of whitespaces to expect, or else
* keyword "tab" for single `\t`
* @param {object} [options]
*/
const rule = function (space) {
const options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}
const isTab = space === "tab"
const indentChar = isTab ? "\t" : _.repeat(" ", space)
const warningWord = isTab ? "tab" : "space"
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: space,
possible: [
_.isNumber,
"tab",
],
}, {
actual: options,
possible: {
except: [
"block",
"value",
"param",
],
ignore: [
"value",
"param",
"inside-parens",
],
indentInsideParens: [
"twice",
"once-at-root-twice-in-block",
],
indentClosingBrace: [_.isBoolean],
},
optional: true,
})
if (!validOptions) {
return
}
// Cycle through all nodes using walk.
root.walk(node => {
const nodeLevel = indentationLevel(node)
const expectedWhitespace = _.repeat(indentChar, nodeLevel)
let before = (node.raws.before || "")
const after = (node.raws.after || "")
// Only inspect the spaces before the node
// if this is the first node in root
// or there is a newline in the `before` string.
// (If there is no newline before a node,
// there is no "indentation" to check.)
const inspectBefore = root.first === node || before.indexOf("\n") !== -1
// Cut out any * hacks from `before`
before = before[before.length - 1] === "*" || before[before.length - 1] === "_" ? before.slice(0, before.length - 1) : before
// Inspect whitespace in the `before` string that is
// *after* the *last* newline character,
// because anything besides that is not indentation for this node:
// it is some other kind of separation, checked by some separate rule
if (inspectBefore && before.slice(before.lastIndexOf("\n") + 1) !== expectedWhitespace) {
report({
message: messages.expected(legibleExpectation(nodeLevel)),
node,
result,
ruleName,
})
}
// Only blocks have the `after` string to check.
// Only inspect `after` strings that start with a newline;
// otherwise there's no indentation involved.
// And check `indentClosingBrace` to see if it should be indented an extra level.
const closingBraceLevel = options.indentClosingBrace ? nodeLevel + 1 : nodeLevel
if (hasBlock(node) && after && after.indexOf("\n") !== -1 && after.slice(after.lastIndexOf("\n") + 1) !== _.repeat(indentChar, closingBraceLevel)) {
report({
message: messages.expected(legibleExpectation(closingBraceLevel)),
node,
index: node.toString().length - 1,
result,
ruleName,
})
}
// If this is a declaration, check the value
if (node.value) {
checkValue(node, nodeLevel)
}
// If this is a rule, check the selector
if (node.selector) {
checkSelector(node, nodeLevel)
}
// If this is an at rule, check the params
if (node.type === "atrule") {
checkAtRuleParams(node, nodeLevel)
}
})
function indentationLevel(node) {
const level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0
if (node.parent.type === "root") {
return level
}
let calculatedLevel
// Indentation level equals the ancestor nodes
// separating this node from root; so recursively
// run this operation
calculatedLevel = indentationLevel(node.parent, level + 1)
// If options.except includes "block",
// blocks are taken down one from their calculated level
// (all blocks are the same level as their parents)
if (optionsMatches(options, "except", "block") && (node.type === "rule" || node.type === "atrule") && hasBlock(node)) {
calculatedLevel--
}
return calculatedLevel
}
function checkValue(decl, declLevel) {
if (decl.value.indexOf("\n") === -1) {
return
}
if (optionsMatches(options, "ignore", "value")) {
return
}
const declString = decl.toString()
const valueLevel = optionsMatches(options, "except", "value") ? declLevel : declLevel + 1
checkMultilineBit(declString, valueLevel, decl)
}
function checkSelector(rule, ruleLevel) {
const selector = rule.selector
// Less mixins have params, and they should be indented extra
if (rule.params) {
ruleLevel += 1
}
checkMultilineBit(selector, ruleLevel, rule)
}
function checkAtRuleParams(atRule, ruleLevel) {
if (optionsMatches(options, "ignore", "param")) {
return
}
// @nest rules should be treated like regular rules, not expected
// to have their params (selectors) indented
const paramLevel = optionsMatches(options, "except", "param") || atRule.name === "nest" ? ruleLevel : ruleLevel + 1
checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule)
}
function checkMultilineBit(source, newlineIndentLevel, node) {
if (source.indexOf("\n") === -1) {
return
}
// `outsideParens` because function arguments and also non-standard parenthesized stuff like
// Sass maps are ignored to allow for arbitrary indentation
let parentheticalDepth = 0
styleSearch({
source,
target: "\n",
outsideParens: optionsMatches(options, "ignore", "inside-parens"),
}, (match, matchCount) => {
const precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1))
if (optionsMatches(options, "ignore", "inside-parens") && (precedesClosingParenthesis || match.insideParens)) {
return
}
let expectedIndentLevel = newlineIndentLevel
// Modififications for parenthetical content
if (!optionsMatches(options, "ignore", "inside-parens") && match.insideParens) {
// If the first match in is within parentheses, reduce the parenthesis penalty
if (matchCount === 1) parentheticalDepth -= 1
// Account for windows line endings
let newlineIndex = match.startIndex
if (source[match.startIndex - 1] === "\r") {
newlineIndex--
}
const followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex))
if (followsOpeningParenthesis) {
parentheticalDepth += 1
}
expectedIndentLevel += parentheticalDepth
if (precedesClosingParenthesis) {
parentheticalDepth -= 1
}
switch (options.indentInsideParens) {
case "twice":
if (!precedesClosingParenthesis || options.indentClosingBrace) {
expectedIndentLevel += 1
}
break
case "once-at-root-twice-in-block":
if (node.parent === root) {
if (precedesClosingParenthesis && !options.indentClosingBrace) {
expectedIndentLevel -= 1
}
break
}
if (!precedesClosingParenthesis || options.indentClosingBrace) {
expectedIndentLevel += 1
}
break
default:
if (precedesClosingParenthesis && !options.indentClosingBrace) {
expectedIndentLevel -= 1
}
}
}
// Starting at the index after the newline, we want to
// check that the whitespace characters (excluding newlines) before the first
// non-whitespace character equal the expected indentation
const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1))
if (!afterNewlineSpaceMatches) {
return
}
const afterNewlineSpace = afterNewlineSpaceMatches[1]
if (afterNewlineSpace !== _.repeat(indentChar, expectedIndentLevel)) {
report({
message: messages.expected(legibleExpectation(expectedIndentLevel)),
node,
index: match.startIndex + afterNewlineSpace.length + 1,
result,
ruleName,
})
}
})
}
}
function legibleExpectation(level) {
const count = isTab ? level : level * space
const quantifiedWarningWord = count === 1 ? warningWord : warningWord + "s"
return `${count} ${quantifiedWarningWord}`
}
}
rule.ruleName = ruleName
rule.messages = messages
module.exports = rule