Skip to content

Commit 960cb40

Browse files
Fix parsing of theme() inside calc() when there are no spaces around operators (#11157)
* Refactor * Don’t resolve functions for anything not using theme or screen * Normalize math operators inside calc when handling functions * Inline postcss-value-parser * Treat all functions the same as calc * Remove workaround for calc + operators without spaces * Remove `postcss-value-parser` dependency * Update lockfile * Update sourcemaps * Update changelog * Update `value-parser` formatting * Stop prettier from complaining
1 parent cdca9cb commit 960cb40

17 files changed

+893
-145
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix issue where some pseudo-element variants generated the wrong selector ([#10943](https://github.com/tailwindlabs/tailwindcss/pull/10943), [#10962](https://github.com/tailwindlabs/tailwindcss/pull/10962))
1313
- Make font settings propagate into buttons, inputs, etc. ([#10940](https://github.com/tailwindlabs/tailwindcss/pull/10940))
14+
- Fix parsing of `theme()` inside `calc()` when there are no spaces around operators ([#11157](https://github.com/tailwindlabs/tailwindcss/pull/11157))
1415

1516
### Added
1617

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.stable.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"postcss-load-config": "^4.0.1",
3030
"postcss-nested": "^6.0.1",
3131
"postcss-selector-parser": "^6.0.11",
32-
"postcss-value-parser": "^4.2.0",
3332
"resolve": "^1.22.2",
3433
"sucrase": "^3.32.0"
3534
},

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
"postcss-load-config": "^4.0.1",
9191
"postcss-nested": "^6.0.1",
9292
"postcss-selector-parser": "^6.0.11",
93-
"postcss-value-parser": "^4.2.0",
9493
"resolve": "^1.22.2",
9594
"sucrase": "^3.32.0"
9695
},

package.stable.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
"postcss-load-config": "^4.0.1",
8888
"postcss-nested": "^6.0.1",
8989
"postcss-selector-parser": "^6.0.11",
90-
"postcss-value-parser": "^4.2.0",
9190
"resolve": "^1.22.2",
9291
"sucrase": "^3.32.0"
9392
},

src/lib/evaluateTailwindFunctions.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dlv from 'dlv'
22
import didYouMean from 'didyoumean'
33
import transformThemeValue from '../util/transformThemeValue'
4-
import parseValue from 'postcss-value-parser'
4+
import parseValue from '../value-parser/index'
55
import { normalizeScreens } from '../util/normalizeScreens'
66
import buildMediaQuery from '../util/buildMediaQuery'
77
import { toPath } from '../util/toPath'
@@ -146,6 +146,9 @@ function resolveVNode(node, vNode, functions) {
146146
}
147147

148148
function resolveFunctions(node, input, functions) {
149+
let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
150+
if (!hasAnyFn) return input
151+
149152
return parseValue(input)
150153
.walk((vNode) => {
151154
resolveVNode(node, vNode, functions)

src/util/dataTypes.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,22 @@ export function normalize(value, isRoot = true) {
4949
value = value.trim()
5050
}
5151

52-
// Add spaces around operators inside math functions like calc() that do not follow an operator
53-
// or '('.
54-
value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
52+
value = normalizeMathOperatorSpacing(value)
53+
54+
return value
55+
}
56+
57+
/**
58+
* Add spaces around operators inside math functions
59+
* like calc() that do not follow an operator or '('.
60+
*
61+
* @param {string} value
62+
* @returns {string}
63+
*/
64+
function normalizeMathOperatorSpacing(value) {
65+
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
5566
let vars = []
67+
5668
return match
5769
.replace(/var\((--.+?)[,)]/g, (match, g1) => {
5870
vars.push(g1)
@@ -61,8 +73,6 @@ export function normalize(value, isRoot = true) {
6173
.replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
6274
.replace(placeholderRe, () => vars.shift())
6375
})
64-
65-
return value
6676
}
6777

6878
export function url(value) {

src/value-parser/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright (c) Bogdan Chadkin <trysound@yandex.ru>
2+
3+
Permission is hereby granted, free of charge, to any person
4+
obtaining a copy of this software and associated documentation
5+
files (the "Software"), to deal in the Software without
6+
restriction, including without limitation the rights to use,
7+
copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the
9+
Software is furnished to do so, subject to the following
10+
conditions:
11+
12+
The above copyright notice and this permission notice shall be
13+
included in all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.

src/value-parser/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# postcss-value-parser (forked + inlined)
2+
3+
This is a customized version of of [PostCSS Value Parser](https://github.com/TrySound/postcss-value-parser) to fix some bugs around parsing CSS functions.

src/value-parser/index.d.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
declare namespace postcssValueParser {
2+
interface BaseNode {
3+
/**
4+
* The offset, inclusive, inside the CSS value at which the node starts.
5+
*/
6+
sourceIndex: number
7+
8+
/**
9+
* The offset, exclusive, inside the CSS value at which the node ends.
10+
*/
11+
sourceEndIndex: number
12+
13+
/**
14+
* The node's characteristic value
15+
*/
16+
value: string
17+
}
18+
19+
interface ClosableNode {
20+
/**
21+
* Whether the parsed CSS value ended before the node was properly closed
22+
*/
23+
unclosed?: true
24+
}
25+
26+
interface AdjacentAwareNode {
27+
/**
28+
* The token at the start of the node
29+
*/
30+
before: string
31+
32+
/**
33+
* The token at the end of the node
34+
*/
35+
after: string
36+
}
37+
38+
interface CommentNode extends BaseNode, ClosableNode {
39+
type: 'comment'
40+
}
41+
42+
interface DivNode extends BaseNode, AdjacentAwareNode {
43+
type: 'div'
44+
}
45+
46+
interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode {
47+
type: 'function'
48+
49+
/**
50+
* Nodes inside the function
51+
*/
52+
nodes: Node[]
53+
}
54+
55+
interface SpaceNode extends BaseNode {
56+
type: 'space'
57+
}
58+
59+
interface StringNode extends BaseNode, ClosableNode {
60+
type: 'string'
61+
62+
/**
63+
* The quote type delimiting the string
64+
*/
65+
quote: '"' | "'"
66+
}
67+
68+
interface UnicodeRangeNode extends BaseNode {
69+
type: 'unicode-range'
70+
}
71+
72+
interface WordNode extends BaseNode {
73+
type: 'word'
74+
}
75+
76+
/**
77+
* Any node parsed from a CSS value
78+
*/
79+
type Node =
80+
| CommentNode
81+
| DivNode
82+
| FunctionNode
83+
| SpaceNode
84+
| StringNode
85+
| UnicodeRangeNode
86+
| WordNode
87+
88+
interface CustomStringifierCallback {
89+
/**
90+
* @param node The node to stringify
91+
* @returns The serialized CSS representation of the node
92+
*/
93+
(nodes: Node): string | undefined
94+
}
95+
96+
interface WalkCallback {
97+
/**
98+
* @param node The currently visited node
99+
* @param index The index of the node in the series of parsed nodes
100+
* @param nodes The series of parsed nodes
101+
* @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call)
102+
*/
103+
(node: Node, index: number, nodes: Node[]): void | boolean
104+
}
105+
106+
/**
107+
* A CSS dimension, decomposed into its numeric and unit parts
108+
*/
109+
interface Dimension {
110+
number: string
111+
unit: string
112+
}
113+
114+
/**
115+
* A wrapper around a parsed CSS value that allows for inspecting and walking nodes
116+
*/
117+
interface ParsedValue {
118+
/**
119+
* The series of parsed nodes
120+
*/
121+
nodes: Node[]
122+
123+
/**
124+
* Walk all parsed nodes, applying a callback
125+
*
126+
* @param callback A visitor callback that will be executed for each node
127+
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
128+
*/
129+
walk(callback: WalkCallback, bubble?: boolean): this
130+
}
131+
132+
interface ValueParser {
133+
/**
134+
* Decompose a CSS dimension into its numeric and unit part
135+
*
136+
* @param value The dimension to decompose
137+
* @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails
138+
*/
139+
unit(value: string): Dimension | false
140+
141+
/**
142+
* Serialize a series of nodes into a CSS value
143+
*
144+
* @param nodes The nodes to stringify
145+
* @param custom A custom stringifier callback
146+
* @returns The generated CSS value
147+
*/
148+
stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string
149+
150+
/**
151+
* Walk a series of nodes, applying a callback
152+
*
153+
* @param nodes The nodes to walk
154+
* @param callback A visitor callback that will be executed for each node
155+
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
156+
*/
157+
walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void
158+
159+
/**
160+
* Parse a CSS value into a series of nodes to operate on
161+
*
162+
* @param value The value to parse
163+
*/
164+
new (value: string): ParsedValue
165+
166+
/**
167+
* Parse a CSS value into a series of nodes to operate on
168+
*
169+
* @param value The value to parse
170+
*/
171+
(value: string): ParsedValue
172+
}
173+
}
174+
175+
declare const postcssValueParser: postcssValueParser.ValueParser
176+
177+
export = postcssValueParser

src/value-parser/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
var parse = require('./parse')
2+
var walk = require('./walk')
3+
var stringify = require('./stringify')
4+
5+
function ValueParser(value) {
6+
if (this instanceof ValueParser) {
7+
this.nodes = parse(value)
8+
return this
9+
}
10+
return new ValueParser(value)
11+
}
12+
13+
ValueParser.prototype.toString = function () {
14+
return Array.isArray(this.nodes) ? stringify(this.nodes) : ''
15+
}
16+
17+
ValueParser.prototype.walk = function (cb, bubble) {
18+
walk(this.nodes, cb, bubble)
19+
return this
20+
}
21+
22+
ValueParser.unit = require('./unit')
23+
24+
ValueParser.walk = walk
25+
26+
ValueParser.stringify = stringify
27+
28+
module.exports = ValueParser

0 commit comments

Comments
 (0)