@stylable/core
Version:
CSS for Components
944 lines (943 loc) • 43.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScopeContext = exports.InferredSelector = exports.StylableTransformer = exports.transformerDiagnostics = void 0;
const is_vendor_prefixed_1 = __importDefault(require("is-vendor-prefixed"));
const postcss = __importStar(require("postcss"));
const diagnostics_1 = require("./diagnostics");
const functions_1 = require("./functions");
const native_reserved_lists_1 = require("./native-reserved-lists");
const selector_1 = require("./helpers/selector");
const eql_1 = require("./helpers/eql");
const css_selector_parser_1 = require("@tokey/css-selector-parser");
const rule_1 = require("./helpers/rule");
const resolve_1 = require("./helpers/resolve");
const features_1 = require("./features");
const stylable_resolver_1 = require("./stylable-resolver");
const css_custom_property_1 = require("./helpers/css-custom-property");
const postcss_ast_extension_1 = require("./deprecated/postcss-ast-extension");
exports.transformerDiagnostics = {
UNKNOWN_PSEUDO_ELEMENT: (0, diagnostics_1.createDiagnosticReporter)('12001', 'error', (name) => `unknown pseudo element "${name}"`),
};
class StylableTransformer {
constructor(options) {
this.directiveNodes = [];
this.containerInferredSelectorMap = new Map();
this.diagnostics = options.diagnostics;
this.keepValues = options.keepValues || false;
this.fileProcessor = options.fileProcessor;
this.replaceValueHook = options.replaceValueHook;
this.postProcessor = options.postProcessor;
this.experimentalSelectorInference = options.experimentalSelectorInference === true;
this.resolver = new stylable_resolver_1.StylableResolver(options.fileProcessor, options.requireModule, options.moduleResolver, options.resolverCache || new Map());
this.mode = options.mode || 'production';
this.defaultStVarOverride = options.stVarOverride || {};
this.getResolvedSymbols = (0, stylable_resolver_1.createSymbolResolverWithCache)(this.resolver, this.diagnostics);
this.evaluator = new functions_1.StylableEvaluator({
valueHook: this.replaceValueHook,
getResolvedSymbols: this.getResolvedSymbols,
});
}
transform(meta) {
meta.exports = {
classes: {},
vars: {},
stVars: {},
keyframes: {},
layers: {},
containers: {},
};
meta.transformedScopes = null;
meta.targetAst = meta.sourceAst.clone();
const context = {
meta,
diagnostics: this.diagnostics,
resolver: this.resolver,
evaluator: this.evaluator,
getResolvedSymbols: this.getResolvedSymbols,
};
features_1.STImport.hooks.transformInit({ context });
features_1.STGlobal.hooks.transformInit({ context });
if (!this.experimentalSelectorInference) {
meta.transformedScopes = validateScopes(this, meta);
}
this.transformAst(meta.targetAst, meta, meta.exports);
meta.transformDiagnostics = this.diagnostics;
const result = { meta, exports: meta.exports };
return this.postProcessor ? this.postProcessor(result, this) : result;
}
transformAst(ast, meta, metaExports, stVarOverride = this.defaultStVarOverride, path = [], mixinTransform = false, inferredSelectorMixin) {
if (meta.type !== 'stylable') {
return;
}
const { evaluator } = this;
const prevStVarOverride = evaluator.stVarOverride;
evaluator.stVarOverride = stVarOverride;
const transformContext = {
meta,
diagnostics: this.diagnostics,
resolver: this.resolver,
evaluator,
getResolvedSymbols: this.getResolvedSymbols,
passedThrough: path.slice(),
inferredSelectorMixin,
};
const transformResolveOptions = {
context: transformContext,
};
prepareAST(transformContext, ast, this.experimentalSelectorInference);
const cssClassResolve = features_1.CSSClass.hooks.transformResolve(transformResolveOptions);
const stVarResolve = features_1.STVar.hooks.transformResolve(transformResolveOptions);
const keyframesResolve = features_1.CSSKeyframes.hooks.transformResolve(transformResolveOptions);
const layerResolve = features_1.CSSLayer.hooks.transformResolve(transformResolveOptions);
const containsResolve = features_1.CSSContains.hooks.transformResolve(transformResolveOptions);
const cssVarsMapping = features_1.CSSCustomProperty.hooks.transformResolve(transformResolveOptions);
const handleAtRule = (atRule) => {
const { name } = atRule;
if (name === 'media') {
features_1.CSSMedia.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: {},
transformer: this,
});
}
else if (name === 'property') {
features_1.CSSCustomProperty.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: cssVarsMapping,
transformer: this,
});
}
else if (name === 'keyframes') {
features_1.CSSKeyframes.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: keyframesResolve,
transformer: this,
});
}
else if (name === 'layer') {
features_1.CSSLayer.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: layerResolve,
transformer: this,
});
}
else if (name === 'import') {
features_1.CSSLayer.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: layerResolve,
transformer: this,
});
}
else if (name === 'container') {
features_1.CSSContains.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: containsResolve,
transformer: this,
});
}
else if (name === 'st-scope') {
features_1.STScope.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: containsResolve,
transformer: this,
});
}
else if (name === 'custom-selector') {
features_1.STCustomSelector.hooks.transformAtRuleNode({
context: transformContext,
atRule,
resolved: containsResolve,
transformer: this,
});
}
};
const handleDeclaration = (decl) => {
if ((0, css_custom_property_1.validateCustomPropertyName)(decl.prop)) {
features_1.CSSCustomProperty.hooks.transformDeclaration({
context: transformContext,
decl,
resolved: cssVarsMapping,
});
}
else if (decl.prop === `animation` || decl.prop === `animation-name`) {
features_1.CSSKeyframes.hooks.transformDeclaration({
context: transformContext,
decl,
resolved: keyframesResolve,
});
}
else if (decl.prop === 'container' || decl.prop === 'container-name') {
features_1.CSSContains.hooks.transformDeclaration({
context: transformContext,
decl,
resolved: containsResolve,
});
}
if (decl.prop.startsWith('-st-')) {
if (this.mode === 'production') {
this.directiveNodes.push(decl);
}
return;
}
decl.value = this.evaluator.evaluateValue(transformContext, {
value: decl.value,
meta,
node: decl,
}).outputValue;
};
ast.walk((node) => {
if (node.type === 'rule') {
if ((0, rule_1.isChildOfAtRule)(node, 'keyframes')) {
return;
}
// get context inferred selector
let currentParent = node.parent;
while (currentParent && !this.containerInferredSelectorMap.has(currentParent)) {
currentParent = currentParent.parent;
}
// transform selector
const { selector, inferredSelector } = this.scopeSelector(meta, node.selector, node, currentParent && this.containerInferredSelectorMap.get(currentParent), inferredSelectorMixin);
// save results
this.containerInferredSelectorMap.set(node, inferredSelector);
node.selector = selector;
}
else if (node.type === 'atrule') {
handleAtRule(node);
}
else if (node.type === 'decl') {
handleDeclaration(node);
}
});
if (!mixinTransform && meta.targetAst && this.mode === 'development') {
features_1.CSSClass.addDevRules(transformContext);
}
const lastPassParams = {
context: transformContext,
ast,
transformer: this,
path,
};
if (this.experimentalSelectorInference) {
features_1.STScope.hooks.transformLastPass(lastPassParams);
}
features_1.STMixin.hooks.transformLastPass(lastPassParams);
if (!mixinTransform) {
features_1.STGlobal.hooks.transformLastPass(lastPassParams);
for (const node of this.directiveNodes) {
node.remove();
}
}
if (metaExports) {
features_1.CSSClass.hooks.transformJSExports({
exports: metaExports,
resolved: cssClassResolve,
});
features_1.STVar.hooks.transformJSExports({
exports: metaExports,
resolved: stVarResolve,
});
features_1.CSSKeyframes.hooks.transformJSExports({
exports: metaExports,
resolved: keyframesResolve,
});
features_1.CSSLayer.hooks.transformJSExports({
exports: metaExports,
resolved: layerResolve,
});
features_1.CSSContains.hooks.transformJSExports({
exports: metaExports,
resolved: containsResolve,
});
features_1.CSSCustomProperty.hooks.transformJSExports({
exports: metaExports,
resolved: cssVarsMapping,
});
}
// restore evaluator state
this.evaluator.stVarOverride = prevStVarOverride;
}
resolveSelectorElements(meta, selector) {
return this.scopeSelector(meta, selector).elements;
}
scopeSelector(originMeta, selector, selectorNode, inferredNestSelector, inferredMixinSelector, unwrapGlobals = false) {
const context = this.createSelectorContext(originMeta, (0, selector_1.parseSelectorWithCache)(selector, { clone: true }), selectorNode || postcss.rule({ selector }), selector, inferredNestSelector, inferredMixinSelector);
const targetSelectorAst = this.scopeSelectorAst(context);
if (unwrapGlobals) {
features_1.STGlobal.unwrapPseudoGlobals(targetSelectorAst);
}
return {
targetSelectorAst,
selector: (0, selector_1.stringifySelector)(targetSelectorAst),
elements: context.elements,
inferredSelector: context.inferredMultipleSelectors,
};
}
createSelectorContext(meta, selectorAst, selectorNode, selectorStr, selectorNest, selectorMixin) {
return new ScopeContext(meta, this.resolver, selectorAst, selectorNode, this.scopeSelectorAst.bind(this), this, selectorNest, selectorMixin, undefined, selectorStr);
}
createInferredSelector(meta, { name, type }) {
const resolvedSymbols = this.getResolvedSymbols(meta);
const resolved = resolvedSymbols[type][name];
return new InferredSelector(this, resolved);
}
scopeSelectorAst(context) {
// group compound selectors: .a.b .c:hover, a .c:hover -> [[[.a.b], [.c:hover]], [[.a], [.c:hover]]]
const selectorList = (0, css_selector_parser_1.groupCompoundSelectors)(context.selectorAst);
// loop over selectors
for (const selector of selectorList) {
context.elements.push([]);
context.selectorIndex++;
context.selector = selector;
// loop over nodes
for (const node of [...selector.nodes]) {
if (node.type !== `compound_selector`) {
if (node.type === 'combinator') {
if (this.experimentalSelectorInference || context.isNested) {
context.setNextSelectorScope(context.inferredSelectorContext, node);
}
}
continue;
}
context.compoundSelector = node;
// loop over each node in a compound selector
for (const compoundNode of node.nodes) {
if (compoundNode.type === 'universal' && this.experimentalSelectorInference) {
context.setNextSelectorScope([
{
_kind: 'css',
meta: context.originMeta,
symbol: features_1.CSSType.createSymbol({ name: '*' }),
},
], node);
}
context.node = compoundNode;
// transform node
this.handleCompoundNode(context);
}
}
// add inferred selector end to multiple selector
context.inferredMultipleSelectors.add(context.inferredSelector);
if (selectorList.length - 1 > context.selectorIndex) {
// reset current anchor for all except last selector
context.inferredSelector = new InferredSelector(this, context.inferredSelectorStart);
}
}
// backwards compatibility for elements - empty selector still have an empty first target
if (selectorList.length === 0) {
context.elements.push([]);
}
const targetAst = (0, css_selector_parser_1.splitCompoundSelectors)(selectorList);
context.splitSelectors.duplicateSelectors(targetAst);
for (let i = 0; i < targetAst.length; i++) {
context.selectorAst[i] = targetAst[i];
}
return targetAst;
}
handleCompoundNode(context) {
const { inferredSelector, node, originMeta } = context;
const transformerContext = {
meta: originMeta,
diagnostics: this.diagnostics,
resolver: this.resolver,
evaluator: this.evaluator,
getResolvedSymbols: this.getResolvedSymbols,
};
if (node.type === 'class') {
features_1.CSSClass.hooks.transformSelectorNode({
context: transformerContext,
selectorContext: context,
node,
});
}
else if (node.type === 'type') {
features_1.CSSType.hooks.transformSelectorNode({
context: transformerContext,
selectorContext: context,
node,
});
}
else if (node.type === 'pseudo_element') {
if (node.value === ``) {
// partial psuedo elemennt: `.x::`
// ToDo: currently the transformer corrects the css without warning,
// should stylable warn?
return;
}
const inferredElement = inferredSelector.getPseudoElements({
isFirstInSelector: context.isFirstInSelector(node),
name: node.value,
experimentalSelectorInference: this.experimentalSelectorInference,
})[node.value];
if (inferredElement) {
context.setNextSelectorScope(inferredElement.inferred, node, node.value);
if (context.transform) {
context.transformIntoMultiSelector(node, inferredElement.selectors);
}
}
else {
// first definition of a part in the extends/alias chain
context.setNextSelectorScope([], node, node.value);
if (!native_reserved_lists_1.nativePseudoElements.includes(node.value) &&
!(0, is_vendor_prefixed_1.default)(node.value) &&
!context.isDuplicateStScopeDiagnostic()) {
this.diagnostics.report(exports.transformerDiagnostics.UNKNOWN_PSEUDO_ELEMENT(node.value), {
node: context.ruleOrAtRule,
word: node.value,
});
}
}
}
else if (node.type === 'pseudo_class') {
const isCustomSelector = features_1.STCustomSelector.hooks.transformSelectorNode({
context: transformerContext,
selectorContext: context,
node,
});
if (!isCustomSelector) {
features_1.CSSPseudoClass.hooks.transformSelectorNode({
context: transformerContext,
selectorContext: context,
node,
});
}
}
else if (node.type === `nesting`) {
context.setNextSelectorScope(context.inferredSelectorNest, node, node.value);
}
else if (node.type === 'attribute') {
features_1.STMixin.hooks.transformSelectorNode({
context: transformerContext,
selectorContext: context,
node,
});
}
}
}
exports.StylableTransformer = StylableTransformer;
function validateScopes(transformer, meta) {
const transformedScopes = {};
for (const scope of meta.scopes) {
const len = transformer.diagnostics.reports.length;
const rule = postcss.rule({ selector: scope.params });
const context = transformer.createSelectorContext(meta, (0, selector_1.parseSelectorWithCache)(rule.selector, { clone: true }), rule, rule.selector);
transformedScopes[rule.selector] = (0, css_selector_parser_1.groupCompoundSelectors)(transformer.scopeSelectorAst(context));
const ruleReports = transformer.diagnostics.reports.splice(len);
for (const { code, message, severity, word } of ruleReports) {
transformer.diagnostics.report({
code,
message,
severity,
}, {
node: scope,
word: word || scope.params,
});
}
}
return transformedScopes;
}
function removeInitialCompoundMarker(selector, meta, structureMode) {
let hadCompoundStart = false;
const compoundedSelector = (0, css_selector_parser_1.groupCompoundSelectors)(selector);
const first = compoundedSelector.nodes.find(({ type }) => type === `compound_selector`);
if (first) {
const matchNode = structureMode
? (node) => node.type === 'nesting'
: (node) => node.type === 'class' && node.value === meta.root;
for (let i = 0; i < first.nodes.length; i++) {
const node = first.nodes[i];
if (node.type === 'comment') {
continue;
}
if (matchNode(node)) {
hadCompoundStart = true;
first.nodes.splice(i, 1);
}
break;
}
}
return { selector: (0, css_selector_parser_1.splitCompoundSelectors)(compoundedSelector), hadCompoundStart };
}
class InferredSelector {
constructor(api, resolve) {
this.api = api;
this.resolveSet = new Set();
if (resolve) {
this.add(resolve);
}
}
isEmpty() {
return this.resolveSet.size === 0;
}
set(resolve) {
if (resolve === this) {
return;
}
this.resolveSet.clear();
this.add(resolve);
}
clone() {
return new InferredSelector(this.api, this);
}
/**
* Adds to the set of inferred resolved CSS
* Assumes passes CSSResolved from the same meta/symbol are
* the same from the same cached transform process to dedupe them.
*/
add(resolve) {
if (resolve instanceof InferredSelector) {
resolve.resolveSet.forEach((resolve) => this.add(resolve));
}
else {
this.resolveSet.add(resolve);
}
}
/**
* Takes a CSS part resolve and use it extend the current set of inferred resolved.
* Used to expand the resolved mapped selector with the part definition
* e.g. part can add nested states/parts that override the inferred mapped selector.
*/
addPartOverride(partResolve) {
const newSet = new Set();
for (const resolve of this.resolveSet) {
newSet.add([partResolve, ...resolve]);
}
if (!this.resolveSet.size) {
newSet.add([partResolve]);
}
this.resolveSet = newSet;
}
getPseudoClasses({ name: searchedName } = {}) {
const collectedStates = {};
const resolvedCount = {};
const expectedIntersectionCount = this.resolveSet.size; // ToDo: dec for any types
const addInferredState = (name, meta, state) => {
const existing = collectedStates[name];
if (!existing) {
collectedStates[name] = { meta, state };
resolvedCount[name] = 1;
}
else {
const isStatesEql = (0, eql_1.isEqual)(existing.state, state);
if (isStatesEql &&
// states from same meta
(existing.meta === meta ||
// global states
typeof state === 'string' ||
state?.type === 'template')) {
resolvedCount[name]++;
}
}
};
// infer states from multiple resolved selectors
for (const resolvedContext of this.resolveSet.values()) {
const resolvedFoundNames = new Set();
resolved: for (const { symbol, meta } of resolvedContext) {
const states = symbol[`-st-states`];
if (!states) {
continue;
}
if (searchedName) {
if (Object.hasOwnProperty.call(states, searchedName)) {
// track state
addInferredState(searchedName, meta, states[searchedName]);
break resolved;
}
}
else {
// get all states
for (const [name, state] of Object.entries(states)) {
if (!resolvedFoundNames.has(name)) {
// track state
resolvedFoundNames.add(name);
addInferredState(name, meta, state);
}
}
}
}
}
// strict: remove states that do not exist on ALL resolved selectors
return expectedIntersectionCount > 1
? Object.entries(collectedStates).reduce((resultStates, [name, InferredState]) => {
if (resolvedCount[name] >= expectedIntersectionCount) {
resultStates[name] = InferredState;
}
return resultStates;
}, {})
: collectedStates;
}
getPseudoElements({ isFirstInSelector, experimentalSelectorInference, name, }) {
const collectedElements = {};
const resolvedCount = {};
const checked = {};
const expectedIntersectionCount = this.resolveSet.size; // ToDo: dec for any types
const addInferredElement = (name, inferred, selectors) => {
const item = (collectedElements[name] || (collectedElements[name] = {
inferred: new InferredSelector(this.api),
selectors: [],
}));
// check inferred matching
if (!item.inferred.matchedElement(inferred)) {
// ToDo: bailout fast
return;
}
// add match
resolvedCount[name]++;
item.inferred.add(inferred);
item.selectors.push(...selectors);
};
// infer elements from multiple resolved selectors
for (const resolvedContext of this.resolveSet.values()) {
/**
* search for elements in each resolved selector.
* start at 1 for legacy flat mode to prefer inherited elements over local
*/
const startIndex = resolvedContext.length === 1 ||
(resolvedContext[0] &&
(features_1.STStructure.isStructureMode(resolvedContext[0].meta) ||
resolvedContext[0].symbol._kind === 'part'))
? 0
: 1;
resolved: for (let i = startIndex; i < resolvedContext.length; i++) {
const { symbol, meta } = resolvedContext[i];
const structureMode = features_1.STStructure.isStructureMode(meta);
if (symbol._kind !== 'part' &&
(symbol.alias || (!structureMode && !symbol['-st-root']))) {
// non-root & alias classes don't have parts: bailout
continue;
}
if (name) {
const cacheContext = symbol._kind === 'part' ? symbol.id : symbol.name;
const uniqueId = meta.source + '::' + cacheContext;
resolvedCount[name] ?? (resolvedCount[name] = 0);
checked[name] || (checked[name] = new Map());
if (checked[name].has(uniqueId)) {
if (checked[name].get(uniqueId)) {
resolvedCount[name]++;
}
continue;
}
// get part symbol
const partDef = features_1.STStructure.getPart(symbol, name);
// save to cache
checked[name].set(uniqueId, !!partDef);
if (!partDef) {
continue;
}
if (Array.isArray(partDef.mapTo)) {
// prefer custom selector
const selectorList = (0, selector_1.cloneSelector)(partDef.mapTo);
const selectorStr = (0, selector_1.stringifySelector)(partDef.mapTo);
selectorList.forEach((selector) => {
const r = removeInitialCompoundMarker(selector, meta, structureMode);
selector.nodes = r.selector.nodes;
selector.before = '';
if (!r.hadCompoundStart && !isFirstInSelector) {
selector.nodes.unshift((0, selector_1.createCombinatorSelector)({ combinator: 'space' }));
}
});
const internalContext = this.api.createSelectorContext(meta, selectorList, postcss.rule({ selector: selectorStr }), selectorStr);
internalContext.isStandaloneSelector = isFirstInSelector;
if (!structureMode && experimentalSelectorInference) {
internalContext.inferredSelectorStart.set(this.api.createInferredSelector(meta, {
name: 'root',
type: 'class',
}));
internalContext.inferredSelector.set(internalContext.inferredSelectorStart);
}
const customAstSelectors = this.api.scopeSelectorAst(internalContext);
const inferred = customAstSelectors.length === 1 || experimentalSelectorInference
? internalContext.inferredMultipleSelectors
: new InferredSelector(this.api, [
{
_kind: 'css',
meta,
symbol: features_1.CSSType.createSymbol({ name: '*' }),
},
]);
// add part resolve to inferred resolve set
if (structureMode) {
inferred.addPartOverride({ _kind: 'css', meta, symbol: partDef });
}
addInferredElement(name, inferred, customAstSelectors);
break resolved;
}
else {
// matching class part
const resolvedPart = this.api.getResolvedSymbols(meta).class[name];
const resolvedBaseSymbol = (0, resolve_1.getOriginDefinition)(resolvedPart);
const nodes = [];
// insert descendant combinator before internal custom element
if (!resolvedBaseSymbol.symbol[`-st-root`] && !isFirstInSelector) {
nodes.push((0, selector_1.createCombinatorSelector)({ combinator: 'space' }));
}
// create part class
const classNode = {};
features_1.CSSClass.namespaceClass(resolvedBaseSymbol.meta, resolvedBaseSymbol.symbol, classNode);
nodes.push(classNode);
addInferredElement(name, new InferredSelector(this.api, resolvedPart), [
{ type: 'selector', after: '', before: '', end: 0, start: 0, nodes },
]);
break resolved;
}
}
else {
// ToDo: implement get all elements
}
}
}
// strict: remove elements that do not exist on ALL resolved selectors
return expectedIntersectionCount > 1
? Object.entries(collectedElements).reduce((resultElements, [name, InferredElement]) => {
if (resolvedCount[name] >= expectedIntersectionCount) {
resultElements[name] = InferredElement;
}
return resultElements;
}, {})
: collectedElements;
}
matchedElement(inferred) {
for (const target of this.resolveSet) {
const targetBaseElementSymbol = (0, resolve_1.getOriginDefinition)(target);
for (const tested of inferred.resolveSet) {
const testedBaseElementSymbol = (0, resolve_1.getOriginDefinition)(tested);
if (targetBaseElementSymbol !== testedBaseElementSymbol) {
return false;
}
}
}
return true;
}
// function to temporarily handle single resolved selector type while refactoring
// ToDo: remove temporarily single resolve
getSingleResolve() {
if (this.resolveSet.size !== 1) {
return [];
}
return this.resolveSet.values().next().value;
}
}
exports.InferredSelector = InferredSelector;
class SelectorMultiplier {
constructor() {
this.dupIndicesPerSelector = [];
}
addSplitPoint(selectorIndex, nodeIndex, selectors) {
var _a;
if (selectors.length) {
(_a = this.dupIndicesPerSelector)[selectorIndex] || (_a[selectorIndex] = []);
this.dupIndicesPerSelector[selectorIndex].push([nodeIndex, selectors]);
}
}
duplicateSelectors(targetSelectors) {
// iterate top level selector
for (const [selectorIndex, insertionPoints] of Object.entries(this.dupIndicesPerSelector)) {
const duplicationList = [targetSelectors[Number(selectorIndex)]];
// iterate insertion points
for (const [nodeIndex, selectors] of insertionPoints) {
// collect the duplicate selectors to be multiplied by following insertion points
const added = [];
// iterate selectors for insertion point
for (const replaceSelector of selectors) {
// duplicate selectors and replace selector at insertion point
for (const originSelector of duplicationList) {
const dupSelector = { ...originSelector, nodes: [...originSelector.nodes] };
dupSelector.nodes[nodeIndex] = replaceSelector;
added.push(dupSelector);
}
}
// add the duplicated selectors from insertion point to
// the list of selector to be duplicated for following insertion
// points and to the target selector list
for (const addedSelector of added) {
duplicationList.push(addedSelector);
targetSelectors.push(addedSelector);
}
}
}
}
}
class ScopeContext {
constructor(originMeta, resolver, selectorAst, ruleOrAtRule, scopeSelectorAst, transformer, inferredSelectorNest, inferredSelectorMixin, inferredSelectorContext, selectorStr) {
this.originMeta = originMeta;
this.resolver = resolver;
this.selectorAst = selectorAst;
this.ruleOrAtRule = ruleOrAtRule;
this.scopeSelectorAst = scopeSelectorAst;
this.transformer = transformer;
this.inferredSelectorMixin = inferredSelectorMixin;
this.transform = true;
// source multi-selector input
this.selectorStr = '';
this.selectorIndex = -1;
this.elements = [];
this.selectorAstResolveMap = new Map();
// store selector duplication points
this.splitSelectors = new SelectorMultiplier();
// selector is not a continuation of another selector
this.isStandaloneSelector = true;
// combined type of the multiple selectors
this.inferredMultipleSelectors = new InferredSelector(this.transformer);
this.isNested = !!(ruleOrAtRule.parent &&
// top level
ruleOrAtRule.parent.type !== 'root' &&
// directly in @st-scope
!features_1.STScope.isStScopeStatement(ruleOrAtRule.parent));
/*
resolve default selector context for initial selector and selector
following a combinator.
Currently set to stylesheet root for top level selectors and selectors
directly nested under @st-scope. But will change in the future to a universal selector
once experimentalSelectorInference will be the default behavior
*/
const inferredContext = inferredSelectorContext ||
(this.isNested || transformer.experimentalSelectorInference
? new InferredSelector(transformer, [
{
_kind: 'css',
meta: originMeta,
symbol: features_1.CSSType.createSymbol({ name: '*' }),
},
])
: transformer.createInferredSelector(originMeta, {
name: originMeta.root,
type: 'class',
}));
// set selector data
this.selectorStr = selectorStr || (0, selector_1.stringifySelector)(selectorAst);
this.inferredSelectorContext = new InferredSelector(this.transformer, inferredContext);
this.inferredSelectorStart = new InferredSelector(this.transformer, inferredContext);
this.inferredSelectorNest = inferredSelectorNest || this.inferredSelectorContext.clone();
this.inferredSelector = new InferredSelector(this.transformer, this.inferredSelectorContext);
}
get experimentalSelectorInference() {
return this.transformer.experimentalSelectorInference;
}
setNextSelectorScope(resolved, node, name) {
if (name && this.selectorIndex !== undefined && this.selectorIndex !== -1) {
this.elements[this.selectorIndex].push({
type: ScopeContext.legacyElementsTypesMapping[node.type] || 'unknown',
name,
resolved: Array.isArray(resolved) ? resolved : resolved.getSingleResolve(),
});
}
this.inferredSelector.set(resolved);
this.selectorAstResolveMap.set(node, this.inferredSelector.clone());
this.lastInferredSelectorNode = node;
}
isFirstInSelector(node) {
const isFirstNode = this.selectorAst[this.selectorIndex].nodes[0] === node;
if (isFirstNode && this.selectorIndex === 0 && !this.isStandaloneSelector) {
// force false incase a this context is a splitted part from another selector
return false;
}
return isFirstNode;
}
createNestedContext(selectorAst, selectorContext) {
const ctx = new ScopeContext(this.originMeta, this.resolver, selectorAst, this.ruleOrAtRule, this.scopeSelectorAst, this.transformer, this.inferredSelectorNest, this.inferredSelectorMixin, selectorContext || this.inferredSelectorContext);
ctx.transform = this.transform;
ctx.selectorAstResolveMap = this.selectorAstResolveMap;
return ctx;
}
transformIntoMultiSelector(node, selectors) {
// transform into the first selector
Object.assign(node, selectors[0]);
// keep track of additional selectors for
// duplication at the end of the selector transform
selectors.shift();
const selectorNode = this.selectorAst[this.selectorIndex];
const nodeIndex = selectorNode.nodes.indexOf(node);
this.splitSelectors.addSplitPoint(this.selectorIndex, nodeIndex, selectors);
}
isDuplicateStScopeDiagnostic() {
if (this.experimentalSelectorInference || this.ruleOrAtRule.type !== 'rule') {
// this check is not required when experimentalSelectorInference is on
// as @st-scope is not flatten at the beginning of the transformation
// and diagnostics on it's selector is only checked once.
return false;
}
// ToDo: should be removed once st-scope transformation moves to the end of the transform process
const transformedScope = this.originMeta.transformedScopes?.[(0, postcss_ast_extension_1.getRuleScopeSelector)(this.ruleOrAtRule) || ``];
if (transformedScope && this.selector && this.compoundSelector) {
const currentCompoundSelector = (0, selector_1.stringifySelector)(this.compoundSelector);
const i = this.selector.nodes.indexOf(this.compoundSelector);
for (const stScopeSelectorCompounded of transformedScope) {
// if we are in a chunk index that is in the rage of the @st-scope param
if (i <= stScopeSelectorCompounded.nodes.length) {
for (const scopeNode of stScopeSelectorCompounded.nodes) {
const scopeNodeSelector = (0, selector_1.stringifySelector)(scopeNode);
// if the two chunks match the error is already reported by the @st-scope validation
if (scopeNodeSelector === currentCompoundSelector) {
return true;
}
}
}
}
}
return false;
}
}
exports.ScopeContext = ScopeContext;
ScopeContext.legacyElementsTypesMapping = {
pseudo_element: 'pseudo-element',
class: 'class',
type: 'element',
};
/**
* in the process of moving transformations that shouldn't be in the analyzer.
* all changes were moved here to be called at the beginning of the transformer,
* and should be inlined in the process in the future.
*/
function prepareAST(context, ast, experimentalSelectorInference) {
// ToDo: inline transformations
const toRemove = [];
ast.walk((node) => {
const input = { context, node, toRemove };
features_1.STNamespace.hooks.prepareAST(input);
features_1.STImport.hooks.prepareAST(input);
if (!experimentalSelectorInference) {
features_1.STScope.hooks.prepareAST(input);
}
features_1.STVar.hooks.prepareAST(input);
if (!experimentalSelectorInference) {
features_1.STCustomSelector.hooks.prepareAST(input);
}
features_1.CSSCustomProperty.hooks.prepareAST(input);
});
for (const removeOrNode of toRemove) {
typeof removeOrNode === 'function' ? removeOrNode() : removeOrNode.remove();
}
}
//# sourceMappingURL=stylable-transformer.js.map