From b2063ee0a88aaa783d2d2261f5d9a9379064ec09 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Sun, 23 Apr 2023 19:14:38 +0300 Subject: [PATCH] feat: support CSS nesting in CSS mixin (#2855) * refactor: mark mixin anchor with unique selector * refactor: transform new mixin anchor * refactor: infer mixin marker in transformation * test: add skipped test to check mixin of CSS nesting * refactor: move asserters to core-test-kit * fix: handle nested when merging CSS fragment * feat: support nesting in CSS mixin * refactor: change marker to attribute * refactor: moved default arg to function signature * refactor: removed unnecessary spread --- packages/core-test-kit/src/index.ts | 1 + .../core-test-kit/src/postcss-node-asserts.ts | 26 ++++ packages/core/src/features/st-mixin.ts | 31 ++++- packages/core/src/helpers/rule.ts | 35 +++-- packages/core/src/helpers/selector.ts | 6 +- packages/core/src/stylable-transformer.ts | 22 ++- packages/core/src/stylable-utils.ts | 23 +-- packages/core/src/stylable.ts | 2 +- packages/core/test/features/st-mixin.spec.ts | 35 +++++ packages/core/test/helpers/rule.spec.ts | 63 +++++---- packages/core/test/helpers/selector.spec.ts | 131 ++++++++++++++++++ .../test/test-kit/postcss-node-asserts.ts | 26 +--- 12 files changed, 322 insertions(+), 79 deletions(-) create mode 100644 packages/core-test-kit/src/postcss-node-asserts.ts diff --git a/packages/core-test-kit/src/index.ts b/packages/core-test-kit/src/index.ts index c151b222b..185c78f9c 100644 --- a/packages/core-test-kit/src/index.ts +++ b/packages/core-test-kit/src/index.ts @@ -29,3 +29,4 @@ export { testStylableCore } from './test-stylable-core'; export { deindent } from './deindent'; export { MinimalDocument, MinimalElement } from './minimal-dom'; export { createTempDirectorySync, copyDirectory } from './native-temp-dir'; +export { assertAtRule, assertComment, assertDecl, assertRule } from './postcss-node-asserts'; diff --git a/packages/core-test-kit/src/postcss-node-asserts.ts b/packages/core-test-kit/src/postcss-node-asserts.ts new file mode 100644 index 000000000..0bcb7aa45 --- /dev/null +++ b/packages/core-test-kit/src/postcss-node-asserts.ts @@ -0,0 +1,26 @@ +import type * as postcss from 'postcss'; + +export function assertRule(node: any, msg?: string): postcss.Rule { + if (node?.type !== 'rule') { + throw new Error('expected postcss rule' + (msg ? ` (${msg})` : '')); + } + return node; +} +export function assertAtRule(node: any, msg?: string): postcss.Rule { + if (node?.type !== 'atrule') { + throw new Error('expected postcss at-rule' + (msg ? ` (${msg})` : '')); + } + return node; +} +export function assertDecl(node: any, msg?: string): postcss.Declaration { + if (node?.type !== 'decl') { + throw new Error('expected postcss declaration' + (msg ? ` (${msg})` : '')); + } + return node; +} +export function assertComment(node: any, msg?: string): postcss.Comment { + if (node?.type !== 'comment') { + throw new Error('expected comment node' + (msg ? ` (${msg})` : '')); + } + return node; +} diff --git a/packages/core/src/features/st-mixin.ts b/packages/core/src/features/st-mixin.ts index 3c84f5cfe..71f8c6f98 100644 --- a/packages/core/src/features/st-mixin.ts +++ b/packages/core/src/features/st-mixin.ts @@ -5,7 +5,7 @@ import * as STCustomSelector from './st-custom-selector'; import * as STVar from './st-var'; import type { ElementSymbol } from './css-type'; import type { ClassSymbol } from './css-class'; -import { createSubsetAst } from '../helpers/rule'; +import { createSubsetAst, isStMixinMarker } from '../helpers/rule'; import { scopeNestedSelector } from '../helpers/selector'; import { mixinHelperDiagnostics, parseStMixin, parseStPartialMixin } from '../helpers/mixin'; import { resolveArgumentsValue } from '../functions'; @@ -110,6 +110,17 @@ export const diagnostics = { // HOOKS export const hooks = createFeature({ + transformSelectorNode({ selectorContext, node }) { + const isMarker = isStMixinMarker(node); + if (isMarker) { + selectorContext.setNextSelectorScope( + selectorContext.inferredSelectorMixin, + node, + node.value + ); + } + return isMarker; + }, transformLastPass({ context, ast, transformer, cssVarsMapping, path }) { ast.walkRules((rule) => appendMixins(context, transformer, rule, cssVarsMapping, path)); }, @@ -422,7 +433,7 @@ function handleJSMixin( meta.source ); - mergeRules(mixinRoot, config.rule, mixDef.data.originDecl, context.diagnostics); + mergeRules(mixinRoot, config.rule, mixDef.data.originDecl, context.diagnostics, true); } function handleCSSMixin( @@ -498,11 +509,23 @@ function handleCSSMixin( } if (roots.length === 1) { - mergeRules(roots[0], config.rule, mixDef.data.originDecl, config.transformer.diagnostics); + mergeRules( + roots[0], + config.rule, + mixDef.data.originDecl, + config.transformer.diagnostics, + false + ); } else if (roots.length > 1) { const mixinRoot = postcss.root(); roots.forEach((root) => mixinRoot.prepend(...root.nodes)); - mergeRules(mixinRoot, config.rule, mixDef.data.originDecl, config.transformer.diagnostics); + mergeRules( + mixinRoot, + config.rule, + mixDef.data.originDecl, + config.transformer.diagnostics, + false + ); } } diff --git a/packages/core/src/helpers/rule.ts b/packages/core/src/helpers/rule.ts index 4eeae571d..850a7d9f9 100644 --- a/packages/core/src/helpers/rule.ts +++ b/packages/core/src/helpers/rule.ts @@ -12,6 +12,7 @@ import { ImmutableSelectorNode, groupCompoundSelectors, SelectorList, + SelectorNode, } from '@tokey/css-selector-parser'; import * as postcss from 'postcss'; import { transformInlineCustomSelectors } from './custom-selector'; @@ -34,8 +35,8 @@ export function isInConditionalGroup(node: postcss.Rule | postcss.AtRule, includ ); } -export function createSubsetAst( - root: postcss.Root | postcss.AtRule, +export function createSubsetAst( + root: postcss.Root | postcss.AtRule | postcss.Rule, selectorPrefix: string, mixinTarget?: T, isRoot = false, @@ -48,7 +49,12 @@ export function createSubsetAst( const containsPrefix = containsMatchInFirstChunk.bind(null, prefixType); const mixinRoot = mixinTarget ? mixinTarget : postcss.root(); root.nodes.forEach((node) => { - if (node.type === `rule` && (node.selector === ':vars' || node.selector === ':import')) { + if (node.type === 'decl') { + mixinTarget?.append(node.clone()); + } else if ( + node.type === `rule` && + (node.selector === ':vars' || node.selector === ':import') + ) { // nodes that don't mix return; } else if (node.type === `rule`) { @@ -70,12 +76,20 @@ export function createSubsetAst( if (!isRoot) { selectorNode = fixChunkOrdering(selectorNode, prefixType); } - replaceTargetWithNesting(selectorNode, prefixType); + replaceTargetWithMixinAnchor(selectorNode, prefixType); return selectorNode; }) ); - mixinRoot.append(node.clone({ selector })); + const clonedRule = createSubsetAst( + node, + selectorPrefix, + node.clone({ selector, nodes: [] }), + isRoot, + getCustomSelector, + true /*isNestedInMixin*/ + ); + mixinRoot.append(clonedRule); } } else if (node.type === `atrule`) { if ( @@ -99,7 +113,7 @@ export function createSubsetAst( if (!isRoot) { selectorNode = fixChunkOrdering(selectorNode, prefixType); } - replaceTargetWithNesting(selectorNode, prefixType); + replaceTargetWithMixinAnchor(selectorNode, prefixType); return selectorNode; }) ); @@ -130,13 +144,16 @@ export function createSubsetAst( return mixinRoot as T; } -function replaceTargetWithNesting(selectorNode: Selector, prefixType: ImmutableSelectorNode) { +export const stMixinMarker = 'st-mixin-marker'; +export const isStMixinMarker = (node: SelectorNode) => + node.type === 'attribute' && node.value === stMixinMarker; +function replaceTargetWithMixinAnchor(selectorNode: Selector, prefixType: ImmutableSelectorNode) { walkSelector(selectorNode, (node) => { if (matchTypeAndValue(node, prefixType)) { convertToSelector(node).nodes = [ { - type: `nesting`, - value: `&`, + type: `attribute`, + value: stMixinMarker, start: node.start, end: node.end, }, diff --git a/packages/core/src/helpers/selector.ts b/packages/core/src/helpers/selector.ts index e2d571341..60553b336 100644 --- a/packages/core/src/helpers/selector.ts +++ b/packages/core/src/helpers/selector.ts @@ -167,6 +167,7 @@ export function isCompRoot(name: string) { return name.charAt(0).match(/[A-Z]/); } +const isNestedNode = (node: SelectorNode) => node.type === 'nesting'; /** * combine 2 selector lists. * - add each scoping selector at the begging of each nested selector @@ -175,7 +176,8 @@ export function isCompRoot(name: string) { export function scopeNestedSelector( scopeSelectorAst: ImmutableSelectorList, nestedSelectorAst: ImmutableSelectorList, - rootScopeLevel = false + rootScopeLevel = false, + isAnchor: (node: SelectorNode) => boolean = isNestedNode ): { selector: string; ast: SelectorList } { const resultSelectors: SelectorList = []; nestedSelectorAst.forEach((targetAst) => { @@ -203,7 +205,7 @@ export function scopeNestedSelector( : false; let nestedMixRoot = false; walkSelector(outputAst, (node, i, nodes) => { - if (node.type === 'nesting') { + if (isAnchor(node)) { nestedMixRoot = true; nodes.splice(i, 1, { type: `selector`, diff --git a/packages/core/src/stylable-transformer.ts b/packages/core/src/stylable-transformer.ts index 0dcc21a0f..62be9a8cd 100644 --- a/packages/core/src/stylable-transformer.ts +++ b/packages/core/src/stylable-transformer.ts @@ -193,7 +193,7 @@ export class StylableTransformer { stVarOverride: Record = this.defaultStVarOverride, path: string[] = [], mixinTransform = false, - inferredNestSelector?: InferredSelector + inferredSelectorMixin?: InferredSelector ) { if (meta.type !== 'stylable') { return; @@ -340,8 +340,8 @@ export class StylableTransformer { meta, node.selector, node, - (currentParent && this.containerInferredSelectorMap.get(currentParent)) || - inferredNestSelector + currentParent && this.containerInferredSelectorMap.get(currentParent), + inferredSelectorMixin ); // save results this.containerInferredSelectorMap.set(node, inferredSelector); @@ -413,6 +413,7 @@ export class StylableTransformer { selector: string, selectorNode?: postcss.Rule | postcss.AtRule, inferredNestSelector?: InferredSelector, + inferredMixinSelector?: InferredSelector, unwrapGlobals = false ): { selector: string; @@ -425,7 +426,8 @@ export class StylableTransformer { parseSelectorWithCache(selector, { clone: true }), selectorNode || postcss.rule({ selector }), selector, - inferredNestSelector + inferredNestSelector, + inferredMixinSelector ); const targetSelectorAst = this.scopeSelectorAst(context); if (unwrapGlobals) { @@ -443,7 +445,8 @@ export class StylableTransformer { selectorAst: SelectorList, selectorNode: postcss.Rule | postcss.AtRule, selectorStr?: string, - selectorNest?: InferredSelector + selectorNest?: InferredSelector, + selectorMixin?: InferredSelector ) { return new ScopeContext( meta, @@ -453,6 +456,7 @@ export class StylableTransformer { this.scopeSelectorAst.bind(this), this, selectorNest, + selectorMixin, undefined, selectorStr ); @@ -595,6 +599,12 @@ export class StylableTransformer { } } else if (node.type === `nesting`) { context.setNextSelectorScope(context.inferredSelectorNest, node, node.value); + } else if (node.type === 'attribute') { + STMixin.hooks.transformSelectorNode({ + context: transformerContext, + selectorContext: context, + node, + }); } } } @@ -990,6 +1000,7 @@ export class ScopeContext { public scopeSelectorAst: StylableTransformer['scopeSelectorAst'], private transformer: StylableTransformer, inferredSelectorNest?: InferredSelector, + public inferredSelectorMixin?: InferredSelector, inferredSelectorContext?: InferredSelector, selectorStr?: string ) { @@ -1072,6 +1083,7 @@ export class ScopeContext { this.scopeSelectorAst, this.transformer, this.inferredSelectorNest, + this.inferredSelectorMixin, selectorContext || this.inferredSelectorContext ); ctx.transform = this.transform; diff --git a/packages/core/src/stylable-utils.ts b/packages/core/src/stylable-utils.ts index 173f2caff..ef5b3a302 100644 --- a/packages/core/src/stylable-utils.ts +++ b/packages/core/src/stylable-utils.ts @@ -2,7 +2,7 @@ import { isAbsolute } from 'path'; import type * as postcss from 'postcss'; import { createDiagnosticReporter, Diagnostics } from './diagnostics'; import type { ImportSymbol, StylableSymbol } from './features'; -import { isChildOfAtRule } from './helpers/rule'; +import { isChildOfAtRule, stMixinMarker, isStMixinMarker } from './helpers/rule'; import { scopeNestedSelector, parseSelectorWithCache } from './helpers/selector'; export function isValidDeclaration(decl: postcss.Declaration) { @@ -22,21 +22,26 @@ export function mergeRules( mixinAst: postcss.Root, rule: postcss.Rule, mixinDecl: postcss.Declaration, - report?: Diagnostics + report: Diagnostics, + useNestingAsAnchor: boolean ) { let mixinRoot: postcss.Rule | null | 'NoRoot' = null; const nestedInKeyframes = isChildOfAtRule(rule, `keyframes`); + const anchorSelector = useNestingAsAnchor ? '&' : '[' + stMixinMarker + ']'; + const anchorNodeCheck = useNestingAsAnchor ? undefined : isStMixinMarker; mixinAst.walkRules((mixinRule: postcss.Rule) => { if (isChildOfAtRule(mixinRule, 'keyframes')) { return; } - if (mixinRule.selector === '&' && !mixinRoot) { + if (mixinRule.selector === anchorSelector && !mixinRoot) { if (mixinRule.parent === mixinAst) { mixinRoot = mixinRule; } else { const { selector } = scopeNestedSelector( parseSelectorWithCache(rule.selector), - parseSelectorWithCache(mixinRule.selector) + parseSelectorWithCache(mixinRule.selector), + false, + anchorNodeCheck ); mixinRoot = 'NoRoot'; mixinRule.selector = selector; @@ -44,7 +49,9 @@ export function mergeRules( } else { const { selector } = scopeNestedSelector( parseSelectorWithCache(rule.selector), - parseSelectorWithCache(mixinRule.selector) + parseSelectorWithCache(mixinRule.selector), + false, + anchorNodeCheck ); mixinRule.selector = selector; } @@ -55,9 +62,9 @@ export function mergeRules( // TODO: handle rules before and after decl on entry mixinAst.nodes.slice().forEach((node) => { if (node === mixinRoot) { - node.walkDecls((node) => { - rule.insertBefore(mixinDecl, node); - }); + for (const nested of [...node.nodes]) { + rule.insertBefore(mixinDecl, nested); + } } else if (node.type === 'decl') { rule.insertBefore(mixinDecl, node); } else if (node.type === 'rule' || node.type === 'atrule') { diff --git a/packages/core/src/stylable.ts b/packages/core/src/stylable.ts index 95db9c41c..a02327ca4 100644 --- a/packages/core/src/stylable.ts +++ b/packages/core/src/stylable.ts @@ -185,7 +185,7 @@ export class Stylable { ): { selector: string; resolved: ResolvedElement[][] } { const meta = typeof pathOrMeta === `string` ? this.analyze(pathOrMeta) : pathOrMeta; const transformer = this.createTransformer(options); - const r = transformer.scopeSelector(meta, selector, undefined, undefined, true); + const r = transformer.scopeSelector(meta, selector, undefined, undefined, undefined, true); return { selector: r.selector, resolved: r.elements, diff --git a/packages/core/test/features/st-mixin.spec.ts b/packages/core/test/features/st-mixin.spec.ts index 06f533a20..c905152b4 100644 --- a/packages/core/test/features/st-mixin.spec.ts +++ b/packages/core/test/features/st-mixin.spec.ts @@ -6,6 +6,9 @@ import { shouldReportNoDiagnostics, matchRuleAndDeclaration, diagnosticBankReportToStrings, + assertRule, + assertDecl, + assertAtRule, } from '@stylable/core-test-kit'; import chai, { expect } from 'chai'; import type * as postcss from 'postcss'; @@ -307,6 +310,38 @@ describe(`features/st-mixin`, () => { } ); }); + it('should support CSS nesting as part of a mixin', () => { + const { sheets } = testStylableCore(` + .mix { + id: mix; + &:hover.mix { + id: hover; + } + @media { + id: atrule; + } + } + + .root { + -st-mixin: mix; + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + const mixedIntoRule = assertRule(meta.targetAst!.nodes[1], 'mixed into'); + expect(mixedIntoRule.selector).to.eql('.entry__root'); + const firstMixedDecl = assertDecl(mixedIntoRule.nodes[0], 'id: mix'); + expect(firstMixedDecl.prop).to.eql('id'); + expect(firstMixedDecl.value).to.eql('mix'); + const nestHoverRule = assertRule(mixedIntoRule.nodes[1], '&:hover.mix'); + expect(nestHoverRule.selector).to.eql('.entry__root&:hover'); + const nestAtRule = assertAtRule(mixedIntoRule.nodes[2], '@media'); + const nestInAtRule = assertDecl(nestAtRule.nodes[0], 'id: atrule'); + expect(nestInAtRule.prop).to.eql('id'); + expect(nestInAtRule.value).to.eql('atrule'); + }); describe(`st-import`, () => { it(`should mix imported class`, () => { const { sheets } = testStylableCore({ diff --git a/packages/core/test/helpers/rule.spec.ts b/packages/core/test/helpers/rule.spec.ts index ff14f8075..06bc9a404 100644 --- a/packages/core/test/helpers/rule.spec.ts +++ b/packages/core/test/helpers/rule.spec.ts @@ -49,31 +49,38 @@ describe(`helpers/rule`, () => { /*not extracted*/ .x .i{} + + /*nesting*/ + .i[out] { .i[in] {} } `), '.i' ); const expected = [ - { selector: '& .x' }, - { selector: '&::x' }, - { selector: '&[data]' }, - { selector: '&:hover' }, - { selector: '&' }, - { selector: '&' }, - { selector: '&.x' }, - { selector: '&.x' }, - { selector: '&.x.y::i.z:hover' }, - { selector: '&:hover .y' }, - { selector: '& .y' }, - { selector: '&:not(.x)' }, - { selector: '& &.x:hover' }, - { selector: '&.y.x' }, - { selector: '&&' }, // TODO: check if possible - { selector: ':not(&) &' }, - { selector: ':nth-child(5n - 1 of &)' }, - { selector: ':nth-child(5n - 2 of &, &)' }, - { selector: ':nth-child(5n - 3 of &, .x, &)' }, // ToDo: check if to remove unrelated nested selectors - { selector: '&' }, + { selector: '[st-mixin-marker] .x' }, + { selector: '[st-mixin-marker]::x' }, + { selector: '[st-mixin-marker][data]' }, + { selector: '[st-mixin-marker]:hover' }, + { selector: '[st-mixin-marker]' }, + { selector: '[st-mixin-marker]' }, + { selector: '[st-mixin-marker].x' }, + { selector: '[st-mixin-marker].x' }, + { selector: '[st-mixin-marker].x.y::i.z:hover' }, + { selector: '[st-mixin-marker]:hover .y' }, + { selector: '[st-mixin-marker] .y' }, + { selector: '[st-mixin-marker]:not(.x)' }, + { selector: '[st-mixin-marker] [st-mixin-marker].x:hover' }, + { selector: '[st-mixin-marker].y.x' }, + { selector: '[st-mixin-marker][st-mixin-marker]' }, // TODO: check if possible + { selector: ':not([st-mixin-marker]) [st-mixin-marker]' }, + { selector: ':nth-child(5n - 1 of [st-mixin-marker])' }, + { selector: ':nth-child(5n - 2 of [st-mixin-marker], [st-mixin-marker])' }, + { selector: ':nth-child(5n - 3 of [st-mixin-marker], .x, [st-mixin-marker])' }, // ToDo: check if to remove unrelated nested selectors + { selector: '[st-mixin-marker]' }, + { + selector: '[st-mixin-marker][out]', + nodes: [{ selector: '[st-mixin-marker][in]' }], + }, ]; testMatcher(expected, res.nodes); @@ -90,7 +97,10 @@ describe(`helpers/rule`, () => { true ); - const expected = [{ selector: ':global(.x)' }, { selector: ':global(.x) &' }]; + const expected = [ + { selector: ':global(.x)' }, + { selector: ':global(.x) [st-mixin-marker]' }, + ]; testMatcher(expected, res.nodes); }); @@ -109,12 +119,15 @@ describe(`helpers/rule`, () => { ); const expected = [ - { selector: '&' }, - { selector: '&:hover' }, + { selector: '[st-mixin-marker]' }, + { selector: '[st-mixin-marker]:hover' }, { type: 'atrule', params: '(max-width: 300px)', - nodes: [{ selector: '&' }, { selector: '&:hover' }], + nodes: [ + { selector: '[st-mixin-marker]' }, + { selector: '[st-mixin-marker]:hover' }, + ], }, ]; @@ -132,7 +145,7 @@ describe(`helpers/rule`, () => { '.i' ); - const expected = [{ selector: '&' }]; + const expected = [{ selector: '[st-mixin-marker]' }]; testMatcher(expected, res.nodes); }); diff --git a/packages/core/test/helpers/selector.spec.ts b/packages/core/test/helpers/selector.spec.ts index fc103942d..f89ec4a8e 100644 --- a/packages/core/test/helpers/selector.spec.ts +++ b/packages/core/test/helpers/selector.spec.ts @@ -135,6 +135,137 @@ describe(`helpers/selector`, () => { }); } }); + describe(`scopeNestedSelector with custom anchor`, () => { + const tests: Array<{ + label: string; + scope: string; + nested: string; + expected: string; + only?: boolean; + }> = [ + { + label: '+ no anchor selector', + scope: '.a', + nested: '.x', + expected: '.a .x', + }, + { + label: '+ complex selector with no anchor selector', + scope: '.a', + nested: '.x:hover', + expected: '.a .x:hover', + }, + { + label: '+ anchor selector', + scope: '.a', + nested: ':xxx', + expected: '.a', + }, + { + label: 'compound scope + anchor selector', + scope: '.a:hover', + nested: ':xxx', + expected: '.a:hover', + }, + { + label: 'compound scope + anchor selector (2)', + scope: '.a.x', + nested: ':xxx', + expected: '.a.x', + }, + { + label: 'complex scope + anchor selector', + scope: '.a.x .b:hover', + nested: ':xxx', + expected: '.a.x .b:hover', + }, + { + label: '+ anchor selector with compound class', + scope: '.a', + nested: ':xxx.x', + expected: '.a.x', + }, + { + label: '+ anchor selector with complex class', + scope: '.a', + nested: ':xxx.x .y', + expected: '.a.x .y', + }, + { + label: 'complex scope + anchor selector with complex class', + scope: '.a .b', + nested: ':xxx.x .y', + expected: '.a .b.x .y', + }, + { + label: '+ multiple anchor selector', + scope: '.a', + nested: ':xxx :xxx', + expected: '.a .a', + }, + { + label: 'multi scopes + multiple anchor selectors', + scope: '.a, .b', + nested: ':xxx :xxx :xxx', + expected: '.a .a .a, .b .b .b', + }, + { + label: 'multi compound scopes + multi anchor selector', + scope: '.a:hover, .b:focus', + nested: ':xxx :xxx :xxx', + expected: '.a:hover .a:hover .a:hover, .b:focus .b:focus .b:focus', + }, + { + label: '+ global before', + scope: '.a', + nested: ':global(.x) :xxx', + expected: ':global(.x) .a', + }, + { + label: '+ nested anchor selector', + scope: '.a', + nested: ':not(:xxx)', + expected: ':not(.a)', + }, + { + label: 'multi scopes + nested anchor selector', + scope: '.a, .b', + nested: ':not(:xxx)', + expected: ':not(.a), :not(.b)', + }, + { + label: '+ nested deep anchor selector', + scope: '.a', + nested: ':not(:xxx, :not(:xxx))', + expected: ':not(.a, :not(.a))', + }, + { + label: '+ nested nth of anchor selector', + scope: '.a', + nested: ':nth-child(5n+2 of :xxx)', + expected: ':nth-child(5n+2 of .a)', + }, + { + label: 'nesting scope persists', + scope: '&', + nested: '.no-parent-re-scoping', + expected: '& .no-parent-re-scoping', + }, + ]; + + for (const { only, scope, expected, nested } of tests) { + const test = only ? it.only : it; + test(`apply "${scope}" on "${nested}" should output "${expected}"`, () => { + const { selector } = scopeNestedSelector( + parseSelector(scope), + parseSelector(nested), + false, + (node) => node.type === 'pseudo_class' && node.value === 'xxx' + ); + expect(selector).to.equal(expected); + }); + } + }); describe(`isSimpleSelector`, () => { it(`should return simple for class selector`, () => { const result = isSimpleSelector(`.a`); diff --git a/packages/language-service/test/test-kit/postcss-node-asserts.ts b/packages/language-service/test/test-kit/postcss-node-asserts.ts index 80a764614..5451056ba 100644 --- a/packages/language-service/test/test-kit/postcss-node-asserts.ts +++ b/packages/language-service/test/test-kit/postcss-node-asserts.ts @@ -1,33 +1,9 @@ import type { Invalid } from '@stylable/language-service/dist/lib-new/invalid-node'; -import type * as postcss from 'postcss'; +export { assertAtRule, assertComment, assertDecl, assertRule } from '@stylable/core-test-kit'; -export function assertRule(node: any, msg?: string): postcss.Rule { - if (node?.type !== 'rule') { - throw new Error('expected postcss rule' + (msg ? ` (${msg})` : '')); - } - return node; -} -export function assertAtRule(node: any, msg?: string): postcss.Rule { - if (node?.type !== 'atrule') { - throw new Error('expected postcss at-rule' + (msg ? ` (${msg})` : '')); - } - return node; -} -export function assertDecl(node: any, msg?: string): postcss.Declaration { - if (node?.type !== 'decl') { - throw new Error('expected postcss declaration' + (msg ? ` (${msg})` : '')); - } - return node; -} export function assertInvalid(node: any, msg?: string): Invalid { if (node?.type !== 'invalid') { throw new Error('expected invalid node' + (msg ? ` (${msg})` : '')); } return node; } -export function assertComment(node: any, msg?: string): postcss.Comment { - if (node?.type !== 'comment') { - throw new Error('expected comment node' + (msg ? ` (${msg})` : '')); - } - return node; -}