Comparing version 0.0.1 to 1.0.0
@@ -10,245 +10,193 @@ /** | ||
fs = require("fs"), | ||
minify = require("jsonminify"), | ||
debug = require("debug"), | ||
glob = require("glob"), | ||
shell = require("shelljs"), | ||
bemNaming = require("bem-naming"), | ||
lodash = require("lodash"), | ||
EventEmitter = require("events").EventEmitter, | ||
rules = require("./rules"), | ||
attrClassRegex = /^(.*<.*?class="(.*?)".*)$/gim; | ||
fs.existsSync = fs.existsSync || path.existsSync; | ||
var defaultOptions = { | ||
elem: '__', | ||
mod: '_', | ||
wordPattern: '[a-zA-Z0-9]+' | ||
}; | ||
Array.prototype.islice = function(array, string) { | ||
var index = array.indexOf(string); | ||
return array.slice(0, index).concat(array.slice(index + 1)); | ||
} | ||
/** | ||
* Object that is responsible for verifying JavaScript text | ||
* @name bemlint | ||
*/ | ||
module.exports = (function() { | ||
var api = Object.create(new EventEmitter()), | ||
messages = [], | ||
currentFilename = null, | ||
sourceCode = null; | ||
function BEMLintParser(options) { | ||
options = Object.assign(Object.create(null), defaultOptions, options); | ||
this.options = options; | ||
bemNaming(options); | ||
}; | ||
function runOnText (text, filePath) { | ||
var match, | ||
/** | ||
* Resets the internal state of the object. | ||
* @returns {void} | ||
*/ | ||
api.reset = function() { | ||
this.removeAllListeners(); | ||
messages = []; | ||
while (match = attrClassRegex.exec(text)) { | ||
var className = match[2], | ||
lineNumber = getLineNumber(text, match[1]), | ||
columnNumber = match[1].indexOf(match[2]), | ||
message = getMessage(className); | ||
if ( message ) { | ||
messages.push({ | ||
line: lineNumber, | ||
column: columnNumber, | ||
message: message | ||
}); | ||
} | ||
} | ||
var result = { | ||
filePath: filePath, | ||
messages: messages | ||
sourceCode = null; | ||
}; | ||
return result; | ||
}; | ||
/** | ||
* Verifies the text against the rules specified by the second argument. | ||
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. | ||
* @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked. | ||
* If this is not set, the filename will default to '<input>' in the rule context. If | ||
* an object, then it has "filename", "saveState" | ||
* @param {boolean} [saveState] Indicates if the state from the last run should be saved. | ||
* Mostly useful for testing purposes. | ||
* @returns {Object[]} The results as an array of messages or null if no messages. | ||
*/ | ||
api.verify = function(textOrSourceCode, options) { | ||
var text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null; | ||
BEMLintParser.prototype = { | ||
currentFilename = options.filename; | ||
constructor: BEMLintParser, | ||
if (!options.saveState) { | ||
this.reset(); | ||
} | ||
executeOnFiles: function (files) { | ||
var startTime = Date.now(), | ||
results = []; | ||
function runOnFile (file) { | ||
var filePath = path.resolve(file); | ||
debug("Processing " + filePath); | ||
var text = fs.readFileSync(filePath, "utf8"); | ||
var result = runOnText(text, filePath); | ||
results.push(result); | ||
}; | ||
files.forEach(function(pattern) { | ||
var file = path.resolve(pattern); | ||
if (shell.test("-f", file)) { | ||
runOnFile(fs.realpathSync(pattern), !shell.test("-d", file)); | ||
} else { | ||
glob.sync(pattern, globOptions).forEach(function(globMatch) { | ||
runOnFile(globMatch, false); | ||
}); | ||
// only do this for text | ||
if (text !== null) { | ||
// there's no input, just exit here | ||
if (text.trim().length === 0) { | ||
return messages; | ||
} | ||
} | ||
}); | ||
var messages = new Parser(text, options); | ||
return { | ||
results: results | ||
}; | ||
return messages; | ||
}; | ||
debug("Linting complete in: " + (Date.now() - startTime) + "ms"); | ||
} | ||
}; | ||
return api; | ||
}()); | ||
function getLineNumber(file, lineString) { | ||
var limit = file.indexOf(lineString), | ||
substring = file.substr(0, limit); | ||
return substring.split("\n").length; | ||
} | ||
Array.prototype.isplice = function(array, index) { | ||
Array.prototype.islice = function(array, string) { | ||
var index = array.indexOf(string); | ||
return array.slice(0, index).concat(array.slice(index + 1)); | ||
} | ||
function Parser(text, options) { | ||
function getMessage(string) { | ||
var arrayClasses = string.split(/\s+/), | ||
messages = [], | ||
parser = { | ||
valid: function() { | ||
return bemNaming.validate(string) ? 'Valid BEM-naming notation' : 'Not valid BEM'; | ||
}, | ||
var match, | ||
messages = []; | ||
checkBlock: function(bemObj, classes, index) { | ||
var message, | ||
spliced = classes.slice(); | ||
/** | ||
* Validates input in source with given rules | ||
* @method validate | ||
* @param {String} input | ||
* @param {Array} source Array of classnames from html attribute | ||
* @param {Integer} line line number | ||
* @param {Integer} column column | ||
* @param {Array} rulesToExclude List of rules to be excluded for validate | ||
*/ | ||
function validate (input, source, line, column, rulesToExclude) { | ||
var currentRules = lodash.assign({}, rules); | ||
if ( spliced.length > 1 ) { | ||
spliced = spliced.isplice(spliced, index); | ||
if ( rulesToExclude ) { | ||
for (var i in rules) { | ||
if ( rulesToExclude.indexOf(i) >= 0 ) { | ||
delete currentRules[i]; | ||
} | ||
} | ||
} | ||
for (var i = spliced.length - 1; i >= 0; i--) { | ||
var className = spliced[i], | ||
currentbemObj = bemNaming.parse(className); | ||
for (var i in currentRules) { | ||
var message = rules[i](input, source); | ||
if ( currentbemObj && currentbemObj.block == bemObj.block ) { | ||
if ( bemNaming.isElem(className) ) { | ||
message = 'Element of the same block in class'; | ||
break; | ||
} | ||
} | ||
if ( message ) { | ||
messages.push({ | ||
message: message, | ||
line: line, | ||
column: column, | ||
rule: i | ||
}); | ||
} | ||
} | ||
}; | ||
return message; | ||
/** | ||
* Checks --bp --bem-prefixes option specified | ||
* @method checkPrefixes | ||
* @param {String} string parsed from attr class name | ||
* @return {Boolean} | ||
*/ | ||
this.checkPrefixes = function(string) { | ||
}, | ||
checkBlockMod: function(bemObj, classes, index) { | ||
var message, | ||
spliced = classes.slice(); | ||
if ( spliced.length > 1 ) { | ||
spliced = spliced.isplice(spliced, index); | ||
for (var i = spliced.length - 1; i >= 0; i--) { | ||
var className = spliced[i], | ||
currentbemObj = bemNaming.parse(className); | ||
if ( currentbemObj && currentbemObj.block == bemObj.block ) { | ||
if ( !bemNaming.isBlock(className) ) { | ||
message = 'Using block_mod_val without Block'; | ||
} else { | ||
message = ''; | ||
break; | ||
} | ||
} else { | ||
message = 'Using block_mod_val without Block'; | ||
} | ||
if ( options.bemPrefixes ) { | ||
var valid = false; | ||
for (var i = options.bemPrefixes.length - 1; i >= 0; i--) { | ||
if ( new RegExp('^' + options.bemPrefixes[i]).test(string) ) { | ||
valid = true; | ||
break; | ||
} | ||
} else { | ||
message = 'Using block__mod_val without Block'; | ||
} | ||
return message; | ||
return valid; | ||
} else { | ||
return true; | ||
} | ||
}, | ||
}; | ||
checkElement: function(bemObj, classes, index) { | ||
var message, | ||
spliced = classes.slice(); | ||
/** | ||
* Validates every classname in attr with Rules | ||
* @method validateString | ||
* @param {String} classString whole `class` attr string | ||
* @param {[type]} line line number | ||
* @param {[type]} column column number | ||
*/ | ||
this.validateString = function(classString, line, column) { | ||
var arrayClasses = classString.split(/\s+/), | ||
prevClass = '', | ||
columnCounted = column, | ||
excludedString = classString; | ||
if ( spliced.length > 1 ) { | ||
spliced = spliced.isplice(spliced, index); | ||
for (var i = 0; i < arrayClasses.length; i++) { | ||
var className = arrayClasses[i], | ||
sliced = arrayClasses, | ||
excludedString = excludedString.replace(className,''); | ||
for (var i = spliced.length - 1; i >= 0; i--) { | ||
var className = spliced[i], | ||
currentbemObj = bemNaming.parse(className); | ||
if ( currentbemObj ) { | ||
if ( currentbemObj.block == bemObj.block ) { | ||
if ( this.checkPrefixes(className) ) { | ||
if ( bemObj.elem !== currentbemObj.elem ) { | ||
message = 'Multiple Elements of the same Block'; | ||
} | ||
if ( arrayClasses.length > 1 ) { | ||
if ( bemObj.elem !== currentbemObj.elem ) { | ||
message = 'Using elem__mod_val without Element'; | ||
} | ||
sliced = arrayClasses.islice(arrayClasses, className); | ||
if ( bemNaming.isBlock(currentbemObj) ) { | ||
message = 'Element of the same block in class'; | ||
} | ||
} else { | ||
message = 'Using elem__mod_val without Element'; | ||
} | ||
} | ||
validate(className, sliced, line, columnCounted); | ||
} else { | ||
validate(className, sliced, line, columnCounted, ['same']); | ||
} | ||
} else { | ||
if ( bemNaming.isElemMod(spliced[0])) { | ||
message = 'Using elem__mod_val without Element'; | ||
} | ||
} | ||
return message; | ||
}, | ||
columnCounted += className.length + excludedString.search(/\S/); | ||
excludedString = excludedString.trim(); | ||
} | ||
}; | ||
parse: function() { | ||
var currentBlock = ''; | ||
for (var i = 0; i < arrayClasses.length; i++) { | ||
var className = arrayClasses[i], | ||
bemObj = bemNaming.parse(className); | ||
while (match = attrClassRegex.exec(text)) { | ||
var line = getLineNumber(text, match[1]), | ||
classString = match[2], | ||
column= match[1].indexOf(classString); | ||
if ( bemObj && bemNaming.isBlock(className) ) { | ||
messages[i] = parser.checkBlock(bemObj, arrayClasses, i); | ||
} | ||
this.validateString(classString, line, column + 1); | ||
} | ||
if ( bemObj && bemNaming.isBlockMod(className) ) { | ||
messages[i] = parser.checkBlockMod(bemObj, arrayClasses, i); | ||
} | ||
if ( bemObj && bemObj.elem ) { | ||
messages[i] = parser.checkElement(bemObj, arrayClasses, i); | ||
} | ||
return lodash.uniqWith(messages, lodash.isEqual); | ||
}; | ||
} | ||
return messages.join(""); | ||
}, | ||
}; | ||
return parser.parse(); | ||
function getLineNumber(file, lineString) { | ||
var limit = file.indexOf(lineString), | ||
substring = file.substr(0, limit); | ||
return substring.split("\n").length; | ||
} | ||
module.exports = BEMLintParser; |
@@ -22,3 +22,3 @@ /** | ||
optionator = require("./options"), | ||
BEMLintParser = require("./bemlint"); | ||
CLIEngine = require("./cli-engine"); | ||
@@ -121,3 +121,3 @@ | ||
parser = new BEMLintParser(currentOptions); | ||
parser = new CLIEngine(currentOptions); | ||
@@ -124,0 +124,0 @@ report = parser.executeOnFiles(files); |
@@ -52,4 +52,12 @@ /** | ||
{ | ||
option: "bem-prefixes", | ||
alias: "bp", | ||
type: "Array", | ||
description: "Array of block names prefix to lint", | ||
example: "['b-', 'l-', 'helper-']" | ||
}, | ||
] | ||
}); | ||
{ | ||
"name": "bemlint", | ||
"version": "0.0.1", | ||
"version": "1.0.0", | ||
"description": "Linting module checks BEM-naming conventions in `class` attribute of the html files", | ||
@@ -9,2 +9,6 @@ "main": "lib/bemlint.js", | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/DesTincT/bemlint.git" | ||
}, | ||
"keywords": [ | ||
@@ -20,7 +24,18 @@ "bem", | ||
"debug": "^2.2.0", | ||
"file-entry-cache": "^1.1.1", | ||
"glob": "^7.0.0", | ||
"jsonminify": "^0.2.3", | ||
"lodash": "^4.5.1", | ||
"optionator": "^0.8.1", | ||
"shelljs": "^0.6.0" | ||
"shelljs": "^0.6.0", | ||
"ignore": "^2.2.19" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/DesTincT/bemlint/issues" | ||
}, | ||
"homepage": "https://github.com/DesTincT/bemlint#readme", | ||
"devDependencies": {}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
} | ||
} |
@@ -1,4 +0,18 @@ | ||
# bemlint | ||
# bemlint (Beta-Schmeta) | ||
This linter checks attribute `class` in accordance to [BEM naming](https://github.com/bem/bem-naming) conventions. | ||
Based on code from [ESLint](https://github.com/eslint/eslint) | ||
Not for production use for now. | ||
## Installation | ||
``` | ||
npm install -g bemlint | ||
``` | ||
## Usage | ||
``` | ||
bemlint test.html test2.html | ||
``` |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
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
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
47598
12
1235
0
1
19
9
6
1
+ Addedfile-entry-cache@^1.1.1
+ Addedignore@^2.2.19
+ Addedlodash@^4.5.1
+ Addedcircular-json@0.3.3(transitive)
+ Addedfile-entry-cache@1.3.1(transitive)
+ Addedflat-cache@1.3.4(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedignore@2.2.19(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedrimraf@2.6.3(transitive)
+ Addedwrite@0.2.1(transitive)