| [9af201e] | 1 | import * as regex from './regex'
|
|---|
| 2 | import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly'
|
|---|
| 3 |
|
|---|
| 4 | export function defaultExtractor(context) {
|
|---|
| 5 | let patterns = Array.from(buildRegExps(context))
|
|---|
| 6 |
|
|---|
| 7 | /**
|
|---|
| 8 | * @param {string} content
|
|---|
| 9 | */
|
|---|
| 10 | return (content) => {
|
|---|
| 11 | /** @type {(string|string)[]} */
|
|---|
| 12 | let results = []
|
|---|
| 13 |
|
|---|
| 14 | for (let pattern of patterns) {
|
|---|
| 15 | for (let result of content.match(pattern) ?? []) {
|
|---|
| 16 | results.push(clipAtBalancedParens(result))
|
|---|
| 17 | }
|
|---|
| 18 | }
|
|---|
| 19 |
|
|---|
| 20 | // Extract any subclasses from languages like Slim and Pug, eg:
|
|---|
| 21 | // div.flex.px-5.underline
|
|---|
| 22 | for (let result of results.slice()) {
|
|---|
| 23 | let segments = splitAtTopLevelOnly(result, '.')
|
|---|
| 24 |
|
|---|
| 25 | for (let idx = 0; idx < segments.length; idx++) {
|
|---|
| 26 | let segment = segments[idx]
|
|---|
| 27 | if (idx >= segments.length - 1) {
|
|---|
| 28 | results.push(segment)
|
|---|
| 29 | continue
|
|---|
| 30 | }
|
|---|
| 31 |
|
|---|
| 32 | // If the next segment is a number, discard both, for example seeing
|
|---|
| 33 | // `px-1` and `5` means the real candidate was `px-1.5` which is already
|
|---|
| 34 | // captured.
|
|---|
| 35 | let next = Number(segments[idx + 1])
|
|---|
| 36 | if (isNaN(next)) {
|
|---|
| 37 | results.push(segment)
|
|---|
| 38 | } else {
|
|---|
| 39 | idx++
|
|---|
| 40 | }
|
|---|
| 41 | }
|
|---|
| 42 | }
|
|---|
| 43 |
|
|---|
| 44 | return results
|
|---|
| 45 | }
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | function* buildRegExps(context) {
|
|---|
| 49 | let separator = context.tailwindConfig.separator
|
|---|
| 50 | let prefix =
|
|---|
| 51 | context.tailwindConfig.prefix !== ''
|
|---|
| 52 | ? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)]))
|
|---|
| 53 | : ''
|
|---|
| 54 |
|
|---|
| 55 | let utility = regex.any([
|
|---|
| 56 | // Arbitrary properties (without square brackets)
|
|---|
| 57 | /\[[^\s:'"`]+:[^\s\[\]]+\]/,
|
|---|
| 58 |
|
|---|
| 59 | // Arbitrary properties with balanced square brackets
|
|---|
| 60 | // This is a targeted fix to continue to allow theme()
|
|---|
| 61 | // with square brackets to work in arbitrary properties
|
|---|
| 62 | // while fixing a problem with the regex matching too much
|
|---|
| 63 | /\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,
|
|---|
| 64 |
|
|---|
| 65 | // Utilities
|
|---|
| 66 | regex.pattern([
|
|---|
| 67 | // Utility Name / Group Name
|
|---|
| 68 | regex.any([
|
|---|
| 69 | /-?(?:\w+)/,
|
|---|
| 70 |
|
|---|
| 71 | // This is here to make sure @container supports everything that other utilities do
|
|---|
| 72 | /@(?:\w+)/,
|
|---|
| 73 | ]),
|
|---|
| 74 |
|
|---|
| 75 | // Normal/Arbitrary values
|
|---|
| 76 | regex.optional(
|
|---|
| 77 | regex.any([
|
|---|
| 78 | regex.pattern([
|
|---|
| 79 | // Arbitrary values
|
|---|
| 80 | regex.any([
|
|---|
| 81 | /-(?:\w+-)*\['[^\s]+'\]/,
|
|---|
| 82 | /-(?:\w+-)*\["[^\s]+"\]/,
|
|---|
| 83 | /-(?:\w+-)*\[`[^\s]+`\]/,
|
|---|
| 84 | /-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/,
|
|---|
| 85 | ]),
|
|---|
| 86 |
|
|---|
| 87 | // Not immediately followed by an `{[(`
|
|---|
| 88 | /(?![{([]])/,
|
|---|
| 89 |
|
|---|
| 90 | // optionally followed by an opacity modifier
|
|---|
| 91 | /(?:\/[^\s'"`\\><$]*)?/,
|
|---|
| 92 | ]),
|
|---|
| 93 |
|
|---|
| 94 | regex.pattern([
|
|---|
| 95 | // Arbitrary values
|
|---|
| 96 | regex.any([
|
|---|
| 97 | /-(?:\w+-)*\['[^\s]+'\]/,
|
|---|
| 98 | /-(?:\w+-)*\["[^\s]+"\]/,
|
|---|
| 99 | /-(?:\w+-)*\[`[^\s]+`\]/,
|
|---|
| 100 | /-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/,
|
|---|
| 101 | ]),
|
|---|
| 102 |
|
|---|
| 103 | // Not immediately followed by an `{[(`
|
|---|
| 104 | /(?![{([]])/,
|
|---|
| 105 |
|
|---|
| 106 | // optionally followed by an opacity modifier
|
|---|
| 107 | /(?:\/[^\s'"`\\$]*)?/,
|
|---|
| 108 | ]),
|
|---|
| 109 |
|
|---|
| 110 | // Normal values w/o quotes — may include an opacity modifier
|
|---|
| 111 | /[-\/][^\s'"`\\$={><]*/,
|
|---|
| 112 | ])
|
|---|
| 113 | ),
|
|---|
| 114 | ]),
|
|---|
| 115 | ])
|
|---|
| 116 |
|
|---|
| 117 | let variantPatterns = [
|
|---|
| 118 | // Without quotes
|
|---|
| 119 | regex.any([
|
|---|
| 120 | // This is here to provide special support for the `@` variant
|
|---|
| 121 | regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]),
|
|---|
| 122 |
|
|---|
| 123 | // With variant modifier (e.g.: group-[..]/modifier)
|
|---|
| 124 | regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/, separator]),
|
|---|
| 125 |
|
|---|
| 126 | regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]),
|
|---|
| 127 | regex.pattern([/[^\s"'`\[\\]+/, separator]),
|
|---|
| 128 | ]),
|
|---|
| 129 |
|
|---|
| 130 | // With quotes allowed
|
|---|
| 131 | regex.any([
|
|---|
| 132 | // With variant modifier (e.g.: group-[..]/modifier)
|
|---|
| 133 | regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/, separator]),
|
|---|
| 134 |
|
|---|
| 135 | regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]),
|
|---|
| 136 | regex.pattern([/[^\s`\[\\]+/, separator]),
|
|---|
| 137 | ]),
|
|---|
| 138 | ]
|
|---|
| 139 |
|
|---|
| 140 | for (const variantPattern of variantPatterns) {
|
|---|
| 141 | yield regex.pattern([
|
|---|
| 142 | // Variants
|
|---|
| 143 | '((?=((',
|
|---|
| 144 | variantPattern,
|
|---|
| 145 | ')+))\\2)?',
|
|---|
| 146 |
|
|---|
| 147 | // Important (optional)
|
|---|
| 148 | /!?/,
|
|---|
| 149 |
|
|---|
| 150 | prefix,
|
|---|
| 151 |
|
|---|
| 152 | utility,
|
|---|
| 153 | ])
|
|---|
| 154 | }
|
|---|
| 155 |
|
|---|
| 156 | // 5. Inner matches
|
|---|
| 157 | yield /[^<>"'`\s.(){}[\]#=%$][^<>"'`\s(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | // We want to capture any "special" characters
|
|---|
| 161 | // AND the characters immediately following them (if there is one)
|
|---|
| 162 | let SPECIALS = /([\[\]'"`])([^\[\]'"`])?/g
|
|---|
| 163 | let ALLOWED_CLASS_CHARACTERS = /[^"'`\s<>\]]+/
|
|---|
| 164 |
|
|---|
| 165 | /**
|
|---|
| 166 | * Clips a string ensuring that parentheses, quotes, etc… are balanced
|
|---|
| 167 | * Used for arbitrary values only
|
|---|
| 168 | *
|
|---|
| 169 | * We will go past the end of the balanced parens until we find a non-class character
|
|---|
| 170 | *
|
|---|
| 171 | * Depth matching behavior:
|
|---|
| 172 | * w-[calc(100%-theme('spacing[some_key][1.5]'))]']
|
|---|
| 173 | * ┬ ┬ ┬┬ ┬ ┬┬ ┬┬┬┬┬┬┬
|
|---|
| 174 | * 1 2 3 4 34 3 210 END
|
|---|
| 175 | * ╰────┴──────────┴────────┴────────┴┴───┴─┴┴┴
|
|---|
| 176 | *
|
|---|
| 177 | * @param {string} input
|
|---|
| 178 | */
|
|---|
| 179 | function clipAtBalancedParens(input) {
|
|---|
| 180 | // We are care about this for arbitrary values
|
|---|
| 181 | if (!input.includes('-[')) {
|
|---|
| 182 | return input
|
|---|
| 183 | }
|
|---|
| 184 |
|
|---|
| 185 | let depth = 0
|
|---|
| 186 | let openStringTypes = []
|
|---|
| 187 |
|
|---|
| 188 | // Find all parens, brackets, quotes, etc
|
|---|
| 189 | // Stop when we end at a balanced pair
|
|---|
| 190 | // This is naive and will treat mismatched parens as balanced
|
|---|
| 191 | // This shouldn't be a problem in practice though
|
|---|
| 192 | let matches = input.matchAll(SPECIALS)
|
|---|
| 193 |
|
|---|
| 194 | // We can't use lookbehind assertions because we have to support Safari
|
|---|
| 195 | // So, instead, we've emulated it using capture groups and we'll re-work the matches to accommodate
|
|---|
| 196 | matches = Array.from(matches).flatMap((match) => {
|
|---|
| 197 | const [, ...groups] = match
|
|---|
| 198 |
|
|---|
| 199 | return groups.map((group, idx) =>
|
|---|
| 200 | Object.assign([], match, {
|
|---|
| 201 | index: match.index + idx,
|
|---|
| 202 | 0: group,
|
|---|
| 203 | })
|
|---|
| 204 | )
|
|---|
| 205 | })
|
|---|
| 206 |
|
|---|
| 207 | for (let match of matches) {
|
|---|
| 208 | let char = match[0]
|
|---|
| 209 | let inStringType = openStringTypes[openStringTypes.length - 1]
|
|---|
| 210 |
|
|---|
| 211 | if (char === inStringType) {
|
|---|
| 212 | openStringTypes.pop()
|
|---|
| 213 | } else if (char === "'" || char === '"' || char === '`') {
|
|---|
| 214 | openStringTypes.push(char)
|
|---|
| 215 | }
|
|---|
| 216 |
|
|---|
| 217 | if (inStringType) {
|
|---|
| 218 | continue
|
|---|
| 219 | } else if (char === '[') {
|
|---|
| 220 | depth++
|
|---|
| 221 | continue
|
|---|
| 222 | } else if (char === ']') {
|
|---|
| 223 | depth--
|
|---|
| 224 | continue
|
|---|
| 225 | }
|
|---|
| 226 |
|
|---|
| 227 | // We've gone one character past the point where we should stop
|
|---|
| 228 | // This means that there was an extra closing `]`
|
|---|
| 229 | // We'll clip to just before it
|
|---|
| 230 | if (depth < 0) {
|
|---|
| 231 | return input.substring(0, match.index - 1)
|
|---|
| 232 | }
|
|---|
| 233 |
|
|---|
| 234 | // We've finished balancing the brackets but there still may be characters that can be included
|
|---|
| 235 | // For example in the class `text-[#336699]/[.35]`
|
|---|
| 236 | // The depth goes to `0` at the closing `]` but goes up again at the `[`
|
|---|
| 237 |
|
|---|
| 238 | // If we're at zero and encounter a non-class character then we clip the class there
|
|---|
| 239 | if (depth === 0 && !ALLOWED_CLASS_CHARACTERS.test(char)) {
|
|---|
| 240 | return input.substring(0, match.index)
|
|---|
| 241 | }
|
|---|
| 242 | }
|
|---|
| 243 |
|
|---|
| 244 | return input
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | // Regular utilities
|
|---|
| 248 | // {{modifier}:}*{namespace}{-{suffix}}*{/{opacityModifier}}?
|
|---|
| 249 |
|
|---|
| 250 | // Arbitrary values
|
|---|
| 251 | // {{modifier}:}*{namespace}-[{arbitraryValue}]{/{opacityModifier}}?
|
|---|
| 252 | // arbitraryValue: no whitespace, balanced quotes unless within quotes, balanced brackets unless within quotes
|
|---|
| 253 |
|
|---|
| 254 | // Arbitrary properties
|
|---|
| 255 | // {{modifier}:}*[{validCssPropertyName}:{arbitraryValue}]
|
|---|