diff options
author | 2021-03-30 10:11:21 -0600 | |
---|---|---|
committer | 2021-03-30 10:11:21 -0600 | |
commit | ee6ef81cf38acc357141143319addf39a99d3e19 (patch) | |
tree | 1fd6032709f74253dcb5a30630da70d2e4373a40 /src/compiler/optimize/postcss-scoped-styles/index.ts | |
parent | d267fa461b73586118437e190b92b9956f9add73 (diff) | |
download | astro-ee6ef81cf38acc357141143319addf39a99d3e19.tar.gz astro-ee6ef81cf38acc357141143319addf39a99d3e19.tar.zst astro-ee6ef81cf38acc357141143319addf39a99d3e19.zip |
Convert CSS Modules to scoped styles (#38)
* Convert CSS Modules to scoped styles
* Update README
* Move class scoping into HTML walker
* Fix SSR styles test
* Fix mustache tags
* Update PostCSS plugin name
* Add JSDoc comment
* Update test
Diffstat (limited to 'src/compiler/optimize/postcss-scoped-styles/index.ts')
-rw-r--r-- | src/compiler/optimize/postcss-scoped-styles/index.ts | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/src/compiler/optimize/postcss-scoped-styles/index.ts b/src/compiler/optimize/postcss-scoped-styles/index.ts new file mode 100644 index 000000000..7949f63b5 --- /dev/null +++ b/src/compiler/optimize/postcss-scoped-styles/index.ts @@ -0,0 +1,79 @@ +import { Plugin } from 'postcss'; + +interface AstroScopedOptions { + className: string; +} + +interface Selector { + start: number; + end: number; + value: string; +} + +const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']); + +/** + * Scope Selectors + * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`) + * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax). + * @param {string} className The CSS class to apply. + */ +export function scopeSelectors(selector: string, className: string) { + const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.' + const selectors: Selector[] = []; + let ss = selector; // final output + + // Pass 1: parse selector string; extract top-level selectors + let start = 0; + let lastValue = ''; + for (let n = 0; n < ss.length; n++) { + const isEnd = n === selector.length - 1; + if (isEnd || CSS_SEPARATORS.has(selector[n])) { + lastValue = selector.substring(start, isEnd ? undefined : n); + if (!lastValue) continue; + selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue }); + start = n + 1; + } + } + + // Pass 2: starting from end, transform selectors w/ scoped class + for (let i = selectors.length - 1; i >= 0; i--) { + const { start, end, value } = selectors[i]; + const head = ss.substring(0, start); + const tail = ss.substring(end); + + // replace '*' with className + if (value === '*') { + ss = head + c + tail; + continue; + } + + // leave :global() alone! + if (value.startsWith(':global(')) { + ss = head + ss.substring(start, end).replace(':global(', '').replace(')', '') + tail; + continue; + } + + // scope everything else + let newSelector = ss.substring(start, end); + const pseudoIndex = newSelector.indexOf(':'); + if (pseudoIndex > 0) { + // if thereās a pseudoclass (:focus) + ss = head + newSelector.substring(start, pseudoIndex) + c + newSelector.substr(pseudoIndex) + tail; + } else { + ss = head + newSelector + c + tail; + } + } + + return ss; +} + +/** PostCSS Scope plugin */ +export default function astroScopedStyles(options: AstroScopedOptions): Plugin { + return { + postcssPlugin: '@astro/postcss-scoped-styles', + Rule(rule) { + rule.selector = scopeSelectors(rule.selector, options.className); + }, + }; +} |