UNPKG

@lwc/style-compiler

Version:

Transform style sheet to be consumed by the LWC engine

153 lines (133 loc) 4.95 kB
/* * Copyright (c) 2018, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isPseudoElement, isCombinator, isPseudoClass, Selector, Root, Node, Pseudo, Tag, attribute, } from 'postcss-selector-parser'; import validateSelectors from './validate'; import { findNode, replaceNodeWith, trimNodeWhitespaces, } from '../utils/selector-parser'; import { SHADOW_ATTRIBUTE, HOST_ATTRIBUTE } from '../utils/selectors-scoping'; export interface SelectorScopingConfig { /** When set to true, the :host selector gets replace with the the scoping token. */ transformHost: boolean; } function isHostPseudoClass(node: Node): node is Pseudo { return isPseudoClass(node) && node.value === ':host'; } /** * Add scoping attributes to all the matching selectors: * h1 -> h1[x-foo_tmpl] * p a -> p[x-foo_tmpl] a[x-foo_tmpl] */ function scopeSelector(selector: Selector) { 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 => { if (isCombinator(node)) { compoundSelectors.push([]); } else { const current = compoundSelectors[compoundSelectors.length - 1]; current.push(node); } }); for (const compoundSelector of compoundSelectors) { // Compound selectors containing :host have a special treatment and should not be scoped like the rest of the // complex selectors. const shouldScopeCompoundSelector = compoundSelector.every(node => !isHostPseudoClass(node)); if (shouldScopeCompoundSelector) { let nodeToScope: Node | 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 shadowAttribute = attribute({ attribute: SHADOW_ATTRIBUTE, value: undefined, raws: {}, }); if (nodeToScope) { // Add the scoping attribute right after the node scope selector.insertAfter(nodeToScope, shadowAttribute); } 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], shadowAttribute); } } } } /** * Mark the :host selector with a placeholder. If the selector has a list of * contextual selector it will generate a rule for each of them. * :host -> [x-foo_tmpl-host] * :host(.foo, .bar) -> [x-foo_tmpl-host].foo, [x-foo_tmpl-host].bar */ function transformHost(selector: Selector) { // Locate the first :host pseudo-class const hostNode = findNode(selector, isHostPseudoClass) as | Pseudo | undefined; if (hostNode) { // Store the original location of the :host in the selector const hostIndex = selector.index(hostNode); // Swap the :host pseudo-class with the host scoping token const hostAttribute = attribute({ attribute: HOST_ATTRIBUTE, value: undefined, raws: {}, }); hostNode.replaceWith(hostAttribute); // Generate a unique contextualized version of the selector for each selector pass as argument // to the :host const contextualSelectors = hostNode.nodes.map( (contextSelectors: Selector) => { const clonedSelector = selector.clone({}) as Selector; const clonedHostNode = clonedSelector.at(hostIndex) as Tag; // Add to the compound selector previously containing the :host pseudo class // the contextual selectors. contextSelectors.each(node => { trimNodeWhitespaces(node); clonedSelector.insertAfter(clonedHostNode, node); }); return clonedSelector; }, ); // Replace the current selector with the different variants replaceNodeWith(selector, ...contextualSelectors); } } export default function transformSelector( root: Root, transformConfig: SelectorScopingConfig, ) { validateSelectors(root); root.each((selector: Selector) => { scopeSelector(selector); }); if (transformConfig.transformHost) { root.each((selector: Selector) => { transformHost(selector); }); } }