From 6015d83f34a11de7d4e53870bc2917176f7e146a Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 30 Nov 2021 15:35:30 +0100 Subject: [PATCH 01/12] postcss-value-parser : postcss-place --- plugins/postcss-place/.tape.js | 2 -- plugins/postcss-place/package.json | 2 +- plugins/postcss-place/src/onCSSDeclaration.js | 19 ++++++++++++------- plugins/postcss-place/test/basic.css | 6 ++++++ plugins/postcss-place/test/basic.expect.css | 14 ++++++++++++++ .../test/basic.preserve-false.expect.css | 12 +++++++++++- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/plugins/postcss-place/.tape.js b/plugins/postcss-place/.tape.js index b8be1eaac..7a94ad68b 100644 --- a/plugins/postcss-place/.tape.js +++ b/plugins/postcss-place/.tape.js @@ -1,11 +1,9 @@ module.exports = { 'basic': { message: 'supports basic usage', - warnings: 1, }, 'basic:preserve-false': { message: 'supports { preserve: false } usage', - warnings: 1, options: { preserve: false } diff --git a/plugins/postcss-place/package.json b/plugins/postcss-place/package.json index dfdefcce7..699c84ffd 100755 --- a/plugins/postcss-place/package.json +++ b/plugins/postcss-place/package.json @@ -29,7 +29,7 @@ "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-place/src/onCSSDeclaration.js b/plugins/postcss-place/src/onCSSDeclaration.js index b941e919b..3d951fb4a 100644 --- a/plugins/postcss-place/src/onCSSDeclaration.js +++ b/plugins/postcss-place/src/onCSSDeclaration.js @@ -1,4 +1,4 @@ -import { parse } from 'postcss-values-parser'; +import valueParser from 'postcss-value-parser'; import options from './options'; export default (decl, { result }) => { @@ -9,7 +9,7 @@ export default (decl, { result }) => { let value; try { - value = parse(decl.value); + value = valueParser(decl.value); } catch (error) { decl.warn( result, @@ -22,11 +22,16 @@ export default (decl, { result }) => { } let alignmentValues = []; - value.walkWords(walk => { - alignmentValues.push( - walk.parent.type === 'root' ? walk.toString() : walk.parent.toString(), - ); - }); + + if (!value.nodes.length) { + alignmentValues = [valueParser.stringify(value)]; + } else { + alignmentValues = value.nodes.filter((node) => { + return node.type === 'word' || node.type === 'function'; + }).map((node) => { + return valueParser.stringify(node); + }); + } decl.cloneBefore({ prop: `align-${alignment}`, diff --git a/plugins/postcss-place/test/basic.css b/plugins/postcss-place/test/basic.css index f8acf3302..e52a31198 100644 --- a/plugins/postcss-place/test/basic.css +++ b/plugins/postcss-place/test/basic.css @@ -22,6 +22,12 @@ d { place-self: first var(--second); } +e { + place-content: var(--first, --fallback) second; + place-items: var(--first, --fallback) second; + place-self: var(--first, --fallback) second; +} + .test-unparseable-var-in-place-declaration { place-content: var(; ); } diff --git a/plugins/postcss-place/test/basic.expect.css b/plugins/postcss-place/test/basic.expect.css index 47fdfd31c..eb6aef986 100644 --- a/plugins/postcss-place/test/basic.expect.css +++ b/plugins/postcss-place/test/basic.expect.css @@ -46,6 +46,20 @@ d { place-self: first var(--second); } +e { + align-content: var(--first, --fallback); + justify-content: second; + place-content: var(--first, --fallback) second; + align-items: var(--first, --fallback); + justify-items: second; + place-items: var(--first, --fallback) second; + align-self: var(--first, --fallback); + justify-self: second; + place-self: var(--first, --fallback) second; +} + .test-unparseable-var-in-place-declaration { + align-content: var(; ); + justify-content: var(; ); place-content: var(; ); } diff --git a/plugins/postcss-place/test/basic.preserve-false.expect.css b/plugins/postcss-place/test/basic.preserve-false.expect.css index 81eb3de9f..9aee8dc2f 100644 --- a/plugins/postcss-place/test/basic.preserve-false.expect.css +++ b/plugins/postcss-place/test/basic.preserve-false.expect.css @@ -34,6 +34,16 @@ d { justify-self: var(--second); } +e { + align-content: var(--first, --fallback); + justify-content: second; + align-items: var(--first, --fallback); + justify-items: second; + align-self: var(--first, --fallback); + justify-self: second; +} + .test-unparseable-var-in-place-declaration { - place-content: var(; ); + align-content: var(; ); + justify-content: var(; ); } From 6025e8e31217980400425e98930b2175f38d12cb Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 30 Nov 2021 17:32:20 +0100 Subject: [PATCH 02/12] improve readability --- .../src/lib/get-image.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/postcss-image-set-function/src/lib/get-image.ts b/plugins/postcss-image-set-function/src/lib/get-image.ts index 4c2e82f6b..786847dab 100644 --- a/plugins/postcss-image-set-function/src/lib/get-image.ts +++ b/plugins/postcss-image-set-function/src/lib/get-image.ts @@ -5,15 +5,24 @@ const imageFuncRegexp = /^(cross-fade|image|(repeating-)?(conic|linear|radial)-g export function getImage(node) { // | | | // the image-set() function can not be nested inside of itself - return Object(node).type === 'func' && + if (!node || !node.type) { + return false; + } + + if (node.type === 'func' && imageFuncRegexp.test(node.name) && !( + node.parent && node.parent.parent && node.parent.parent.type === 'func' && imageSetFunctionMatchRegExp.test(node.parent.parent.name) - ) - ? (node.raws.before || '') + String(node) - : Object(node).type === 'quoted' - ? node.value - : false; + )) { + return (node.raws.before || '') + String(node); + } + + if (node.type === 'quoted') { + return node.value; + } + + return false; } From 71e55baccd6a416993dd634e1ab4ef79479c0873 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 30 Nov 2021 18:23:36 +0100 Subject: [PATCH 03/12] postcss-value-parser : postcss-image-set-function --- plugins/postcss-image-set-function/.tape.js | 4 +- .../postcss-image-set-function/package.json | 2 +- .../postcss-image-set-function/src/index.ts | 37 ++++++++++++++++--- .../src/lib/get-comma.ts | 2 +- .../src/lib/get-image.ts | 22 +++++------ .../src/lib/get-media.ts | 20 +++++++--- .../src/lib/process-image-set.ts | 9 +++-- .../postcss-image-set-function/test/basic.css | 14 ++++++- .../test/basic.expect.css | 37 ++++++++++++++++++- .../test/basic.no-preserve.expect.css | 33 ++++++++++++++++- 10 files changed, 145 insertions(+), 35 deletions(-) diff --git a/plugins/postcss-image-set-function/.tape.js b/plugins/postcss-image-set-function/.tape.js index ff20d1584..82f720c45 100644 --- a/plugins/postcss-image-set-function/.tape.js +++ b/plugins/postcss-image-set-function/.tape.js @@ -1,11 +1,9 @@ module.exports = { 'basic': { message: 'supports basic usage', - warnings: 2, }, 'basic:no-preserve': { message: 'supports { preserve: false } usage', - warnings: 2, options: { preserve: false } @@ -22,7 +20,7 @@ module.exports = { }, expect: 'invalid.css', result: 'invalid.css', - warnings: 10 + warnings: 9 }, 'invalid:throw': { message: 'throws invalid usage when { onvalid: "throw" }', diff --git a/plugins/postcss-image-set-function/package.json b/plugins/postcss-image-set-function/package.json index a55ccae1e..9ccdc824f 100644 --- a/plugins/postcss-image-set-function/package.json +++ b/plugins/postcss-image-set-function/package.json @@ -30,7 +30,7 @@ "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-image-set-function/src/index.ts b/plugins/postcss-image-set-function/src/index.ts index 617f07cfd..08267d364 100644 --- a/plugins/postcss-image-set-function/src/index.ts +++ b/plugins/postcss-image-set-function/src/index.ts @@ -1,6 +1,7 @@ -import { parse } from 'postcss-values-parser'; +import valueParser from 'postcss-value-parser'; import { processImageSet } from './lib/process-image-set'; import type { PluginCreator } from 'postcss'; +import { handleInvalidation } from './lib/handle-invalidation'; const imageSetValueMatchRegExp = /(^|[^\w-])(-webkit-)?image-set\(/i; const imageSetFunctionMatchRegExp = /^(-webkit-)?image-set$/i; @@ -23,7 +24,7 @@ const creator: PluginCreator<{ preserve: boolean, oninvalid: string }> = (opts?: let valueAST; try { - valueAST = parse(value, { ignoreUnknownWords: true }); + valueAST = valueParser(value); } catch (error) { decl.warn( result, @@ -36,12 +37,38 @@ const creator: PluginCreator<{ preserve: boolean, oninvalid: string }> = (opts?: } // process every image-set() function - valueAST.walkFuncs((node) => { - if (!imageSetFunctionMatchRegExp.test(node.name)) { + valueAST.walk((node) => { + if (node.type !== 'function') { return; } - processImageSet(node.nodes, decl, { + if (!imageSetFunctionMatchRegExp.test(node.value)) { + return; + } + + let foundNestedImageSet = false; + valueParser.walk(node.nodes, (child) => { + if ( + child.type === 'function' && + imageSetFunctionMatchRegExp.test(child.value) + ) { + foundNestedImageSet = true; + } + }); + if (foundNestedImageSet) { + handleInvalidation({ + decl, + oninvalid, + result: result, + }, 'nested image-set functions are not allowed', valueParser.stringify(node)); + return false; + } + + const relevantNodes = node.nodes.filter((x) => { + return x.type !== 'comment' && x.type !== 'space'; + }); + + processImageSet(relevantNodes, decl, { decl, oninvalid, preserve, diff --git a/plugins/postcss-image-set-function/src/lib/get-comma.ts b/plugins/postcss-image-set-function/src/lib/get-comma.ts index f462049f2..3277e8cea 100644 --- a/plugins/postcss-image-set-function/src/lib/get-comma.ts +++ b/plugins/postcss-image-set-function/src/lib/get-comma.ts @@ -1,4 +1,4 @@ // return whether a node is a valid comma export function getComma(node) { - return Object(node).type === 'punctuation' && Object(node).value === ','; + return Object(node).type === 'div' && Object(node).value === ','; } diff --git a/plugins/postcss-image-set-function/src/lib/get-image.ts b/plugins/postcss-image-set-function/src/lib/get-image.ts index 786847dab..c82775962 100644 --- a/plugins/postcss-image-set-function/src/lib/get-image.ts +++ b/plugins/postcss-image-set-function/src/lib/get-image.ts @@ -1,6 +1,6 @@ -const imageSetFunctionMatchRegExp = /^(-webkit-)?image-set$/i; +import valueParser from 'postcss-value-parser'; -const imageFuncRegexp = /^(cross-fade|image|(repeating-)?(conic|linear|radial)-gradient|url)$/i; +const imageFuncRegexp = /^(cross-fade|image|(repeating-)?(conic|linear|radial)-gradient|url|var)$/i; export function getImage(node) { // | | | @@ -9,19 +9,15 @@ export function getImage(node) { return false; } - if (node.type === 'func' && - imageFuncRegexp.test(node.name) && - !( - node.parent && - node.parent.parent && - node.parent.parent.type === 'func' && - imageSetFunctionMatchRegExp.test(node.parent.parent.name) - )) { - return (node.raws.before || '') + String(node); + if (node.type === 'string') { + return valueParser.stringify(node); } - if (node.type === 'quoted') { - return node.value; + if ( + node.type === 'function' && + imageFuncRegexp.test(node.value) + ) { + return valueParser.stringify(node); } return false; diff --git a/plugins/postcss-image-set-function/src/lib/get-media.ts b/plugins/postcss-image-set-function/src/lib/get-media.ts index a0cc3d82c..f9246f2a7 100644 --- a/plugins/postcss-image-set-function/src/lib/get-media.ts +++ b/plugins/postcss-image-set-function/src/lib/get-media.ts @@ -1,4 +1,4 @@ -import type { Node, Numeric } from 'postcss-values-parser'; +import type { Node } from 'postcss-value-parser'; const dpiRatios = { dpcm: 2.54, dpi: 1, dppx: 96, x: 96 }; @@ -20,15 +20,25 @@ export function getMedia(dpi: number | false, postcss) { } export function getMediaDPI(node: Node) { - if (Object(node).type !== 'numeric') { + if (!node) { return false; } - const numeric = node as Numeric; + if (node.type !== 'word') { + return false; + } + + const unit = node.value.replace(/^(\d+)/, ''); + if (unit === node.value) { + // No numeric value found. + return false; + } + + const value = node.value.slice(0, -unit.length); - if (numeric.unit in dpiRatios) { + if (unit.toLowerCase() in dpiRatios) { // calculate min-device-pixel-ratio and min-resolution - return Number(numeric.value) * dpiRatios[numeric.unit.toLowerCase()]; + return Number(value) * dpiRatios[unit.toLowerCase()]; } else { return false; } diff --git a/plugins/postcss-image-set-function/src/lib/process-image-set.ts b/plugins/postcss-image-set-function/src/lib/process-image-set.ts index b0ff74d0b..2fc2c4415 100644 --- a/plugins/postcss-image-set-function/src/lib/process-image-set.ts +++ b/plugins/postcss-image-set-function/src/lib/process-image-set.ts @@ -1,3 +1,4 @@ +import valueParser from 'postcss-value-parser'; import { getComma } from './get-comma'; import { getImage } from './get-image'; import { getMedia, getMediaDPI } from './get-media'; @@ -20,13 +21,13 @@ export const processImageSet = (imageSetOptionNodes, decl: Declaration, opts: { // handle invalidations if (!comma) { - handleInvalidation(opts, 'unexpected comma', imageSetOptionNodes[index]); + handleInvalidation(opts, 'expected a comma', valueParser.stringify(imageSetOptionNodes)); return; } else if (!value) { - handleInvalidation(opts, 'unexpected image', imageSetOptionNodes[index + 1]); + handleInvalidation(opts, 'unexpected image', valueParser.stringify(imageSetOptionNodes)); return; } else if (!media || !mediaDPI || mediasByDpr.has(mediaDPI)) { - handleInvalidation(opts, 'unexpected resolution', imageSetOptionNodes[index + 2]); + handleInvalidation(opts, 'unexpected resolution', valueParser.stringify(imageSetOptionNodes)); return; } @@ -68,4 +69,6 @@ export const processImageSet = (imageSetOptionNodes, decl: Declaration, opts: { parent.remove(); } } + + return; }; diff --git a/plugins/postcss-image-set-function/test/basic.css b/plugins/postcss-image-set-function/test/basic.css index 378376847..e35600287 100644 --- a/plugins/postcss-image-set-function/test/basic.css +++ b/plugins/postcss-image-set-function/test/basic.css @@ -29,6 +29,15 @@ order: 5; } +.test-variables { + order: 1; + background-image: image-set( + url(img/test.png) 1x, + var(--test-image-2x) 2x, + var(--test-image-3x, url(img/test-var-fallback-3x.png)) 3x + ); +} + .test-mixed-units { order: 1; background-image: image-set( @@ -104,7 +113,10 @@ } } -.test-unparseable-image-set-function { +.test-unparseable-image-set-function-a { background-image: image-set(url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==) 1x, url(img/test-2x.png) 2x); +} + +.test-unparseable-image-set-function-b { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } diff --git a/plugins/postcss-image-set-function/test/basic.expect.css b/plugins/postcss-image-set-function/test/basic.expect.css index 08b5ef9a0..0fd1f0e0b 100644 --- a/plugins/postcss-image-set-function/test/basic.expect.css +++ b/plugins/postcss-image-set-function/test/basic.expect.css @@ -54,6 +54,30 @@ } } +.test-variables { + order: 1; + background-image: url(img/test.png); + background-image: image-set( + url(img/test.png) 1x, + var(--test-image-2x) 2x, + var(--test-image-3x, url(img/test-var-fallback-3x.png)) 3x + ); +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + +.test-variables { + background-image: var(--test-image-2x); +} +} + +@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) { + +.test-variables { + background-image: var(--test-image-3x, url(img/test-var-fallback-3x.png)); +} +} + .test-mixed-units { order: 1; background-image: url(img/test.png); @@ -235,7 +259,18 @@ } } -.test-unparseable-image-set-function { +.test-unparseable-image-set-function-a { + background-image: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==); background-image: image-set(url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==) 1x, url(img/test-2x.png) 2x); +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + +.test-unparseable-image-set-function-a { + background-image: url(img/test-2x.png); +} +} + +.test-unparseable-image-set-function-b { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } diff --git a/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css b/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css index 76f2c54ab..88dd21bd4 100644 --- a/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css +++ b/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css @@ -39,6 +39,25 @@ } } +.test-variables { + order: 1; + background-image: url(img/test.png); +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + +.test-variables { + background-image: var(--test-image-2x); +} +} + +@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) { + +.test-variables { + background-image: var(--test-image-3x, url(img/test-var-fallback-3x.png)); +} +} + .test-mixed-units { order: 1; background-image: url(img/test.png); @@ -178,7 +197,17 @@ } } -.test-unparseable-image-set-function { - background-image: image-set(url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==) 1x, url(img/test-2x.png) 2x); +.test-unparseable-image-set-function-a { + background-image: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==); +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + +.test-unparseable-image-set-function-a { + background-image: url(img/test-2x.png); +} +} + +.test-unparseable-image-set-function-b { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } From a8d60c1e7a85732de533dd26616dbfb7f3122eec Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 30 Nov 2021 18:34:01 +0100 Subject: [PATCH 04/12] feedback + rename test cases --- .../src/lib/get-media.ts | 28 ++++++++++++++----- .../postcss-image-set-function/test/basic.css | 4 +-- .../test/basic.expect.css | 6 ++-- .../test/basic.no-preserve.expect.css | 6 ++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/plugins/postcss-image-set-function/src/lib/get-media.ts b/plugins/postcss-image-set-function/src/lib/get-media.ts index f9246f2a7..2b96949cd 100644 --- a/plugins/postcss-image-set-function/src/lib/get-media.ts +++ b/plugins/postcss-image-set-function/src/lib/get-media.ts @@ -1,4 +1,5 @@ -import type { Node } from 'postcss-value-parser'; +import type { Node, Dimension } from 'postcss-value-parser'; +import valueParser from 'postcss-value-parser'; const dpiRatios = { dpcm: 2.54, dpi: 1, dppx: 96, x: 96 }; @@ -28,18 +29,31 @@ export function getMediaDPI(node: Node) { return false; } - const unit = node.value.replace(/^(\d+)/, ''); - if (unit === node.value) { - // No numeric value found. + if (!isNumericNode(node)) { return false; } - const value = node.value.slice(0, -unit.length); + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } - if (unit.toLowerCase() in dpiRatios) { + if (unitAndValue.unit.toLowerCase() in dpiRatios) { // calculate min-device-pixel-ratio and min-resolution - return Number(value) * dpiRatios[unit.toLowerCase()]; + return Number(unitAndValue.number) * dpiRatios[unitAndValue.unit.toLowerCase()]; } else { return false; } } + +function isNumericNode(node): Dimension | false { + if (!node || !node.value) { + return false; + } + + try { + return valueParser.unit(node.value); + } catch (e) { + return false; + } +} diff --git a/plugins/postcss-image-set-function/test/basic.css b/plugins/postcss-image-set-function/test/basic.css index e35600287..506ccc72c 100644 --- a/plugins/postcss-image-set-function/test/basic.css +++ b/plugins/postcss-image-set-function/test/basic.css @@ -113,10 +113,10 @@ } } -.test-unparseable-image-set-function-a { +.test-valid-data-url { background-image: image-set(url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==) 1x, url(img/test-2x.png) 2x); } -.test-unparseable-image-set-function-b { +.test-invalid-data-url { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } diff --git a/plugins/postcss-image-set-function/test/basic.expect.css b/plugins/postcss-image-set-function/test/basic.expect.css index 0fd1f0e0b..242ce0d1f 100644 --- a/plugins/postcss-image-set-function/test/basic.expect.css +++ b/plugins/postcss-image-set-function/test/basic.expect.css @@ -259,18 +259,18 @@ } } -.test-unparseable-image-set-function-a { +.test-valid-data-url { background-image: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==); background-image: image-set(url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==) 1x, url(img/test-2x.png) 2x); } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { -.test-unparseable-image-set-function-a { +.test-valid-data-url { background-image: url(img/test-2x.png); } } -.test-unparseable-image-set-function-b { +.test-invalid-data-url { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } diff --git a/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css b/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css index 88dd21bd4..b27f333b4 100644 --- a/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css +++ b/plugins/postcss-image-set-function/test/basic.no-preserve.expect.css @@ -197,17 +197,17 @@ } } -.test-unparseable-image-set-function-a { +.test-valid-data-url { background-image: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==); } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { -.test-unparseable-image-set-function-a { +.test-valid-data-url { background-image: url(img/test-2x.png); } } -.test-unparseable-image-set-function-b { +.test-invalid-data-url { background-image: image-set(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==, url(img/test-2x.png) 2x); } From 3b3702c826dba1ecdbf4fd197f16a7c32a73a275 Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Tue, 30 Nov 2021 18:52:20 +0100 Subject: [PATCH 05/12] postcss-value-parser : postcss-double-position-gradients --- .../package.json | 2 +- .../src/index.js | 57 ++++++++++++------- .../test/basic.expect.css | 2 +- .../test/basic.preserve.expect.css | 2 +- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/plugins/postcss-double-position-gradients/package.json b/plugins/postcss-double-position-gradients/package.json index b08676fb0..7de4f50a5 100644 --- a/plugins/postcss-double-position-gradients/package.json +++ b/plugins/postcss-double-position-gradients/package.json @@ -29,7 +29,7 @@ "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-double-position-gradients/src/index.js b/plugins/postcss-double-position-gradients/src/index.js index d8dad963a..9aa7a0f53 100644 --- a/plugins/postcss-double-position-gradients/src/index.js +++ b/plugins/postcss-double-position-gradients/src/index.js @@ -1,11 +1,29 @@ -const { parse } = require('postcss-values-parser'); -const Punctuation = require('postcss-values-parser/lib/nodes/Punctuation'); +const valueParser = require('postcss-value-parser'); // whether the value has a lab() or lch() matcher const gradientRegExp = /(repeating-)?(conic|linear|radial)-gradient\([\W\w]*\)/i; const gradientPartsRegExp = /^(repeating-)?(conic|linear|radial)-gradient$/i; -const isPunctuationCommaNode = node => node.type === 'punctuation' && node.value === ','; +const isPunctuationCommaNode = node => node.type === 'div' && node.value === ','; + +function insertBefore(nodes, node, ...values) { + const index = nodes.findIndex(n => n === node); + nodes.splice.apply(nodes, [index - 1, 0].concat( + Array.prototype.slice.call(...values, 0)), + ); +} + +function isNumericNode(node) { + let result = false; + + try { + result = valueParser.unit(node?.value) !== false; + } catch (e) { + // Error silently + } + + return result; +} /** * Transform double-position gradients in CSS. @@ -25,7 +43,7 @@ module.exports = function creator(opts) { let valueAST; try { - valueAST = parse(decl.value, { ignoreUnknownWords: true }); + valueAST = valueParser(decl.value); } catch (error) { decl.warn( result, @@ -38,37 +56,38 @@ module.exports = function creator(opts) { return; } - valueAST.walkFuncs((func) => { - if (!gradientPartsRegExp.test(func.name)) { + valueAST.walk(func => { + if (func.type !== 'function' || !gradientPartsRegExp.test(func.value)) { return; } const nodes = func.nodes; nodes.slice(0).forEach((node, index, nodes) => { - const node1back = Object(nodes[index - 1]); - const node2back = Object(nodes[index - 2]); - const node1next = Object(nodes[index + 1]); - - const isDoublePositionLength = node2back.type && node1back.type === 'numeric' && node.type === 'numeric'; + const oneValueBack = Object(nodes[index - 2]); // Skip one for space + const twoValuesBack = Object(nodes[index - 4]); + const nextNode = Object(nodes[index + 2]); + const isDoublePositionLength = twoValuesBack.type && isNumericNode(oneValueBack) && isNumericNode(node); // if the argument concludes a double-position gradient if (isDoublePositionLength) { // insert the fallback colors - const color = node2back.clone(); - const comma = new Punctuation({ + const color = { type: twoValuesBack.type, value: twoValuesBack.value }; + const comma = { + type: 'div', value: ',', - raws: isPunctuationCommaNode(node1next) - ? Object.assign({}, node1next.clone().raws) - : { before: '', after: '' }, - }); + before: isPunctuationCommaNode(nextNode) ? nextNode.before : '', + after: isPunctuationCommaNode(nextNode) ? '' : ' ', + }; - func.insertBefore(node, [comma, color]); + insertBefore(func.nodes, node, [comma, color]); } }); + + return false; }); - const modifiedValue = String(valueAST); + const modifiedValue = valueAST.toString(); if (modifiedValue !== decl.value) { if (preserve) decl.cloneBefore({ value: modifiedValue }); diff --git a/plugins/postcss-double-position-gradients/test/basic.expect.css b/plugins/postcss-double-position-gradients/test/basic.expect.css index 34a118a1a..cb434b9d6 100644 --- a/plugins/postcss-double-position-gradients/test/basic.expect.css +++ b/plugins/postcss-double-position-gradients/test/basic.expect.css @@ -4,7 +4,7 @@ } .test-conic-gradient { - background-image: conic-gradient(yellowgreen 40%, gold 0deg , gold 75% , #f06 0deg); + background-image: conic-gradient(yellowgreen 40%, gold 0deg, gold 75% , #f06 0deg); background-image: conic-gradient(yellowgreen 40%, gold 0deg 75% , #f06 0deg); } diff --git a/plugins/postcss-double-position-gradients/test/basic.preserve.expect.css b/plugins/postcss-double-position-gradients/test/basic.preserve.expect.css index afebbf2cd..31d8777be 100644 --- a/plugins/postcss-double-position-gradients/test/basic.preserve.expect.css +++ b/plugins/postcss-double-position-gradients/test/basic.preserve.expect.css @@ -3,7 +3,7 @@ } .test-conic-gradient { - background-image: conic-gradient(yellowgreen 40%, gold 0deg , gold 75% , #f06 0deg); + background-image: conic-gradient(yellowgreen 40%, gold 0deg, gold 75% , #f06 0deg); } .test-invalid-function { From 8f997ca56f29d141166edbbfdc0132377cb6361d Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Tue, 30 Nov 2021 19:04:28 +0100 Subject: [PATCH 06/12] Feedback --- .../postcss-double-position-gradients/src/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/postcss-double-position-gradients/src/index.js b/plugins/postcss-double-position-gradients/src/index.js index 9aa7a0f53..3fd9cfdd0 100644 --- a/plugins/postcss-double-position-gradients/src/index.js +++ b/plugins/postcss-double-position-gradients/src/index.js @@ -61,12 +61,13 @@ module.exports = function creator(opts) { return; } - const nodes = func.nodes; + // Discarding commas and spaces + const nodes = func.nodes.filter(n => n.type === 'word'); - nodes.slice(0).forEach((node, index, nodes) => { - const oneValueBack = Object(nodes[index - 2]); // Skip one for space - const twoValuesBack = Object(nodes[index - 4]); - const nextNode = Object(nodes[index + 2]); + nodes.forEach((node, index, nodes) => { + const oneValueBack = Object(nodes[index - 1]); + const twoValuesBack = Object(nodes[index - 2]); + const nextNode = Object(nodes[index + 1]); const isDoublePositionLength = twoValuesBack.type && isNumericNode(oneValueBack) && isNumericNode(node); // if the argument concludes a double-position gradient From 7d13a204b5d4ff38112579da8ff18886438229a4 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 30 Nov 2021 23:04:02 +0100 Subject: [PATCH 07/12] postcss-value-parser : postcss-lab-function --- .../src/lib/get-media.ts | 4 +- plugins/postcss-lab-function/.tape.js | 2 - plugins/postcss-lab-function/package.json | 2 +- plugins/postcss-lab-function/src/index.ts | 19 +- .../src/on-css-function.ts | 287 +++++++++++++----- 5 files changed, 223 insertions(+), 91 deletions(-) diff --git a/plugins/postcss-image-set-function/src/lib/get-media.ts b/plugins/postcss-image-set-function/src/lib/get-media.ts index 2b96949cd..257457992 100644 --- a/plugins/postcss-image-set-function/src/lib/get-media.ts +++ b/plugins/postcss-image-set-function/src/lib/get-media.ts @@ -46,13 +46,13 @@ export function getMediaDPI(node: Node) { } } -function isNumericNode(node): Dimension | false { +function isNumericNode(node): boolean { if (!node || !node.value) { return false; } try { - return valueParser.unit(node.value); + return valueParser.unit(node.value) !== false; } catch (e) { return false; } diff --git a/plugins/postcss-lab-function/.tape.js b/plugins/postcss-lab-function/.tape.js index 8cc7f74c5..fb5870499 100644 --- a/plugins/postcss-lab-function/.tape.js +++ b/plugins/postcss-lab-function/.tape.js @@ -1,11 +1,9 @@ module.exports = { 'basic': { message: 'supports basic usage', - warnings: 1, }, 'basic:preserve-true': { message: 'supports { preserve: true } usage', - warnings: 1, options: { preserve: true } diff --git a/plugins/postcss-lab-function/package.json b/plugins/postcss-lab-function/package.json index 0a847926b..e7c60dc65 100644 --- a/plugins/postcss-lab-function/package.json +++ b/plugins/postcss-lab-function/package.json @@ -31,7 +31,7 @@ "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-lab-function/src/index.ts b/plugins/postcss-lab-function/src/index.ts index c0aa16f5c..85a6e34c1 100644 --- a/plugins/postcss-lab-function/src/index.ts +++ b/plugins/postcss-lab-function/src/index.ts @@ -1,5 +1,6 @@ import { hasSupportsAtRuleAncestor } from './has-supports-at-rule-ancestor'; -import { parse, Root } from 'postcss-values-parser'; +import valueParser from 'postcss-value-parser'; +import type { ParsedValue, FunctionNode } from 'postcss-value-parser'; import type { Declaration, Postcss, Result } from 'postcss'; import onCSSFunction from './on-css-function'; @@ -24,10 +25,10 @@ const postcssPlugin: PluginCreator<{ preserve: boolean }> = (opts?: { preserve: return; } - let valueAST: Root|undefined; + let valueAST: ParsedValue|undefined; try { - valueAST = parse(originalValue, { ignoreUnknownWords: true }); + valueAST = valueParser(originalValue); } catch (error) { decl.warn( result, @@ -39,7 +40,17 @@ const postcssPlugin: PluginCreator<{ preserve: boolean }> = (opts?: { preserve: return; } - valueAST.walkType('func', onCSSFunction); + valueAST.walk((node) => { + if (!node.type || node.type !== 'function') { + return; + } + + if (node.value !== 'lab' && node.value !== 'lch') { + return; + } + + onCSSFunction(node as FunctionNode); + }); const modifiedValue = String(valueAST); if (modifiedValue === originalValue) { diff --git a/plugins/postcss-lab-function/src/on-css-function.ts b/plugins/postcss-lab-function/src/on-css-function.ts index 5b147007c..b7630dd8b 100644 --- a/plugins/postcss-lab-function/src/on-css-function.ts +++ b/plugins/postcss-lab-function/src/on-css-function.ts @@ -1,45 +1,46 @@ -import { parse } from 'postcss-values-parser'; -import type { Func, Numeric, Operator } from 'postcss-values-parser'; +import valueParser from 'postcss-value-parser'; +import type { FunctionNode, Dimension, Node, DivNode, WordNode } from 'postcss-value-parser'; import { labToSRgb, lchToSRgb } from './color'; -function onCSSFunction(node: Func) { - const name = node.name; +function onCSSFunction(node: FunctionNode) { + const value = node.value; const rawNodes = node.nodes; - if (name !== 'lab' && name !== 'lch') { - return; - } + const relevantNodes = rawNodes.slice().filter((x) => { + return x.type !== 'comment' && x.type !== 'space'; + }); let nodes: Lch | Lab | null = null; - if (name === 'lab') { - nodes = labFunctionContents(rawNodes); - } else if (name === 'lch') { - nodes = lchFunctionContents(rawNodes); + if (value === 'lab') { + nodes = labFunctionContents(relevantNodes); + } else if (value === 'lch') { + nodes = lchFunctionContents(relevantNodes); } if (!nodes) { return; } - if (rawNodes.length > 3 && (!nodes.slash || !nodes.alpha)) { + if (relevantNodes.length > 3 && (!nodes.slash || !nodes.alpha)) { return; } // rename the Color function to `rgb` - node.name = 'rgb'; + node.value = 'rgb'; transformAlpha(node, nodes.slash, nodes.alpha); /** Extracted Color channels. */ const [channelNode1, channelNode2, channelNode3] = channelNodes(nodes); + const [channelDimension1, channelDimension2, channelDimension3] = channelDimensions(nodes); /** Corresponding Color transformer. */ - const toRGB = name === 'lab' ? labToSRgb : lchToSRgb; + const toRGB = value === 'lab' ? labToSRgb : lchToSRgb; /** RGB channels from the source color. */ const channelNumbers: [number, number, number] = [ - channelNode1.value, - channelNode2.value, - channelNode3.value, + channelDimension1.number, + channelDimension2.number, + channelDimension3.number, ].map( channelNumber => parseFloat(channelNumber), ) as [number, number, number]; @@ -50,69 +51,151 @@ function onCSSFunction(node: Func) { channelValue => Math.max(Math.min(Math.round(channelValue * 2.55), 255), 0), ); - channelNode3.replaceWith( - channelNode3.clone({ value: String(rgbValues[2]) }), - ); + node.nodes.splice(node.nodes.indexOf(channelNode1) + 1, 0, commaNode()); + node.nodes.splice(node.nodes.indexOf(channelNode2) + 1, 0, commaNode()); - channelNode2.replaceWith( - channelNode2.clone({ value: String(rgbValues[1]) }), - commaNode.clone(), - ); + replaceWith(node.nodes, channelNode1, { + ...channelNode1, + value: String(rgbValues[0]), + }); + + replaceWith(node.nodes, channelNode2, { + ...channelNode2, + value: String(rgbValues[1]), + }); + + replaceWith(node.nodes, channelNode3, { + ...channelNode3, + value: String(rgbValues[2]), + }); - channelNode1.replaceWith( - channelNode1.clone({ value: String(rgbValues[0]), unit: '' }), - commaNode.clone(), - ); } export default onCSSFunction; -const commaNode = parse(',').first; +function commaNode(): DivNode { + return { + sourceIndex: 0, + sourceEndIndex: 1, + value: ',', + type: 'div', + before: '', + after: '', + }; +} + +function isNumericNode(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } -function isNumericNode(node): node is Numeric { - return node && node.type === 'numeric'; + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return !!unitAndValue.number; } -function isNumericNodeNumber(node): node is Numeric { - return node && node.type === 'numeric' && node.unit === ''; +function isNumericNodeNumber(node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return !!unitAndValue.number && unitAndValue.unit === ''; } -function isNumericNodeHueLike(node): node is Numeric { - return node && node.type === 'numeric' && ( - node.unit === 'deg' || - node.unit === 'grad' || - node.unit === 'rad' || - node.unit === 'turn' || - node.unit === '' +function isNumericNodeHueLike(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return !!unitAndValue.number && ( + unitAndValue.unit === 'deg' || + unitAndValue.unit === 'grad' || + unitAndValue.unit === 'rad' || + unitAndValue.unit === 'turn' || + unitAndValue.unit === '' ); } -function isNumericNodePercentage(node): node is Numeric { - return node && node.type === 'numeric' && node.unit === '%'; +function isNumericNodePercentage(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return unitAndValue.unit === '%'; } -function isNumericNodePercentageOrNumber(node): node is Numeric { - return node && node.type === 'numeric' && (node.unit === '' || node.unit === '%'); +function isNumericNodePercentageOrNumber(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return unitAndValue.unit === '%' || unitAndValue.unit === ''; } -function isCalcNode(node): node is Func { - return node && node.type === 'func' && node.name === 'calc'; +function isCalcNode(node: Node): node is FunctionNode { + return node && node.type === 'function' && node.value === 'calc'; } -function isVarNode(node): node is Func { - return node && node.type === 'func' && node.name === 'var'; +function isVarNode(node: Node): node is FunctionNode { + return node && node.type === 'function' && node.value === 'var'; } -function isSlashNode(node): node is Operator { - return node && node.type === 'operator' && node.value === '/'; +function isSlashNode(node: Node): node is DivNode { + return node && node.type === 'div' && node.value === '/'; } type Lch = { - l: Numeric, - c: Numeric, - h: Numeric, - slash?: Operator, - alpha?: Numeric|Func, + l: Dimension, + lNode: Node, + c: Dimension, + cNode: Node, + h: Dimension, + hNode: Node, + slash?: DivNode, + alpha?: WordNode|FunctionNode, } function lchFunctionContents(nodes): Lch|null { @@ -129,9 +212,12 @@ function lchFunctionContents(nodes): Lch|null { } const out: Lch = { - l: nodes[0], - c: nodes[1], - h: nodes[2], + l: valueParser.unit(nodes[0].value) as Dimension, + lNode: nodes[0], + c: valueParser.unit(nodes[1].value) as Dimension, + cNode: nodes[1], + h: valueParser.unit(nodes[2].value) as Dimension, + hNode: nodes[2], }; normalizeHueNode(out.h); @@ -151,11 +237,14 @@ function lchFunctionContents(nodes): Lch|null { } type Lab = { - l: Numeric, - a: Numeric, - b: Numeric, - slash?: Operator, - alpha?: Numeric | Func, + l: Dimension, + lNode: Node, + a: Dimension, + aNode: Node, + b: Dimension, + bNode: Node, + slash?: DivNode, + alpha?: WordNode | FunctionNode, } function labFunctionContents(nodes): Lab|null { @@ -172,9 +261,12 @@ function labFunctionContents(nodes): Lab|null { } const out: Lab = { - l: nodes[0], - a: nodes[1], - b: nodes[2], + l: valueParser.unit(nodes[0].value) as Dimension, + lNode: nodes[0], + a: valueParser.unit(nodes[1].value) as Dimension, + aNode: nodes[1], + b: valueParser.unit(nodes[2].value) as Dimension, + bNode: nodes[2], }; if (isSlashNode(nodes[3])) { @@ -196,7 +288,15 @@ function isLab(x: Lch | Lab): x is Lab { return false; } -function channelNodes(x: Lch | Lab): [Numeric, Numeric, Numeric] { +function channelNodes(x: Lch | Lab): [Node, Node, Node] { + if (isLab(x)) { + return [x.lNode, x.aNode, x.bNode]; + } + + return [x.lNode, x.cNode, x.hNode]; +} + +function channelDimensions(x: Lch | Lab): [Dimension, Dimension, Dimension] { if (isLab(x)) { return [x.l, x.a, x.b]; } @@ -204,46 +304,69 @@ function channelNodes(x: Lch | Lab): [Numeric, Numeric, Numeric] { return [x.l, x.c, x.h]; } -function transformAlpha(node: Func, slashNode: Operator | undefined, alphaNode: Numeric | Func | undefined) { +function transformAlpha(node: FunctionNode, slashNode: DivNode | undefined, alphaNode: WordNode | FunctionNode | undefined) { if (!slashNode || !alphaNode) { return; } - node.name = 'rgba'; - slashNode.replaceWith(commaNode.clone()); + node.value = 'rgba'; + slashNode.value = ','; + slashNode.before = ''; if (!isNumericNode(alphaNode)) { return; } - if (alphaNode.unit === '%') { + const unitAndValue = valueParser.unit(alphaNode.value); + if (!unitAndValue) { + return; + } + + if (unitAndValue.unit === '%') { // transform the Alpha channel from a Percentage to (0-1) Number - alphaNode.value = String(parseFloat(alphaNode.value) / 100); - alphaNode.unit = ''; + unitAndValue.number = String(parseFloat(unitAndValue.number) / 100); + alphaNode.value = String(unitAndValue.number); } } -function normalizeHueNode(node: Numeric) { - switch (node.unit) { +function replaceWith(nodes: Array, oldNode: Node, newNode: Node) { + const index = nodes.indexOf(oldNode); + nodes[index] = newNode; +} + +function normalizeHueNode(dimension: Dimension) { + switch (dimension.unit) { case 'deg': - node.unit = ''; + dimension.unit = ''; return; case 'rad': // radians -> degrees - node.unit = ''; - node.value = (parseFloat(node.value) * 180 / Math.PI).toString(); + dimension.unit = ''; + dimension.number = (parseFloat(dimension.number) * 180 / Math.PI).toString(); return; case 'grad': // grades -> degrees - node.unit = ''; - node.value = (parseFloat(node.value) * 0.9).toString(); + dimension.unit = ''; + dimension.number = (parseFloat(dimension.number) * 0.9).toString(); return; case 'turn': // turns -> degrees - node.unit = ''; - node.value = (parseFloat(node.value) * 360).toString(); + dimension.unit = ''; + dimension.number = (parseFloat(dimension.number) * 360).toString(); return; } } + +function canParseAsUnit(node : Node): boolean { + if (!node || !node.value) { + return false; + } + + try { + return valueParser.unit(node.value) !== false; + } catch (e) { + return false; + } +} From 10b770ef99d9a77ba65f27510be5d3f93705adbd Mon Sep 17 00:00:00 2001 From: romainmenke Date: Wed, 1 Dec 2021 09:55:24 +0100 Subject: [PATCH 08/12] postcss-value-parser : postcss-color-functional-notation --- .../.tape.js | 11 +- .../package.json | 4 +- .../src/{cli.js => cli.ts} | 0 .../src/has-supports-at-rule-ancestor.ts | 19 ++ .../src/index.js | 18 - .../src/index.ts | 102 ++++++ .../src/on-css-function.ts | 314 ++++++++++++++++++ .../src/onCSSDeclaration.js | 44 --- .../src/onCSSFunction.js | 141 -------- .../src/options.js | 4 - .../test/basic.css | 4 + .../test/basic.expect.css | 26 +- .../test/basic.preserve-true.expect.css | 32 +- .../test/variables.css | 10 + .../test/variables.expect.css | 10 + .../test/variables.preserve-true.expect.css | 19 ++ .../tsconfig.json | 9 + 17 files changed, 533 insertions(+), 234 deletions(-) rename plugins/postcss-color-functional-notation/src/{cli.js => cli.ts} (100%) create mode 100644 plugins/postcss-color-functional-notation/src/has-supports-at-rule-ancestor.ts delete mode 100644 plugins/postcss-color-functional-notation/src/index.js create mode 100644 plugins/postcss-color-functional-notation/src/index.ts create mode 100644 plugins/postcss-color-functional-notation/src/on-css-function.ts delete mode 100644 plugins/postcss-color-functional-notation/src/onCSSDeclaration.js delete mode 100644 plugins/postcss-color-functional-notation/src/onCSSFunction.js delete mode 100644 plugins/postcss-color-functional-notation/src/options.js create mode 100644 plugins/postcss-color-functional-notation/test/variables.css create mode 100644 plugins/postcss-color-functional-notation/test/variables.expect.css create mode 100644 plugins/postcss-color-functional-notation/test/variables.preserve-true.expect.css create mode 100644 plugins/postcss-color-functional-notation/tsconfig.json diff --git a/plugins/postcss-color-functional-notation/.tape.js b/plugins/postcss-color-functional-notation/.tape.js index e179a15bf..fb5870499 100644 --- a/plugins/postcss-color-functional-notation/.tape.js +++ b/plugins/postcss-color-functional-notation/.tape.js @@ -1,11 +1,18 @@ module.exports = { 'basic': { message: 'supports basic usage', - warnings: 1, }, 'basic:preserve-true': { message: 'supports { preserve: true } usage', - warnings: 1, + options: { + preserve: true + } + }, + 'variables': { + message: 'supports variables', + }, + 'variables:preserve-true': { + message: 'supports variables with { preserve: true } usage', options: { preserve: true } diff --git a/plugins/postcss-color-functional-notation/package.json b/plugins/postcss-color-functional-notation/package.json index 0a35e3f8d..2ccafb10c 100644 --- a/plugins/postcss-color-functional-notation/package.json +++ b/plugins/postcss-color-functional-notation/package.json @@ -22,14 +22,14 @@ "prepublishOnly": "npm run build && npm run test", "lint": "eslint src/**/*.js", "test": "postcss-tape --ci", - "build": "rollup -c ../../rollup/default.js", + "build": "rollup -c ../../rollup/default.ts.js", "stryker": "stryker run --logLevel error" }, "engines": { "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-color-functional-notation/src/cli.js b/plugins/postcss-color-functional-notation/src/cli.ts similarity index 100% rename from plugins/postcss-color-functional-notation/src/cli.js rename to plugins/postcss-color-functional-notation/src/cli.ts diff --git a/plugins/postcss-color-functional-notation/src/has-supports-at-rule-ancestor.ts b/plugins/postcss-color-functional-notation/src/has-supports-at-rule-ancestor.ts new file mode 100644 index 000000000..951061b6a --- /dev/null +++ b/plugins/postcss-color-functional-notation/src/has-supports-at-rule-ancestor.ts @@ -0,0 +1,19 @@ +import type { Node, AtRule } from 'postcss'; + +export function hasSupportsAtRuleAncestor(node: Node): boolean { + let parent = node.parent; + while (parent) { + if (parent.type !== 'atrule') { + parent = parent.parent; + continue; + } + + if ((parent as AtRule).name === 'supports' && (parent as AtRule).params.indexOf('(color: rgb(0 0 0 / 0.5)) and (color: hsl(0 0% 0% / 0.5))') !== -1) { + return true; + } + + parent = parent.parent; + } + + return false; +} diff --git a/plugins/postcss-color-functional-notation/src/index.js b/plugins/postcss-color-functional-notation/src/index.js deleted file mode 100644 index 84fa5b3a5..000000000 --- a/plugins/postcss-color-functional-notation/src/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import onCSSDeclaration from './onCSSDeclaration'; -import options from './options'; - -/** Transform space and slash separated color functions in CSS. */ -export default function postcssColorFunctionalNotation(opts) { - options.preserve = 'preserve' in Object(opts) ? Boolean(opts.preserve) : false; - - return { - postcssPlugin: 'postcss-color-functional-notation', - Declaration: onCSSDeclaration, - }; -} - -postcssColorFunctionalNotation.postcss = true; - -/** @typedef {import('postcss').Root} CSSRoot */ -/** @typedef {(root: CSSRoot) => void} PostCSSTransformCallback */ -/** @typedef {(opts: options) => PostCSSTransformCallback} PostCSSPluginInitializer */ diff --git a/plugins/postcss-color-functional-notation/src/index.ts b/plugins/postcss-color-functional-notation/src/index.ts new file mode 100644 index 000000000..aa5701132 --- /dev/null +++ b/plugins/postcss-color-functional-notation/src/index.ts @@ -0,0 +1,102 @@ +import valueParser from 'postcss-value-parser'; +import type { ParsedValue, FunctionNode } from 'postcss-value-parser'; +import type { Declaration, Postcss, Result } from 'postcss'; +import onCSSFunction from './on-css-function'; + +import type { PluginCreator } from 'postcss'; +import { hasSupportsAtRuleAncestor } from './has-supports-at-rule-ancestor'; + +/** Transform lab() and lch() functions in CSS. */ +const postcssPlugin: PluginCreator<{ preserve: boolean }> = (opts?: { preserve: boolean }) => { + const preserve = 'preserve' in Object(opts) ? Boolean(opts.preserve) : false; + + return { + postcssPlugin: 'postcss-color-functional-notation', + Declaration: (decl: Declaration, { result, postcss }: { result: Result, postcss: Postcss }) => { + if (preserve && hasSupportsAtRuleAncestor(decl)) { + return; + } + + const originalValue = decl.value; + if (!(/(^|[^\w-])(hsla?|rgba?)\(/i.test(originalValue))) { + return; + } + + let valueAST: ParsedValue|undefined; + + try { + valueAST = valueParser(originalValue); + } catch (error) { + decl.warn( + result, + `Failed to parse value '${originalValue}' as a hsl or rgb function. Leaving the original value intact.`, + ); + } + + if (typeof valueAST === 'undefined') { + return; + } + + valueAST.walk((node) => { + if (!node.type || node.type !== 'function') { + return; + } + + if ( + node.value !== 'hsl' && + node.value !== 'hsla' && + node.value !== 'rgb' && + node.value !== 'rgba' + ) { + return; + } + + onCSSFunction(node as FunctionNode); + }); + const modifiedValue = String(valueAST); + + if (modifiedValue === originalValue) { + return; + } + + if (preserve && decl.variable) { + const parent = decl.parent; + const atSupportsParams = '(color: rgb(0 0 0 / 0.5)) and (color: hsl(0 0% 0% / 0.5))'; + const atSupports = postcss.atRule({ name: 'supports', params: atSupportsParams, source: decl.source }); + + const parentClone = parent.clone(); + parentClone.removeAll(); + + parentClone.append(decl.clone()); + atSupports.append(parentClone); + + // Ensure correct order of @supports rules + // Find the last one created by us or the current parent and insert after. + let insertAfter = parent; + let nextInsertAfter = parent.next(); + while ( + insertAfter && + nextInsertAfter && + nextInsertAfter.type === 'atrule' && + nextInsertAfter.name === 'supports' && + nextInsertAfter.params === atSupportsParams + ) { + insertAfter = nextInsertAfter; + nextInsertAfter = nextInsertAfter.next(); + } + + insertAfter.after(atSupports); + + decl.value = modifiedValue; + } else if (preserve) { + decl.cloneBefore({ value: modifiedValue }); + } else { + decl.value = modifiedValue; + } + }, + }; +}; + +postcssPlugin.postcss = true; + +export default postcssPlugin; diff --git a/plugins/postcss-color-functional-notation/src/on-css-function.ts b/plugins/postcss-color-functional-notation/src/on-css-function.ts new file mode 100644 index 000000000..5947f8b75 --- /dev/null +++ b/plugins/postcss-color-functional-notation/src/on-css-function.ts @@ -0,0 +1,314 @@ +import valueParser from 'postcss-value-parser'; +import type { FunctionNode, Dimension, Node, DivNode, WordNode } from 'postcss-value-parser'; + +function onCSSFunction(node: FunctionNode) { + const value = node.value; + const rawNodes = node.nodes; + const relevantNodes = rawNodes.slice().filter((x) => { + return x.type !== 'comment' && x.type !== 'space'; + }); + + let nodes: Rgb | Hsl | null = null; + if (value === 'hsl' || value === 'hsla') { + nodes = hslFunctionContents(relevantNodes); + } else if (value === 'rgb' || value === 'rgba') { + nodes = rgbFunctionContents(relevantNodes); + } + + if (!nodes) { + return; + } + + if (relevantNodes.length > 3 && (!nodes.slash || !nodes.alpha)) { + return; + } + + transformAlpha(node, nodes.slash, nodes.alpha); + + /** Extracted Color channels. */ + const [channelNode1, channelNode2] = channelNodes(nodes); + + node.nodes.splice(node.nodes.indexOf(channelNode1) + 1, 0, commaNode()); + node.nodes.splice(node.nodes.indexOf(channelNode2) + 1, 0, commaNode()); +} + +export default onCSSFunction; + +function commaNode(): DivNode { + return { + sourceIndex: 0, + sourceEndIndex: 1, + value: ',', + type: 'div', + before: '', + after: '', + }; +} + +function isNumericNode(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return !!unitAndValue.number; +} + +function isNumericNodeHueLike(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return !!unitAndValue.number && ( + unitAndValue.unit === 'deg' || + unitAndValue.unit === 'grad' || + unitAndValue.unit === 'rad' || + unitAndValue.unit === 'turn' || + unitAndValue.unit === '' + ); +} + +function isNumericNodePercentageOrNumber(node: Node): node is WordNode { + if (!node || node.type !== 'word') { + return false; + } + + if (!canParseAsUnit(node)) { + return false; + } + + const unitAndValue = valueParser.unit(node.value); + if (!unitAndValue) { + return false; + } + + return unitAndValue.unit === '%' || unitAndValue.unit === ''; +} + +function isCalcNode(node: Node): node is FunctionNode { + return node && node.type === 'function' && node.value === 'calc'; +} + +function isVarNode(node: Node): node is FunctionNode { + return node && node.type === 'function' && node.value === 'var'; +} + +function isSlashNode(node: Node): node is DivNode { + return node && node.type === 'div' && node.value === '/'; +} + +type Hsl = { + h: Dimension, + hNode: Node, + s: Dimension, + sNode: Node, + l: Dimension, + lNode: Node, + slash?: DivNode, + alpha?: WordNode|FunctionNode, +} + +function hslFunctionContents(nodes): Hsl|null { + if (!isNumericNodeHueLike(nodes[0])) { + return null; + } + + if (!isNumericNodePercentageOrNumber(nodes[1])) { + return null; + } + + if (!isNumericNodePercentageOrNumber(nodes[2])) { + return null; + } + + const out: Hsl = { + h: valueParser.unit(nodes[0].value) as Dimension, + hNode: nodes[0], + s: valueParser.unit(nodes[1].value) as Dimension, + sNode: nodes[1], + l: valueParser.unit(nodes[2].value) as Dimension, + lNode: nodes[2], + }; + + normalizeHueNode(out.h); + if (out.h.unit !== '') { + return null; + } + + out.hNode.value = out.h.number; + + if (isSlashNode(nodes[3])) { + out.slash = nodes[3]; + } + + if ((isNumericNodePercentageOrNumber(nodes[4]) || isCalcNode(nodes[4]) || isVarNode(nodes[4]))) { + out.alpha = nodes[4]; + } + + return out; +} + +type Rgb = { + r: Dimension, + rNode: Node, + g: Dimension, + gNode: Node, + b: Dimension, + bNode: Node, + slash?: DivNode, + alpha?: WordNode | FunctionNode, +} + +function rgbFunctionContents(nodes): Rgb|null { + if (!isNumericNodePercentageOrNumber(nodes[0])) { + return null; + } + + if (!isNumericNodePercentageOrNumber(nodes[1])) { + return null; + } + + if (!isNumericNodePercentageOrNumber(nodes[2])) { + return null; + } + + const out: Rgb = { + r: valueParser.unit(nodes[0].value) as Dimension, + rNode: nodes[0], + g: valueParser.unit(nodes[1].value) as Dimension, + gNode: nodes[1], + b: valueParser.unit(nodes[2].value) as Dimension, + bNode: nodes[2], + }; + + if (out.r.unit === '%') { + out.r.number = String(Math.round(Number(out.r.number) / 100 * 255)); + out.rNode.value = out.r.number; + } + + if (out.g.unit === '%') { + out.g.number = String(Math.round(Number(out.g.number) / 100 * 255)); + out.gNode.value = out.g.number; + } + + if (out.b.unit === '%') { + out.b.number = String(Math.round(Number(out.b.number) / 100 * 255)); + out.bNode.value = out.b.number; + } + + if (isSlashNode(nodes[3])) { + out.slash = nodes[3]; + } + + if ((isNumericNodePercentageOrNumber(nodes[4]) || isCalcNode(nodes[4]))) { + out.alpha = nodes[4]; + } + + return out; +} + +function isRgb(x: Hsl | Rgb): x is Rgb { + if (typeof (x as Rgb).r !== 'undefined') { + return true; + } + + return false; +} + +function channelNodes(x: Hsl | Rgb): [Node, Node, Node] { + if (isRgb(x)) { + return [x.rNode, x.gNode, x.bNode]; + } + + return [x.hNode, x.sNode, x.lNode]; +} + +function transformAlpha(node: FunctionNode, slashNode: DivNode | undefined, alphaNode: WordNode | FunctionNode | undefined) { + if (node.value === 'hsl' || node.value === 'hsla') { + node.value = 'hsl'; + } else if (node.value === 'rgb' || node.value === 'rgba') { + node.value = 'rgb'; + } + + if (!slashNode || !alphaNode) { + return; + } + + if (node.value === 'hsl') { + node.value = 'hsla'; + } else { + node.value = 'rgba'; + } + + slashNode.value = ','; + slashNode.before = ''; + + if (!isNumericNode(alphaNode)) { + return; + } + + const unitAndValue = valueParser.unit(alphaNode.value); + if (!unitAndValue) { + return; + } + + if (unitAndValue.unit === '%') { + // transform the Alpha channel from a Percentage to (0-1) Number + unitAndValue.number = String(parseFloat(unitAndValue.number) / 100); + alphaNode.value = String(unitAndValue.number); + } +} + +function normalizeHueNode(dimension: Dimension) { + switch (dimension.unit) { + case 'deg': + dimension.unit = ''; + return; + case 'rad': + // radians -> degrees + dimension.unit = ''; + dimension.number = Math.round(parseFloat(dimension.number) * 180 / Math.PI).toString(); + return; + + case 'grad': + // grades -> degrees + dimension.unit = ''; + dimension.number = Math.round(parseFloat(dimension.number) * 0.9).toString(); + return; + + case 'turn': + // turns -> degrees + dimension.unit = ''; + dimension.number = Math.round(parseFloat(dimension.number) * 360).toString(); + return; + } +} + +function canParseAsUnit(node : Node): boolean { + if (!node || !node.value) { + return false; + } + + try { + return valueParser.unit(node.value) !== false; + } catch (e) { + return false; + } +} diff --git a/plugins/postcss-color-functional-notation/src/onCSSDeclaration.js b/plugins/postcss-color-functional-notation/src/onCSSDeclaration.js deleted file mode 100644 index 342729cc9..000000000 --- a/plugins/postcss-color-functional-notation/src/onCSSDeclaration.js +++ /dev/null @@ -1,44 +0,0 @@ -import { parse } from 'postcss-values-parser'; -import onCSSFunction from './onCSSFunction'; -import options from './options'; - -/** @type {(decl: CSSDeclaration) => void} Transform space and slash separated color functions in CSS Declarations. */ -const onCSSDeclaration = (decl, { result }) => { - const { value: originalValue } = decl; - - if (hasAnyColorFunction(originalValue)) { - let valueAST; - - try { - valueAST = parse(originalValue, { ignoreUnknownWords: true }); - } catch (error) { - decl.warn( - result, - `Failed to parse value '${originalValue}' as a color function. Leaving the original value intact.`, - ); - } - - if (typeof valueAST === 'undefined') { - return; - } - - valueAST.walkType('func', onCSSFunction); - - const modifiedValue = String(valueAST); - - if (modifiedValue !== originalValue) { - if (options.preserve) decl.cloneBefore({ value: modifiedValue }); - else decl.value = modifiedValue; - } - } -}; - -export default onCSSDeclaration; - -/** @type {(value: RegExp) => (value: string) => boolean} Return a function that checks whether the expression exists in a value. */ -const createRegExpTest = Function.bind.bind(RegExp.prototype.test); - -/** Return whether the value has an `hsl()`, `hsla()`, `rgb()`, or `rgba()` function. */ -const hasAnyColorFunction = createRegExpTest(/(^|[^\w-])(hsla?|rgba?)\(/i); - -/** @typedef {import('postcss').Declaration} CSSDeclaration */ diff --git a/plugins/postcss-color-functional-notation/src/onCSSFunction.js b/plugins/postcss-color-functional-notation/src/onCSSFunction.js deleted file mode 100644 index ad72c5969..000000000 --- a/plugins/postcss-color-functional-notation/src/onCSSFunction.js +++ /dev/null @@ -1,141 +0,0 @@ -import { parse } from 'postcss-values-parser'; - -/** @type {(decl: CSSFunction) => void} Transform a space and slash separated color function. */ -const onCSSFunction = node => { - /** @type {{ name: string, nodes: CSSNode[] }} */ - const { name, nodes } = node; - - if (isColorFunctionName(name)) { - const isHsl = isHslColorFunctionName(name) && isHslFunctionContents(nodes); - const isRgbWithNumbers = isRgbColorFunctionName(name) && isRgbNumberFunctionContents(nodes); - const isRgbWithPercents = isRgbColorFunctionName(name) && isRgbPercentFunctionContents(nodes); - - if (isHsl || isRgbWithNumbers || isRgbWithPercents) { - // rename the Color function to `hsl` or `rgb` - Object.assign(node, { name: isHsl ? 'hsl' : 'rgb' }); - - /** @type {CSSPunctuation} Slash punctuation before the Alpha channel. */ - const slashNode = nodes[3]; - - /** @type {CSSNumber} Alpha channel. */ - const alphaNode = nodes[4]; - - if (alphaNode) { - if (isPercentage(alphaNode) && !isCalc(alphaNode)) { - // transform the Alpha channel from a Percentage to (0-1) Number - Object.assign(alphaNode, { value: String(alphaNode.value / 100), unit: '' }); - } - - // if the color is fully opaque (i.e. the Alpha channel is `1`) - if (alphaNode.value === '1') { - // remove the Slash and Alpha channel - slashNode.remove(); - alphaNode.remove(); - } else { - // otherwise, rename the Color function to `hsla` or `rgba` - Object.assign(node, { name: isHsl ? 'hsla' : 'rgba' }); - } - } - - // replace a remaining Slash with a Comma - if (slashNode && isSlash(slashNode)) { - slashNode.replaceWith(commaNode.clone()); - } - - /** Extracted Color channels. */ - let [channelNode1, channelNode2, channelNode3] = nodes; - - if (isRgbWithPercents) { - Object.assign(channelNode1, { value: to255ColorValue(channelNode1.value), unit: '' }); - Object.assign(channelNode2, { value: to255ColorValue(channelNode2.value), unit: '' }); - Object.assign(channelNode3, { value: to255ColorValue(channelNode3.value), unit: '' }); - } - - channelNode2.after( - commaNode.clone(), - ); - - channelNode1.after( - commaNode.clone(), - ); - } - } -}; - -export default onCSSFunction; - -const commaNode = parse(',').first; - -/** Return a value transformed from a scale of 0-100 to a scale of 0-255 */ -function to255ColorValue(value) { - return String(Math.floor(value * 255 / 100)); -} - -/** @type {(value: RegExp) => (value: string) => boolean} Return a function that checks whether the expression exists in a value. */ -const createRegExpTest = Function.bind.bind(RegExp.prototype.test); - -/** Return whether the function name is `hsl()`, `hsla()`, `rgb()`, or `rgba()`. */ -const isColorFunctionName = createRegExpTest(/^(hsl|rgb)a?$/i); - -/** Return whether the function name is `hsl()` or `hsla()`. */ -const isHslColorFunctionName = createRegExpTest(/^hsla?$/i); - -/** Return whether the function name is `rgb()` or `rgba()`. */ -const isRgbColorFunctionName = createRegExpTest(/^rgba?$/i); - -/** Return whether the function name is `calc`. */ -const isCalcFunctionName = createRegExpTest(/^calc$/i); - -/** Return whether the unit is alpha-like. */ -const isAlphaLikeUnit = createRegExpTest(/^%?$/i); - -/** Return whether the unit is hue-like. */ -const isHueLikeUnit = createRegExpTest(/^(deg|grad|rad|turn)?$/i); - -/** @type {(node: CSSNumber) => boolean} Return whether the node is an Alpha-like unit. */ -const isAlphaValue = node => isCalc(node) || node.type === 'numeric' && isAlphaLikeUnit(node.unit); - -/** @type {(node: CSSFunction) => boolean} Return whether the node is a calc() function. */ -const isCalc = node => node.type === 'func' && isCalcFunctionName(node.name); - -/** @type {(node: CSSNumber) => boolean} Return whether the node is a Hue-like unit. */ -const isHue = node => isCalc(node) || node.type === 'numeric' && isHueLikeUnit(node.unit); - -/** @type {(node: CSSNumber) => boolean} Return whether the node is a Number unit. */ -const isNumber = node => isCalc(node) || node.type === 'numeric' && node.unit === ''; - -/** @type {(node: CSSNumber) => boolean} Return whether the node is a Percentage unit. */ -const isPercentage = node => isCalc(node) || node.type === 'numeric' && (node.unit === '%' || node.unit === '' && node.value === '0'); - -/** @type {(node: CSSOperator) => boolean} Return whether the node is a Slash delimiter. */ -const isSlash = node => node.type === 'operator' && node.value === '/'; - -/** @type {(nodes: Node[]) => boolean} Return whether a set of nodes is an hsl() function. */ -const isHslFunctionContents = nodes => nodes.every( - (node, index) => typeof hslFunctionContents[index] === 'function' && hslFunctionContents[index](node), -); - -/** Set of nodes in a lab() function. */ -const hslFunctionContents = [isHue, isPercentage, isPercentage, isSlash, isAlphaValue]; - -/** @type {(nodes: Node[]) => boolean} Return whether a set of nodes is an rgb() function with numbers. */ -const isRgbNumberFunctionContents = nodes => nodes.every( - (node, index) => typeof rgbNumberFunctionContents[index] === 'function' && rgbNumberFunctionContents[index](node), -); - -/** Set of nodes in a rgb() function with numbers. */ -const rgbNumberFunctionContents = [isNumber, isNumber, isNumber, isSlash, isAlphaValue]; - -/** @type {(nodes: Node[]) => boolean} Return whether a set of nodes is an rgb() function with percentages. */ -const isRgbPercentFunctionContents = nodes => nodes.every( - (node, index) => typeof rgbPercentFunctionContents[index] === 'function' && rgbPercentFunctionContents[index](node), -); - -/** Set of nodes in a rgb() function with percentages. */ -const rgbPercentFunctionContents = [isPercentage, isPercentage, isPercentage, isSlash, isAlphaValue]; - -/** @typedef {import('postcss-values-parser').Func} CSSFunction */ -/** @typedef {import('postcss-values-parser').Node} CSSNode */ -/** @typedef {import('postcss-values-parser').Numeric} CSSNumber */ -/** @typedef {import('postcss-values-parser').Operator} CSSOperator */ -/** @typedef {import('postcss-values-parser').Punctuation} CSSPunctuation */ diff --git a/plugins/postcss-color-functional-notation/src/options.js b/plugins/postcss-color-functional-notation/src/options.js deleted file mode 100644 index d5fa8460c..000000000 --- a/plugins/postcss-color-functional-notation/src/options.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - /** Whether to preserve the original functional color declaration. */ - preserve: false, -}; diff --git a/plugins/postcss-color-functional-notation/test/basic.css b/plugins/postcss-color-functional-notation/test/basic.css index bf5a631dd..337065984 100644 --- a/plugins/postcss-color-functional-notation/test/basic.css +++ b/plugins/postcss-color-functional-notation/test/basic.css @@ -27,6 +27,10 @@ color: hsl(120 100% 50% / 100%); color: hsl(120 100% 50% / 50%); color: hsla(120deg 100% 50%); + + color: hsl(0.5turn 100% 50%); + color: hsl(200grad 100% 50%); + color: hsl(3.14159rad 100% 50%); } .test-hsla { diff --git a/plugins/postcss-color-functional-notation/test/basic.expect.css b/plugins/postcss-color-functional-notation/test/basic.expect.css index 7b29c3894..417056bc4 100644 --- a/plugins/postcss-color-functional-notation/test/basic.expect.css +++ b/plugins/postcss-color-functional-notation/test/basic.expect.css @@ -1,39 +1,43 @@ .test-rgb { color: rgb(178, 34, 34); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgba(178, 34, 34, .5); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgba(178, 34, 34, 0.5); } .test-rgba { color: rgb(178, 34, 34); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgba(178, 34, 34, .5); } .test-rgb-percentages { - color: rgb(178, 34, 34); - color: rgb(178, 34, 34); - color: rgba(178, 34, 34, 0.5); + color: rgb(179, 34, 34); + color: rgba(179, 34, 34, 1); + color: rgba(179, 34, 34, 0.5); } .test-hsl { color: hsl(0, 0%, 100%); - color: hsl(120deg, 100%, 50%); color: hsl(120, 100%, 50%); color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120, 100%, 50%, .5); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120, 100%, 50%, 0.5); - color: hsl(120deg, 100%, 50%); + color: hsl(120, 100%, 50%); + + color: hsl(180, 100%, 50%); + color: hsl(180, 100%, 50%); + color: hsl(180, 100%, 50%); } .test-hsla { color: hsl(120, 100%, 50%); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120, 100%, 50%, .5); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120, 100%, 50%, 0.5); } diff --git a/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css b/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css index 541469e25..a0ea0cfd4 100644 --- a/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css +++ b/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css @@ -1,11 +1,11 @@ .test-rgb { color: rgb(178, 34, 34); color: rgb(178 34 34); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgb(178 34 34 / 1); color: rgba(178, 34, 34, .5); color: rgb(178 34 34 / .5); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgb(178 34 34 / 100%); color: rgba(178, 34, 34, 0.5); color: rgb(178 34 34 / 50%); @@ -14,48 +14,56 @@ .test-rgba { color: rgb(178, 34, 34); color: rgba(178 34 34); - color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); color: rgba(178 34 34 / 1); color: rgba(178, 34, 34, .5); color: rgba(178 34 34 / .5); } .test-rgb-percentages { - color: rgb(178, 34, 34); + color: rgb(179, 34, 34); color: rgba(70% 13.5% 13.5%); - color: rgb(178, 34, 34); + color: rgba(179, 34, 34, 1); color: rgba(70% 13.5% 13.5% / 100%); - color: rgba(178, 34, 34, 0.5); + color: rgba(179, 34, 34, 0.5); color: rgba(70% 13.5% 13.5% / 50%); } .test-hsl { color: hsl(0, 0%, 100%); color: hsl(0 0% 100%); - color: hsl(120deg, 100%, 50%); + color: hsl(120, 100%, 50%); color: hsl(120deg 100% 50%); color: hsl(120, 100%, 50%); color: hsl(120 100% 50%); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsl(120 100% 50% / 1); color: hsla(120, 100%, 50%, .5); color: hsl(120 100% 50% / .5); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsl(120 100% 50% / 100%); color: hsla(120, 100%, 50%, 0.5); color: hsl(120 100% 50% / 50%); - color: hsl(120deg, 100%, 50%); + color: hsl(120, 100%, 50%); color: hsla(120deg 100% 50%); + + color: hsl(180, 100%, 50%); + + color: hsl(0.5turn 100% 50%); + color: hsl(180, 100%, 50%); + color: hsl(200grad 100% 50%); + color: hsl(180, 100%, 50%); + color: hsl(3.14159rad 100% 50%); } .test-hsla { color: hsl(120, 100%, 50%); color: hsla(120 100% 50%); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120 100% 50% / 1); color: hsla(120, 100%, 50%, .5); color: hsla(120 100% 50% / .5); - color: hsl(120, 100%, 50%); + color: hsla(120, 100%, 50%, 1); color: hsla(120 100% 50% / 100%); color: hsla(120, 100%, 50%, 0.5); color: hsla(120 100% 50% / 50%); diff --git a/plugins/postcss-color-functional-notation/test/variables.css b/plugins/postcss-color-functional-notation/test/variables.css new file mode 100644 index 000000000..f74f78c67 --- /dev/null +++ b/plugins/postcss-color-functional-notation/test/variables.css @@ -0,0 +1,10 @@ +:root { + --firebrick: rgb(40% 56.6 39); + --firebrick-a50: rgb(40% 68.8 34.5 / 50%); + + --opacity-50: 0.5; + --firebrick-a50-var: hsl(40 68.8% 34.5% / var(--opacity-50)); + + --fifty: 50%; + --firebrick-var: hsl(40 var(--fifty) 34.5% / var(--opacity-50)); +} diff --git a/plugins/postcss-color-functional-notation/test/variables.expect.css b/plugins/postcss-color-functional-notation/test/variables.expect.css new file mode 100644 index 000000000..19a4c4b35 --- /dev/null +++ b/plugins/postcss-color-functional-notation/test/variables.expect.css @@ -0,0 +1,10 @@ +:root { + --firebrick: rgb(102, 56.6, 39); + --firebrick-a50: rgba(102, 68.8, 34.5, 0.5); + + --opacity-50: 0.5; + --firebrick-a50-var: hsla(40, 68.8%, 34.5%, var(--opacity-50)); + + --fifty: 50%; + --firebrick-var: hsl(40 var(--fifty) 34.5% / var(--opacity-50)); +} diff --git a/plugins/postcss-color-functional-notation/test/variables.preserve-true.expect.css b/plugins/postcss-color-functional-notation/test/variables.preserve-true.expect.css new file mode 100644 index 000000000..734ff989b --- /dev/null +++ b/plugins/postcss-color-functional-notation/test/variables.preserve-true.expect.css @@ -0,0 +1,19 @@ +:root { + --firebrick: rgb(102, 56.6, 39); + --firebrick-a50: rgba(102, 68.8, 34.5, 0.5); + + --opacity-50: 0.5; + --firebrick-a50-var: hsla(40, 68.8%, 34.5%, var(--opacity-50)); + + --fifty: 50%; + --firebrick-var: hsl(40 var(--fifty) 34.5% / var(--opacity-50)); +}@supports (color: rgb(0 0 0 / 0.5)) and (color: hsl(0 0% 0% / 0.5)) {:root { + --firebrick: rgb(40% 56.6 39); +} +}@supports (color: rgb(0 0 0 / 0.5)) and (color: hsl(0 0% 0% / 0.5)) {:root { + --firebrick-a50: rgb(40% 68.8 34.5 / 50%); +} +}@supports (color: rgb(0 0 0 / 0.5)) and (color: hsl(0 0% 0% / 0.5)) {:root { + --firebrick-a50-var: hsl(40 68.8% 34.5% / var(--opacity-50)); +} +} diff --git a/plugins/postcss-color-functional-notation/tsconfig.json b/plugins/postcss-color-functional-notation/tsconfig.json new file mode 100644 index 000000000..68a2606f6 --- /dev/null +++ b/plugins/postcss-color-functional-notation/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} From 84d1ea3fb7b878b30acf8cc8d61921941e56fb39 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Wed, 1 Dec 2021 10:00:25 +0100 Subject: [PATCH 09/12] minimize impact --- plugins/postcss-color-functional-notation/package.json | 1 + .../src/on-css-function.ts | 6 +++--- .../postcss-color-functional-notation/test/basic.expect.css | 6 +++--- .../test/basic.preserve-true.expect.css | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/postcss-color-functional-notation/package.json b/plugins/postcss-color-functional-notation/package.json index 2ccafb10c..ca86c6921 100644 --- a/plugins/postcss-color-functional-notation/package.json +++ b/plugins/postcss-color-functional-notation/package.json @@ -8,6 +8,7 @@ "bugs": "https://github.com/csstools/postcss-plugins/issues", "main": "dist/index.cjs", "module": "dist/index.mjs", + "types": "./dist/index.d.ts", "files": [ "CHANGELOG.md", "INSTALL.md", diff --git a/plugins/postcss-color-functional-notation/src/on-css-function.ts b/plugins/postcss-color-functional-notation/src/on-css-function.ts index 5947f8b75..d0ce08be0 100644 --- a/plugins/postcss-color-functional-notation/src/on-css-function.ts +++ b/plugins/postcss-color-functional-notation/src/on-css-function.ts @@ -199,17 +199,17 @@ function rgbFunctionContents(nodes): Rgb|null { }; if (out.r.unit === '%') { - out.r.number = String(Math.round(Number(out.r.number) / 100 * 255)); + out.r.number = String(Math.floor(Number(out.r.number) / 100 * 255)); out.rNode.value = out.r.number; } if (out.g.unit === '%') { - out.g.number = String(Math.round(Number(out.g.number) / 100 * 255)); + out.g.number = String(Math.floor(Number(out.g.number) / 100 * 255)); out.gNode.value = out.g.number; } if (out.b.unit === '%') { - out.b.number = String(Math.round(Number(out.b.number) / 100 * 255)); + out.b.number = String(Math.floor(Number(out.b.number) / 100 * 255)); out.bNode.value = out.b.number; } diff --git a/plugins/postcss-color-functional-notation/test/basic.expect.css b/plugins/postcss-color-functional-notation/test/basic.expect.css index 417056bc4..0fc06f01d 100644 --- a/plugins/postcss-color-functional-notation/test/basic.expect.css +++ b/plugins/postcss-color-functional-notation/test/basic.expect.css @@ -13,9 +13,9 @@ } .test-rgb-percentages { - color: rgb(179, 34, 34); - color: rgba(179, 34, 34, 1); - color: rgba(179, 34, 34, 0.5); + color: rgb(178, 34, 34); + color: rgba(178, 34, 34, 1); + color: rgba(178, 34, 34, 0.5); } .test-hsl { diff --git a/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css b/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css index a0ea0cfd4..d313f0b40 100644 --- a/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css +++ b/plugins/postcss-color-functional-notation/test/basic.preserve-true.expect.css @@ -21,11 +21,11 @@ } .test-rgb-percentages { - color: rgb(179, 34, 34); + color: rgb(178, 34, 34); color: rgba(70% 13.5% 13.5%); - color: rgba(179, 34, 34, 1); + color: rgba(178, 34, 34, 1); color: rgba(70% 13.5% 13.5% / 100%); - color: rgba(179, 34, 34, 0.5); + color: rgba(178, 34, 34, 0.5); color: rgba(70% 13.5% 13.5% / 50%); } From 4cb3c9221cd51908f0eddbf37d2e75c2c04d6ba6 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Wed, 1 Dec 2021 10:04:20 +0100 Subject: [PATCH 10/12] fix lint --- plugins/postcss-image-set-function/src/lib/get-media.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/postcss-image-set-function/src/lib/get-media.ts b/plugins/postcss-image-set-function/src/lib/get-media.ts index 257457992..8b33320c0 100644 --- a/plugins/postcss-image-set-function/src/lib/get-media.ts +++ b/plugins/postcss-image-set-function/src/lib/get-media.ts @@ -1,4 +1,4 @@ -import type { Node, Dimension } from 'postcss-value-parser'; +import type { Node } from 'postcss-value-parser'; import valueParser from 'postcss-value-parser'; const dpiRatios = { dpcm: 2.54, dpi: 1, dppx: 96, x: 96 }; From 304f43455963c427e6c830091d39bb7e81fc2d0d Mon Sep 17 00:00:00 2001 From: romainmenke Date: Wed, 1 Dec 2021 10:07:35 +0100 Subject: [PATCH 11/12] fix lint --- plugins/postcss-color-functional-notation/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/postcss-color-functional-notation/package.json b/plugins/postcss-color-functional-notation/package.json index ca86c6921..d5981c56c 100644 --- a/plugins/postcss-color-functional-notation/package.json +++ b/plugins/postcss-color-functional-notation/package.json @@ -21,7 +21,7 @@ }, "scripts": { "prepublishOnly": "npm run build && npm run test", - "lint": "eslint src/**/*.js", + "lint": "eslint src/**/*.ts", "test": "postcss-tape --ci", "build": "rollup -c ../../rollup/default.ts.js", "stryker": "stryker run --logLevel error" From da7bf11c780ba3cd70d02c29b34524c75ae5d08f Mon Sep 17 00:00:00 2001 From: Antonio Laguna Date: Wed, 1 Dec 2021 13:07:40 +0100 Subject: [PATCH 12/12] postcss-value-parser : postcss-env-function --- plugins/postcss-env-function/.tape.js | 10 ----- plugins/postcss-env-function/package.json | 2 +- .../src/lib/get-replaced-value.js | 23 ++++++---- .../src/lib/import-from.js | 11 +---- .../src/lib/is-env-func.js | 2 +- .../src/lib/update-env-value.js | 44 ------------------- .../src/lib/walk-env-funcs.js | 14 ------ 7 files changed, 18 insertions(+), 88 deletions(-) delete mode 100644 plugins/postcss-env-function/src/lib/update-env-value.js delete mode 100644 plugins/postcss-env-function/src/lib/walk-env-funcs.js diff --git a/plugins/postcss-env-function/.tape.js b/plugins/postcss-env-function/.tape.js index f6c471b15..0170c8d3a 100644 --- a/plugins/postcss-env-function/.tape.js +++ b/plugins/postcss-env-function/.tape.js @@ -1,11 +1,9 @@ module.exports = { 'basic': { message: 'supports basic usage', - warnings: 1 }, 'basic:import': { message: 'supports { importFrom: { environmentVariables: { ... } } } usage', - warnings: 1, options: { importFrom: { environmentVariables: { @@ -17,7 +15,6 @@ module.exports = { }, 'basic:import-fn': { message: 'supports { importFrom() } usage', - warnings: 1, options: { importFrom() { return { @@ -33,7 +30,6 @@ module.exports = { }, 'basic:import-fn-promise': { message: 'supports { async importFrom() } usage', - warnings: 1, options: { importFrom() { return new Promise(resolve => { @@ -51,7 +47,6 @@ module.exports = { }, 'basic:import-json': { message: 'supports { importFrom: "test/import-variables.json" } usage', - warnings: 1, options: { importFrom: 'test/import-variables.json' }, @@ -60,7 +55,6 @@ module.exports = { }, 'basic:import-js': { message: 'supports { importFrom: "test/import-variables.js" } usage', - warnings: 1, options: { importFrom: 'test/import-variables.js' }, @@ -69,7 +63,6 @@ module.exports = { }, 'basic:import-cjs': { message: 'supports { importFrom: "test/import-variables.cjs" } usage', - warnings: 1, options: { importFrom: 'test/import-variables.cjs' }, @@ -78,7 +71,6 @@ module.exports = { }, 'basic:import-js-from': { message: 'supports { importFrom: { from: "test/import-variables.js" } } usage', - warnings: 1, options: { importFrom: { from: 'test/import-variables.js' } }, @@ -87,7 +79,6 @@ module.exports = { }, 'basic:import-js-from-type': { message: 'supports { importFrom: [ { from: "test/import-variables.js", type: "js" } ] } usage', - warnings: 1, options: { importFrom: [ { from: 'test/import-variables.js', type: 'js' } ] }, @@ -96,7 +87,6 @@ module.exports = { }, 'basic:import-is-empty': { message: 'supports { importFrom: {} } usage', - warnings: 1, options: { importFrom: {} } diff --git a/plugins/postcss-env-function/package.json b/plugins/postcss-env-function/package.json index 2b1b074f4..bcb5285d8 100644 --- a/plugins/postcss-env-function/package.json +++ b/plugins/postcss-env-function/package.json @@ -29,7 +29,7 @@ "node": "^12 || ^14 || >=16" }, "dependencies": { - "postcss-values-parser": "^6.0.1" + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "postcss": "^8.3.6", diff --git a/plugins/postcss-env-function/src/lib/get-replaced-value.js b/plugins/postcss-env-function/src/lib/get-replaced-value.js index cfc295d55..dc89629df 100644 --- a/plugins/postcss-env-function/src/lib/get-replaced-value.js +++ b/plugins/postcss-env-function/src/lib/get-replaced-value.js @@ -1,6 +1,5 @@ -import { parse } from 'postcss-values-parser'; -import updateEnvValue from './update-env-value'; -import walkEnvFuncs from './walk-env-funcs'; +import valuesParser from 'postcss-value-parser'; +import isEnvFunc from './is-env-func'; /** * @param {string} originalValue @@ -9,14 +8,20 @@ import walkEnvFuncs from './walk-env-funcs'; */ export default (originalValue, variables) => { // get the ast of the original value - const ast = parse(originalValue, { ignoreUnknownWords: true }); + const ast = valuesParser(originalValue); - // walk all of the css env() functions - walkEnvFuncs(ast, node => { - // update the environment value for the css env() function - updateEnvValue(node, variables); + ast.walk(node => { + if (isEnvFunc(node)) { + const [valueNode] = node.nodes; + + if (valueNode.type === 'word' && typeof variables[valueNode.value] !== 'undefined') { + node.nodes = []; + node.type = 'word'; + node.value = variables[valueNode.value]; + } + } }); // return the stringified ast - return String(ast); + return ast.toString(); }; diff --git a/plugins/postcss-env-function/src/lib/import-from.js b/plugins/postcss-env-function/src/lib/import-from.js index 16af1713e..bcfa44c3e 100644 --- a/plugins/postcss-env-function/src/lib/import-from.js +++ b/plugins/postcss-env-function/src/lib/import-from.js @@ -1,23 +1,16 @@ import fs from 'fs'; import path from 'path'; -import { parse } from 'postcss-values-parser'; /** * Import Custom Properties from Object * @param {{environmentVariables: Record, 'environment-variables': Record}} object - * @returns {Record} + * @returns {Object} */ function importEnvironmentVariablesFromObject(object) { - const environmentVariables = Object.assign( + return Object.assign( {}, Object(object).environmentVariables || Object(object)['environment-variables'], ); - - for (const key in environmentVariables) { - environmentVariables[key] = parse(environmentVariables[key], { ignoreUnknownWords: true }).nodes; - } - - return environmentVariables; } /** diff --git a/plugins/postcss-env-function/src/lib/is-env-func.js b/plugins/postcss-env-function/src/lib/is-env-func.js index 12d9146b5..7480e3116 100644 --- a/plugins/postcss-env-function/src/lib/is-env-func.js +++ b/plugins/postcss-env-function/src/lib/is-env-func.js @@ -1,2 +1,2 @@ // returns whether a node is a css env() function -export default (node) => node && node.type === 'func' && node.name === 'env'; +export default (node) => node && node.type === 'function' && node.value === 'env'; diff --git a/plugins/postcss-env-function/src/lib/update-env-value.js b/plugins/postcss-env-function/src/lib/update-env-value.js deleted file mode 100644 index a2ef8b2a8..000000000 --- a/plugins/postcss-env-function/src/lib/update-env-value.js +++ /dev/null @@ -1,44 +0,0 @@ -import getFnValue from './get-fn-value'; - -// update a node with an environment value -export default (node, variables) => { - // get the value of a css function as a string - const value = getFnValue(node); - - if (typeof value === 'string' && value in variables) { - node.replaceWith( - ...asClonedArrayWithBeforeSpacing(variables[value], node.raws.before), - ); - } -}; - -// return an array with its nodes cloned, preserving the raw -const asClonedArrayWithBeforeSpacing = (array, beforeSpacing) => { - const clonedArray = asClonedArray(array, null); - - if (clonedArray[0]) { - clonedArray[0].raws.before = beforeSpacing; - } - - return clonedArray; -}; - -// return an array with its nodes cloned -const asClonedArray = (array, parent) => array.map(node => asClonedNode(node, parent)); - -// return a cloned node -const asClonedNode = (node, parent) => { - const cloneNode = new node.constructor(node); - - for (const key in node) { - if (key === 'parent') { - cloneNode.parent = parent; - } else if (Object(node[key]).constructor === Array) { - cloneNode[key] = asClonedArray(node.nodes, cloneNode); - } else if (Object(node[key]).constructor === Object) { - cloneNode[key] = Object.assign({}, node[key]); - } - } - - return cloneNode; -}; diff --git a/plugins/postcss-env-function/src/lib/walk-env-funcs.js b/plugins/postcss-env-function/src/lib/walk-env-funcs.js deleted file mode 100644 index 070f30e9c..000000000 --- a/plugins/postcss-env-function/src/lib/walk-env-funcs.js +++ /dev/null @@ -1,14 +0,0 @@ -import isEnvFunc from './is-env-func'; - -// walks a node recursively and runs a function using its children -export default function walk (node, fn) { - node.nodes.slice(0).forEach(childNode => { - if (childNode.nodes) { - walk(childNode, fn); - } - - if (isEnvFunc(childNode)) { - fn(childNode); - } - }); -}