Skip to content

Commit 0394fb1

Browse files
authored
feat(comments): implements control directives through comments (#18)
* feat(comments): implements control directives through comments * Polyfill for Object.values and replace Array.prototype.includes by Array.prototype.indexOf * Solve Eslint style errors
1 parent 86999d6 commit 0394fb1

File tree

5 files changed

+205
-48
lines changed

5 files changed

+205
-48
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,34 @@ In this case, it would:
9090
* remove a rule with the exact selector `.hello`
9191
* remove any rule that contains the `.world` class.
9292

93+
#### Control directives
94+
95+
You can ignore certain rules or certain block of rules to avoid them being removed, even if they match the criteria, adding comments with control directives. These comments will be removed from the final code.
96+
97+
```javascript
98+
var rulesToRemove = ['.hello .h1', '.world']
99+
```
100+
101+
###### input
102+
103+
```css
104+
a { font-size: 12px; }
105+
/* byebye:ignore */
106+
.hello .h1 { background: red }
107+
.hello .h1 { text-align: left }
108+
/* byebye:begin:ignore */
109+
.world { color: blue }
110+
.world { border: 1px solid #CCC }
111+
/* byebye:end:ignore */
112+
.world { background: white }
113+
```
114+
115+
###### output
116+
117+
```css
118+
a { font-size: 12px; }
119+
.hello .h1 { background: red }
120+
.world { color: blue }
121+
.world { border: 1px solid #CCC }
122+
```
123+

lib/css-byebye.js

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,12 @@
33
*/
44

55
const postcss = require('postcss')
6-
7-
/**
8-
* Escape a string so that it can be turned into a regex
9-
* @param {String} str String to transform
10-
* @return {String} Escaped string
11-
*/
12-
function escapeRegExp(str) {
13-
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
14-
}
15-
16-
/**
17-
* Turn strings from rules to remove into a regexp to concat them later
18-
* @param {Mixed Array} rulesToRemove
19-
* @return {RegExp Array}
20-
*/
21-
function regexize(rulesToRemove) {
22-
const rulesRegexes = []
23-
for (let i = 0, l = rulesToRemove.length; i < l; i++) {
24-
if (typeof rulesToRemove[i] === 'string') {
25-
rulesRegexes.push(new RegExp('^\\s*' + escapeRegExp(rulesToRemove[i]) + '\\s*$'))
26-
} else {
27-
rulesRegexes.push(rulesToRemove[i])
28-
}
29-
}
30-
return rulesRegexes
31-
}
32-
33-
/**
34-
* Concat various regular expressions into one
35-
* @param {RegExp Array} regexes
36-
* @return {RegExp} concatanated regexp
37-
*/
38-
function concatRegexes(regexes) {
39-
let rconcat = ''
40-
41-
if (Array.isArray(regexes)) {
42-
for (let i = 0, l = regexes.length; i < l; i++) {
43-
rconcat += regexes[i].source + '|'
44-
}
45-
46-
rconcat = rconcat.substr(0, rconcat.length - 1)
47-
48-
return new RegExp(rconcat)
49-
}
50-
}
6+
const {
7+
CONTROL_DIRECTIVES,
8+
CONTROL_DIRECTIVES_BLOCKS,
9+
getControlDirective,
10+
} = require('./utilities/directives')
11+
const {regexize, concatRegexes} = require('./utilities/regexps')
5112

5213
/**
5314
* Return the actual postcss plugin to remove rules from the css
@@ -56,8 +17,42 @@ const cssbyebye = postcss.plugin('css-byebye', function (options) {
5617
return function byebye(css) {
5718
const remregexes = regexize(options.rulesToRemove)
5819
const regex = concatRegexes(remregexes)
20+
let controlDirective = null
21+
22+
css.walk(walkNodes)
23+
24+
function walkNodes(node) {
25+
if (node.type === 'comment') {
26+
const cd = filterComments(node)
27+
if (cd) {
28+
controlDirective = cd.block === CONTROL_DIRECTIVES_BLOCKS.END ? null : cd
29+
}
30+
return
31+
}
32+
33+
// ignore directive
34+
if (controlDirective && controlDirective.directive === CONTROL_DIRECTIVES.IGNORE) {
35+
if (typeof controlDirective.block === 'undefined') {
36+
controlDirective = null
37+
}
38+
return
39+
}
5940

60-
css.walkRules(filterRule)
41+
if (node.type === 'rule') {
42+
filterRule(node)
43+
} else if (node.type === 'atrule') {
44+
filterAtRule(node)
45+
}
46+
}
47+
48+
function filterComments(comment) {
49+
const cd = getControlDirective(comment)
50+
if (cd) {
51+
comment.remove()
52+
return cd
53+
}
54+
return null
55+
}
6156

6257
function filterRule(rule) {
6358
const selectors = rule.selectors
@@ -78,8 +73,6 @@ const cssbyebye = postcss.plugin('css-byebye', function (options) {
7873
}
7974
}
8075

81-
css.walkAtRules(filterAtRule)
82-
8376
function filterAtRule(rule) {
8477
const ruleName = '@' + rule.name
8578

lib/css-byebye.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,25 @@ describe('cssbyebye', function () {
6464

6565
assert.strictEqual(result.css, expected)
6666
})
67+
68+
it('should ignore rules preceded by a directive ignore', function () {
69+
const css =
70+
'a { font-size: 12px; } /* byebye:ignore */ .hello .h1 { background: red } .hello .h1 { text-align: left } .world { color: blue }'
71+
const rulesToRemove = ['.hello .h1', '.world']
72+
const expected = 'a { font-size: 12px; } .hello .h1 { background: red }'
73+
const options = {rulesToRemove: rulesToRemove, map: false}
74+
const result = postcss(cssbyebye(options)).process(css)
75+
assert.strictEqual(result.css, expected)
76+
})
77+
78+
it('should ignore block of rules with directive ignore:start and ignore:end', function () {
79+
const css =
80+
'a { font-size: 12px; } /* byebye:begin:ignore */ .hello .h1 { background: red } .hello .h1 { text-align: left } .world { color: blue } /* byebye:end:ignore */ .world { font-size: 10px; }'
81+
const rulesToRemove = ['.hello .h1', '.world']
82+
const expected =
83+
'a { font-size: 12px; } .hello .h1 { background: red } .hello .h1 { text-align: left } .world { color: blue }'
84+
const options = {rulesToRemove: rulesToRemove, map: false}
85+
const result = postcss(cssbyebye(options)).process(css)
86+
assert.strictEqual(result.css, expected)
87+
})
6788
})

lib/utilities/directives.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Object.getValues polyfill
3+
* @param {Object} obj object
4+
* @return {Array}
5+
*/
6+
function getValues(obj) {
7+
return Object.keys(obj).map(function (key) {
8+
return obj[key]
9+
})
10+
}
11+
12+
const CONTROL_DIRECTIVE_REG_EXP = /^\/\*!? *byebye:?(begin|end)?:(\w+) *\*\/$/
13+
14+
const CONTROL_DIRECTIVES = {
15+
IGNORE: 'ignore',
16+
}
17+
18+
const CONTROL_DIRECTIVES_BLOCKS = {
19+
BEGIN: 'begin',
20+
END: 'end',
21+
}
22+
23+
const controlDirectivesValues = getValues(CONTROL_DIRECTIVES)
24+
const controlDirectivesBlockValues = getValues(CONTROL_DIRECTIVES_BLOCKS)
25+
26+
/**
27+
* Check if a directive match is a valid one
28+
* @param {Mixed Array} match string match
29+
* @return {Boolean}
30+
*/
31+
function isValidMatchDirective(match) {
32+
if (Array.isArray(match)) {
33+
return (
34+
controlDirectivesValues.indexOf(match[2]) >= 0 &&
35+
(typeof match[1] === 'undefined' || controlDirectivesBlockValues.indexOf(match[1]) >= 0)
36+
)
37+
}
38+
return false
39+
}
40+
41+
/**
42+
* Extract a control directive from a comment
43+
* @param {Comment} comment postcss comment
44+
* @return {Directive object}
45+
*/
46+
function getControlDirective(comment) {
47+
const commentStr = comment.toString()
48+
const match = commentStr.match(CONTROL_DIRECTIVE_REG_EXP)
49+
if (match && isValidMatchDirective(match)) {
50+
const controlDirective = {directive: match[2]}
51+
if (match[1]) {
52+
controlDirective.block = match[1]
53+
}
54+
return controlDirective
55+
}
56+
return null
57+
}
58+
59+
module.exports = {
60+
CONTROL_DIRECTIVES,
61+
CONTROL_DIRECTIVES_BLOCKS,
62+
getControlDirective,
63+
}

lib/utilities/regexps.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Escape a string so that it can be turned into a regex
3+
* @param {String} str String to transform
4+
* @return {String} Escaped string
5+
*/
6+
function escapeRegExp(str) {
7+
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
8+
}
9+
10+
/**
11+
* Turn strings from rules to remove into a regexp to concat them later
12+
* @param {Mixed Array} rulesToRemove
13+
* @return {RegExp Array}
14+
*/
15+
function regexize(rulesToRemove) {
16+
const rulesRegexes = []
17+
for (let i = 0, l = rulesToRemove.length; i < l; i++) {
18+
if (typeof rulesToRemove[i] === 'string') {
19+
rulesRegexes.push(new RegExp('^\\s*' + escapeRegExp(rulesToRemove[i]) + '\\s*$'))
20+
} else {
21+
rulesRegexes.push(rulesToRemove[i])
22+
}
23+
}
24+
return rulesRegexes
25+
}
26+
27+
/**
28+
* Concat various regular expressions into one
29+
* @param {RegExp Array} regexes
30+
* @return {RegExp} concatanated regexp
31+
*/
32+
function concatRegexes(regexes) {
33+
let rconcat = ''
34+
35+
if (Array.isArray(regexes)) {
36+
for (let i = 0, l = regexes.length; i < l; i++) {
37+
rconcat += regexes[i].source + '|'
38+
}
39+
40+
rconcat = rconcat.substr(0, rconcat.length - 1)
41+
42+
return new RegExp(rconcat)
43+
}
44+
}
45+
46+
module.exports = {
47+
regexize,
48+
concatRegexes,
49+
}

0 commit comments

Comments
 (0)