Skip to content

Commit

Permalink
fix: Transform standalone pseudo class selectors (#310)
Browse files Browse the repository at this point in the history
* fix: add complex support for pseudo class selectors

* fix: add support for standalone pseudo element selectors

* fix: comments typos
  • Loading branch information
pmdartus authored May 17, 2018
1 parent 7413286 commit 9adea2d
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ describe('selectors', () => {
expect(css).toBe(`h1[x-foo_tmpl] {}`);
});

it('should handle pseudo element', async () => {
const { css } = await process('ul li:first-child a {}');
expect(css).toBe(
`ul[x-foo_tmpl] li[x-foo_tmpl]:first-child a[x-foo_tmpl] {}`,
);
it('should handle * selectors', async () => {
const { css } = await process('* {}');
expect(css).toBe(`*[x-foo_tmpl] {}`);
});

it('should handle multiple selectors', async () => {
Expand Down Expand Up @@ -44,9 +42,6 @@ describe('selectors', () => {
it('should handle complex CSS selectors', async () => {
let res;

res = await process('h1::before {}');
expect(res.css).toBe(`h1[x-foo_tmpl]::before {}`);

res = await process('h1 > a {}');
expect(res.css).toBe(`h1[x-foo_tmpl] > a[x-foo_tmpl] {}`);

Expand All @@ -64,6 +59,37 @@ describe('selectors', () => {
});
});

describe('pseudo class selectors', () => {
it('should handle simple pseudo class selectors', async () => {
const { css } = await process(':checked {}');
expect(css).toBe(`:checked[x-foo_tmpl] {}`);
});

it('should handle complex pseudo class selectors', async () => {
const { css } = await process('ul li:first-child a {}');
expect(css).toBe(
`ul[x-foo_tmpl] li:first-child[x-foo_tmpl] a[x-foo_tmpl] {}`,
);
});

it('should handle functional pseudo class selectors', async () => {
const { css } = await process(':not(p) {}');
expect(css).toBe(`:not(p)[x-foo_tmpl] {}`);
});
});

describe('pseudo element selectors', () => {
it('should handle simple pseudo element selectors', async () => {
const { css } = await process('::after {}');
expect(css).toBe(`[x-foo_tmpl]::after {}`);
});

it('should handle complex pseudo element selectors', async () => {
const { css } = await process('h1::before {}');
expect(css).toBe(`h1[x-foo_tmpl]::before {}`);
});
});

describe('custom-element', () => {
it('should handle custom element', async () => {
const { css } = await process('x-bar {}');
Expand Down
67 changes: 45 additions & 22 deletions packages/postcss-plugin-lwc/src/selector-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
combinator,
isTag,
isPseudo,
isPseudoElement,
isCombinator,
Processor,
Selector,
Expand All @@ -24,6 +25,8 @@ import {
findNode,
replaceNodeWith,
trimNodeWhitespaces,
isHostContextPseudoClass,
isHostPseudoClass,
} from './selector-utils';

const HOST_SELECTOR_PLACEHOLDER = '$HOST$';
Expand Down Expand Up @@ -122,27 +125,49 @@ function customElementSelector(selectors: Root) {
* Add scoping attributes to all the matching selectors:
* h1 -> h1[x-foo_tmpl]
* p a -> p[x-foo_tmpl] a[x-foo_tmpl]
*
* The scoping attribute should only get applied to the last selector of the
* chain: h1.active -> h1.active[x-foo_tmpl]. We need to keep track of the next selector
* candidate and add the scoping attribute before starting a new selector chain.
*/
function scopeSelector(selector: Selector, config: PluginConfig) {
let candidate: Node | undefined;
const compoundSelectors: Node[][] = [[]];

// Split the selector per compound selector. Compound selectors are interleaved with combinator nodes.
// https://drafts.csswg.org/selectors-4/#typedef-complex-selector
selector.each(node => {
const isScopableSelector = !isPseudo(node) && !isCombinator(node);
const isSelectorChainEnd = isCombinator(node) || node === selector.last;

if (isScopableSelector) {
candidate = node;
if (isCombinator(node)) {
compoundSelectors.push([]);
} else {
const current = compoundSelectors[compoundSelectors.length - 1];
current.push(node);
}
});

for (const compoundSelector of compoundSelectors) {
// Compound selectors containing :host or :host-context have a special treatment and should
// not be scoped like the rest of the complex selectors
const shouldScopeCompoundSelector = compoundSelector.every(node => {
return !isHostPseudoClass(node) && !isHostContextPseudoClass(node);
});

if (shouldScopeCompoundSelector) {
let nodeToScope: Node | undefined;

if (candidate && isSelectorChainEnd) {
selector.insertAfter(candidate, scopeAttribute(config));
candidate = undefined;
// In each compound selector we need to locate the last selector to scope.
for (const node of compoundSelector) {
if (!isPseudoElement(node)) {
nodeToScope = node;
}
}

const scopeAttributeNode = scopeAttribute(config);
if (nodeToScope) {
// Add the scoping attribute right after the node scope
selector.insertAfter(nodeToScope, scopeAttributeNode);
} else {
// Add the scoping token in the first position of the compound selector as a fallback
// when there is no node to scope. For example: ::after {}
selector.insertBefore(compoundSelector[0], scopeAttributeNode);
}
}
});
}
}

/**
Expand All @@ -152,10 +177,9 @@ function scopeSelector(selector: Selector, config: PluginConfig) {
* :host(.foo, .bar) -> $HOST$.foo, $HOST$.bar
*/
function transformHost(selector: Selector) {
const hostNode = findNode(
selector,
node => isPseudo(node) && node.value === ':host',
) as Pseudo | undefined;
const hostNode = findNode(selector, isHostPseudoClass) as
| Pseudo
| undefined;

if (hostNode) {
const placeholder = hostPlaceholder();
Expand Down Expand Up @@ -189,10 +213,9 @@ function transformHost(selector: Selector) {
* If the selector already contains :host, the selector should not be scoped twice.
*/
function transformHostContext(selector: Selector) {
const hostContextNode = findNode(
selector,
node => isPseudo(node) && node.value === ':host-context',
) as Pseudo | undefined;
const hostContextNode = findNode(selector, isHostContextPseudoClass) as
| Pseudo
| undefined;

const hostNode = findNode(selector, isHostPlaceholder);

Expand Down
10 changes: 9 additions & 1 deletion packages/postcss-plugin-lwc/src/selector-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { isTag, Node, Container, Tag } from 'postcss-selector-parser';
import { isTag, Node, Container, Tag, Pseudo, isPseudoClass } from 'postcss-selector-parser';

export function isCustomElement(node: Node): node is Tag {
return isTag(node) && node.value.includes('-');
}

export function isHostPseudoClass(node: Node): node is Pseudo {
return isPseudoClass(node) && node.value === ':host';
}

export function isHostContextPseudoClass(node: Node): node is Pseudo {
return isPseudoClass(node) && node.value === ':host-context';
}

export function findNode(
container: Container,
predicate: (node: Node) => boolean,
Expand Down

0 comments on commit 9adea2d

Please sign in to comment.