mirror of https://github.com/vuejs/vitepress
commit
c4466b97df
@ -0,0 +1,77 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`node/postcss/isolateStyles > transforms selectors and skips keyframes 1`] = `
|
||||||
|
"
|
||||||
|
/* simple classes */
|
||||||
|
.example:not(:where(.vp-raw, .vp-raw *)) { color: red; }
|
||||||
|
.class-a:not(:where(.vp-raw, .vp-raw *)) { color: coral; }
|
||||||
|
.class-b:not(:where(.vp-raw, .vp-raw *)) { color: deepskyblue; }
|
||||||
|
|
||||||
|
/* escaped colon in class */
|
||||||
|
.baz\\:not\\(.bar\\):not(:where(.vp-raw, .vp-raw *)) { display: block; }
|
||||||
|
.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled { opacity: .5; }
|
||||||
|
|
||||||
|
/* pseudos (class + element) */
|
||||||
|
.button:not(:where(.vp-raw, .vp-raw *)):hover { color: pink; }
|
||||||
|
.button:not(:where(.vp-raw, .vp-raw *)):focus:hover { color: hotpink; }
|
||||||
|
.item:not(:where(.vp-raw, .vp-raw *))::before { content: '•'; }
|
||||||
|
:not(:where(.vp-raw, .vp-raw *))::first-letter { color: pink; }
|
||||||
|
:not(:where(.vp-raw, .vp-raw *))::before { content: ''; }
|
||||||
|
|
||||||
|
/* universal + :not */
|
||||||
|
*:not(:where(.vp-raw, .vp-raw *)) { background-color: red; }
|
||||||
|
*:not(:where(.vp-raw, .vp-raw *)):not(.b) { text-transform: uppercase; }
|
||||||
|
|
||||||
|
/* combinators */
|
||||||
|
.foo:hover .bar:not(:where(.vp-raw, .vp-raw *)) { background: blue; }
|
||||||
|
ul > li.active:not(:where(.vp-raw, .vp-raw *)) { color: green; }
|
||||||
|
a + b ~ c:not(:where(.vp-raw, .vp-raw *)) { color: orange; }
|
||||||
|
|
||||||
|
/* ids + attribute selectors */
|
||||||
|
#wow:not(:where(.vp-raw, .vp-raw *)) { color: yellow; }
|
||||||
|
[data-world] .d:not(:where(.vp-raw, .vp-raw *)) { padding: 10px 20px; }
|
||||||
|
|
||||||
|
/* :root and chained tags */
|
||||||
|
:not(:where(.vp-raw, .vp-raw *)):root { --bs-blue: #0d6efd; }
|
||||||
|
:root .a:not(:where(.vp-raw, .vp-raw *)) { --bs-green: #bada55; }
|
||||||
|
html:not(:where(.vp-raw, .vp-raw *)) { margin: 0; }
|
||||||
|
body:not(:where(.vp-raw, .vp-raw *)) { padding: 0; }
|
||||||
|
html body div:not(:where(.vp-raw, .vp-raw *)) { color: blue; }
|
||||||
|
|
||||||
|
/* grouping with commas */
|
||||||
|
.a:not(:where(.vp-raw, .vp-raw *)), .b:not(:where(.vp-raw, .vp-raw *)) { color: red; }
|
||||||
|
|
||||||
|
/* multiple repeated groups to ensure stability */
|
||||||
|
.a:not(:where(.vp-raw, .vp-raw *)), .b:not(:where(.vp-raw, .vp-raw *)) { color: coral; }
|
||||||
|
.a:not(:where(.vp-raw, .vp-raw *)) { animation: glow 1s linear infinite alternate; }
|
||||||
|
|
||||||
|
/* nested blocks */
|
||||||
|
.foo:not(:where(.vp-raw, .vp-raw *)) {
|
||||||
|
svg:not(:where(.vp-raw, .vp-raw *)) { display: none; }
|
||||||
|
.bar:not(:where(.vp-raw, .vp-raw *)) { display: inline; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* standalone pseudos */
|
||||||
|
:not(:where(.vp-raw, .vp-raw *)):first-child { color: pink; }
|
||||||
|
:not(:where(.vp-raw, .vp-raw *)):hover { color: blue; }
|
||||||
|
:not(:where(.vp-raw, .vp-raw *)):active { color: red; }
|
||||||
|
|
||||||
|
/* keyframes (should be ignored) */
|
||||||
|
@keyframes fade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@-webkit-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
@-moz-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
@-o-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
@ -1,44 +1,93 @@
|
|||||||
import {
|
import { postcssIsolateStyles } from 'node/postcss/isolateStyles'
|
||||||
postcssIsolateStyles,
|
import postcss from 'postcss'
|
||||||
splitSelectorPseudo
|
|
||||||
} from 'node/postcss/isolateStyles'
|
const INPUT_CSS = `
|
||||||
|
/* simple classes */
|
||||||
// helper to run plugin transform on selector
|
.example { color: red; }
|
||||||
function apply(
|
.class-a { color: coral; }
|
||||||
prefixPlugin: ReturnType<typeof postcssIsolateStyles>,
|
.class-b { color: deepskyblue; }
|
||||||
selector: string
|
|
||||||
) {
|
/* escaped colon in class */
|
||||||
// `prepare` is available on the runtime plugin but missing from the types, thus cast to `any`
|
.baz\\:not\\(.bar\\) { display: block; }
|
||||||
const { Rule } = (prefixPlugin as any).prepare({
|
.disabled\\:opacity-50:disabled { opacity: .5; }
|
||||||
root: { source: { input: { file: 'foo/base.css' } } }
|
|
||||||
})
|
/* pseudos (class + element) */
|
||||||
const rule = { selectors: [selector] }
|
.button:hover { color: pink; }
|
||||||
Rule(rule, { result: {} })
|
.button:focus:hover { color: hotpink; }
|
||||||
return rule.selectors[0]
|
.item::before { content: '•'; }
|
||||||
|
::first-letter { color: pink; }
|
||||||
|
::before { content: ''; }
|
||||||
|
|
||||||
|
/* universal + :not */
|
||||||
|
* { background-color: red; }
|
||||||
|
*:not(.b) { text-transform: uppercase; }
|
||||||
|
|
||||||
|
/* combinators */
|
||||||
|
.foo:hover .bar { background: blue; }
|
||||||
|
ul > li.active { color: green; }
|
||||||
|
a + b ~ c { color: orange; }
|
||||||
|
|
||||||
|
/* ids + attribute selectors */
|
||||||
|
#wow { color: yellow; }
|
||||||
|
[data-world] .d { padding: 10px 20px; }
|
||||||
|
|
||||||
|
/* :root and chained tags */
|
||||||
|
:root { --bs-blue: #0d6efd; }
|
||||||
|
:root .a { --bs-green: #bada55; }
|
||||||
|
html { margin: 0; }
|
||||||
|
body { padding: 0; }
|
||||||
|
html body div { color: blue; }
|
||||||
|
|
||||||
|
/* grouping with commas */
|
||||||
|
.a, .b { color: red; }
|
||||||
|
|
||||||
|
/* multiple repeated groups to ensure stability */
|
||||||
|
.a, .b { color: coral; }
|
||||||
|
.a { animation: glow 1s linear infinite alternate; }
|
||||||
|
|
||||||
|
/* nested blocks */
|
||||||
|
.foo {
|
||||||
|
svg { display: none; }
|
||||||
|
.bar { display: inline; }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('node/postcss/isolateStyles', () => {
|
/* standalone pseudos */
|
||||||
const plugin = postcssIsolateStyles()
|
:first-child { color: pink; }
|
||||||
|
:hover { color: blue; }
|
||||||
|
:active { color: red; }
|
||||||
|
|
||||||
test('splitSelectorPseudo skips escaped colon', () => {
|
/* keyframes (should be ignored) */
|
||||||
const input = '.foo\\:bar'
|
@keyframes fade {
|
||||||
const [selector, pseudo] = splitSelectorPseudo(input)
|
from { opacity: 0; }
|
||||||
expect(selector).toBe(input)
|
to { opacity: 1; }
|
||||||
expect(pseudo).toBe('')
|
}
|
||||||
})
|
@-webkit-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
@-moz-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
@-o-keyframes glow {
|
||||||
|
from { color: coral; }
|
||||||
|
to { color: red; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
test('splitSelectorPseudo splits on pseudo selectors', () => {
|
describe('node/postcss/isolateStyles', () => {
|
||||||
const input = '.button:hover'
|
test('transforms selectors and skips keyframes', () => {
|
||||||
const [selector, pseudo] = splitSelectorPseudo(input)
|
const out = run(INPUT_CSS)
|
||||||
expect(selector).toBe('.button')
|
expect(out.css).toMatchSnapshot()
|
||||||
expect(pseudo).toBe(':hover')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('postcssIsolateStyles inserts :not(...) in the right place', () => {
|
test('idempotent (running twice produces identical CSS)', () => {
|
||||||
const input = '.disabled\\:opacity-50:disabled'
|
const first = run(INPUT_CSS).css
|
||||||
const result = apply(plugin, input)
|
const second = run(first).css
|
||||||
expect(result).toBe(
|
expect(second).toBe(first)
|
||||||
'.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function run(css: string, from = 'src/styles/vp-doc.css') {
|
||||||
|
return postcss([postcssIsolateStyles()]).process(css, { from })
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,38 @@
|
|||||||
import postcssPrefixSelector from 'postcss-prefix-selector'
|
import type { Plugin } from 'postcss'
|
||||||
|
import selectorParser from 'postcss-selector-parser'
|
||||||
|
|
||||||
export function postcssIsolateStyles(
|
type Options = {
|
||||||
options: Parameters<typeof postcssPrefixSelector>[0] = {}
|
includeFiles?: RegExp[]
|
||||||
): ReturnType<typeof postcssPrefixSelector> {
|
ignoreFiles?: RegExp[]
|
||||||
return postcssPrefixSelector({
|
prefix?: string
|
||||||
prefix: ':not(:where(.vp-raw, .vp-raw *))',
|
|
||||||
includeFiles: [/base\.css/],
|
|
||||||
transform(prefix, _selector) {
|
|
||||||
// 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] {
|
export function postcssIsolateStyles({
|
||||||
const [base, pseudo = ''] = selector.split(/(?<!\\)(:\S*)$/)
|
includeFiles = [/vp-doc\.css/, /base\.css/],
|
||||||
return [base, pseudo]
|
ignoreFiles,
|
||||||
|
prefix = ':not(:where(.vp-raw, .vp-raw *))'
|
||||||
|
}: Options = {}): Plugin {
|
||||||
|
const prefixNodes = selectorParser().astSync(prefix).first.nodes
|
||||||
|
|
||||||
|
return /* prettier-ignore */ {
|
||||||
|
postcssPlugin: 'postcss-isolate-styles',
|
||||||
|
Once(root) {
|
||||||
|
const file = root.source?.input.file
|
||||||
|
if (file && includeFiles?.length && !includeFiles.some((re) => re.test(file))) return
|
||||||
|
if (file && ignoreFiles?.length && ignoreFiles.some((re) => re.test(file))) return
|
||||||
|
|
||||||
|
root.walkRules((rule) => {
|
||||||
|
if (!rule.selector || rule.selector.includes(prefix)) return
|
||||||
|
if (rule.parent?.type === 'atrule' && /\bkeyframes$/i.test(rule.parent.name)) return
|
||||||
|
|
||||||
|
rule.selector = selectorParser((selectors) => {
|
||||||
|
selectors.each((sel) => {
|
||||||
|
if (!sel.nodes.length) return
|
||||||
|
const insertionIndex = sel.nodes.findLastIndex((n) => n.type !== 'pseudo') + 1
|
||||||
|
sel.nodes.splice(insertionIndex, 0, ...prefixNodes.map((n) => n.clone() as any))
|
||||||
|
})
|
||||||
|
}).processSync(rule.selector)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue