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

Commit 4df4169

Browse files
committed
Merge pull request #79 from twbs/fix-62-redundant-col-classes
Warn about usage of redundant column classes
2 parents 6bb888e + 766f272 commit 4df4169

File tree

4 files changed

+267
-1
lines changed

4 files changed

+267
-1
lines changed

src/bootlint.js

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,29 @@ var cheerio = require('cheerio');
1010

1111
(function (exports) {
1212
'use strict';
13+
var NUM_COLS = 12;
14+
var COL_REGEX = /\bcol-(xs|sm|md|lg)-(\d{0,2})\b/;
15+
var COL_REGEX_G = /\bcol-(xs|sm|md|lg)-(\d{0,2})\b/g;
1316
var COL_CLASSES = [];
1417
var SCREENS = ['xs', 'sm', 'md', 'lg'];
1518
SCREENS.forEach(function (screen) {
16-
for (var n = 1; n <= 12; n++) {
19+
for (var n = 1; n <= NUM_COLS; n++) {
1720
COL_CLASSES.push('.col-' + screen + '-' + n);
1821
}
1922
});
23+
var SCREEN2NUM = {
24+
'xs': 0,
25+
'sm': 1,
26+
'md': 2,
27+
'lg': 3
28+
};
29+
var NUM2SCREEN = ['xs', 'sm', 'md', 'lg'];
2030
var IN_NODE_JS = !!(cheerio.load);
2131

32+
function compareNums(a, b) {
33+
return a - b;
34+
}
35+
2236
function isDoctype(node) {
2337
return node.type === 'directive' && node.name === '!doctype';
2438
}
@@ -32,6 +46,107 @@ var cheerio = require('cheerio');
3246
return filename;
3347
}
3448

49+
function withoutClass(classes, klass) {
50+
return classes.replace(new RegExp('\\b' + klass + '\\b', 'g'), '');
51+
}
52+
53+
function columnClassKey(colClass) {
54+
return SCREEN2NUM[COL_REGEX.exec(colClass)[1]];
55+
}
56+
57+
function compareColumnClasses(a, b) {
58+
return columnClassKey(a) - columnClassKey(b);
59+
}
60+
61+
/**
62+
* Moves any grid column classes to the end of the class string and sorts the grid classes by ascending screen size.
63+
* @param {string} classes The "class" attribute of a DOM node
64+
* @returns {string}
65+
*/
66+
function sortedColumnClasses(classes) {
67+
// extract column classes
68+
var colClasses = [];
69+
while (true) {
70+
var match = COL_REGEX.exec(classes);
71+
if (!match) {
72+
break;
73+
}
74+
var colClass = match[0];
75+
colClasses.push(colClass);
76+
classes = withoutClass(classes, colClass);
77+
}
78+
79+
colClasses.sort(compareColumnClasses);
80+
return classes + ' ' + colClasses.join(' ');
81+
}
82+
83+
/**
84+
* @param {string} classes The "class" attribute of a DOM node
85+
* @returns {Object.<string, integer[]>} Object mapping grid column widths (1 thru 12) to sorted arrays of screen size numbers (see SCREEN2NUM)
86+
* Widths not used in the classes will not have an entry in the object.
87+
*/
88+
function width2screensFor(classes) {
89+
var width = null;
90+
var width2screens = {};
91+
while (true) {
92+
var match = COL_REGEX_G.exec(classes);
93+
if (!match) {
94+
break;
95+
}
96+
var screen = match[1];
97+
width = match[2];
98+
var screens = width2screens[width];
99+
if (!screens) {
100+
screens = width2screens[width] = [];
101+
}
102+
screens.push(SCREEN2NUM[screen]);
103+
}
104+
105+
for (width in width2screens) {
106+
if (width2screens.hasOwnProperty(width)) {
107+
width2screens[width].sort(compareNums);
108+
}
109+
}
110+
111+
return width2screens;
112+
}
113+
114+
/**
115+
* Given a sorted array of integers, this finds all contiguous runs where each item is incremented by 1 from the next.
116+
* For example:
117+
* [0, 2, 3, 5] has one such run: [2, 3]
118+
* [0, 2, 3, 4, 6, 8, 9, 11] has two such runs: [2, 3, 4], [8, 9]
119+
* [0, 2, 4] has no runs.
120+
* @param {integer[]} list Sorted array of integers
121+
* @returns {integer[][]} Array of pairs of start and end values of runs
122+
*/
123+
function incrementingRunsFrom(list) {
124+
list = list.concat([Infinity]);// use Infinity to ensure any nontrivial (length >= 2) run ends before the end of the loop
125+
var runs = [];
126+
var start = null;
127+
var prev = null;
128+
for (var i = 0; i < list.length; i++) {
129+
var current = list[i];
130+
if (start === null) {
131+
// first element starts a trivial run
132+
start = current;
133+
}
134+
else if (prev + 1 !== current) {
135+
// run ended
136+
if (start !== prev) {
137+
// run is nontrivial
138+
runs.push([start, prev]);
139+
}
140+
// start new run
141+
start = current;
142+
}
143+
// else: the run continues
144+
145+
prev = current;
146+
}
147+
return runs;
148+
}
149+
35150
exports.lintDoctype = (function () {
36151
var MISSING_DOCTYPE = "Document is missing a DOCTYPE declaration";
37152
var NON_HTML5_DOCTYPE = "Document declares a non-HTML5 DOCTYPE";
@@ -387,6 +502,53 @@ var cheerio = require('cheerio');
387502
return "`.table-responsive` is supposed to be used on the table's parent wrapper <div>, not on the table itself";
388503
}
389504
};
505+
exports.lintRedundantColumnClasses = function ($) {
506+
var columns = $(COL_CLASSES.join(','));
507+
var errs = [];
508+
columns.each(function (_index, column) {
509+
var classes = $(column).attr('class');
510+
var simplifiedClasses = classes;
511+
var width2screens = width2screensFor(classes);
512+
var isRedundant = false;
513+
for (var width = 1; width <= NUM_COLS; width++) {
514+
var screens = width2screens[width];
515+
if (!screens) {
516+
continue;
517+
}
518+
var runs = incrementingRunsFrom(screens);
519+
if (!runs.length) {
520+
continue;
521+
}
522+
523+
isRedundant = true;
524+
525+
for (var i = 0; i < runs.length; i++) {
526+
var run = runs[i];
527+
var min = run[0];
528+
var max = run[1];
529+
530+
// remove redundant classes
531+
for (var screenNum = min + 1; screenNum <= max; screenNum++) {
532+
var colClass = 'col-' + NUM2SCREEN[screenNum] + '-' + width;
533+
simplifiedClasses = withoutClass(simplifiedClasses, colClass);
534+
}
535+
}
536+
}
537+
if (!isRedundant) {
538+
return;
539+
}
540+
541+
simplifiedClasses = sortedColumnClasses(simplifiedClasses);
542+
simplifiedClasses = simplifiedClasses.replace(/ {2,}/g, ' ').trim();
543+
var oldClass = 'class="' + classes + '"';
544+
var newClass = 'class="' + simplifiedClasses + '"';
545+
errs.push(
546+
"Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), " +
547+
oldClass + " is redundant and can be simplified to " + newClass
548+
);
549+
});
550+
return errs;
551+
};
390552

391553
exports._lint = function ($) {
392554
var errs = [];
@@ -424,6 +586,7 @@ var cheerio = require('cheerio');
424586
errs = errs.concat(this.lintInputGroupFormControlTypes($));
425587
errs = errs.concat(this.lintInlineCheckboxes($));
426588
errs = errs.concat(this.lintInlineRadios($));
589+
errs = errs.concat(this.lintRedundantColumnClasses($));
427590
errs = errs.filter(function (item) {
428591
return item !== undefined;
429592
});

test/bootlint_test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,25 @@ exports['bootlint'] = {
374374
'should complain when .table-responsive is used on the table itself.'
375375
);
376376
test.done();
377+
},
378+
379+
'redundant grid column classes': function (test) {
380+
test.expect(2);
381+
test.deepEqual(bootlint.lintHtml(utf8Fixture('grid/cols-not-redundant.html')),
382+
[],
383+
'should not complain when there are non-redundant grid column classes.'
384+
);
385+
test.deepEqual(bootlint.lintHtml(utf8Fixture('grid/cols-redundant.html')),
386+
[
387+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="abc col-xs-2 def col-sm-1 ghi col-md-1 jkl col-lg-1" is redundant and can be simplified to class="abc def ghi jkl col-xs-2 col-sm-1"',
388+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-10 abc col-sm-10 def col-md-10 ghi col-lg-12 jkl" is redundant and can be simplified to class="abc def ghi jkl col-xs-10 col-lg-12"',
389+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-6 col-sm-6 col-md-6 col-lg-6" is redundant and can be simplified to class="col-xs-6"',
390+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-5 col-sm-5" is redundant and can be simplified to class="col-xs-5"',
391+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-sm-4 col-md-4" is redundant and can be simplified to class="col-sm-4"',
392+
'Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-md-3 col-lg-3" is redundant and can be simplified to class="col-md-3"'
393+
],
394+
'should complain when there are redundant grid column classes.'
395+
);
396+
test.done();
377397
}
378398
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<title>Test</title>
8+
<!--[if lt IE 9]>
9+
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
10+
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
11+
<![endif]-->
12+
<script src="../../lib/jquery.min.js"></script>
13+
14+
<link rel="stylesheet" href="../../lib/qunit-1.14.0.css">
15+
<script src="../../lib/qunit-1.14.0.js"></script>
16+
<script src="../../../dist/browser/bootlint.js"></script>
17+
<script src="../generic-qunit.js"></script>
18+
</head>
19+
<body>
20+
<div class="container">
21+
<div class="row">
22+
<div class="col-xs-1 col-sm-2 col-md-1 col-lg-2">Single-digit columns</div>
23+
</div>
24+
<div class="row">
25+
<div class="col-xs-12 col-sm-11 col-md-10 col-lg-11">Double-digit columns</div>
26+
</div>
27+
</div>
28+
29+
<div id="qunit"></div>
30+
<ol id="bootlint"></ol>
31+
</body>
32+
</html>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<title>Test</title>
8+
<!--[if lt IE 9]>
9+
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
10+
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
11+
<![endif]-->
12+
<script src="../../lib/jquery.min.js"></script>
13+
14+
<link rel="stylesheet" href="../../lib/qunit-1.14.0.css">
15+
<script src="../../lib/qunit-1.14.0.js"></script>
16+
<script src="../../../dist/browser/bootlint.js"></script>
17+
<script src="../generic-qunit.js"></script>
18+
</head>
19+
<body>
20+
<div class="container">
21+
<div class="row">
22+
<div class="abc col-xs-2 def col-sm-1 ghi col-md-1 jkl col-lg-1">Single-digit columns; sm-lg redundant; random classes interleaved</div>
23+
</div>
24+
<div class="row">
25+
<div class="col-xs-10 abc col-sm-10 def col-md-10 ghi col-lg-12 jkl">Double-digit columns; xs-md redundant; random classes interleaved</div>
26+
</div>
27+
<div class="row">
28+
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6">xs-lg redundant</div>
29+
</div>
30+
<div class="row">
31+
<div class="col-xs-5 col-sm-5">xs-sm redundant</div>
32+
</div>
33+
<div class="row">
34+
<div class="col-sm-4 col-md-4">sm-md redundant</div>
35+
</div>
36+
<div class="row">
37+
<div class="col-md-3 col-lg-3">md-lg redundant</div>
38+
</div>
39+
</div>
40+
41+
<div id="qunit"></div>
42+
<ol id="bootlint">
43+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="abc col-xs-2 def col-sm-1 ghi col-md-1 jkl col-lg-1" is redundant and can be simplified to class="abc def ghi jkl col-xs-2 col-sm-1"'></li>
44+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-10 abc col-sm-10 def col-md-10 ghi col-lg-12 jkl" is redundant and can be simplified to class="abc def ghi jkl col-xs-10 col-lg-12"'></li>
45+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-6 col-sm-6 col-md-6 col-lg-6" is redundant and can be simplified to class="col-xs-6"'></li>
46+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-xs-5 col-sm-5" is redundant and can be simplified to class="col-xs-5"'></li>
47+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-sm-4 col-md-4" is redundant and can be simplified to class="col-sm-4"'></li>
48+
<li data-lint='Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), class="col-md-3 col-lg-3" is redundant and can be simplified to class="col-md-3"'></li>
49+
</ol>
50+
</body>
51+
</html>

0 commit comments

Comments
 (0)