diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 2a1e3079..4a11e6c7 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -13,8 +13,9 @@ "test": "vitest" }, "dependencies": { - "@csstools/css-parser-algorithms": "2.1.1", - "@csstools/css-tokenizer": "2.1.1", + "@csstools/css-calc": "2.1.2", + "@csstools/css-parser-algorithms": "3.0.4", + "@csstools/css-tokenizer": "3.0.3", "@csstools/media-query-list-parser": "2.0.4", "@types/culori": "^2.1.0", "@types/moo": "0.5.3", diff --git a/packages/tailwindcss-language-service/src/util/rewriting/calc.ts b/packages/tailwindcss-language-service/src/util/rewriting/calc.ts index 2420800f..e9949014 100644 --- a/packages/tailwindcss-language-service/src/util/rewriting/calc.ts +++ b/packages/tailwindcss-language-service/src/util/rewriting/calc.ts @@ -1,56 +1,30 @@ -function parseLength(length: string): [number, string] | null { - let regex = /^(-?\d*\.?\d+)([a-z%]*)$/i - let match = length.match(regex) - - if (!match) return null - - let numberPart = parseFloat(match[1]) - if (isNaN(numberPart)) return null - - return [numberPart, match[2]] -} - -function round(n: number, precision: number): number { - return Math.round(n * Math.pow(10, precision)) / Math.pow(10, precision) -} +import { stringify, tokenize } from '@csstools/css-tokenizer' +import { isFunctionNode, parseComponentValue } from '@csstools/css-parser-algorithms' +import { calcFromComponentValues } from '@csstools/css-calc' export function evaluateExpression(str: string): string | null { - // We're only interested simple calc expressions of the form - // A + B, A - B, A * B, A / B + let tokens = tokenize({ css: `calc(${str})` }) - let parts = str.split(/\s+([+*/-])\s+/) + let components = parseComponentValue(tokens, {}) + if (!components) return null - if (parts.length === 1) return null - if (parts.length !== 3) return null + let result = calcFromComponentValues([[components]], { + // Ensure evaluation of random() is deterministic + randomSeed: 1, - let a = parseLength(parts[0]) - let b = parseLength(parts[2]) + // Limit precision to keep values environment independent + precision: 4, + }) - // Not parsable - if (!a || !b) { - return null - } - - // Addition and subtraction require the same units - if ((parts[1] === '+' || parts[1] === '-') && a[1] !== b[1]) { - return null - } - - // Multiplication and division require at least one unit to be empty - if ((parts[1] === '*' || parts[1] === '/') && a[1] !== '' && b[1] !== '') { - return null - } + // The result array is the same shape as the original so we're guaranteed to + // have an element here + let node = result[0][0] - switch (parts[1]) { - case '+': - return round(a[0] + b[0], 4).toString() + a[1] - case '*': - return round(a[0] * b[0], 4).toString() + a[1] - case '-': - return round(a[0] - b[0], 4).toString() + a[1] - case '/': - return round(a[0] / b[0], 4).toString() + a[1] + // If we have a top-level `calc(…)` node then the evaluation did not resolve + // to a single value and we consider it to be incomplete + if (isFunctionNode(node)) { + if (node.name[1] === 'calc(') return null } - return null + return stringify(...node.tokens()) } diff --git a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts index 6eb840ef..3adbdb44 100644 --- a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts +++ b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts @@ -95,6 +95,8 @@ test('Evaluating CSS calc expressions', () => { expect(replaceCssCalc('calc(1.25 / 0.875)', (node) => evaluateExpression(node.value))).toBe( '1.4286', ) + + expect(replaceCssCalc('calc(1/4 * 100%)', (node) => evaluateExpression(node.value))).toBe('25%') }) test('Inlining calc expressions using the design system', () => { diff --git a/packages/tailwindcss-language-service/tsconfig.json b/packages/tailwindcss-language-service/tsconfig.json index 883356e7..46eb230c 100644 --- a/packages/tailwindcss-language-service/tsconfig.json +++ b/packages/tailwindcss-language-service/tsconfig.json @@ -1,7 +1,7 @@ { "include": ["src", "../../types"], "compilerOptions": { - "module": "NodeNext", + "module": "ES2022", "lib": ["ES2022"], "target": "ES2022", "importHelpers": true, @@ -13,7 +13,7 @@ "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "NodeNext", + "moduleResolution": "Bundler", "skipLibCheck": true, "jsx": "react", "esModuleInterop": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2255d6ad..f0971dbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,15 +248,18 @@ importers: packages/tailwindcss-language-service: dependencies: + '@csstools/css-calc': + specifier: 2.1.2 + version: 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/css-parser-algorithms': - specifier: 2.1.1 - version: 2.1.1(@csstools/css-tokenizer@2.1.1) + specifier: 3.0.4 + version: 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': - specifier: 2.1.1 - version: 2.1.1 + specifier: 3.0.3 + version: 3.0.3 '@csstools/media-query-list-parser': specifier: 2.0.4 - version: 2.0.4(@csstools/css-parser-algorithms@2.1.1(@csstools/css-tokenizer@2.1.1))(@csstools/css-tokenizer@2.1.1) + version: 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@types/culori': specifier: ^2.1.0 version: 2.1.1 @@ -440,15 +443,22 @@ packages: resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} engines: {node: '>=6.9.0'} - '@csstools/css-parser-algorithms@2.1.1': - resolution: {integrity: sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==} - engines: {node: ^14 || ^16 || >=18} + '@csstools/css-calc@2.1.2': + resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^2.1.1 + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 - '@csstools/css-tokenizer@2.1.1': - resolution: {integrity: sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==} - engines: {node: ^14 || ^16 || >=18} + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} '@csstools/media-query-list-parser@2.0.4': resolution: {integrity: sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==} @@ -800,13 +810,11 @@ packages: '@parcel/watcher-darwin-arm64@2.5.1': resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.5.1': resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.5.1': @@ -830,31 +838,26 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [win32] '@parcel/watcher-win32-ia32@2.5.1': @@ -866,7 +869,6 @@ packages: '@parcel/watcher-win32-x64@2.5.1': resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [win32] '@parcel/watcher@2.5.1': @@ -2908,16 +2910,21 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@csstools/css-parser-algorithms@2.1.1(@csstools/css-tokenizer@2.1.1)': + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-tokenizer': 2.1.1 + '@csstools/css-tokenizer': 3.0.3 - '@csstools/css-tokenizer@2.1.1': {} + '@csstools/css-tokenizer@3.0.3': {} - '@csstools/media-query-list-parser@2.0.4(@csstools/css-parser-algorithms@2.1.1(@csstools/css-tokenizer@2.1.1))(@csstools/css-tokenizer@2.1.1)': + '@csstools/media-query-list-parser@2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-parser-algorithms': 2.1.1(@csstools/css-tokenizer@2.1.1) - '@csstools/css-tokenizer': 2.1.1 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@esbuild/aix-ppc64@0.21.5': optional: true