Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
GianlucaGuarini committed Oct 26, 2022
1 parent 3d17302 commit 8937835
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 32 deletions.
78 changes: 47 additions & 31 deletions src/generators/css/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import cssEscape from 'cssesc'
import getPreprocessorTypeByAttribute from '../../utils/get-preprocessor-type-by-attribute'
import preprocess from '../../utils/preprocess-node'

const HOST = ':host'
const DISABLED_SELECTORS = ['from', 'to']

/**
* Matches valid, multiline JavaScript comments in almost all its forms.
* @const {RegExp}
Expand All @@ -25,6 +28,44 @@ const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\

const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g')

/**
* Matches the list of css selectors excluding the pseudo selectors
* @const {RegExp}
*/

const CSS_SELECTOR_LIST = /([^,]+):\w+(?:[\s|\S]*?\))?|([^,]+)/g

/**
* Scope the css selectors prefixing them with the tag name
* @param {string} tag - Tag name of the root element
* @param {string} selectorList - list of selectors we need to scope
* @returns {string} scoped selectors
*/
export function addScopeToSelectorList(tag, selectorList) {
return selectorList.replace(CSS_SELECTOR_LIST, (match, selector) => {
const trimmedMatch = match.trim()
const trimmedSelector = selector ? selector.trim() : trimmedMatch

// skip selectors already using the tag name
if (trimmedSelector.indexOf(tag) === 0) {
return match
}

// skips the keywords and percents of css animations
if (!trimmedSelector || DISABLED_SELECTORS.indexOf(trimmedSelector) > -1 || trimmedSelector.slice(-1) === '%') {
return match
}

// replace the `:host` pseudo-selector, where it is, with the root tag name;
// if `:host` was not included, add the tag name as prefix, and mirror all `[is]`
if (trimmedSelector.indexOf(HOST) < 0) {
return `${tag} ${trimmedMatch},[is="${tag}"] ${trimmedMatch}`
} else {
return `${trimmedMatch.replace(HOST, tag)},${trimmedMatch.replace(HOST, `[is="${tag}"]`)}`
}
})
}

/**
* Parses styles enclosed in a "scoped" tag
* The "css" string is received without comments or surrounding spaces.
Expand All @@ -33,41 +74,16 @@ const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|
* @param {string} css - The CSS code
* @returns {string} CSS with the styles scoped to the root element
*/
function scopedCSS(tag, css) {
const host = ':host'
const selectorsBlacklist = ['from', 'to']

return css.replace(CSS_SELECTOR, function(m, p1, p2) {
export function generateScopedCss(tag, css) {
return css.replace(CSS_SELECTOR, function(m, cssChunk, selectorList) {
// skip quoted strings
if (!p2) return m
if (!selectorList) return m

// we have a selector list, parse each individually
p2 = p2.replace(/[^,]+/g, function(sel) {
const s = sel.trim()

// skip selectors already using the tag name
if (s.indexOf(tag) === 0) {
return sel
}

// skips the keywords and percents of css animations
if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') {
return sel
}

// replace the `:host` pseudo-selector, where it is, with the root tag name;
// if `:host` was not included, add the tag name as prefix, and mirror all
// `[data-is]`
if (s.indexOf(host) < 0) {
return `${tag} ${s},[is="${tag}"] ${s}`
} else {
return `${s.replace(host, tag)},${
s.replace(host, `[is="${tag}"]`)}`
}
})
const scopedSelectorList = addScopeToSelectorList(tag, selectorList)

// add the danling bracket char and return the processed selector list
return p1 ? `${p1} ${p2}` : p2
return cssChunk ? `${cssChunk} ${scopedSelectorList}` : scopedSelectorList
})
}

Expand Down Expand Up @@ -101,7 +117,7 @@ export default function css(sourceNode, source, meta, ast) {
const escapedCssIdentifier = escapeIdentifier(meta.tagName)

const cssCode = (options.scopedCss ?
scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) :
generateScopedCss(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) :
escapeBackslashes(normalizedCssCode)
).trim()

Expand Down
20 changes: 19 additions & 1 deletion test/generators/css.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import compileCSS, {addScopeToSelectorList} from '../../src/generators/css'
import {evaluateScript, sassPreprocessor} from '../helpers'
import {register, unregister} from '../../src/preprocessors'
import compileCSS from '../../src/generators/css'
import {createInitialInput} from '../../src/index'
import {expect} from 'chai'
import parser from '@riotjs/parser'
Expand Down Expand Up @@ -138,4 +138,22 @@ describe('Generators - CSS', () => {
expect(output.default.tag).to.be.not.ok
expect(output.default.template).to.be.not.ok
})

it('scoped css are properly generated', () => {
expect(addScopeToSelectorList('my-tag', '')).to.be.equal('')
expect(addScopeToSelectorList('my-tag', 'from')).to.be.equal('from')
expect(addScopeToSelectorList('my-tag', 'to')).to.be.equal('to')
expect(addScopeToSelectorList('my-tag', 'my-tag')).to.be.equal('my-tag')
expect(addScopeToSelectorList('my-tag', 'my-tag:hover')).to.be.equal('my-tag:hover')
expect(addScopeToSelectorList('my-tag', ':host')).to.be.equal('my-tag,[is="my-tag"]')
expect(addScopeToSelectorList('my-tag', ':host:hover')).to.be.equal('my-tag:hover,[is="my-tag"]:hover')
expect(addScopeToSelectorList('my-tag', 'input')).to.be.equal('my-tag input,[is="my-tag"] input')
expect(addScopeToSelectorList('my-tag', '.foo:hover, .foo:focus')).to.be.equal('my-tag .foo:hover,[is="my-tag"] .foo:hover,my-tag .foo:focus,[is="my-tag"] .foo:focus')
expect(addScopeToSelectorList('my-tag', '.foo:has(.bar,.baz)')).to.be.equal('my-tag .foo:has(.bar,.baz),[is="my-tag"] .foo:has(.bar,.baz)')
expect(addScopeToSelectorList('my-tag', '.foo:not(.bar,.baz)')).to.be.equal('my-tag .foo:not(.bar,.baz),[is="my-tag"] .foo:not(.bar,.baz)')
expect(addScopeToSelectorList('my-tag', '.foo:where(.bar,.baz)')).to.be.equal('my-tag .foo:where(.bar,.baz),[is="my-tag"] .foo:where(.bar,.baz)')
expect(addScopeToSelectorList('my-tag', '.foo:is(.bar,.baz)')).to.be.equal('my-tag .foo:is(.bar,.baz),[is="my-tag"] .foo:is(.bar,.baz)')
expect(addScopeToSelectorList('my-tag', '.foo:is(.bar,.baz), .bar')).to.be.equal('my-tag .foo:is(.bar,.baz),[is="my-tag"] .foo:is(.bar,.baz),my-tag .bar,[is="my-tag"] .bar')
expect(addScopeToSelectorList('my-tag', '.foo, .bar')).to.be.equal('my-tag .foo,[is="my-tag"] .foo,my-tag .bar,[is="my-tag"] .bar')
})
})

0 comments on commit 8937835

Please sign in to comment.