From 5a6ca0ec81a254e764475efb31a0fc4189069ab6 Mon Sep 17 00:00:00 2001 From: alexchexes Date: Mon, 7 Jul 2025 17:58:03 +0100 Subject: [PATCH] fix(node/postcss): ignore escaped ':' when splitting selector in 'postcssIsolateStyles'. Fixes vuejs/vitepress#4829 --- .../unit/node/postcss/isolateStyles.test.ts | 45 +++++++++++++++++++ src/node/postcss/isolateStyles.ts | 8 +++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/node/postcss/isolateStyles.test.ts diff --git a/__tests__/unit/node/postcss/isolateStyles.test.ts b/__tests__/unit/node/postcss/isolateStyles.test.ts new file mode 100644 index 00000000..e57800e2 --- /dev/null +++ b/__tests__/unit/node/postcss/isolateStyles.test.ts @@ -0,0 +1,45 @@ +import { + postcssIsolateStyles, + splitSelectorPseudo +} from 'node/postcss/isolateStyles' +import { describe, it, expect } from 'vitest' + +// helper to run plugin transform on selector +function apply( + prefixPlugin: ReturnType, + selector: string +) { + // `prepare` is available on the runtime plugin but missing from the types, thus cast to `any` + const { Rule } = (prefixPlugin as any).prepare({ + root: { source: { input: { file: 'foo/base.css' } } } + }) + const rule = { selectors: [selector] } + Rule(rule as any, { result: {} } as any) + return rule.selectors[0] +} + +describe('node/postcss/isolateStyles', () => { + const plugin = postcssIsolateStyles() + + test('splitSelectorPseudo skips escaped colon', () => { + const input = '.foo\\:bar' + const [selector, pseudo] = splitSelectorPseudo(input) + expect(selector).toBe(input) + expect(pseudo).toBe('') + }) + + test('splitSelectorPseudo splits on pseudo selectors', () => { + const input = '.button:hover' + const [selector, pseudo] = splitSelectorPseudo(input) + expect(selector).toBe('.button') + expect(pseudo).toBe(':hover') + }) + + it('postcssIsolateStyles inserts :not(...) in the right place', () => { + const input = '.disabled\\:opacity-50:disabled' + const result = apply(plugin, input) + expect(result).toBe( + '.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled' + ) + }) +}) diff --git a/src/node/postcss/isolateStyles.ts b/src/node/postcss/isolateStyles.ts index dadd5cbd..4e3bf193 100644 --- a/src/node/postcss/isolateStyles.ts +++ b/src/node/postcss/isolateStyles.ts @@ -7,9 +7,15 @@ export function postcssIsolateStyles( prefix: ':not(:where(.vp-raw, .vp-raw *))', includeFiles: [/base\.css/], transform(prefix, _selector) { - const [selector, pseudo = ''] = _selector.split(/(:\S*)$/) + // split selector from its pseudo part if the trailing colon is not escaped + const [selector, pseudo] = splitSelectorPseudo(_selector) return selector + prefix + pseudo }, ...options }) } + +export function splitSelectorPseudo(selector: string): [string, string] { + const [base, pseudo = ''] = selector.split(/(?