diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8fe48da --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: test +on: + push: + branches: + - 'main' + - 'master' + pull_request: + +concurrency: + group: branch-node-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] + node: [14, 16, 18] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: install + run: | + yarn install + + - name: build + run: yarn build + + - name: test + run: yarn test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f6bef04..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -# https://docs.travis-ci.com/user/travis-lint - -language: node_js - -node_js: - - 14 - - 12 - - 10 - -install: - - npm install --ignore-scripts diff --git a/index.js b/index.js index 6845243..cf7e048 100644 --- a/index.js +++ b/index.js @@ -1,120 +1,503 @@ -const feature_unit = { - 'width': 'px', - 'height': 'px', - 'device-width': 'px', - 'device-height': 'px', +const cssParserAlgorithms = require("@csstools/css-parser-algorithms"); +const cssTokenizer = require("@csstools/css-tokenizer"); +const mediaQueryListParser = require("@csstools/media-query-list-parser"); + +const unitsForFeature = { 'aspect-ratio': '', - 'device-aspect-ratio': '', 'color': '', 'color-index': '', + 'device-aspect-ratio': '', + 'device-height': 'px', + 'device-width': 'px', + 'height': 'px', 'monochrome': '', - 'resolution': 'dpi' + 'resolution': 'dpi', + 'width': 'px', }; -// Supported min-/max- attributes -const feature_name = Object.keys(feature_unit); +function featureNamePrefix(operator) { + if ( + operator === mediaQueryListParser.MediaFeatureLT.LT || + operator === mediaQueryListParser.MediaFeatureLT.LT_OR_EQ + ) { + return 'max-'; + } -const step = .001; // smallest even number that won’t break complex queries (1in = 96px) + if ( + operator === mediaQueryListParser.MediaFeatureGT.GT || + operator === mediaQueryListParser.MediaFeatureGT.GT_OR_EQ + ) { + return 'min-'; + } + + return ''; +} const power = { '>': 1, - '<': -1 + '<': -1, }; -const minmax = { - '>': 'min', - '<': 'max' -}; +const step = .001; // smallest even number that won’t break complex queries (1in = 96px) -function create_query(name, gtlt, eq, value) { - return value.replace(/([-\d\.]+)(.*)/, function (_match, number, unit) { - const initialNumber = parseFloat(number); +function transformSingleNameValuePair(name, operator, value, nameBeforeValue) { + let tokensBefore = value.before; + let tokensAfter = value.after; + if (!nameBeforeValue) { + tokensBefore = value.after; + tokensAfter = value.before; + } - if (parseFloat(number) || eq) { - // if eq is true, then number remains same - if (!eq) { - // change integer pixels value only on integer pixel - if (unit === 'px' && initialNumber === parseInt(number, 10)) { - number = initialNumber + power[gtlt]; - } else { - number = Number(Math.round(parseFloat(number) + step * power[gtlt] + 'e6')+'e-6'); - } - } - } else { - number = power[gtlt] + feature_unit[name]; + if (!nameBeforeValue) { + const invertedOperator = mediaQueryListParser.invertComparison(operator); + if (invertedOperator === false) { + return; } - return '(' + minmax[gtlt] + '-' + name + ': ' + number + unit + ')'; - }); -} + operator = invertedOperator; + } -function transform(rule) { - /** - * 转换 <|>= - * $1 $2 $3 - * (width >= 300px) => (min-width: 300px) - * (width <= 900px) => (max-width: 900px) - */ + if ( + operator === mediaQueryListParser.MediaFeatureEQ.EQ || + operator === mediaQueryListParser.MediaFeatureLT.LT_OR_EQ || + operator === mediaQueryListParser.MediaFeatureGT.GT_OR_EQ + ) { + if (Array.isArray(value.value)) { + return mediaQueryListParser.newMediaFeaturePlain( + featureNamePrefix(operator) + name, + ...tokensBefore, + ...value.value.flatMap(x => x.tokens()), + ...tokensAfter, + ); + } else { + return mediaQueryListParser.newMediaFeaturePlain( + featureNamePrefix(operator) + name, + ...tokensBefore, + ...value.value.tokens(), + ...tokensAfter, + ); + } + } + + if (Array.isArray(value.value) && mediaQueryListParser.matchesRatioExactly(value.value)) { + // TODO : handle ratio + return; + } - if (!rule.params.includes('<') && !rule.params.includes('>')) { - return + let valueNode; + if (Array.isArray(value.value)) { + valueNode = value.value.find((x) => { + return cssParserAlgorithms.isFunctionNode(x) || cssParserAlgorithms.isTokenNode(x); + }); + } else { + valueNode = value.value; } - // The value doesn't support negative values - // But -0 is always equivalent to 0 in CSS, and so is also accepted as a valid value. + if ( + cssParserAlgorithms.isFunctionNode(valueNode) && + valueNode.getName().toLowerCase() === 'calc' + ) { + let valueToken; + if (unitsForFeature[name.toLowerCase()]) { + const tokenValue = power[operator]; + const tokenUnit = unitsForFeature[name.toLowerCase()]; + + valueToken = [cssTokenizer.TokenType.Dimension, `${tokenValue.toString()}${tokenUnit}`, -1, -1, { value: tokenValue, unit: tokenUnit, type: cssTokenizer.NumberType.Integer }]; + } else { + const tokenValue = power[operator]; - rule.params = rule.params.replace(/\(\s*([a-z-]+?)\s*([<>])(=?)\s*((?:-?\d*\.?(?:\s*\/?\s*)?\d+[a-z]*)?)\s*\)/gi, function($0, $1, $2, $3, $4) { - if (feature_name.indexOf($1) > -1) { - return create_query($1, $2, $3, $4); + valueToken = [cssTokenizer.TokenType.Number, tokenValue.toString(), -1, -1, { value: tokenValue, type: cssTokenizer.NumberType.Integer }]; } - // If it is not the specified attribute, don't replace - return $0; - }) - - /** - * 转换 <|<= <|<= - * 转换 >|>= >|>= - * $1 $2$3 $4 $5$6 $7 - * (500px <= width <= 1200px) => (min-width: 500px) and (max-width: 1200px) - * (500px < width <= 1200px) => (min-width: 501px) and (max-width: 1200px) - * (900px >= width >= 300px) => (min-width: 300px) and (max-width: 900px) - */ - - rule.params = rule.params.replace(/\(\s*((?:-?\d*\.?(?:\s*\/?\s*)?\d+[a-z]*)?)\s*(<|>)(=?)\s*([a-z-]+)\s*(<|>)(=?)\s*((?:-?\d*\.?(?:\s*\/?\s*)?\d+[a-z]*)?)\s*\)/gi, function($0, $1, $2, $3, $4, $5, $6, $7) { - - if (feature_name.indexOf($4) > -1) { - if ($2 === '<' && $5 === '<' || $2 === '>' && $5 === '>') { - const min = ($2 === '<') ? $1 : $7; - const max = ($2 === '<') ? $7 : $1; - - // output differently depended on expression direction - // <|<= <|<= - // or - // >|>= >|>= - let equals_for_min = $3; - let equals_for_max = $6; - - if ($2 === '>') { - equals_for_min = $6; - equals_for_max = $3; + + return mediaQueryListParser.newMediaFeaturePlain( + featureNamePrefix(operator) + name, + ...tokensBefore, + [cssTokenizer.TokenType.Function, 'calc(', -1, -1, { value: 'calc(' }], + [cssTokenizer.TokenType.OpenParen, '(', -1, -1, undefined], + ...valueNode.tokens().slice(1), + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.Delim, '+', -1, -1, undefined], + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + valueToken, + [cssTokenizer.TokenType.CloseParen, ')', -1, -1, undefined], + ...tokensAfter, + ); + } else if (cssParserAlgorithms.isTokenNode(valueNode)) { + let token = valueNode.value; + let tokenValue; + let tokenUnit = false; + + if ( + (token[0] === cssTokenizer.TokenType.Dimension || token[0] === cssTokenizer.TokenType.Number) && + token[4].value === 0 + ) { + + // Zero values: + // - convert to "1" or "-1" + // - assign a unit when needed + tokenValue = power[operator]; + tokenUnit = unitsForFeature[name.toLowerCase()]; + } else if ( + token[0] === cssTokenizer.TokenType.Dimension && + token[4].unit.toLowerCase() === 'px' && + token[4].type === cssTokenizer.NumberType.Integer + ) { + + // Integer pixel values + // - add "+1" or "-1" + tokenValue = token[4].value + power[operator]; + } else if ( + token[0] === cssTokenizer.TokenType.Dimension || + token[0] === cssTokenizer.TokenType.Number + ) { + + // Float or non-pixel values + // - add "+step" or "-step" + tokenValue = Number(Math.round(Number(token[4].value + step * power[operator] + 'e6')) + 'e-6'); + } else { + return; + } + + if (tokenUnit !== false) { + token = [ + cssTokenizer.TokenType.Dimension, + token[1], + token[2], + token[3], + { + value: token[4].value, + unit: tokenUnit, + type: token[4].type, + }, + ]; + } + + token[4].value = tokenValue; + if (token[0] === cssTokenizer.TokenType.Dimension) { + token[1] = token[4].value.toString() + token[4].unit; + } else { + token[1] = token[4].value.toString(); + } + + return mediaQueryListParser.newMediaFeaturePlain( + featureNamePrefix(operator) + name, + ...tokensBefore, + token, + ...tokensAfter, + ); + } +} + +const supportedFeatureNames = new Set([ + 'aspect-ratio', + 'color', + 'color-index', + 'device-aspect-ratio', + 'device-height', + 'device-width', + 'height', + 'horizontal-viewport-segments', + 'monochrome', + 'resolution', + 'vertical-viewport-segments', + 'width', +]); + +function transform(mediaQueries) { + return mediaQueries.map((mediaQuery, mediaQueryIndex) => { + const ancestry = cssParserAlgorithms.gatherNodeAncestry(mediaQuery); + + mediaQuery.walk((entry) => { + const node = entry.node; + if (!mediaQueryListParser.isMediaFeatureRange(node)) { + return; + } + + const parent = entry.parent; + if (!mediaQueryListParser.isMediaFeature(parent)) { + return; + } + + const name = node.name.getName(); + if (!supportedFeatureNames.has(name.toLowerCase())) { + return; + } + + if (mediaQueryListParser.isMediaFeatureRangeNameValue(node) || mediaQueryListParser.isMediaFeatureRangeValueName(node)) { + const operator = node.operatorKind(); + if (operator === false) { + return; + } + + const transformed = transformSingleNameValuePair(name, operator, node.value, mediaQueryListParser.isMediaFeatureRangeNameValue(node)); + if (transformed) { + parent.feature = transformed.feature; } - return create_query($4, '>', equals_for_min, min) + ' and ' + create_query($4, '<', equals_for_max, max); + return; } - } - // If it is not the specified attribute, don't replace - return $0; - }); + + const grandParent = ancestry.get(parent); + if (!mediaQueryListParser.isMediaInParens(grandParent)) { + return; + } + + let featureOne = null; + let featureTwo = null; + { + const operator = node.valueOneOperatorKind(); + if (operator === false) { + return; + } + + const transformed = transformSingleNameValuePair(name, operator, node.valueOne, false); + if (!transformed) { + return; + } + + if (operator === mediaQueryListParser.MediaFeatureLT.LT || operator === mediaQueryListParser.MediaFeatureLT.LT_OR_EQ) { + featureOne = transformed; + featureOne.before = parent.before + } else { + featureTwo = transformed; + featureTwo.after = parent.after + } + } + + { + const operator = node.valueTwoOperatorKind(); + if (operator === false) { + return; + } + + const transformed = transformSingleNameValuePair(name, operator, node.valueTwo, true); + if (!transformed) { + return; + } + + if (operator === mediaQueryListParser.MediaFeatureLT.LT || operator === mediaQueryListParser.MediaFeatureLT.LT_OR_EQ) { + featureTwo = transformed; + featureTwo.before = parent.before + } else { + featureOne = transformed; + featureOne.after = parent.after + } + } + + const parensOne = new mediaQueryListParser.MediaInParens( + featureOne, + ); + + const parensTwo = new mediaQueryListParser.MediaInParens( + featureTwo, + ); + + // ((color) and (300px < width < 400px)) + // ((300px < width < 400px) and (color)) + const andList = getMediaConditionListWithAndFromAncestry(grandParent, ancestry); + if (andList) { + if (andList.leading === grandParent) { + andList.leading = parensOne; + + andList.list = [ + new mediaQueryListParser.MediaAnd( + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.Ident, 'and', -1, -1, { value: 'and' }], + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + ], + parensTwo, + ), + ...andList.list, + ]; + + return; + } + + andList.list.splice( + andList.indexOf(ancestry.get(grandParent)), + 1, + new mediaQueryListParser.MediaAnd( + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.Ident, 'and', -1, -1, { value: 'and' }], + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + ], + parensOne, + ), + new mediaQueryListParser.MediaAnd( + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.Ident, 'and', -1, -1, { value: 'and' }], + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + ], + parensTwo, + ), + ); + + return; + } + + const conditionList = new mediaQueryListParser.MediaConditionListWithAnd( + parensOne, + [ + new mediaQueryListParser.MediaAnd( + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.Ident, 'and', -1, -1, { value: 'and' }], + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + ], + parensTwo, + ), + ], + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + ], + ); + + // @media screen and (300px < width < 400px) + // @media (300px < width < 400px) + const conditionInShallowQuery = getMediaConditionInShallowMediaQueryFromAncestry(grandParent, mediaQuery, ancestry); + if (conditionInShallowQuery) { + conditionInShallowQuery.media = conditionList; + return; + } + + // Remaining (more complex) cases. + // Wrapped in extra parens. + grandParent.media = new mediaQueryListParser.MediaCondition( + new mediaQueryListParser.MediaInParens( + new mediaQueryListParser.MediaCondition( + conditionList, + ), + [ + [cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined], + [cssTokenizer.TokenType.OpenParen, '(', -1, -1, undefined], + ], + [ + [cssTokenizer.TokenType.CloseParen, ')', -1, -1, undefined], + ], + ), + ); + }); + + const tokens = mediaQuery.tokens(); + return cssTokenizer.stringify( + ...tokens.filter((x, i) => { + // The algorithms above will err on the side of caution and might insert to much whitespace. + + if (i === 0 && mediaQueryIndex === 0 && x[0] === cssTokenizer.TokenType.Whitespace) { + // Trim leading whitespace from the first media query. + return false; + } + + if (x[0] === cssTokenizer.TokenType.Whitespace && tokens[i + 1] && tokens[i + 1][0] === cssTokenizer.TokenType.Whitespace) { + // Collapse multiple sequential whitespace tokens + return false; + } + + return true; + }) + ); + }).join(','); +} + +function getMediaConditionListWithAndFromAncestry(mediaInParens, ancestry) { + let focus = mediaInParens; + if (!focus) { + return; + } + + focus = ancestry.get(focus); + if (mediaQueryListParser.isMediaConditionListWithAnd(focus)) { + return focus; + } + + if (!mediaQueryListParser.isMediaAnd(focus)) { + return; + } + + focus = ancestry.get(focus); + if (mediaQueryListParser.isMediaConditionListWithAnd(focus)) { + return focus; + } + + return; +} + +function getMediaConditionInShallowMediaQueryFromAncestry(mediaInParens, mediaQuery, ancestry) { + let focus = mediaInParens; + if (!focus) { + return; + } + + focus = ancestry.get(focus); + if (!mediaQueryListParser.isMediaCondition(focus)) { + return; + } + + const condition = focus; + + focus = ancestry.get(focus); + if (!mediaQueryListParser.isMediaQuery(focus)) { + return; + } + + if (focus !== mediaQuery) { + return; + } + + return condition; } module.exports = () => ({ postcssPlugin: 'postcss-media-minmax', AtRule: { media: (atRule) => { - transform(atRule); + if (!(atRule.params.includes('<') || atRule.params.includes('>') || atRule.params.includes('='))) { + return; + } + + const mediaQueries = mediaQueryListParser.parse(atRule.params, { + preserveInvalidMediaQueries: true, + onParseError: () => { + throw atRule.error(`Unable to parse media query "${atRule.params}"`); + }, + }); + + const transformed = transform(mediaQueries); + if (atRule.params === transformed) { + return; + } + + atRule.params = transformed }, 'custom-media': (atRule) => { - transform(atRule); + if (!(atRule.params.includes('<') || atRule.params.includes('>') || atRule.params.includes('='))) { + return; + } + + const customMedia = mediaQueryListParser.parseCustomMedia(atRule.params, { + preserveInvalidMediaQueries: true, + onParseError: () => { + throw atRule.error(`Unable to parse media query "${atRule.params}"`); + }, + }); + if (!customMedia) { + return + } + + if (!customMedia.hasMediaQueryList()) { + return; + } + + const originalMediaQueries = customMedia.mediaQueryList.map((x) => x.toString()).join(','); + const transformed = transform(customMedia.mediaQueryList); + if (originalMediaQueries === transformed) { + return; + } + + atRule.params = atRule.params.replace(originalMediaQueries, ' ' + transformed) }, }, }); diff --git a/package.json b/package.json index 84eb249..fecc171 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "css3", "postcss", "postcss-plugin", - "media querie", + "media query", "media queries" ], "author": "yisi", @@ -24,7 +24,12 @@ "index.js" ], "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" + }, + "dependencies": { + "@csstools/css-parser-algorithms": "^2.0.0", + "@csstools/css-tokenizer": "^2.0.0", + "@csstools/media-query-list-parser": "^2.0.0" }, "peerDependencies": { "postcss": "8.4.5" diff --git a/test/fixtures/aspect-ratio.css b/test/fixtures/aspect-ratio.css index 85264be..a7cd2e0 100644 --- a/test/fixtures/aspect-ratio.css +++ b/test/fixtures/aspect-ratio.css @@ -5,3 +5,5 @@ @media screen and (0/0 <= aspect-ratio <= 16/9) {} @media screen and (aspect-ratio) and (1 / 1000 <= aspect-ratio <= 16 / 9) {} + +@media screen and (0/0 < aspect-ratio < 16/9) {} diff --git a/test/fixtures/aspect-ratio.output.css b/test/fixtures/aspect-ratio.output.css index bd8a56f..beb8514 100644 --- a/test/fixtures/aspect-ratio.output.css +++ b/test/fixtures/aspect-ratio.output.css @@ -5,3 +5,5 @@ @media screen and (min-aspect-ratio: 0/0) and (max-aspect-ratio: 16/9) {} @media screen and (aspect-ratio) and (min-aspect-ratio: 1 / 1000) and (max-aspect-ratio: 16 / 9) {} + +@media screen and (0/0 < aspect-ratio < 16/9) {} diff --git a/test/fixtures/calc.css b/test/fixtures/calc.css new file mode 100644 index 0000000..0a18c9e --- /dev/null +++ b/test/fixtures/calc.css @@ -0,0 +1,35 @@ +@media (width < calc(10px * 50)) {} + +@media (width <= calc(10px * 50)) {} + +@media (width = calc(10px * 50)) {} + +@media (width > calc(10px * 50)) {} + +@media (width >= calc(10px * 50)) {} + +@media (calc(10px * 50) < width) {} + +@media (calc(10px * 50) <= width) {} + +@media (calc(10px * 50) = width) {} + +@media (calc(10px * 50) > width) {} + +@media (calc(10px * 50) >= width) {} + +@media (calc(10px * 50) < width < calc(100px * 10)) {} + +@media (calc(10px * 50) <= width < calc(100px * 10)) {} + +@media (calc(10px * 50) < width <= calc(100px * 10)) {} + +@media (calc(10px * 50) <= width <= calc(100px * 10)) {} + +@media (calc(10px * 50) > width > calc(100px * 10)) {} + +@media (calc(10px * 50) >= width > calc(100px * 10)) {} + +@media (calc(10px * 50) > width >= calc(100px * 10)) {} + +@media (calc(10px * 50) >= width >= calc(100px * 10)) {} diff --git a/test/fixtures/calc.output.css b/test/fixtures/calc.output.css new file mode 100644 index 0000000..94a5d7e --- /dev/null +++ b/test/fixtures/calc.output.css @@ -0,0 +1,35 @@ +@media (max-width: calc((10px * 50) + -1px)) {} + +@media (max-width: calc(10px * 50)) {} + +@media (width: calc(10px * 50)) {} + +@media (min-width: calc((10px * 50) + 1px)) {} + +@media (min-width: calc(10px * 50)) {} + +@media (min-width: calc((10px * 50) + 1px)) {} + +@media (min-width: calc(10px * 50)) {} + +@media (width: calc(10px * 50)) {} + +@media (max-width: calc((10px * 50) + -1px)) {} + +@media (max-width: calc(10px * 50)) {} + +@media (min-width: calc((10px * 50) + 1px)) and (max-width: calc((100px * 10) + -1px)) {} + +@media (min-width: calc(10px * 50)) and (max-width: calc((100px * 10) + -1px)) {} + +@media (min-width: calc((10px * 50) + 1px)) and (max-width: calc(100px * 10)) {} + +@media (min-width: calc(10px * 50)) and (max-width: calc(100px * 10)) {} + +@media (min-width: calc((100px * 10) + 1px)) and (max-width: calc((10px * 50) + -1px)) {} + +@media (min-width: calc((100px * 10) + 1px)) and (max-width: calc(10px * 50)) {} + +@media (min-width: calc(100px * 10)) and (max-width: calc((10px * 50) + -1px)) {} + +@media (min-width: calc(100px * 10)) and (max-width: calc(10px * 50)) {} diff --git a/test/fixtures/complex.css b/test/fixtures/complex.css new file mode 100644 index 0000000..1173450 --- /dev/null +++ b/test/fixtures/complex.css @@ -0,0 +1,14 @@ +@media screen and (not (200px <=width < 500px)) {} + +@media (min-height: 500px) and (200px <= width < 500px) {} + +@media screen and not (200px <=width < 500px) {} + +@media (not (200px <=width < 500px)) {} + +@media ((min-width: 300px) or (200px <=width < 500px)) {} + +@media screen and (not (200px <=width < 500px)), + screen and not (200px <=width < 500px), + (not (200px <=width < 500px)), + ((min-width: 300px) or (200px <=width < 500px)) {} diff --git a/test/fixtures/complex.output.css b/test/fixtures/complex.output.css new file mode 100644 index 0000000..c1782ed --- /dev/null +++ b/test/fixtures/complex.output.css @@ -0,0 +1,14 @@ +@media screen and (not ( (min-width: 200px) and (max-width: 499px))) {} + +@media (min-height: 500px) and (min-width: 200px) and (max-width: 499px) {} + +@media screen and not ( (min-width: 200px) and (max-width: 499px)) {} + +@media (not ( (min-width: 200px) and (max-width: 499px))) {} + +@media ((min-width: 300px) or ( (min-width: 200px) and (max-width: 499px))) {} + +@media screen and (not ( (min-width: 200px) and (max-width: 499px))), + screen and not ( (min-width: 200px) and (max-width: 499px)), + (not ( (min-width: 200px) and (max-width: 499px))), + ((min-width: 300px) or ( (min-width: 200px) and (max-width: 499px))) {} diff --git a/test/fixtures/custom-media.css b/test/fixtures/custom-media.css new file mode 100644 index 0000000..84df9c9 --- /dev/null +++ b/test/fixtures/custom-media.css @@ -0,0 +1,9 @@ +@custom-media --one (width < 300px) {} + +@custom-media --two (width <= 300px), (width=300px) {} + +@custom-media --three (width = 300px) {} + +@custom-media /* a comment */ --four (width > 300px) {} + +@custom-media /* a comment */ --five /* a comment */ (width >= 300px) /* a comment */ {} diff --git a/test/fixtures/custom-media.output.css b/test/fixtures/custom-media.output.css new file mode 100644 index 0000000..6106095 --- /dev/null +++ b/test/fixtures/custom-media.output.css @@ -0,0 +1,9 @@ +@custom-media --one (max-width: 299px) {} + +@custom-media --two (max-width: 300px), (width:300px) {} + +@custom-media --three (width: 300px) {} + +@custom-media /* a comment */ --four (min-width: 301px) {} + +@custom-media /* a comment */ --five (min-width: 300px) /* a comment */ {} diff --git a/test/fixtures/operators.css b/test/fixtures/operators.css new file mode 100644 index 0000000..8d41709 --- /dev/null +++ b/test/fixtures/operators.css @@ -0,0 +1,41 @@ +@media (width < 300px) {} + +@media (width <= 300px) {} + +@media (width = 300px) {} + +@media (width > 300px) {} + +@media (width >= 300px) {} + +@media (300px < width) {} + +@media (300px <= width) {} + +@media (300px = width) {} + +@media (300px > width) {} + +@media (300px >= width) {} + +@media (300px < width < 1000px) {} + +@media (300px <= width < 1000px) {} + +@media (300px < width <= 1000px) {} + +@media (300px <= width <= 1000px) {} + +@media (300px > width > 1000px) {} + +@media (300px >= width > 1000px) {} + +@media (300px > width >= 1000px) {} + +@media (300px >= width >= 1000px) {} + +@media (300px > width < 1000px) {} + +@media (300px < width > 1000px) {} + +@media (300px = width = 1000px) {} diff --git a/test/fixtures/operators.output.css b/test/fixtures/operators.output.css new file mode 100644 index 0000000..7ffbbb0 --- /dev/null +++ b/test/fixtures/operators.output.css @@ -0,0 +1,41 @@ +@media (max-width: 299px) {} + +@media (max-width: 300px) {} + +@media (width: 300px) {} + +@media (min-width: 301px) {} + +@media (min-width: 300px) {} + +@media (min-width: 301px) {} + +@media (min-width: 300px) {} + +@media (width: 300px) {} + +@media (max-width: 299px) {} + +@media (max-width: 300px) {} + +@media (min-width: 301px) and (max-width: 999px) {} + +@media (min-width: 300px) and (max-width: 999px) {} + +@media (min-width: 301px) and (max-width: 1000px) {} + +@media (min-width: 300px) and (max-width: 1000px) {} + +@media (min-width: 1001px) and (max-width: 299px) {} + +@media (min-width: 1001px) and (max-width: 300px) {} + +@media (min-width: 1000px) and (max-width: 299px) {} + +@media (min-width: 1000px) and (max-width: 300px) {} + +@media (300px > width < 1000px) {} + +@media (300px < width > 1000px) {} + +@media (300px = width = 1000px) {} diff --git a/test/fixtures/shorthands.css b/test/fixtures/shorthands.css index 4212ca8..5c53edf 100644 --- a/test/fixtures/shorthands.css +++ b/test/fixtures/shorthands.css @@ -9,3 +9,5 @@ @media (1024px > width > 768px) {} @media (768px < width < 1024px) {} + +@media (1024px > width > 768px), (768px < width < 1024px) {} diff --git a/test/fixtures/shorthands.output.css b/test/fixtures/shorthands.output.css index dbc2542..21d0478 100644 --- a/test/fixtures/shorthands.output.css +++ b/test/fixtures/shorthands.output.css @@ -9,3 +9,5 @@ @media (min-width: 769px) and (max-width: 1023px) {} @media (min-width: 769px) and (max-width: 1023px) {} + +@media (min-width: 769px) and (max-width: 1023px), (min-width: 769px) and (max-width: 1023px) {} diff --git a/test/fixtures/unknown-feature.css b/test/fixtures/unknown-feature.css new file mode 100644 index 0000000..d979bf7 --- /dev/null +++ b/test/fixtures/unknown-feature.css @@ -0,0 +1 @@ +@media screen and (unknown-feature >=500px) and (foo <=1200px) {} diff --git a/test/fixtures/unknown-feature.output.css b/test/fixtures/unknown-feature.output.css new file mode 100644 index 0000000..d979bf7 --- /dev/null +++ b/test/fixtures/unknown-feature.output.css @@ -0,0 +1 @@ +@media screen and (unknown-feature >=500px) and (foo <=1200px) {} diff --git a/test/index.js b/test/index.js index 86c402e..67c2843 100644 --- a/test/index.js +++ b/test/index.js @@ -23,9 +23,12 @@ test("@media", function(t) { compareFixtures(t, "device-width-height", "should transform") compareFixtures(t, "aspect-ratio", "should transform") compareFixtures(t, "device-aspect-ratio", "should transform") + compareFixtures(t, "calc", "should transform") + compareFixtures(t, "complex", "should transform") compareFixtures(t, "color", "should transform") compareFixtures(t, "color-index", "should transform") compareFixtures(t, "monochrome", "should transform") + compareFixtures(t, "operators", "should transform") compareFixtures(t, "resolution", "should transform") compareFixtures(t, "comment", "should transform") @@ -36,5 +39,13 @@ test("@media", function(t) { compareFixtures(t, "min-max", "should transform") compareFixtures(t, "shorthands", "should transform shorthands") + compareFixtures(t, "unknown-feature", "should not transform") + + t.end() +}) + +test("@custom-media", function (t) { + compareFixtures(t, "custom-media", "should transform") + t.end() }) diff --git a/yarn.lock b/yarn.lock index 1618ac8..f558705 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,21 @@ # yarn lockfile v1 +"@csstools/css-parser-algorithms@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.0.0.tgz#2f27a1f2aef94f5d009d3d83f08f1e095dff2b30" + integrity sha512-RbukP8OjQvuH85veuzOq8abPjsvqvleZaQC6W0GJFGpwLUh8XmFMQjvtuIM9bQ589YFx4lwwAcSwN4nfcvxIEw== + +"@csstools/css-tokenizer@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.0.0.tgz#fa2a7e8f4ed965e73ba30ee80c00fa64980fd11e" + integrity sha512-IB6EFP0Hc/YEz1sJVD47oFqJP6TXMB+OW1jXSYnOk5g+6wpk2/zkuBa0gm5edIMM9nVUZ3hF0xCBnyFbK5OIyg== + +"@csstools/media-query-list-parser@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.0.tgz#06c6dce65656e4970927a603e910c6f949c5c07f" + integrity sha512-84kEbyJjh2T4Lnz8EkVQrwNANP+dtNb0SDkI3P7kqKnGorPknQUuq8Iqf2v5UsaH08XzPp3ouVJNsyPOdI2B/Q== + array.prototype.every@^1.1.3: version "1.1.3" resolved "https://registry.npmmirror.com/array.prototype.every/-/array.prototype.every-1.1.3.tgz#31f01b48e1160bc4b49ecab246bf7f765c6686f9"