|
import * as regex from './regex' |
|
|
|
export function defaultExtractor(context) { |
|
let patterns = Array.from(buildRegExps(context)) |
|
|
|
|
|
|
|
|
|
return (content) => { |
|
|
|
let results = [] |
|
|
|
for (let pattern of patterns) { |
|
for (let result of content.match(pattern) ?? []) { |
|
results.push(clipAtBalancedParens(result)) |
|
} |
|
} |
|
|
|
return results |
|
} |
|
} |
|
|
|
function* buildRegExps(context) { |
|
let separator = context.tailwindConfig.separator |
|
let prefix = |
|
context.tailwindConfig.prefix !== '' |
|
? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)])) |
|
: '' |
|
|
|
let utility = regex.any([ |
|
|
|
/\[[^\s:'"`]+:[^\s\[\]]+\]/, |
|
|
|
|
|
|
|
|
|
|
|
/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, |
|
|
|
|
|
regex.pattern([ |
|
|
|
regex.any([ |
|
/-?(?:\w+)/, |
|
|
|
|
|
/@(?:\w+)/, |
|
]), |
|
|
|
|
|
regex.optional( |
|
regex.any([ |
|
regex.pattern([ |
|
|
|
regex.any([ |
|
/-(?:\w+-)*\['[^\s]+'\]/, |
|
/-(?:\w+-)*\["[^\s]+"\]/, |
|
/-(?:\w+-)*\[`[^\s]+`\]/, |
|
/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/, |
|
]), |
|
|
|
|
|
/(?![{([]])/, |
|
|
|
|
|
/(?:\/[^\s'"`\\><$]*)?/, |
|
]), |
|
|
|
regex.pattern([ |
|
|
|
regex.any([ |
|
/-(?:\w+-)*\['[^\s]+'\]/, |
|
/-(?:\w+-)*\["[^\s]+"\]/, |
|
/-(?:\w+-)*\[`[^\s]+`\]/, |
|
/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/, |
|
]), |
|
|
|
|
|
/(?![{([]])/, |
|
|
|
|
|
/(?:\/[^\s'"`\\$]*)?/, |
|
]), |
|
|
|
|
|
/[-\/][^\s'"`\\$={><]*/, |
|
]) |
|
), |
|
]), |
|
]) |
|
|
|
let variantPatterns = [ |
|
|
|
regex.any([ |
|
|
|
regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]), |
|
|
|
|
|
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]), |
|
|
|
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]), |
|
regex.pattern([/[^\s"'`\[\\]+/, separator]), |
|
]), |
|
|
|
|
|
regex.any([ |
|
|
|
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]), |
|
|
|
regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]), |
|
regex.pattern([/[^\s`\[\\]+/, separator]), |
|
]), |
|
] |
|
|
|
for (const variantPattern of variantPatterns) { |
|
yield regex.pattern([ |
|
|
|
'((?=((', |
|
variantPattern, |
|
')+))\\2)?', |
|
|
|
|
|
/!?/, |
|
|
|
prefix, |
|
|
|
utility, |
|
]) |
|
} |
|
|
|
|
|
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 - 1) |
|
} |
|
|
|
// 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}] |
|
|