tailwindcss 3.0.0-alpha.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/lib/cli.js +58 -58
  2. package/lib/corePluginList.js +3 -0
  3. package/lib/corePlugins.js +138 -65
  4. package/lib/css/preflight.css +2 -1
  5. package/lib/lib/detectNesting.js +17 -2
  6. package/lib/lib/evaluateTailwindFunctions.js +6 -2
  7. package/lib/lib/expandApplyAtRules.js +4 -4
  8. package/lib/lib/expandTailwindAtRules.js +2 -0
  9. package/lib/lib/generateRules.js +36 -0
  10. package/lib/lib/setupContextUtils.js +8 -65
  11. package/lib/lib/substituteScreenAtRules.js +7 -4
  12. package/lib/util/buildMediaQuery.js +13 -24
  13. package/lib/util/dataTypes.js +14 -3
  14. package/lib/util/formatVariantSelector.js +88 -4
  15. package/lib/util/isValidArbitraryValue.js +64 -0
  16. package/lib/util/nameClass.js +1 -0
  17. package/lib/util/normalizeScreens.js +61 -0
  18. package/lib/util/resolveConfig.js +8 -8
  19. package/package.json +10 -10
  20. package/peers/.svgo.yml +75 -0
  21. package/peers/index.js +3784 -3146
  22. package/peers/orders/concentric-css.json +299 -0
  23. package/peers/orders/smacss.json +299 -0
  24. package/peers/orders/source.json +295 -0
  25. package/src/.DS_Store +0 -0
  26. package/src/corePluginList.js +1 -1
  27. package/src/corePlugins.js +114 -60
  28. package/src/css/preflight.css +2 -1
  29. package/src/lib/detectNesting.js +22 -3
  30. package/src/lib/evaluateTailwindFunctions.js +5 -2
  31. package/src/lib/expandTailwindAtRules.js +2 -0
  32. package/src/lib/generateRules.js +34 -0
  33. package/src/lib/setupContextUtils.js +6 -58
  34. package/src/lib/substituteScreenAtRules.js +6 -3
  35. package/src/util/buildMediaQuery.js +14 -18
  36. package/src/util/dataTypes.js +11 -6
  37. package/src/util/formatVariantSelector.js +92 -1
  38. package/src/util/isValidArbitraryValue.js +61 -0
  39. package/src/util/nameClass.js +1 -1
  40. package/src/util/normalizeScreens.js +45 -0
  41. package/stubs/defaultConfig.stub.js +17 -0
  42. package/CHANGELOG.md +0 -1825
@@ -6,6 +6,9 @@ import prefixSelector from '../util/prefixSelector'
6
6
  import { updateAllClasses } from '../util/pluginUtils'
7
7
  import log from '../util/log'
8
8
  import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
9
+ import { asClass } from '../util/nameClass'
10
+ import { normalize } from '../util/dataTypes'
11
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
9
12
 
10
13
  let classNameParser = selectorParser((selectors) => {
11
14
  return selectors.first.filter(({ type }) => type === 'class').pop().value
@@ -245,11 +248,42 @@ function parseRules(rule, cache, options = {}) {
245
248
  return [cache.get(rule), options]
246
249
  }
247
250
 
251
+ function extractArbitraryProperty(classCandidate, context) {
252
+ let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
253
+
254
+ if (value === undefined) {
255
+ return null
256
+ }
257
+
258
+ let normalized = normalize(value)
259
+
260
+ if (!isValidArbitraryValue(normalized)) {
261
+ return null
262
+ }
263
+
264
+ return [
265
+ [
266
+ { sort: context.arbitraryPropertiesSort, layer: 'utilities' },
267
+ () => ({
268
+ [asClass(classCandidate)]: {
269
+ [property]: normalized,
270
+ },
271
+ }),
272
+ ],
273
+ ]
274
+ }
275
+
248
276
  function* resolveMatchedPlugins(classCandidate, context) {
249
277
  if (context.candidateRuleMap.has(classCandidate)) {
250
278
  yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
251
279
  }
252
280
 
281
+ yield* (function* (arbitraryPropertyRule) {
282
+ if (arbitraryPropertyRule !== null) {
283
+ yield [arbitraryPropertyRule, 'DEFAULT']
284
+ }
285
+ })(extractArbitraryProperty(classCandidate, context))
286
+
253
287
  let candidatePrefix = classCandidate
254
288
  let negative = false
255
289
 
@@ -18,6 +18,7 @@ import { env } from './sharedState'
18
18
  import { toPath } from '../util/toPath'
19
19
  import log from '../util/log'
20
20
  import negateValue from '../util/negateValue'
21
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
21
22
 
22
23
  function parseVariantFormatString(input) {
23
24
  if (input.includes('{')) {
@@ -130,64 +131,6 @@ function withIdentifiers(styles) {
130
131
  })
131
132
  }
132
133
 
133
- let matchingBrackets = new Map([
134
- ['{', '}'],
135
- ['[', ']'],
136
- ['(', ')'],
137
- ])
138
- let inverseMatchingBrackets = new Map(
139
- Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
140
- )
141
-
142
- let quotes = new Set(['"', "'", '`'])
143
-
144
- // Arbitrary values must contain balanced brackets (), [] and {}. Escaped
145
- // values don't count, and brackets inside quotes also don't count.
146
- //
147
- // E.g.: w-[this-is]w-[weird-and-invalid]
148
- // E.g.: w-[this-is\\]w-\\[weird-but-valid]
149
- // E.g.: content-['this-is-also-valid]-weirdly-enough']
150
- function isValidArbitraryValue(value) {
151
- let stack = []
152
- let inQuotes = false
153
-
154
- for (let i = 0; i < value.length; i++) {
155
- let char = value[i]
156
-
157
- // Non-escaped quotes allow us to "allow" anything in between
158
- if (quotes.has(char) && value[i - 1] !== '\\') {
159
- inQuotes = !inQuotes
160
- }
161
-
162
- if (inQuotes) continue
163
- if (value[i - 1] === '\\') continue // Escaped
164
-
165
- if (matchingBrackets.has(char)) {
166
- stack.push(char)
167
- } else if (inverseMatchingBrackets.has(char)) {
168
- let inverse = inverseMatchingBrackets.get(char)
169
-
170
- // Nothing to pop from, therefore it is unbalanced
171
- if (stack.length <= 0) {
172
- return false
173
- }
174
-
175
- // Popped value must match the inverse value, otherwise it is unbalanced
176
- if (stack.pop() !== inverse) {
177
- return false
178
- }
179
- }
180
- }
181
-
182
- // If there is still something on the stack, it is also unbalanced
183
- if (stack.length > 0) {
184
- return false
185
- }
186
-
187
- // All good, totally balanced!
188
- return true
189
- }
190
-
191
134
  function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) {
192
135
  function getConfigValue(path, defaultValue) {
193
136
  return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig
@@ -575,6 +518,7 @@ function resolvePlugins(context, root) {
575
518
  variantPlugins['darkVariants'],
576
519
  variantPlugins['printVariant'],
577
520
  variantPlugins['screenVariants'],
521
+ variantPlugins['orientationVariants'],
578
522
  ]
579
523
 
580
524
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
@@ -617,6 +561,10 @@ function registerPlugins(plugins, context) {
617
561
  ])
618
562
  let reservedBits = BigInt(highestOffset.toString(2).length)
619
563
 
564
+ // A number one less than the top range of the highest offset area
565
+ // so arbitrary properties are always sorted at the end.
566
+ context.arbitraryPropertiesSort = ((1n << reservedBits) << 0n) - 1n
567
+
620
568
  context.layerOrder = {
621
569
  base: (1n << reservedBits) << 0n,
622
570
  components: (1n << reservedBits) << 1n,
@@ -1,16 +1,19 @@
1
+ import { normalizeScreens } from '../util/normalizeScreens'
1
2
  import buildMediaQuery from '../util/buildMediaQuery'
2
3
 
3
4
  export default function ({ tailwindConfig: { theme } }) {
4
5
  return function (css) {
5
6
  css.walkAtRules('screen', (atRule) => {
6
- const screen = atRule.params
7
+ let screen = atRule.params
8
+ let screens = normalizeScreens(theme.screens)
9
+ let screenDefinition = screens.find(({ name }) => name === screen)
7
10
 
8
- if (!theme.screens?.hasOwnProperty?.(screen)) {
11
+ if (!screenDefinition) {
9
12
  throw atRule.error(`No \`${screen}\` screen found.`)
10
13
  }
11
14
 
12
15
  atRule.name = 'media'
13
- atRule.params = buildMediaQuery(theme.screens[screen])
16
+ atRule.params = buildMediaQuery(screenDefinition)
14
17
  })
15
18
  }
16
19
  }
@@ -1,24 +1,20 @@
1
1
  export default function buildMediaQuery(screens) {
2
- if (typeof screens === 'string') {
3
- screens = { min: screens }
4
- }
5
-
6
- if (!Array.isArray(screens)) {
7
- screens = [screens]
8
- }
2
+ screens = Array.isArray(screens) ? screens : [screens]
9
3
 
10
4
  return screens
11
- .map((screen) => {
12
- if (screen?.hasOwnProperty?.('raw')) {
13
- return screen.raw
14
- }
5
+ .map((screen) =>
6
+ screen.values.map((screen) => {
7
+ if (screen.raw !== undefined) {
8
+ return screen.raw
9
+ }
15
10
 
16
- return Object.entries(screen)
17
- .map(([feature, value]) => {
18
- feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
19
- return `(${feature}: ${value})`
20
- })
21
- .join(' and ')
22
- })
11
+ return [
12
+ screen.min && `(min-width: ${screen.min})`,
13
+ screen.max && `(max-width: ${screen.max})`,
14
+ ]
15
+ .filter(Boolean)
16
+ .join(' and ')
17
+ })
18
+ )
23
19
  .join(', ')
24
20
  }
@@ -1,6 +1,8 @@
1
1
  import { parseColor } from './color'
2
2
  import { parseBoxShadowValue } from './parseBoxShadowValue'
3
3
 
4
+ let cssFunctions = ['min', 'max', 'clamp', 'calc']
5
+
4
6
  // Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
5
7
 
6
8
  let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
@@ -51,11 +53,11 @@ export function url(value) {
51
53
  }
52
54
 
53
55
  export function number(value) {
54
- return !isNaN(Number(value))
56
+ return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
55
57
  }
56
58
 
57
59
  export function percentage(value) {
58
- return /%$/g.test(value) || /^calc\(.+?%\)/g.test(value)
60
+ return /%$/g.test(value) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(value))
59
61
  }
60
62
 
61
63
  let lengthUnits = [
@@ -78,10 +80,13 @@ let lengthUnits = [
78
80
  ]
79
81
  let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
80
82
  export function length(value) {
81
- return (
82
- new RegExp(`${lengthUnitsPattern}$`).test(value) ||
83
- new RegExp(`^calc\\(.+?${lengthUnitsPattern}`).test(value)
84
- )
83
+ return value.split(UNDERSCORE).every((part) => {
84
+ return (
85
+ part === '0' ||
86
+ new RegExp(`${lengthUnitsPattern}$`).test(part) ||
87
+ cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?${lengthUnitsPattern}`).test(part))
88
+ )
89
+ })
85
90
  }
86
91
 
87
92
  let lineWidths = new Set(['thin', 'medium', 'thick'])
@@ -30,7 +30,17 @@ export function formatVariantSelector(current, ...others) {
30
30
  }
31
31
 
32
32
  export function finalizeSelector(format, { selector, candidate, context }) {
33
- let base = candidate.split(context?.tailwindConfig?.separator ?? ':').pop()
33
+ let separator = context?.tailwindConfig?.separator ?? ':'
34
+
35
+ // Split by the separator, but ignore the separator inside square brackets:
36
+ //
37
+ // E.g.: dark:lg:hover:[paint-order:markers]
38
+ // ┬ ┬ ┬ ┬
39
+ // │ │ │ ╰── We will not split here
40
+ // ╰──┴─────┴─────────────── We will split here
41
+ //
42
+ let splitter = new RegExp(`\\${separator}(?![^[]*\\])`)
43
+ let base = candidate.split(splitter).pop()
34
44
 
35
45
  if (context?.tailwindConfig?.prefix) {
36
46
  format = prefixSelector(context.tailwindConfig.prefix, format)
@@ -74,11 +84,92 @@ export function finalizeSelector(format, { selector, candidate, context }) {
74
84
  return p
75
85
  })
76
86
 
87
+ // This will make sure to move pseudo's to the correct spot (the end for
88
+ // pseudo elements) because otherwise the selector will never work
89
+ // anyway.
90
+ //
91
+ // E.g.:
92
+ // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
93
+ // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
94
+ //
95
+ // `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
96
+ function collectPseudoElements(selector) {
97
+ let nodes = []
98
+
99
+ for (let node of selector.nodes) {
100
+ if (isPseudoElement(node)) {
101
+ nodes.push(node)
102
+ selector.removeChild(node)
103
+ }
104
+
105
+ if (node?.nodes) {
106
+ nodes.push(...collectPseudoElements(node))
107
+ }
108
+ }
109
+
110
+ return nodes
111
+ }
112
+
113
+ let pseudoElements = collectPseudoElements(selector)
114
+ if (pseudoElements.length > 0) {
115
+ selector.nodes.push(pseudoElements.sort(sortSelector))
116
+ }
117
+
77
118
  return selector
78
119
  })
79
120
  }).processSync(selector)
80
121
  }
81
122
 
123
+ // Note: As a rule, double colons (::) should be used instead of a single colon
124
+ // (:). This distinguishes pseudo-classes from pseudo-elements. However, since
125
+ // this distinction was not present in older versions of the W3C spec, most
126
+ // browsers support both syntaxes for the original pseudo-elements.
127
+ let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
128
+
129
+ // These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
130
+ let pseudoElementExceptions = ['::file-selector-button']
131
+
132
+ // This will make sure to move pseudo's to the correct spot (the end for
133
+ // pseudo elements) because otherwise the selector will never work
134
+ // anyway.
135
+ //
136
+ // E.g.:
137
+ // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
138
+ // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
139
+ //
140
+ // `::before:hover` doesn't work, which means that we can make it work
141
+ // for you by flipping the order.
142
+ function sortSelector(a, z) {
143
+ // Both nodes are non-pseudo's so we can safely ignore them and keep
144
+ // them in the same order.
145
+ if (a.type !== 'pseudo' && z.type !== 'pseudo') {
146
+ return 0
147
+ }
148
+
149
+ // If one of them is a combinator, we need to keep it in the same order
150
+ // because that means it will start a new "section" in the selector.
151
+ if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
152
+ return 0
153
+ }
154
+
155
+ // One of the items is a pseudo and the other one isn't. Let's move
156
+ // the pseudo to the right.
157
+ if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
158
+ return (a.type === 'pseudo') - (z.type === 'pseudo')
159
+ }
160
+
161
+ // Both are pseudo's, move the pseudo elements (except for
162
+ // ::file-selector-button) to the right.
163
+ return isPseudoElement(a) - isPseudoElement(z)
164
+ }
165
+
166
+ function isPseudoElement(node) {
167
+ if (node.type !== 'pseudo') return false
168
+ if (pseudoElementExceptions.includes(node.value)) return false
169
+
170
+ return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
171
+ }
172
+
82
173
  function resolveFunctionArgument(haystack, needle, arg) {
83
174
  let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
84
175
  if (startIdx === -1) return null
@@ -0,0 +1,61 @@
1
+ let matchingBrackets = new Map([
2
+ ['{', '}'],
3
+ ['[', ']'],
4
+ ['(', ')'],
5
+ ])
6
+ let inverseMatchingBrackets = new Map(
7
+ Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
8
+ )
9
+
10
+ let quotes = new Set(['"', "'", '`'])
11
+
12
+ // Arbitrary values must contain balanced brackets (), [] and {}. Escaped
13
+ // values don't count, and brackets inside quotes also don't count.
14
+ //
15
+ // E.g.: w-[this-is]w-[weird-and-invalid]
16
+ // E.g.: w-[this-is\\]w-\\[weird-but-valid]
17
+ // E.g.: content-['this-is-also-valid]-weirdly-enough']
18
+ export default function isValidArbitraryValue(value) {
19
+ let stack = []
20
+ let inQuotes = false
21
+
22
+ for (let i = 0; i < value.length; i++) {
23
+ let char = value[i]
24
+
25
+ if (char === ':' && !inQuotes && stack.length === 0) {
26
+ return false
27
+ }
28
+
29
+ // Non-escaped quotes allow us to "allow" anything in between
30
+ if (quotes.has(char) && value[i - 1] !== '\\') {
31
+ inQuotes = !inQuotes
32
+ }
33
+
34
+ if (inQuotes) continue
35
+ if (value[i - 1] === '\\') continue // Escaped
36
+
37
+ if (matchingBrackets.has(char)) {
38
+ stack.push(char)
39
+ } else if (inverseMatchingBrackets.has(char)) {
40
+ let inverse = inverseMatchingBrackets.get(char)
41
+
42
+ // Nothing to pop from, therefore it is unbalanced
43
+ if (stack.length <= 0) {
44
+ return false
45
+ }
46
+
47
+ // Popped value must match the inverse value, otherwise it is unbalanced
48
+ if (stack.pop() !== inverse) {
49
+ return false
50
+ }
51
+ }
52
+ }
53
+
54
+ // If there is still something on the stack, it is also unbalanced
55
+ if (stack.length > 0) {
56
+ return false
57
+ }
58
+
59
+ // All good, totally balanced!
60
+ return true
61
+ }
@@ -1,7 +1,7 @@
1
1
  import escapeClassName from './escapeClassName'
2
2
  import escapeCommas from './escapeCommas'
3
3
 
4
- function asClass(name) {
4
+ export function asClass(name) {
5
5
  return escapeCommas(`.${escapeClassName(name)}`)
6
6
  }
7
7
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A function that normalizes the various forms that the screens object can be
3
+ * provided in.
4
+ *
5
+ * Input(s):
6
+ * - ['100px', '200px'] // Raw strings
7
+ * - { sm: '100px', md: '200px' } // Object with string values
8
+ * - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
9
+ * - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
10
+ *
11
+ * Output(s):
12
+ * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
13
+ */
14
+ export function normalizeScreens(screens, root = true) {
15
+ if (Array.isArray(screens)) {
16
+ return screens.map((screen) => {
17
+ if (root && Array.isArray(screen)) {
18
+ throw new Error('The tuple syntax is not supported for `screens`.')
19
+ }
20
+
21
+ if (typeof screen === 'string') {
22
+ return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
23
+ }
24
+
25
+ let [name, options] = screen
26
+ name = name.toString()
27
+
28
+ if (typeof options === 'string') {
29
+ return { name, values: [{ min: options, max: undefined }] }
30
+ }
31
+
32
+ if (Array.isArray(options)) {
33
+ return { name, values: options.map((option) => resolveValue(option)) }
34
+ }
35
+
36
+ return { name, values: [resolveValue(options)] }
37
+ })
38
+ }
39
+
40
+ return normalizeScreens(Object.entries(screens ?? {}), false)
41
+ }
42
+
43
+ function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
44
+ return { min, max, raw }
45
+ }
@@ -797,6 +797,23 @@ module.exports = {
797
797
  },
798
798
  textColor: ({ theme }) => theme('colors'),
799
799
  textDecorationColor: ({ theme }) => theme('colors'),
800
+ textDecorationThickness: {
801
+ auto: 'auto',
802
+ 'from-font': 'from-font',
803
+ 0: '0px',
804
+ 1: '1px',
805
+ 2: '2px',
806
+ 4: '4px',
807
+ 8: '8px',
808
+ },
809
+ textUnderlineOffset: {
810
+ auto: 'auto',
811
+ 0: '0px',
812
+ 1: '1px',
813
+ 2: '2px',
814
+ 4: '4px',
815
+ 8: '8px',
816
+ },
800
817
  textIndent: ({ theme }) => ({
801
818
  ...theme('spacing'),
802
819
  }),