Skip to content

Commit

Permalink
implement *-of-type selectors. close #4.
Browse files Browse the repository at this point in the history
  • Loading branch information
leeoniya committed Apr 8, 2019
1 parent 08ae865 commit 46ae87c
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 14 deletions.
15 changes: 6 additions & 9 deletions README.md
Expand Up @@ -94,6 +94,11 @@ The `shouldDrop` hook is called for every CSS selector that could not be matched
- `:only-child`
- `:nth-child()`
- `:nth-last-child()`
- `:first-of-type`
- `:last-of-type`
- `:only-of-type`
- `:nth-of-type()`
- `:nth-last-of-type()`

---
### Performance
Expand Down Expand Up @@ -182,15 +187,7 @@ A full **[Stress Test](https://github.com/leeoniya/dropcss/tree/master/test/benc
---
### TODO

- All `-of-type` selectors are currently unimplemented, so will not be removed unless already disqualified by a paired selector, (e.g. `.card:first-of-type` when `.card` is absent altogether). This is pretty easy to implement and a good first issue for those interested in contributing: [Issue #4](https://github.com/leeoniya/dropcss/issues/4).
- `:first-of-type`
- `:last-of-type`
- `:only-of-type`
- `:nth-of-type()`
- `:nth-last-of-type()`
- `:nth-only-of-type()`

- Moar tests. Hundreds of additional, granular tests. DropCSS is currently developed against gigantic blobs of diverse, real-world CSS and HTML. These inputs & outputs are also used for perf testing and regression detection. While not all output was verified by hand (this would be infeasible for giganitic mis-matched HTML/CSS inputs), it was loosely verified against what other cleaners remove and what they leave behind. Writing tests is additonally challenging because the way selectors are drop-tested is optimized to fast-path many cases; a complex-looking test like `.foo > ul + p:not([foo*=bar]):hover` will actually short circuit early if `.foo`, `ul` or `p` are missing from the dom, and will never continue to structural/context or negation assertions. Tests must be carefully written to ensure they hit all the desired paths; it's easy to waste a lot of time writing useless tests that add no value. Unfortunately, even 100% cumulative code coverage of the test suite would only serve as a starting point. Good tests would be a diverse set of real-world inputs and manually verified outputs.
- Moar tests. DropCSS is currently developed against gigantic blobs of diverse, real-world CSS and HTML. These inputs & outputs are also used for perf testing and regression detection. While not all output was verified by hand (this would be infeasible for giganitic mis-matched HTML/CSS inputs), it was loosely verified against what other cleaners remove and what they leave behind. Writing tests is additonally challenging because the way selectors are drop-tested is optimized to fast-path many cases; a complex-looking test like `.foo > ul + p:not([foo*=bar]):hover` will actually short circuit early if `.foo`, `ul` or `p` are missing from the dom, and will never continue to structural/context or negation assertions. Tests must be carefully written to ensure they hit all the desired paths; it's easy to waste a lot of time writing useless tests that add no value. Unfortunately, even 100% cumulative code coverage of the test suite would only serve as a starting point. Good tests would be a diverse set of real-world inputs and manually verified outputs.

---
### Caveats
Expand Down
44 changes: 44 additions & 0 deletions dist/dropcss.cjs.js
Expand Up @@ -93,6 +93,28 @@ function node(parent, tagName, attrs) {
};
}

// adds ._ofTypes: {<tagName>: [...]} to parent
// adds ._typeIdx to childNodes
function getSibsOfType(par, tagName) {
if (par != null) {
var ofTypes = (par._ofTypes = par._ofTypes || {});

if (!(tagName in ofTypes)) {
var typeIdx = 0;
ofTypes[tagName] = par.childNodes.filter(function (n) {
if (n.tagName == tagName) {
n._typeIdx = typeIdx++;
return true;
}
});
}

return ofTypes[tagName];
}

return null;
}

function build(tokens, each) {
var targ = node(null, "root", EMPTY_SET), idx;

Expand Down Expand Up @@ -606,9 +628,11 @@ function find(m, ctx) {
val = m[--ctx.idx];

var n = ctx.node;
var tag = n.tagName;
tidx = n.idx;
par = n.parentNode;
var len = par ? par.childNodes.length : 1;
var tsibs = (void 0);

switch (name) {
case 'not':
Expand All @@ -629,6 +653,26 @@ function find(m, ctx) {
case 'nth-last-child':
res = _nthChild(len - tidx, val);
break;
case 'first-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == 0;
break;
case 'last-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == tsibs.length - 1;
break;
case 'only-of-type':
tsibs = getSibsOfType(par, tag);
res = tsibs.length == 1;
break;
case 'nth-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(n._typeIdx + 1, val);
break;
case 'nth-last-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(tsibs.length - n._typeIdx, val);
break;
}

ctx.idx--;
Expand Down
44 changes: 44 additions & 0 deletions dist/dropcss.js
Expand Up @@ -97,6 +97,28 @@
};
}

// adds ._ofTypes: {<tagName>: [...]} to parent
// adds ._typeIdx to childNodes
function getSibsOfType(par, tagName) {
if (par != null) {
var ofTypes = (par._ofTypes = par._ofTypes || {});

if (!(tagName in ofTypes)) {
var typeIdx = 0;
ofTypes[tagName] = par.childNodes.filter(function (n) {
if (n.tagName == tagName) {
n._typeIdx = typeIdx++;
return true;
}
});
}

return ofTypes[tagName];
}

return null;
}

function build(tokens, each) {
var targ = node(null, "root", EMPTY_SET), idx;

Expand Down Expand Up @@ -610,9 +632,11 @@
val = m[--ctx.idx];

var n = ctx.node;
var tag = n.tagName;
tidx = n.idx;
par = n.parentNode;
var len = par ? par.childNodes.length : 1;
var tsibs = (void 0);

switch (name) {
case 'not':
Expand All @@ -633,6 +657,26 @@
case 'nth-last-child':
res = _nthChild(len - tidx, val);
break;
case 'first-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == 0;
break;
case 'last-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == tsibs.length - 1;
break;
case 'only-of-type':
tsibs = getSibsOfType(par, tag);
res = tsibs.length == 1;
break;
case 'nth-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(n._typeIdx + 1, val);
break;
case 'nth-last-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(tsibs.length - n._typeIdx, val);
break;
}

ctx.idx--;
Expand Down
2 changes: 1 addition & 1 deletion dist/dropcss.min.js

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/html.js
Expand Up @@ -82,6 +82,28 @@ function node(parent, tagName, attrs) {
};
}

// adds ._ofTypes: {<tagName>: [...]} to parent
// adds ._typeIdx to childNodes
function getSibsOfType(par, tagName) {
if (par != null) {
let ofTypes = (par._ofTypes = par._ofTypes || {});

if (!(tagName in ofTypes)) {
let typeIdx = 0;
ofTypes[tagName] = par.childNodes.filter(n => {
if (n.tagName == tagName) {
n._typeIdx = typeIdx++;
return true;
}
});
}

return ofTypes[tagName];
}

return null;
}

function build(tokens, each) {
let targ = node(null, "root", EMPTY_SET), idx;

Expand Down Expand Up @@ -137,6 +159,8 @@ function postProc(node, idx, ctx) {
ctx.nodes.push(node);
}

exports.getSibsOfType = getSibsOfType;

exports.parse = html => {
html = html.replace(NASTIES, '');

Expand Down
23 changes: 23 additions & 0 deletions src/sel.js
@@ -1,4 +1,5 @@
const matches = require('./matches');
const { getSibsOfType } = require('./html');

// assumes stripPseudos(sel); has already been called
function parse(sel) {
Expand Down Expand Up @@ -173,9 +174,11 @@ function find(m, ctx) {
val = m[--ctx.idx];

let n = ctx.node;
let tag = n.tagName;
tidx = n.idx;
par = n.parentNode;
let len = par ? par.childNodes.length : 1;
let tsibs;

switch (name) {
case 'not':
Expand All @@ -196,6 +199,26 @@ function find(m, ctx) {
case 'nth-last-child':
res = _nthChild(len - tidx, val);
break;
case 'first-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == 0;
break;
case 'last-of-type':
tsibs = getSibsOfType(par, tag);
res = n._typeIdx == tsibs.length - 1;
break;
case 'only-of-type':
tsibs = getSibsOfType(par, tag);
res = tsibs.length == 1;
break;
case 'nth-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(n._typeIdx + 1, val);
break;
case 'nth-last-of-type':
tsibs = getSibsOfType(par, tag);
res = _nthChild(tsibs.length - n._typeIdx, val);
break;
}

ctx.idx--;
Expand Down
6 changes: 3 additions & 3 deletions test/coverage.txt
@@ -1,11 +1,11 @@
------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
------------|----------|----------|----------|----------|-------------------|
All files | 96.73 | 87.69 | 100 | 96.61 | |
All files | 96.71 | 87.98 | 100 | 96.6 | |
css.js | 97.75 | 90 | 100 | 97.67 | 109,110 |
dropcss.js | 99.14 | 97.5 | 100 | 99.07 | 163 |
html.js | 100 | 96.15 | 100 | 100 | 60 |
html.js | 98.82 | 94.12 | 100 | 98.77 | 104 |
matches.js | 100 | 100 | 100 | 100 | |
nth.js | 56.25 | 45 | 100 | 56.25 |... 38,41,47,52,54 |
sel.js | 96.6 | 88.06 | 100 | 96.55 |... 09,114,121,223 |
sel.js | 96.95 | 88.89 | 100 | 96.91 |... 10,115,122,246 |
------------|----------|----------|----------|----------|-------------------|
115 changes: 114 additions & 1 deletion test/src/3-context-aware-multi-sel.js
@@ -1,6 +1,119 @@
const dropcss = require('../../src/dropcss.js');
const assert = require('assert');

/* e.g.
.x .y + a:not(.y)
.foo > bar:not([foo*=z])
*/
*/

describe('Context-aware, multi selector', () => {
describe(':first-of-type', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img></div>',
css: '.foo:first-of-type {}',
});
assert.equal(out, '.foo:first-of-type{}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="bar"></span><img><span class="foo"></span><img></div>',
css: '.foo:first-of-type {}',
});
assert.equal(out, '');
});
});

describe(':last-of-type', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="bar"></span><span class="foo"></span><img></div>',
css: '.foo:last-of-type {}',
});
assert.equal(out, '.foo:last-of-type{}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img><span class="bar"></span><img></div>',
css: '.foo:last-of-type {}',
});
assert.equal(out, '');
});
});

describe(':only-of-type', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img></div>',
css: '.foo:only-of-type {}',
});
assert.equal(out, '.foo:only-of-type{}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img><span class="foo"></span><img></div>',
css: '.foo:only-of-type {}',
});
assert.equal(out, '');
});
});

describe('', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img></div>',
css: '.foo:only-of-type {}',
});
assert.equal(out, '.foo:only-of-type{}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img><span class="foo"></span><img></div>',
css: '.foo:only-of-type {}',
});
assert.equal(out, '');
});
});

describe(':nth-of-type()', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="bar"></span><img><span class="foo"></span><img></div>',
css: '.foo:nth-of-type(2) {}',
});
assert.equal(out, '.foo:nth-of-type(2){}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img><span class="bar"></span><img></div>',
css: '.foo:nth-of-type(2) {}',
});
assert.equal(out, '');
});
});

describe(':nth-last-of-type()', () => {
it('should retain present', function() {
let {css: out} = dropcss({
html: '<div><img><span class="bar"></span><img><span class="foo"></span><img></div>',
css: '.foo:nth-last-of-type(1) {}',
});
assert.equal(out, '.foo:nth-last-of-type(1){}');
});

it('should drop absent', function() {
let {css: out} = dropcss({
html: '<div><img><span class="foo"></span><img><span class="bar"></span><img></div>',
css: '.foo:nth-last-of-type(1) {}',
});
assert.equal(out, '');
});
});
});

0 comments on commit 46ae87c

Please sign in to comment.