Skip to content
This repository was archived by the owner on Feb 1, 2022. It is now read-only.

Commit eb50814

Browse files
committed
Merge pull request #172 from twbs/fix-29
Node.js version: implement reporting of locations of problematic elements
2 parents 52a9886 + 01df0cf commit eb50814

File tree

7 files changed

+372
-403
lines changed

7 files changed

+372
-403
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Bootlint represents the lint problems it reports using the `LintError` and `Lint
5858
* `id` - Unique string ID for this type of lint problem. Of the form "W###" (e.g. "W123").
5959
* `message` - Human-readable string describing the problem
6060
* `elements` - jQuery or Cheerio collection of referenced DOM elements pointing to all problem locations in the document
61+
* (**Only available under Node.js**): When available from the underlying HTML parser (which is most of the time), the DOM elements in the collection will have a `.startLocation` property that is a `Location` (see below) indicating the location of the element in the document's HTML source
6162
* `LintError`
6263
* Represents an error. Under the assumptions explained in the above "Caveats" section, it should never have any false-positives.
6364
* Constructor: `LintError(id, message, elements)`
@@ -66,6 +67,14 @@ Bootlint represents the lint problems it reports using the `LintError` and `Lint
6667
* `message` - Human-readable string describing the problem
6768
* `elements` - jQuery or Cheerio collection of referenced DOM elements pointing to all problem locations in the document
6869

70+
Bootlint defines the following public utility class:
71+
* `Location` (**Only available under Node.js**)
72+
* Represents a location in the HTML source
73+
* Constructor: `Location(line, column)`
74+
* Properties:
75+
* `line` - 0-based line number
76+
* `column` - 0-based column number
77+
6978
A ***reporter*** is a function that accepts exactly 1 argument of type `LintWarning` or `LintError`. Its return value is ignored. It should somehow record the problem or display it to the user.
7079

7180
### Browser

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"time-grunt": "^1.0.0"
4747
},
4848
"dependencies": {
49+
"binary-search": "^1.2.0",
4950
"body-parser": "^1.9.2",
5051
"chalk": "^0.5.1",
5152
"cheerio": "^0.18.0",
@@ -62,7 +63,8 @@
6263
},
6364
"browser": {
6465
"cheerio": "jquery",
65-
"src/cli.js": false
66+
"src/cli.js": false,
67+
"src/location.js": false
6668
},
6769
"files": [
6870
"bin",

src/bootlint.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
var cheerio = require('cheerio');
1111
var semver = require('semver');
12+
var _location = require('./location');
13+
var LocationIndex = _location.LocationIndex;
1214

1315
(function (exports) {
1416
'use strict';
@@ -753,14 +755,29 @@ var semver = require('semver');
753755
}
754756
});
755757

756-
exports._lint = function ($, reporter, disabledIdList) {
758+
exports._lint = function ($, reporter, disabledIdList, html) {
759+
var locationIndex = IN_NODE_JS ? new LocationIndex(html) : null;
760+
var reporterWrapper = IN_NODE_JS ? function (problem) {
761+
if (problem.elements) {
762+
problem.elements = problem.elements.each(function (i, element) {
763+
if (element.startIndex !== undefined) {
764+
var location = locationIndex.locationOf(element.startIndex);
765+
if (location) {
766+
element.startLocation = location;
767+
}
768+
}
769+
});
770+
}
771+
reporter(problem);
772+
} : reporter;
773+
757774
var disabledIdSet = {};
758775
disabledIdList.forEach(function (disabledId) {
759776
disabledIdSet[disabledId] = true;
760777
});
761778
Object.keys(allLinters).sort().forEach(function (linterId) {
762779
if (!disabledIdSet[linterId]) {
763-
allLinters[linterId]($, reporter);
780+
allLinters[linterId]($, reporterWrapper);
764781
}
765782
});
766783
};
@@ -775,7 +792,7 @@ var semver = require('semver');
775792
*/
776793
exports.lintHtml = function (html, reporter, disabledIds) {
777794
var $ = cheerio.load(html, {withStartIndices: true});
778-
this._lint($, reporter, disabledIds);
795+
this._lint($, reporter, disabledIds, html);
779796
};
780797
}
781798
else {

src/cli.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,19 @@ program.args.forEach(function (pattern) {
2727
filenames.forEach(function (filename) {
2828
var reporter = function (lint) {
2929
var lintId = (lint.id[0] === 'E') ? chalk.bgGreen.white(lint.id) : chalk.bgRed.white(lint.id);
30-
console.log(filename + ":", lintId, lint.message);
31-
totalErrCount++;
30+
var output = false;
31+
if (lint.elements) {
32+
lint.elements.each(function (_, element) {
33+
var loc = element.startLocation;
34+
console.log(filename + ":" + (loc.line + 1) + ":" + (loc.column + 1), lintId, lint.message);
35+
totalErrCount++;
36+
output = true;
37+
});
38+
}
39+
if (!output) {
40+
console.log(filename + ":", lintId, lint.message);
41+
totalErrCount++;
42+
}
3243
};
3344

3445
var html = null;

src/location.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*eslint-env node */
2+
var binarySearch = require('binary-search');
3+
4+
(function () {
5+
'use strict';
6+
7+
/*
8+
* line is a 0-based line index
9+
* column is a 0-based column index
10+
*/
11+
function Location(line, column) {
12+
this.line = line;
13+
this.column = column;
14+
}
15+
exports.Location = Location;
16+
17+
/**
18+
* Maps code unit indices into the string to line numbers and column numbers.
19+
* @param {string} String to construct the index for
20+
*/
21+
function LocationIndex(string) {
22+
// ensure newline termination
23+
if (string[string.length - 1] !== '\n') {
24+
string += '\n';
25+
}
26+
this._stringLength = string.length;
27+
/**
28+
* Each triple in _lineStartEndTriples consists of:
29+
* [0], the 0-based line index of the line the triple represents
30+
* [1], the 0-based code unit index (into the string) of the start of the line (inclusive)
31+
* [2], the 0-based code unit index (into the string) of the start of the next line (or the length of the string, if it is the last line)
32+
* A line starts with a non-newline character,
33+
* and always ends in a newline character, unless it is the very last line in the string.
34+
*/
35+
this._lineStartEndTriples = [[0, 0]];
36+
var nextLineIndex = 1;
37+
var charIndex = 0;
38+
while (charIndex < string.length) {
39+
charIndex = string.indexOf("\n", charIndex);
40+
if (charIndex === -1) {
41+
break;
42+
}
43+
charIndex++;// go past the newline
44+
this._lineStartEndTriples[this._lineStartEndTriples.length - 1].push(charIndex);
45+
this._lineStartEndTriples.push([nextLineIndex, charIndex]);
46+
47+
nextLineIndex++;
48+
}
49+
this._lineStartEndTriples.pop();
50+
}
51+
exports.LocationIndex = LocationIndex;
52+
53+
/**
54+
* Translates a code unit index into its corresponding Location (line index and column index) within the string
55+
* @param {integer} 0-based code unit index into the string
56+
* @returns {Location|null} A Location corresponding to the index, or null if the index is out of bounds
57+
*/
58+
LocationIndex.prototype.locationOf = function (charIndex) {
59+
if (charIndex < 0 || charIndex >= this._stringLength) {
60+
return null;
61+
}
62+
var index = binarySearch(this._lineStartEndTriples, charIndex, function (bucket, needle) {
63+
if (needle < bucket[1]) {
64+
return 1;
65+
}
66+
else if (needle >= bucket[2]) {
67+
return -1;
68+
}
69+
else {
70+
return 0;
71+
}
72+
});
73+
if (index < 0) { // binarySearch returns a negative number (but not necessarily -1) when match not found
74+
return null;
75+
}
76+
var lineStartEnd = this._lineStartEndTriples[index];
77+
var lineIndex = lineStartEnd[0];
78+
var lineStartIndex = lineStartEnd[1];
79+
var columnIndex = charIndex - lineStartIndex;
80+
return new Location(lineIndex, columnIndex);
81+
};
82+
})();

0 commit comments

Comments
 (0)