sass-lint
Version:
All Node Sass linter!
421 lines (358 loc) • 11.7 kB
JavaScript
;
var util = require('util'),
fs = require('fs'),
path = require('path'),
yaml = require('js-yaml'),
gonzales = require('gonzales-pe-sl');
var helpers = {};
helpers.log = function log (input) {
console.log(util.inspect(input, false, null));
};
helpers.propertySearch = function (haystack, needle, property) {
var length = haystack.length,
i;
for (i = 0; i < length; i++) {
if (haystack[i][property] === needle) {
return i;
}
}
return -1;
};
helpers.isEqual = function (a, b) {
var startLine = a.start.line === b.start.line ? true : false,
endLine = a.end.line === b.end.line ? true : false,
type = a.type === b.type ? true : false,
length = a.content.length === b.content.length ? true : false;
if (startLine && endLine && type && length) {
return true;
}
else {
return false;
}
};
helpers.isUnique = function (results, item) {
var search = this.propertySearch(results, item.line, 'line');
if (search === -1) {
return true;
}
else if (results[search].column === item.column && results[search].message === item.message) {
return false;
}
else {
return true;
}
};
helpers.addUnique = function (results, item) {
if (this.isUnique(results, item)) {
results.push(item);
}
return results;
};
helpers.sortDetects = function (a, b) {
if (a.line < b.line) {
return -1;
}
if (a.line > b.line) {
return 1;
}
if (a.line === b.line) {
if (a.column < b.column) {
return -1;
}
if (a.column > b.column) {
return 1;
}
return 0;
}
return 0;
};
helpers.isNumber = function (val) {
if (isNaN(parseInt(val, 10))) {
return false;
}
return true;
};
helpers.isUpperCase = function (str) {
var pieces = str.split(''),
i,
result = 0;
for (i = 0; i < pieces.length; i++) {
if (!helpers.isNumber(pieces[i])) {
if (pieces[i] === pieces[i].toUpperCase() && pieces[i] !== pieces[i].toLowerCase()) {
result++;
}
else {
return false;
}
}
}
if (result) {
return true;
}
return false;
};
helpers.isLowerCase = function (str) {
var pieces = str.split(''),
i,
result = 0;
for (i = 0; i < pieces.length; i++) {
if (!helpers.isNumber(pieces[i])) {
if (pieces[i] === pieces[i].toLowerCase() && pieces[i] !== pieces[i].toUpperCase()) {
result++;
}
else {
return false;
}
}
}
if (result) {
return true;
}
return false;
};
/**
* Determines if a given string adheres to camel-case format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to camel-case format
*/
helpers.isCamelCase = function (str) {
return /^[a-z][a-zA-Z0-9]*$/.test(str);
};
/**
* Determines if a given string adheres to pascal-case format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to pascal-case format
*/
helpers.isPascalCase = function (str) {
return /^[A-Z][a-zA-Z0-9]*$/.test(str);
};
/**
* Determines if a given string adheres to hyphenated-lowercase format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to hyphenated-lowercase format
*/
helpers.isHyphenatedLowercase = function (str) {
return !(/[^\-a-z0-9]/.test(str));
};
/**
* Determines if a given string adheres to snake-case format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to snake-case format
*/
helpers.isSnakeCase = function (str) {
return !(/[^_a-z0-9]/.test(str));
};
/**
* Determines if a given string adheres to strict-BEM format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to strict-BEM format
*/
helpers.isStrictBEM = function (str) {
return /^[a-z](\-?[a-z0-9]+)*(__[a-z0-9](\-?[a-z0-9]+)*)?((_[a-z0-9](\-?[a-z0-9]+)*){0,2})?$/.test(str);
};
/**
* Determines if a given string adheres to hyphenated-BEM format
* @param {string} str String to test
* @returns {boolean} Whether str adheres to hyphenated-BEM format
*/
helpers.isHyphenatedBEM = function (str) {
return !(/[A-Z]|-{3}|_{3}|[^_]_[^_]/.test(str));
};
helpers.isValidHex = function (str) {
if (str.match(/^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)) {
return true;
}
return false;
};
/**
* Check if a node is a newline character or not
*
* @param {Object} node - The node to test
* @returns {boolean} Whether the node is a newline or not
*/
helpers.isNewLine = function (node) {
// using type === instead of is just in case node happens to be a string
return !!(node && node.type === 'space' && node.content.match('\n'));
};
/**
* Check if a node is a non newline space character or not
*
* @param {Object} node - The node to test
* @returns {boolean} Whether the node is a non newline space or not
*/
helpers.isSpace = function (node) {
return !!(node && node.type === 'space' && !node.content.match('\n'));
};
helpers.loadConfigFile = function (configPath) {
var fileDir = path.dirname(configPath),
fileName = path.basename(configPath),
fileExtension = path.extname(fileName),
filePath = path.join(__dirname, 'config', fileDir, fileName),
file = fs.readFileSync(filePath, 'utf8') || false;
if (file) {
if (fileExtension === '.yml') {
return yaml.safeLoad(file);
}
}
return file;
};
helpers.hasEOL = function (str) {
return /\r\n|\n/.test(str);
};
helpers.isEmptyLine = function (str) {
return /(\r\n|\n){2}/.test(str);
};
helpers.stripQuotes = function (str) {
return str.substring(1, str.length - 1);
};
/**
* Strips vendor prefixes from a string
*
* @param {string} str - The string we wish to remove vendor prefixes from
* @returns {string} The string without vendor prefixes
*/
helpers.stripPrefix = function (str) {
var modPropertyArr = str.split('-'),
modProperty = '',
prefLength = modPropertyArr[2] === 'osx' ? 2 : 1;
modPropertyArr.splice(1, prefLength);
modPropertyArr.forEach(function (item, index) {
modProperty = modProperty + item;
if (index > 0 && index < modPropertyArr.length - 1) {
modProperty = modProperty + '-';
}
});
return modProperty;
};
/**
* Removes the trailing space from a string
* @param {string} curSelector - the current selector string
* @returns {string} curSelector - the current selector minus any trailing space.
*/
helpers.stripLastSpace = function (selector) {
if (selector.charAt(selector.length - 1) === ' ') {
return selector.substr(0, selector.length - 1);
}
return selector;
};
/**
* Checks the current selector value against the previous selector value and assesses whether they are
* a) currently an enforced selector type for nesting (user specified - all true by default)
* b) whether they should be nested
* @param {object} currentVal - the current node / part of our selector
* @param {object} previousVal - the previous node / part of our selector
* @param {array} elements - a complete array of nestable selector types
* @param {array} nestable - an array of the types of selector to nest
* @returns {object} Returns whether we or we should nest and the previous val
*/
helpers.isNestable = function (currentVal, previousVal, elements, nestable) {
// check if they are nestable by checking the previous element against one
// of the user specified selector types
if (elements.indexOf(previousVal) !== -1 && nestable.indexOf(currentVal) !== -1) {
return true;
}
return false;
};
/**
* Tries to traverse the AST, following a specified path
* @param {object} node Starting node
* @param {array} traversalPath Array of Node types to traverse, starting from the first element
* @returns {array} Nodes at the end of the path. Empty array if the traversal failed
*/
helpers.attemptTraversal = function (node, traversalPath) {
var i,
nextNodeList,
currentNodeList = [],
processChildNode = function processChildNode (child) {
child.forEach(traversalPath[i], function (n) {
if (n.content && typeof n.content !== 'string' && n.contains('interpolation')) {
return false;
}
return nextNodeList.push(n);
});
};
node.forEach(traversalPath[0], function (n) {
currentNodeList.push(n);
});
for (i = 1; i < traversalPath.length; i++) {
if (currentNodeList.length === 0) {
return [];
}
nextNodeList = [];
currentNodeList.forEach(processChildNode);
currentNodeList = nextNodeList;
}
return currentNodeList;
};
/**
* Collects all suffix extensions for a selector
* @param {object} ruleset ASTNode of type ruleset, containing a selector with nested suffix extensions
* @param {string} selectorType Node type of the selector (e.g. class, id)
* @returns {array} Array of Nodes with the content property replaced by the complete selector
* (without '.', '#', etc) resulting from suffix extensions
*/
helpers.collectSuffixExtensions = function (ruleset, selectorType) {
var parentSelectors = helpers.attemptTraversal(ruleset, ['selector', selectorType, 'ident']),
childSuffixes = helpers.attemptTraversal(ruleset, ['block', 'ruleset']),
selectorList = [];
if (parentSelectors.length === 0) {
return [];
}
// Goes recursively through all nodes that look like suffix extensions. There may be multiple parents that are
// extended, so lots of looping is required.
var processChildSuffix = function (child, parents) {
var currentParents = [],
selectors = helpers.attemptTraversal(child, ['selector', 'parentSelectorExtension', 'ident']),
nestedChildSuffixes = helpers.attemptTraversal(child, ['block', 'ruleset']);
selectors.forEach(function (childSuffixNode) {
// append suffix extension to all parent selectors
parents.forEach(function (parent) {
// clone so we don't modify the actual AST
var clonedChildSuffixNode = gonzales.createNode(childSuffixNode);
clonedChildSuffixNode.content = parent.content + clonedChildSuffixNode.content;
currentParents.push(clonedChildSuffixNode);
});
});
selectorList = selectorList.concat(currentParents);
nestedChildSuffixes.forEach(function (childSuffix) {
processChildSuffix(childSuffix, currentParents);
});
};
childSuffixes.forEach(function (childSuffix) {
processChildSuffix(childSuffix, parentSelectors);
});
return parentSelectors.concat(selectorList);
};
/**
* Check for the partial match of a string in an array
*
* @param {string} needle - The value to match
* @param {Array} haystack - The array of values to try and match to
* @returns {Boolean} Whether there is a partial match or not
*/
helpers.isPartialStringMatch = function (needle, haystack) {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i].indexOf(needle) >= 0) {
return true;
}
}
return false;
};
/**
* A copy of the the stripBom module from https://github.com/sindresorhus/strip-bom/blob/master/index.js
* The module requires node > 4 whereas we support earlier versions.
* This function strips the BOM marker from the beginning of a file
*
* @param {string} str - The string we wish to strip the BOM marker from
* @returns {string} The string without a BOM marker
*/
helpers.stripBom = function (str) {
if (typeof str !== 'string') {
throw new TypeError('Expected a string, got ' + typeof str);
}
if (str.charCodeAt(0) === 0xFEFF) {
return str.slice(1);
}
return str;
};
module.exports = helpers;