canonical
Version:
Canonical code style linter and formatter for JavaScript, SCSS, CSS and JSON.
458 lines (377 loc) • 11.9 kB
JavaScript
'use strict';
var util = require('util'),
fs = require('fs'),
path = require('path'),
yaml = require('js-yaml'),
merge = require('merge');
/**
* Easy access to the 'merge' library's cloning functionality
* @param {object} obj Object to clone
* @returns {object} Clone of obj
*/
var clone = function (obj) {
return merge(true, obj);
};
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 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]+)*){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;
};
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);
};
helpers.stripPrefix = function (str) {
var modProperty = str.slice(1),
prefixLength = modProperty.indexOf('-');
return modProperty.slice(prefixLength + 1);
};
/**
* 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;
};
/**
* Scans through our selectors and keeps track of the order of selectors and delimiters
* @param {object} selector - the current selector tree from our AST
* @returns {array} mappedElements - an array / list representing the order of selectors and delimiters encountered
*/
helpers.mapDelims = function (val) {
if (val.type === 'simpleSelector') {
return 's';
}
if (val.type === 'delimiter') {
return 'd';
}
return false;
};
/**
* Constructs a syntax complete selector
* @param {object} node - the current node / part of our selector
* @returns {object} constructedSelector - The constructed selector
* @returns {string} constructedSelector.content - The selector string
* @returns {string} constructedSelector.type - The type of the selector
*/
helpers.constructSelector = function (node) {
var content = node.content,
type = '';
if (node.type === 'id') {
content = '#' + node.content;
type = 'id';
}
else if (node.type === 'class') {
content = '.' + node.content;
type = 'class';
}
else if (node.type === 'ident') {
content = node.content;
type = 'selector';
}
else if (node.type === 'attribute') {
var selector = '[';
node.forEach(function (attrib) {
var selectorPiece = helpers.constructSelector(attrib);
selector += selectorPiece.content;
});
content = selector + ']';
type = 'attribute';
}
else if (node.type === 'pseudoClass') {
content = ':' + node.content;
type = 'pseudoClass';
}
else if (node.type === 'pseudoElement') {
content = '::' + node.content;
type = 'pseudoElement';
}
else if (node.type === 'nth') {
content = '(' + node.content + ')';
type = 'nth';
}
else if (node.type === 'nthSelector') {
var nthSelector = ':';
node.forEach(function (attrib) {
var selectorPiece = helpers.constructSelector(attrib);
nthSelector += selectorPiece.content;
});
content = nthSelector;
type = 'nthSelector';
}
else if (node.type === 'space') {
content = ' ';
}
else if (node.type === 'parentSelector') {
content = node.content;
type = 'parentSelector';
}
else if (node.type === 'combinator') {
content = node.content;
type = 'combinator';
}
return {
content: content,
type: type
};
};
/**
* 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) {
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', 'simpleSelector', 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', 'simpleSelector']),
nestedChildSuffixes = helpers.attemptTraversal(child, ['block', 'ruleset']);
selectors.forEach(function (childSuffixNode) {
var extendedNode;
if (childSuffixNode.length >= 2 &&
childSuffixNode.contains('parentSelector') &&
childSuffixNode.contains('ident')) {
// append suffix extension to all parent selectors
parents.forEach(function (parent) {
// clone so we don't modify the actual AST
extendedNode = clone(childSuffixNode.first('ident'));
extendedNode.content = parent.content + extendedNode.content;
currentParents.push(extendedNode);
});
}
});
selectorList = selectorList.concat(currentParents);
nestedChildSuffixes.forEach(function (childSuffix) {
processChildSuffix(childSuffix, currentParents);
});
};
childSuffixes.forEach(function (childSuffix) {
processChildSuffix(childSuffix, parentSelectors);
});
return parentSelectors.concat(selectorList);
};
module.exports = helpers;