import { describe, it, expect } from 'vitest' import * as Style from '../../src/style/index.js' import * as CSS from '../../src/utils/css.js' import { getSelectorWeight, getSelectorWithWeight } from '../../src/index.js' describe('CSS.parse', () => { describe('Happy path', () => { it('should round trip serializer/parser ', () => { for (const tree of [ [ ['property', 'a', 'b'], ['property', 'b', 'c'] ], [['rule', ['[a="a]#"]'], ['property', 'a', 'b']]], [['rule', [':not(a,b)'], ['property', 'a', 'b']]], [['rule', [':not("a,b")'], ['property', 'a', 'b']]], [['rule', [':not("a,b")', ':not(a,b)'], ['property', 'a', 'b']]], // not parsing selectors properly // [['rule', '[a="a]{}"]', ['property', 'a', 'b']]], [['property', 'a', 'b']], [['comment', 'test']], [['rule', ['test'], ['property', 'color', 'red'], ['comment', 'test']]], [['rule', ['test'], ['property', 'color', 'red']]], [['rule', ['test'], ['property', 'color', '5m8px']]], [['rule', ['#test .abc'], ['property', 'color', ['length', { unit: 'em', value: 1 }]]]], [['media', 'only screen and (min-width: 1px)', ['property', 'color', ['length', { unit: 'em', value: 1 }]]]], [['media', 'only screen and (min-width: .1px)', ['property', 'color', ['length', { unit: 'em', value: 0.1 }]]]], [ [ 'media', '(orientation: landscape)', ['property', 'color', ['length', { unit: 'em', value: 1 }]], ['property', 'color', 'top left'] ] ], [ [ 'media', '(min-width: 100px) and (max-width: 200px)', ['property', 'color', ['length', { unit: 'em', value: 1 }]], ['property', 'color', 'top left'], ['rule', ['test'], ['property', 'color', '5m8px']], ['rule', ['#test .abc'], ['property', 'color', ['length', { unit: 'em', value: 1 }]]] ] ] ]) { expect(CSS.parse(CSS.stringify(tree))).toEqual(tree) } }) it('should round-trip parse/serialize', () => { for (const css of [ `&[class*="-block--media"] { width: 100%; flex-grow: 0 !important; flex-shrink: 0 !important; } > picture { position: relative; overflow: hidden; } > picture > img { object-fit: cover; object-position: 33px 100%; position: absolute; top: 0px; left: 0px; right: 0px; bottom: 0px; height: 100%; width: 100%; } > picture:before { content: ""; display: block; padding-top: 60%; }` ]) { expect(CSS.stringify(CSS.parse(css))).toEqual(css) } }) it('should parse imports', () => { expect(CSS.parse('@import url("abc");')).to.eql([['import', 'abc']]) expect(CSS.parse('@import "abc";')).to.eql([['import', 'abc']]) expect(CSS.parse("@import url('abc');")).to.eql([['import', 'abc']]) expect(CSS.parse("@import 'abc';")).to.eql([['import', 'abc']]) expect(CSS.parse('@import url(abc);')).to.eql([['import', 'abc']]) expect(CSS.parse('@import abc;')).to.eql([['import', 'abc']]) expect(CSS.parse('@import url( abc);')).to.eql([['import', 'abc']]) expect(CSS.parse('@import abc ;')).to.eql([['import', 'abc']]) expect(CSS.parse('@import url("abc")')).to.eql([['import', 'abc']]) expect(CSS.parse('@import "abc"')).to.eql([['import', 'abc']]) expect(CSS.parse("@import url('abc')")).to.eql([['import', 'abc']]) expect(CSS.parse("@import 'abc'")).to.eql([['import', 'abc']]) expect(CSS.parse('@import url(abc)')).to.eql([['import', 'abc']]) expect(CSS.parse('@import abc')).to.eql([['import', 'abc']]) expect(CSS.parse('@import url(abc )')).to.eql([['import', 'abc']]) expect(CSS.parse('@import abc')).to.eql([['import', 'abc']]) }) it('should hoist imports', () => { expect(CSS.stringify(CSS.parse('@import url("abc");'))).to.eql('@import url("abc");') expect(CSS.stringify(CSS.parse('.a{@import url("abc")}'))).to.eql(`@import url("abc");\n.a {\n \n}`) }) it('should parse various syntaxes', () => { expect(CSS.parse('a{color:red}')).to.eql([['rule', ['a'], ['property', 'color', 'red']]]) expect(CSS.parse('a{color:red }')).to.eql([['rule', ['a'], ['property', 'color', 'red']]]) expect(CSS.parse('a{color:red\n}')).to.eql([['rule', ['a'], ['property', 'color', 'red']]]) expect(CSS.parse('a{/*test*/}')).to.eql([['rule', ['a'], ['comment', 'test']]]) expect(CSS.parse('a{color:red;;}')).to.eql([['rule', ['a'], ['property', 'color', 'red']]]) expect(CSS.parse('a{;}')).to.eql([['rule', ['a']]]) expect(CSS.parse('a{;;}')).to.eql([['rule', ['a']]]) }) it('should parse deeply nested rules', () => { expect( CSS.stringify([ 'rule', '#TARGET', ...CSS.parse(`&[class*="-block--media"] { width: 100%; flex-grow: 0 !important; flex-shrink: 0 !important; @media (min-width: 100px) { .test { color: black; .z + & { font: red; } } } }`) ]) ).toEqual(`#TARGET[class*="-block--media"] { width: 100%; flex-grow: 0 !important; flex-shrink: 0 !important; } @media (min-width: 100px) { #TARGET[class*="-block--media"] .test { color: black; } #TARGET[class*="-block--media"] .z + .test { font: red; } }`) }) // BEGIN: complex-media-query it('should handle complex media queries', () => { expect( CSS.parse(`@media screen and (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) and (prefers-color-scheme: dark), (min-width: 1025px) and (max-width: 1280px) and (orientation: portrait) and (prefers-color-scheme: light) { .container { width: 100%; height: 100%; background-color: blue; } }`) ).toEqual([ [ 'media', 'screen and (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) and (prefers-color-scheme: dark), ' + '(min-width: 1025px) and (max-width: 1280px) and (orientation: portrait) and (prefers-color-scheme: light)', [ 'rule', ['.container'], ['property', 'width', ['length', { unit: '%', value: 100 }]], ['property', 'height', ['length', { unit: '%', value: 100 }]], ['property', 'background-color', 'blue'] ] ] ]) }) // END: complex-media-query it('should parse nested rules', () => { expect( CSS.parse(`@media (min-width: 880px) and (max-width: 990px) { .abc { width: 1px; height: 2px; content: "}"; /* Ignore this } comment */ } test: 1px; } test2: 2px; #cde:not([data="abc"]) { width: 1px; } `) ).to.eql([ [ 'media', '(min-width: 880px) and (max-width: 990px)', [ 'rule', ['.abc'], ['property', 'width', ['length', { unit: 'px', value: 1 }]], ['property', 'height', ['length', { unit: 'px', value: 2 }]], ['property', 'content', ['string', '}']], ['comment', 'Ignore this } comment'] ], ['property', 'test', ['length', { value: 1, unit: 'px' }]] ], ['property', 'test2', ['length', { value: 2, unit: 'px' }]], ['rule', ['#cde:not([data="abc"])'], ['property', 'width', ['length', { unit: 'px', value: 1 }]]] ]) }) it('should parse selectors', () => { expect(CSS.parse('div, p{}')).to.eql([['rule', ['div', 'p']]]) expect(CSS.parse('#id, .class{}')).to.eql([['rule', ['#id', '.class']]]) expect(CSS.parse('ul li, ol li{}')).to.eql([['rule', ['ul li', 'ol li']]]) expect(CSS.parse('h1, h2, h3{}')).to.eql([['rule', ['h1', 'h2', 'h3']]]) expect(CSS.parse('.container > .item, .list > .item{}')).to.eql([ ['rule', ['.container > .item', '.list > .item']] ]) expect(CSS.parse('a:hover, a:active{}')).to.eql([['rule', ['a:hover', 'a:active']]]) expect(CSS.parse('input[type="text"], input[type="password"]{}')).to.eql([ ['rule', ['input[type="text"]', 'input[type="password"]']] ]) expect(CSS.parse('.box, .box::before, .box::after{}')).to.eql([['rule', ['.box', '.box::before', '.box::after']]]) expect(CSS.parse('::placeholder, ::selection{}')).to.eql([['rule', ['::placeholder', '::selection']]]) expect(CSS.parse('[data-foo="bar"], [data-baz="qux"]{}')).to.eql([ ['rule', ['[data-foo="bar"]', '[data-baz="qux"]']] ]) expect(CSS.parse('div[data-content="a, b"], p{}')).to.eql([['rule', ['div[data-content="a, b"]', 'p']]]) expect(CSS.parse('a:not(.class1, .class2), .link{}')).to.eql([['rule', ['a:not(.class1, .class2)', '.link']]]) expect(CSS.parse('input[value="comma, separated"], button{}')).to.eql([ ['rule', ['input[value="comma, separated"]', 'button']] ]) expect(CSS.parse('[title="hello, world"], [title="goodbye, world"]{}')).to.eql([ ['rule', ['[title="hello, world"]', '[title="goodbye, world"]']] ]) expect(CSS.parse('div:before, div[data-attr="comma, here"]{}')).to.eql([ ['rule', ['div:before', 'div[data-attr="comma, here"]']] ]) expect(CSS.parse('a[title="a, b"]:hover, a[title="c, d"]:active{}')).to.eql([ ['rule', ['a[title="a, b"]:hover', 'a[title="c, d"]:active']] ]) expect(CSS.parse('::after, [data-content="comma, test"]{}')).to.eql([ ['rule', ['::after', '[data-content="comma, test"]']] ]) expect(CSS.parse(':not(span, [class*="comma,here"]), .container > .item{}')).to.eql([ ['rule', [':not(span, [class*="comma,here"])', '.container > .item']] ]) expect(CSS.parse('.class1, .class2[data-value="a, b, c"]{}')).to.eql([ ['rule', ['.class1', '.class2[data-value="a, b, c"]']] ]) expect(CSS.parse('input[type="text, number"], input[type="password"]{}')).to.eql([ ['rule', ['input[type="text, number"]', 'input[type="password"]']] ]) }) }) describe('error handling', () => { it('should throw on invalid syntax', () => { expect(CSS.parse('ab+1: 1')).to.eql([ [ 'error', 'Failed to parse', 1, 2, ` ab+1: 1 --^`.trim() ] ]) expect(CSS.parse('ab: ')).to.eql([ [ 'error', 'Missing value', 1, 4, ` ab: ----^`.trim() ] ]) expect(CSS.parse('--test 1')).to.eql([ [ 'error', 'Failed to parse', 1, 8, ` --test 1 --------^`.trim() ] ]) expect(CSS.parse('.abc{')).to.eql([ [ 'error', 'Missing closed brace for rule', 1, 5, ` .abc{ -----^`.trim() ] ]) expect(CSS.parse('@media{.abc{}')).to.eql([ [ 'error', 'Invalid media query', 1, 6, ` @media{.abc{} ------^`.trim() ] ]) expect(CSS.parse('@media(min-width: 1em){a:1')).to.eql([ [ 'error', 'Missing closed brace for media', 1, 26, ` @media(min-width: 1em){a:1 --------------------------^`.trim() ] ]) expect(CSS.parse('@media only screen{a:1')).to.eql([ [ 'error', 'Missing closed brace for media', 1, 22, ` @media only screen{a:1 ----------------------^`.trim() ] ]) expect(CSS.parse('@media(max-z: 1)')).to.eql([ [ 'error', 'Invalid media query', 1, 6, ` @media(max-z: 1) ------^`.trim() ] ]) expect(CSS.parse('@media(orientation: lol)')).to.eql([ [ 'error', 'Invalid media query', 1, 6, ` @media(orientation: lol) ------^`.trim() ] ]) }) }) // Call the fuzzNesting function with the desired number of test examples and max nesting level it('fuzz test', () => { fuzzNesting(20, 5) }) it('getFonts', () => { expect( CSS.getFonts( CSS.parse(`/* latin-ext */ @font-face { font-family: 'Marcellus SC'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/marcellussc/v13/ke8iOgUHP1dg-Rmi6RWjbLE_iNacOqu0hYYt.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Marcellus SC'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/marcellussc/v13/ke8iOgUHP1dg-Rmi6RWjbLE_htacOqu0hQ.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @font-face { font-family: 'PT Sans Narrow'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCLwR2oefDofMY.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @font-face { font-family: 'PT Sans Narrow'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCCwR2oefDofMY.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* latin-ext */ @font-face { font-family: 'PT Sans Narrow'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCIwR2oefDofMY.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'PT Sans Narrow'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCGwR2oefDo.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz0dL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzQdL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzwdL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzMdL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz8dL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz4dL-vwnYh2eg.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzAdL-vwnYg.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @font-face { font-family: 'Saira Extra Condensed'; font-style: normal; font-weight: 600; src: url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh1AphmGy-oO3K.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @font-face { font-family: 'Saira Extra Condensed'; font-style: normal; font-weight: 600; src: url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh1QphmGy-oO3K.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { font-family: 'Saira Extra Condensed'; font-style: normal; font-weight: 600; src: url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh2wphmGy-oA.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }`) ) ).toEqual([ { name: 'normal 400', familyName: 'Marcellus SC', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/marcellussc/v13/ke8iOgUHP1dg-Rmi6RWjbLE_iNacOqu0hYYt.woff2) format('woff2')", unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF' }, { name: 'normal 400', familyName: 'Marcellus SC', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/marcellussc/v13/ke8iOgUHP1dg-Rmi6RWjbLE_htacOqu0hQ.woff2) format('woff2')", unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' }, { name: 'normal 400', familyName: 'PT Sans Narrow', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCLwR2oefDofMY.woff2) format('woff2')", unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' }, { name: 'normal 400', familyName: 'PT Sans Narrow', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCCwR2oefDofMY.woff2) format('woff2')", unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' }, { name: 'normal 400', familyName: 'PT Sans Narrow', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCIwR2oefDofMY.woff2) format('woff2')", unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF' }, { name: 'normal 400', familyName: 'PT Sans Narrow', weight: 400, style: 'normal', src: "url(https://fonts.gstatic.com/s/ptsansnarrow/v18/BngRUXNadjH0qYEzV7ab-oWlsbCGwR2oefDo.woff2) format('woff2')", unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz0dL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzQdL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzwdL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+1F00-1FFF' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzMdL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+0370-03FF' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz8dL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz4dL-vwnYh2eg.woff2) format('woff2')", unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF' }, { name: 'italic 100', familyName: 'Roboto', weight: 100, style: 'italic', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzAdL-vwnYg.woff2) format('woff2')", unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+1F00-1FFF' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+0370-03FF' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2) format('woff2')", unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF' }, { name: 'normal 100', familyName: 'Roboto', weight: 100, style: 'normal', src: "url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2) format('woff2')", unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' }, { name: 'normal 600', familyName: 'Saira Extra Condensed', weight: 600, style: 'normal', src: "url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh1AphmGy-oO3K.woff2) format('woff2')", unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' }, { name: 'normal 600', familyName: 'Saira Extra Condensed', weight: 600, style: 'normal', src: "url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh1QphmGy-oO3K.woff2) format('woff2')", unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF' }, { name: 'normal 600', familyName: 'Saira Extra Condensed', weight: 600, style: 'normal', src: "url(https://fonts.gstatic.com/s/sairaextracondensed/v13/-nFvOHYr-vcC7h8MklGBkrvmUG9rbpkisrTrN2zh2wphmGy-oA.woff2) format('woff2')", unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' } ]) }) }) /** * write a function that can fuzz the nesting of rule, media query, property and comment up to 5 levels of nesting. Use loop to specify number of produced test examples. Use expect(CSS.parse(CSS.stringify(tree))).toEqual(tree) to assert that serialization and parsing returns the same result as input. IMPORTANT: Ensure that fuzzed tree uses my types (ParsedAST). E.g. instead pf {type: 'comment', comment: ...} it should be ['comment', ...]. Make it more compact code with not too many functions. Add another parameter to specify max nesting level. Rule and Media can nest any other declarations inside. */ function fuzzNesting(numExamples: number, maxNestingLevel: number): void { for (let i = 0; i < numExamples; i++) { const tree = generateFuzzedTree(maxNestingLevel) expect(CSS.parse(CSS.concat(CSS.process(CSS.reify(tree))))).toEqual(tree) expect(() => CSS.stringify(tree)).not.toThrow() expect(() => CSS.parse(CSS.stringify(tree))[0]?.[0]).not.eql(0) } } function generateFuzzedTree(maxNestingLevel: number): CSS.AnyAST[] { const tree: CSS.AnyAST[] = [] for (let i = 0; i < maxNestingLevel; i++) { const type = getRandomType() switch (type) { case 'rule': tree.push(['rule', [generateRandomString()], ...generateFuzzedDeclarations(maxNestingLevel - 1)]) case 'media': tree.push(['media', generateFuzzyMediaQueryExpression(), ...generateFuzzedDeclarations(maxNestingLevel - 1)]) case 'property': tree.push(['property', generateRandomString(), generateRandomValue()]) case 'comment': tree.push(['comment', generateRandomString()]) } } return tree } function generateFuzzyMediaQueryExpression(): string { const clauses: string[] = [] const clauseTypes = ['max-width', 'min-width', 'orientation', 'screen', 'prefers-color-scheme'] const joinOperators = [' AND ', ','] for (let i = 0; i < clauseTypes.length; i++) { const randomIndex = Math.floor(Math.random() * clauseTypes.length) const clauseType = clauseTypes[randomIndex] switch (clauseType) { case 'min-width': case 'max-width': clauses.push(`(${clauseType}: ${generateRandomLength()})`) break case 'orientation': clauses.push(`(${clauseType}: ${generateRandomOrientation()})`) break case 'screen': clauses.push(`${clauseType}`) break case 'prefers-color-scheme': clauses.push(`(${clauseType}: ${generateRandomColorScheme()})`) break } } const randomJoinOperator = joinOperators[Math.floor(Math.random() * joinOperators.length)] return clauses.join(`${randomJoinOperator}`) } function generateRandomLength(): string { const formats = ['.1', '1', '1.1'] const randomIndex = Math.floor(Math.random() * formats.length) const format = formats[randomIndex] const randomNumber = Math.random() * 10 // Generate a random number between 0 and 10 const randomUnit = ['px', 'em', 'rem', 'vh', 'vw'][Math.floor(Math.random() * 5)] // Choose a random unit return format.replace('1', Math.floor(randomNumber).toString()) + randomUnit } function generateRandomOrientation(): string { const orientations = ['portrait', 'landscape'] return orientations[Math.floor(Math.random() * orientations.length)] } function generateRandomColorScheme(): string { const colorSchemes = ['light', 'dark'] return colorSchemes[Math.floor(Math.random() * colorSchemes.length)] } function generateRandomValue(): CSS.LeafAST | string { const valueTypes = ['length', 'string', 'keyword'] const randomIndex = Math.floor(Math.random() * valueTypes.length) const valueType = valueTypes[randomIndex] switch (valueType) { case 'length': return ['length', { unit: 'px', value: 1 }] case 'string': return ['string', generateRandomString()] case 'keyword': return 'x-' + generateRandomString() } } function generateFuzzedDeclarations(maxNestingLevel: number): CSS.AnyAST[] { const numDeclarations = Math.floor(Math.random() * 5) + 1 const declarations: CSS.AnyAST[] = [] for (let i = 0; i < numDeclarations; i++) { declarations.push(['property', generateRandomString(), generateRandomValue()]) } if (maxNestingLevel > 0) { declarations.push(...generateFuzzedRules(maxNestingLevel)) } return declarations } function generateFuzzedRules(maxNestingLevel: number): CSS.AnyAST[] { const numRules = Math.floor(Math.random() * 5) + 1 const rules: CSS.AnyAST[] = [] for (let i = 0; i < numRules; i++) { rules.push(['rule', [generateRandomString()], ...generateFuzzedDeclarations(maxNestingLevel - 1)]) } return rules } function getRandomType(): string { const types = ['rule', 'media', 'property', 'comment'] const randomIndex = Math.floor(Math.random() * types.length) return types[randomIndex] } function generateRandomString(): string { return Math.random().toString(36).substring(7) }