Skip to content

Commit

Permalink
feat: add CSS selector generator
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Jan 21, 2017
1 parent 155da6b commit 378853b
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 1 deletion.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,34 @@ This parser is implemented using [Earley parser algorithm](https://en.wikipedia.

```js
import {
createGenerator,
createParser
} from 'scalpel';

const generator = createGenerator();
const parser = createParser();

const tokens: SelectorTokenType = parser.parse('.foo.bar');
const tokens: Array<SelectorTokenType | CombinatorTokenType> = parser.parse('.foo.bar');

// [
// {
// type: 'selector',
// body: [
// {
// type: 'classSelector',
// name: 'foo'
// },
// {
// type: 'classSelector',
// name: 'bar'
// }
// ]
// }
// ]

const selector: string = generator.generate(token);

// .foo.bar

```

Expand Down
78 changes: 78 additions & 0 deletions src/createGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow

import type {
CombinatorTokenType,
SelectorTokenType
} from './types';

const escapeValue = (value: string): string => {
return JSON.stringify(value);
};

const renderSelector = (selectorToken: SelectorTokenType) => {
const tokens = selectorToken.body;
const parts = [];

for (const token of tokens) {
let part;

if (token.type === 'universalSelector') {
part = '*';
} else if (token.type === 'typeSelector') {
part = token.name;
} else if (token.type === 'idSelector') {
part = '#' + token.name;
} else if (token.type === 'classSelector') {
part = '.' + token.name;
} else if (token.type === 'attributePresenceSelector') {
part = '[' + token.name + ']';
} else if (token.type === 'attributeValueSelector') {
part = '[' + token.name + token.operator + escapeValue(token.value) + ']';
} else if (token.type === 'pseudoClassSelector') {
part = ':' + token.name;

if (token.parameters.length) {
part += '(' + token.parameters.map(escapeValue).join(', ') + ')';
}
} else if (token.type === 'pseudoElementSelector') {
part = '::' + token.name;
} else {
throw new Error('Unknown token.');
}

parts.push(part);
}

return parts.join('');
};

export default () => {
const generate = (tokens: Array<SelectorTokenType | CombinatorTokenType>): string => {
/**
* @todo Think of a better name. This array contains selectors or combinators.
*/
const sequences: Array<string> = [];

for (const token of tokens) {
if (token.type === 'selector') {
sequences.push(renderSelector(token));
} else if (token.type === 'descendantCombinator') {
sequences.push(' ');
} else if (token.type === 'childCombinator') {
sequences.push(' > ');
} else if (token.type === 'adjacentSiblingCombinator') {
sequences.push(' + ');
} else if (token.type === 'generalSiblingCombinator') {
sequences.push(' ~ ');
} else {
throw new Error('Unknown token.');
}
}

return sequences.join('');
};

return {
generate
};
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @flow

import createGenerator from './createGenerator';
import createParser from './createParser';

export {
createGenerator,
createParser
};
43 changes: 43 additions & 0 deletions test/generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @flow

import test from 'ava';
import {
generate,
parse
} from './helpers';

const MIRROR = {};

/* eslint-disable sort-keys */
const validSelectors = {
'*': MIRROR,
'::grault': MIRROR,
'foo > bar > baz': MIRROR,
'foo ~ bar ~ baz': MIRROR,
'foo + bar + baz': MIRROR,
'foo bar baz': MIRROR,
'foo:corge()': 'foo:corge',
'foo:corge(foo)': 'foo:corge("foo")',
'foo.baz': MIRROR,
'foo[qux]': MIRROR,
'foo[qux^="val1"]': MIRROR,
'foo[qux="val1"]': MIRROR,
'foo[qux=\'val1\']': 'foo[qux="val1"]',
'foo[qux=val1]': 'foo[qux="val1"]',
'foo#bar.baz': MIRROR,
'foo#bar': MIRROR
};

/* eslint-enable */

for (const [input, output] of Object.entries(validSelectors)) {
const expectedResult = output === MIRROR ? input : output;

if (typeof expectedResult !== 'string') {
throw new Error('Unexpected state.');
}

test('\ninput:\t' + input + '\noutput:\t' + expectedResult, (t): void => {
t.true(expectedResult === generate(parse(input)));
});
}
11 changes: 11 additions & 0 deletions test/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
// @flow

import type {
CombinatorTokenType,
SelectorTokenType
} from '../../src/types';
import {
createGenerator,
createParser
} from '../../src';

export const generate = (selectors: Array<SelectorTokenType | CombinatorTokenType>) => {
const generator = createGenerator();

return generator.generate(selectors);
};

export const parse = (selector: string) => {
const parser = createParser();

Expand Down

0 comments on commit 378853b

Please sign in to comment.