diff --git a/README.md b/README.md index b894127..1d554a7 100644 --- a/README.md +++ b/README.md @@ -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 = parser.parse('.foo.bar'); + +// [ +// { +// type: 'selector', +// body: [ +// { +// type: 'classSelector', +// name: 'foo' +// }, +// { +// type: 'classSelector', +// name: 'bar' +// } +// ] +// } +// ] + +const selector: string = generator.generate(token); + +// .foo.bar ``` diff --git a/src/createGenerator.js b/src/createGenerator.js new file mode 100644 index 0000000..dea90c6 --- /dev/null +++ b/src/createGenerator.js @@ -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): string => { + /** + * @todo Think of a better name. This array contains selectors or combinators. + */ + const sequences: Array = []; + + 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 + }; +}; diff --git a/src/index.js b/src/index.js index 13a2d40..8b08060 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ // @flow +import createGenerator from './createGenerator'; import createParser from './createParser'; export { + createGenerator, createParser }; diff --git a/test/generator.js b/test/generator.js new file mode 100644 index 0000000..c02b937 --- /dev/null +++ b/test/generator.js @@ -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))); + }); +} diff --git a/test/helpers/index.js b/test/helpers/index.js index 08d2247..b3a03ff 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,9 +1,20 @@ // @flow +import type { + CombinatorTokenType, + SelectorTokenType +} from '../../src/types'; import { + createGenerator, createParser } from '../../src'; +export const generate = (selectors: Array) => { + const generator = createGenerator(); + + return generator.generate(selectors); +}; + export const parse = (selector: string) => { const parser = createParser();