Skip to content

Commit

Permalink
feat(comments): implements control directives through comments (#18)
Browse files Browse the repository at this point in the history
* feat(comments): implements control directives through comments

* Polyfill for Object.values and replace Array.prototype.includes by Array.prototype.indexOf

* Solve Eslint style errors
  • Loading branch information
elchininet authored Nov 17, 2021
1 parent 86999d6 commit 0394fb1
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 48 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,34 @@ In this case, it would:
* remove a rule with the exact selector `.hello`
* remove any rule that contains the `.world` class.

#### Control directives

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.

```javascript
var rulesToRemove = ['.hello .h1', '.world']
```

###### input

```css
a { font-size: 12px; }
/* byebye:ignore */
.hello .h1 { background: red }
.hello .h1 { text-align: left }
/* byebye:begin:ignore */
.world { color: blue }
.world { border: 1px solid #CCC }
/* byebye:end:ignore */
.world { background: white }
```

###### output

```css
a { font-size: 12px; }
.hello .h1 { background: red }
.world { color: blue }
.world { border: 1px solid #CCC }
```

89 changes: 41 additions & 48 deletions lib/css-byebye.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,12 @@
*/

const postcss = require('postcss')

/**
* Escape a string so that it can be turned into a regex
* @param {String} str String to transform
* @return {String} Escaped string
*/
function escapeRegExp(str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
}

/**
* Turn strings from rules to remove into a regexp to concat them later
* @param {Mixed Array} rulesToRemove
* @return {RegExp Array}
*/
function regexize(rulesToRemove) {
const rulesRegexes = []
for (let i = 0, l = rulesToRemove.length; i < l; i++) {
if (typeof rulesToRemove[i] === 'string') {
rulesRegexes.push(new RegExp('^\\s*' + escapeRegExp(rulesToRemove[i]) + '\\s*$'))
} else {
rulesRegexes.push(rulesToRemove[i])
}
}
return rulesRegexes
}

/**
* Concat various regular expressions into one
* @param {RegExp Array} regexes
* @return {RegExp} concatanated regexp
*/
function concatRegexes(regexes) {
let rconcat = ''

if (Array.isArray(regexes)) {
for (let i = 0, l = regexes.length; i < l; i++) {
rconcat += regexes[i].source + '|'
}

rconcat = rconcat.substr(0, rconcat.length - 1)

return new RegExp(rconcat)
}
}
const {
CONTROL_DIRECTIVES,
CONTROL_DIRECTIVES_BLOCKS,
getControlDirective,
} = require('./utilities/directives')
const {regexize, concatRegexes} = require('./utilities/regexps')

/**
* Return the actual postcss plugin to remove rules from the css
Expand All @@ -56,8 +17,42 @@ const cssbyebye = postcss.plugin('css-byebye', function (options) {
return function byebye(css) {
const remregexes = regexize(options.rulesToRemove)
const regex = concatRegexes(remregexes)
let controlDirective = null

css.walk(walkNodes)

function walkNodes(node) {
if (node.type === 'comment') {
const cd = filterComments(node)
if (cd) {
controlDirective = cd.block === CONTROL_DIRECTIVES_BLOCKS.END ? null : cd
}
return
}

// ignore directive
if (controlDirective && controlDirective.directive === CONTROL_DIRECTIVES.IGNORE) {
if (typeof controlDirective.block === 'undefined') {
controlDirective = null
}
return
}

css.walkRules(filterRule)
if (node.type === 'rule') {
filterRule(node)
} else if (node.type === 'atrule') {
filterAtRule(node)
}
}

function filterComments(comment) {
const cd = getControlDirective(comment)
if (cd) {
comment.remove()
return cd
}
return null
}

function filterRule(rule) {
const selectors = rule.selectors
Expand All @@ -78,8 +73,6 @@ const cssbyebye = postcss.plugin('css-byebye', function (options) {
}
}

css.walkAtRules(filterAtRule)

function filterAtRule(rule) {
const ruleName = '@' + rule.name

Expand Down
21 changes: 21 additions & 0 deletions lib/css-byebye.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,25 @@ describe('cssbyebye', function () {

assert.strictEqual(result.css, expected)
})

it('should ignore rules preceded by a directive ignore', function () {
const css =
'a { font-size: 12px; } /* byebye:ignore */ .hello .h1 { background: red } .hello .h1 { text-align: left } .world { color: blue }'
const rulesToRemove = ['.hello .h1', '.world']
const expected = 'a { font-size: 12px; } .hello .h1 { background: red }'
const options = {rulesToRemove: rulesToRemove, map: false}
const result = postcss(cssbyebye(options)).process(css)
assert.strictEqual(result.css, expected)
})

it('should ignore block of rules with directive ignore:start and ignore:end', function () {
const css =
'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; }'
const rulesToRemove = ['.hello .h1', '.world']
const expected =
'a { font-size: 12px; } .hello .h1 { background: red } .hello .h1 { text-align: left } .world { color: blue }'
const options = {rulesToRemove: rulesToRemove, map: false}
const result = postcss(cssbyebye(options)).process(css)
assert.strictEqual(result.css, expected)
})
})
63 changes: 63 additions & 0 deletions lib/utilities/directives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Object.getValues polyfill
* @param {Object} obj object
* @return {Array}
*/
function getValues(obj) {
return Object.keys(obj).map(function (key) {
return obj[key]
})
}

const CONTROL_DIRECTIVE_REG_EXP = /^\/\*!? *byebye:?(begin|end)?:(\w+) *\*\/$/

const CONTROL_DIRECTIVES = {
IGNORE: 'ignore',
}

const CONTROL_DIRECTIVES_BLOCKS = {
BEGIN: 'begin',
END: 'end',
}

const controlDirectivesValues = getValues(CONTROL_DIRECTIVES)
const controlDirectivesBlockValues = getValues(CONTROL_DIRECTIVES_BLOCKS)

/**
* Check if a directive match is a valid one
* @param {Mixed Array} match string match
* @return {Boolean}
*/
function isValidMatchDirective(match) {
if (Array.isArray(match)) {
return (
controlDirectivesValues.indexOf(match[2]) >= 0 &&
(typeof match[1] === 'undefined' || controlDirectivesBlockValues.indexOf(match[1]) >= 0)
)
}
return false
}

/**
* Extract a control directive from a comment
* @param {Comment} comment postcss comment
* @return {Directive object}
*/
function getControlDirective(comment) {
const commentStr = comment.toString()
const match = commentStr.match(CONTROL_DIRECTIVE_REG_EXP)
if (match && isValidMatchDirective(match)) {
const controlDirective = {directive: match[2]}
if (match[1]) {
controlDirective.block = match[1]
}
return controlDirective
}
return null
}

module.exports = {
CONTROL_DIRECTIVES,
CONTROL_DIRECTIVES_BLOCKS,
getControlDirective,
}
49 changes: 49 additions & 0 deletions lib/utilities/regexps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Escape a string so that it can be turned into a regex
* @param {String} str String to transform
* @return {String} Escaped string
*/
function escapeRegExp(str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
}

/**
* Turn strings from rules to remove into a regexp to concat them later
* @param {Mixed Array} rulesToRemove
* @return {RegExp Array}
*/
function regexize(rulesToRemove) {
const rulesRegexes = []
for (let i = 0, l = rulesToRemove.length; i < l; i++) {
if (typeof rulesToRemove[i] === 'string') {
rulesRegexes.push(new RegExp('^\\s*' + escapeRegExp(rulesToRemove[i]) + '\\s*$'))
} else {
rulesRegexes.push(rulesToRemove[i])
}
}
return rulesRegexes
}

/**
* Concat various regular expressions into one
* @param {RegExp Array} regexes
* @return {RegExp} concatanated regexp
*/
function concatRegexes(regexes) {
let rconcat = ''

if (Array.isArray(regexes)) {
for (let i = 0, l = regexes.length; i < l; i++) {
rconcat += regexes[i].source + '|'
}

rconcat = rconcat.substr(0, rconcat.length - 1)

return new RegExp(rconcat)
}
}

module.exports = {
regexize,
concatRegexes,
}

0 comments on commit 0394fb1

Please sign in to comment.