* register arbitrary variants
With the new `addVariant` API, we have a beautiful way of creating new
variants.
You can use it as:
```js
addVariant('children', '& > *')
```
Now you can use the `children:` variant. The API uses a `&` as a
reference for the candidate, which means that:
```html
children:pl-4
```
Will result in:
```css
.children\:pl-4 > * { .. }
```
Notice that the `&` was replaced by `.children\:pl-4`.
We can leverage this API to implement arbitrary variants, this means
that you can write those `&>*` (Notice that we don't have spaces) inside
a variant directly. An example of this can be:
```html
<ul class="[&>*]:underline">
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
```
Which generates the following css:
```css
.\[\&\>\*\]\:underline > * {
text-decoration-line: underline;
}
```
Now all the children of the `ul` will have an `underline`. The selector
itself is a bit crazy since it contains the candidate which is the
selector itself, it is just escaped.
* add tests for arbitrary variants
This still requires some work to the `defaultExtractor` to make sure it
all works with existing code.
* update changelog
* Fix candidate detection for arbitrary variants
* Refactor
* Add support for at rules
* Add test for attribute selectors
* Fix test
* Add attribute selector support
* Split top-level comma parsing into a generalized splitting routine
We can now split on any character at the top level with any nesting. We don’t balance brackets directly here but this is probably “enough”
* Split variants by separator at the top-level only
This means that the separator has to be ouside of balanced brackets
* Fix extraction when using custom variant separators
* Support custom separators when top-level splitting variants
* Add a second multi-character separator test
* Split tests for at-rule and at-rule with selector changes
* Add nested at-rule tests
* Fix space-less at-rule parsing in addVariant
* Add test for using with `@apply`
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
177 lines
4.9 KiB
JavaScript
177 lines
4.9 KiB
JavaScript
import * as regex from './regex'
|
|
|
|
export function defaultExtractor(context) {
|
|
let patterns = Array.from(buildRegExps(context))
|
|
|
|
/**
|
|
* @param {string} content
|
|
*/
|
|
return (content) => {
|
|
/** @type {(string|string)[]} */
|
|
let results = []
|
|
|
|
for (let pattern of patterns) {
|
|
results.push(...(content.match(pattern) ?? []))
|
|
}
|
|
|
|
return results.filter((v) => v !== undefined).map(clipAtBalancedParens)
|
|
}
|
|
}
|
|
|
|
function* buildRegExps(context) {
|
|
let separator = context.tailwindConfig.separator
|
|
|
|
yield regex.pattern([
|
|
// Variants
|
|
'((?=((',
|
|
regex.any(
|
|
[regex.pattern([/\[[^\s"'\\]+\]/, separator]), regex.pattern([/[^\s"'\[\\]+/, separator])],
|
|
true
|
|
),
|
|
')+))\\2)?',
|
|
|
|
// Important (optional)
|
|
/!?/,
|
|
|
|
regex.any([
|
|
// Arbitrary properties
|
|
/\[[^\s:'"]+:[^\s\]]+\]/,
|
|
|
|
// Utilities
|
|
regex.pattern([
|
|
// Utility Name / Group Name
|
|
/-?(?:\w+)/,
|
|
|
|
// Normal/Arbitrary values
|
|
regex.optional(
|
|
regex.any([
|
|
regex.pattern([
|
|
// Arbitrary values
|
|
/-\[[^\s:]+\]/,
|
|
|
|
// Not immediately followed by an `{[(`
|
|
/(?![{([]])/,
|
|
|
|
// optionally followed by an opacity modifier
|
|
/(?:\/[^\s'"\\$]*)?/,
|
|
]),
|
|
|
|
regex.pattern([
|
|
// Arbitrary values
|
|
/-\[[^\s]+\]/,
|
|
|
|
// Not immediately followed by an `{[(`
|
|
/(?![{([]])/,
|
|
|
|
// optionally followed by an opacity modifier
|
|
/(?:\/[^\s'"\\$]*)?/,
|
|
]),
|
|
|
|
// Normal values w/o quotes — may include an opacity modifier
|
|
/[-\/][^\s'"\\$={]*/,
|
|
])
|
|
),
|
|
]),
|
|
]),
|
|
])
|
|
|
|
// 5. Inner matches
|
|
// yield /[^<>"'`\s.(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g
|
|
}
|
|
|
|
// We want to capture any "special" characters
|
|
// AND the characters immediately following them (if there is one)
|
|
let SPECIALS = /([\[\]'"`])([^\[\]'"`])?/g
|
|
let ALLOWED_CLASS_CHARACTERS = /[^"'`\s<>\]]+/
|
|
|
|
/**
|
|
* Clips a string ensuring that parentheses, quotes, etc… are balanced
|
|
* Used for arbitrary values only
|
|
*
|
|
* We will go past the end of the balanced parens until we find a non-class character
|
|
*
|
|
* Depth matching behavior:
|
|
* w-[calc(100%-theme('spacing[some_key][1.5]'))]']
|
|
* ┬ ┬ ┬┬ ┬ ┬┬ ┬┬┬┬┬┬┬
|
|
* 1 2 3 4 34 3 210 END
|
|
* ╰────┴──────────┴────────┴────────┴┴───┴─┴┴┴
|
|
*
|
|
* @param {string} input
|
|
*/
|
|
function clipAtBalancedParens(input) {
|
|
// We are care about this for arbitrary values
|
|
if (!input.includes('-[')) {
|
|
return input
|
|
}
|
|
|
|
let depth = 0
|
|
let openStringTypes = []
|
|
|
|
// Find all parens, brackets, quotes, etc
|
|
// Stop when we end at a balanced pair
|
|
// This is naive and will treat mismatched parens as balanced
|
|
// This shouldn't be a problem in practice though
|
|
let matches = input.matchAll(SPECIALS)
|
|
|
|
// We can't use lookbehind assertions because we have to support Safari
|
|
// So, instead, we've emulated it using capture groups and we'll re-work the matches to accommodate
|
|
matches = Array.from(matches).flatMap((match) => {
|
|
const [, ...groups] = match
|
|
|
|
return groups.map((group, idx) =>
|
|
Object.assign([], match, {
|
|
index: match.index + idx,
|
|
0: group,
|
|
})
|
|
)
|
|
})
|
|
|
|
for (let match of matches) {
|
|
let char = match[0]
|
|
let inStringType = openStringTypes[openStringTypes.length - 1]
|
|
|
|
if (char === inStringType) {
|
|
openStringTypes.pop()
|
|
} else if (char === "'" || char === '"' || char === '`') {
|
|
openStringTypes.push(char)
|
|
}
|
|
|
|
if (inStringType) {
|
|
continue
|
|
} else if (char === '[') {
|
|
depth++
|
|
continue
|
|
} else if (char === ']') {
|
|
depth--
|
|
continue
|
|
}
|
|
|
|
// We've gone one character past the point where we should stop
|
|
// This means that there was an extra closing `]`
|
|
// We'll clip to just before it
|
|
if (depth < 0) {
|
|
return input.substring(0, match.index)
|
|
}
|
|
|
|
// We've finished balancing the brackets but there still may be characters that can be included
|
|
// For example in the class `text-[#336699]/[.35]`
|
|
// The depth goes to `0` at the closing `]` but goes up again at the `[`
|
|
|
|
// If we're at zero and encounter a non-class character then we clip the class there
|
|
if (depth === 0 && !ALLOWED_CLASS_CHARACTERS.test(char)) {
|
|
return input.substring(0, match.index)
|
|
}
|
|
}
|
|
|
|
return input
|
|
}
|
|
|
|
// Regular utilities
|
|
// {{modifier}:}*{namespace}{-{suffix}}*{/{opacityModifier}}?
|
|
|
|
// Arbitrary values
|
|
// {{modifier}:}*{namespace}-[{arbitraryValue}]{/{opacityModifier}}?
|
|
// arbitraryValue: no whitespace, balanced quotes unless within quotes, balanced brackets unless within quotes
|
|
|
|
// Arbitrary properties
|
|
// {{modifier}:}*[{validCssPropertyName}:{arbitraryValue}]
|