Skip to content

Commit

Permalink
Merge pull request #1 from jonatanpedersen/upside-down
Browse files Browse the repository at this point in the history
Adds support for selectors with nesting operators > + ~
  • Loading branch information
jonatanpedersen authored Feb 4, 2017
2 parents 0f7edaf + c689365 commit 21033b6
Show file tree
Hide file tree
Showing 4 changed files with 437 additions and 253 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ Rules with supported selectors will be removed when the selector is not found in

| Selector | Example |
| --- | --- |
| [*](http://www.w3schools.com/cssref/sel_all.asp) | * |
| [.class](http://www.w3schools.com/cssref/sel_class.asp) | .foo |
| [.class.class](http://www.w3schools.com/cssref/sel_class.asp) | .foo.bar |
| [#id](http://www.w3schools.com/cssref/sel_id.asp) | #baz |
| [#id.class](http://www.w3schools.com/cssref/sel_id.asp) | #baz.foo |
| [element](http://www.w3schools.com/cssref/sel_element.asp) | p |
| [element.class](http://www.w3schools.com/cssref/sel_element.asp) | p.foo |
| [element,element](http://www.w3schools.com/cssref/sel_element_comma.asp) | div, p | [element element](http://www.w3schools.com/cssref/sel_element_element.asp) | div p |
| [element,element](http://www.w3schools.com/cssref/sel_element_comma.asp) | div, p |
| [element element](http://www.w3schools.com/cssref/sel_element_element.asp) | div p |
| [element>element](http://www.w3schools.com/cssref/sel_element_gt.asp) | div > p |
| [element+element](http://www.w3schools.com/cssref/sel_element_pluss.asp) | div + p |
| [element~element](http://www.w3schools.com/cssref/sel_gen_sibling.asp) | p ~ ul |

class names in both the class and the data-class attribute of html elements will be used.

Expand All @@ -50,14 +55,10 @@ class names in both the class and the data-class attribute of html elements will
```

### Unsupported
Rules with unsupported selectors will not be removed.
The attributes, pseudo-classes and pseudo-elements of selectors are ignored when matching elements to selectors.

| Selector | Example |
| --- | --- |
| [*](http://www.w3schools.com/cssref/sel_all.asp) | * |
| [element>element](http://www.w3schools.com/cssref/sel_element_gt.asp) | div > p |
| [element+element](http://www.w3schools.com/cssref/sel_element_pluss.asp) | div + p |
| [element1~element2](http://www.w3schools.com/cssref/sel_gen_sibling.asp) | p ~ ul |
| [[attribute]](http://www.w3schools.com/cssref/sel_attribute.asp) | [target] |
| [[attribute=value]](http://www.w3schools.com/cssref/sel_attribute_value.asp) | [target=_blank] |
| [[attribute~=value]](http://www.w3schools.com/cssref/sel_attribute_value_contains.asp) | [title~=flower] |
Expand Down Expand Up @@ -107,8 +108,8 @@ Rules with unsupported selectors will not be removed.

| Library | Duration |
| --- | --- |
| css-bingo | 21s|
| [purify-css](https://github.com/purifycss/purifycss) | 149s|
| css-bingo | 65s |
| [purify-css](https://github.com/purifycss/purifycss) | 118s |

## Licence
The MIT License (MIT)
Expand Down
213 changes: 156 additions & 57 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,182 @@
var css = require('css');
var debug = require('debug')('css-bingo');
var htmlparser = require('htmlparser2');
const css = require('css');
const htmlparser = require('htmlparser2');
const CssSelectorParser = require('css-selector-parser').CssSelectorParser;
const debug = require('debug')('css-bingo');

module.exports = cssBingo;

function cssBingo (cssCode, htmlCode) {
const knownSelectors = new Set();

var parser = new htmlparser.Parser({
onopentag: (name, attribs) => {
knownSelectors.add(name);

const id = attribs.id;
const classNames = [attribs['class'], attribs['data-class']].filter(Boolean).join(' ').split(' ').filter(Boolean).sort();

// .class.class
const classNamesLength = classNames.length;

for (var i = 0; i < classNamesLength; i++) {
for (var j = i + 1; j < classNamesLength; j++) {
classNames.push(classNames.slice(i, j + 1).join('.'));
function cssBingo(cssCode, htmlCode) {
const cssAst = css.parse(cssCode);
const selectorRules = getSelectorRulesFromCssAst(cssAst);
const unmatchedSelectors = matchSelectorsInHtml(selectorRules, htmlCode);
const newCssAst = filterSelectorsFromCssAst(cssAst, unmatchedSelectors);

return css.stringify(newCssAst, { compress: true });
}

function getSelectorRulesFromCssAst(cssAst) {
const cssSelectorParser = new CssSelectorParser();
cssSelectorParser.registerSelectorPseudos('has');
cssSelectorParser.registerNestingOperators('>', '+', '~');
cssSelectorParser.registerAttrEqualityMods('^', '$', '*', '~');

const selectors = new Set();
const selectorRules = [];

walk(cssAst.stylesheet);

return selectorRules;

function walk(node) {
for (var j = 0; j < node.rules.length; j++) {
const rule = node.rules[j];

if (rule.type === 'rule' && rule.selectors) {
for (var i = 0; i < rule.selectors.length; i++) {
const selector = rule.selectors[i];

selectorRules.push({
selector: selector,
rule: cssSelectorParser.parse(selector).rule
});

selectors.add(selector);
}
} else if (rule.type === 'media') {
walk(rule);
}
}
}
}

function matchSelectorsInHtml(selectorRules, htmlCode) {
var levelIdx = -1;
var levels = [];

const matchedSelectors = new Set();
const unmatchedSelectors = new Set();
const unmatchedSelectorRules = [];

for (var x = 0; x < selectorRules.length; x++) {
const selectorRule = selectorRules[x];

unmatchedSelectors.add(selectorRule.selector);
unmatchedSelectorRules.push(selectorRule);
}

classNames.forEach(className => {
knownSelectors.add(`.${className}`);
knownSelectors.add(`${name}.${className}`);
const parser = new htmlparser.Parser({
onopentag: (name, attrs) => {
levelIdx++;

if (id) {
knownSelectors.add(`#${id}.${className}`);
const levelSelectorRules = levels[levelIdx] = levels[levelIdx] || [];
const nextLevelSelectorRules = levels[levelIdx + 1] = levels[levelIdx + 1] || [];

const element = {
name: name,
id: attrs.id,
attrs: attrs,
classNames: new Set([attrs['class'], attrs['data-class']].join(' ').split(' ').filter(Boolean))
};

const tmpSelectorRules = [];

for (var i = 0; i < unmatchedSelectorRules.length; i++) {
tmpSelectorRules.push(unmatchedSelectorRules[i]);
}

for (var j = 0; j < levelSelectorRules.length; j++) {
tmpSelectorRules.push(levelSelectorRules[j]);
}

for (var k = 0; k < tmpSelectorRules.length; k++) {
processSelectorRules(tmpSelectorRules[k]);
}

function processSelectorRules(levelSelectorRule) {
const matches = match(element, levelSelectorRule.rule);

if (matches) {
// If there is a match then we check whether the rule has any child rules,
// If it does we send the child rule to the next level,
// otherwise it means that the whole selector has been matched can remove it
// from the unmatched selectors set and add it matched selectors set.
if (levelSelectorRule.rule.rule) {
const nestingOperator = levelSelectorRule.rule.rule.nestingOperator;
const newLevelSelectorRule = {
rule: levelSelectorRule.rule.rule,
selector: levelSelectorRule.selector
};

if (nestingOperator === null || nestingOperator === '>') {
nextLevelSelectorRules.push(newLevelSelectorRule);
} else {
levelSelectorRules.push(newLevelSelectorRule);
}
} else {
for (var y = 0; y < unmatchedSelectorRules.length; y++) {
if (unmatchedSelectorRules[y].selector === levelSelectorRule.selector) {
unmatchedSelectorRules.splice(y, 1);
matchedSelectors.add(levelSelectorRule.selector);
unmatchedSelectors.delete(levelSelectorRule.selector);
break;
}
}
}
} else {
// If we don't have a match then check the nesting operator.
// If the nesting operator is null i.e. .class .class then we
// can push the levelSelectorRule to the next level.
if (levelSelectorRule.rule.nestingOperator === null) {
nextLevelSelectorRules.push(levelSelectorRule);
}
}
});
}

function match(element, rule) {
const id = (!rule.id || (rule.id === element.id));
const name = (!rule.tagName || rule.tagName === element.name || rule.tagName === '*');
const classNames = (!rule.classNames || (rule.classNames.filter(ruleClassName => element.classNames.has(ruleClassName)).length === rule.classNames.length));

if (id) {
knownSelectors.add(`#${id}`);
return id && name && classNames;
}
},
onclosetag: () => {
delete levels[levelIdx + 1];
levelIdx--;
}
});

parser.write(htmlCode);
parser.end();

const ast = css.parse(cssCode);

walk(ast.stylesheet);
return unmatchedSelectors;
}

function filterSelectorsFromCssAst(cssAst, selectors) {
walk(cssAst.stylesheet);

return css.stringify(ast, {compress:true});
return cssAst;

function walk (node) {
function walk(node) {
node.rules = node.rules.reduce((rules, rule) => {
switch (rule.type) {
case 'rule':
rule.selectors = rule.selectors.filter(selector => {
if (/^[#\.]?[^:>\*\s+~]+$/.test(selector)) {
const sortedSelector = selector.split('.').sort().join('.');
return knownSelectors.has(sortedSelector);
} else {
return true;
}
});
if (rule.type === 'rule') {
rule.selectors = rule.selectors.filter(selector => !selectors.has(selector));

if (rule.selectors.length > 0) {
rules.push(rule);
} else {
debug('removed rule: %o', rule);
}
break;
case 'media':
walk(rule);
if (rule.rules.length > 0) {
rules.push(rule);
}
break;
default:
if (rule.selectors.length > 0) {
rules.push(rule);
} else {
debug('removed rule: %o', rule);
}
} else if (rule.type === 'media') {
walk(rule);
if (rule.rules.length > 0) {
rules.push(rule);
break;
}
} else {
rules.push(rule);
}

return rules;
}, []);
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"homepage": "https://github.com/jonatanpedersen/css-bingo#readme",
"dependencies": {
"css": "^2.2.1",
"css-selector-parser": "^1.3.0",
"debug": "^2.6.0",
"htmlparser2": "^3.9.2"
},
Expand Down
Loading

0 comments on commit 21033b6

Please sign in to comment.