diff --git a/__tests__/edge-cases/broken-split-46.spec.ts b/__tests__/edge-cases/broken-split-46.spec.ts new file mode 100644 index 0000000..8e9185d --- /dev/null +++ b/__tests__/edge-cases/broken-split-46.spec.ts @@ -0,0 +1,44 @@ +import { loadStyleDefinitions } from '../../src'; +import { getCriticalStyles } from '../../src/getCSS'; + +describe('missing styles', () => { + it('result should contain full fill value', async () => { + const styles = loadStyleDefinitions( + () => ['test.css'], + () => ` + .-lottie-player svg path[fill="rgb(255,255,255)"] { + fill: var(--color-background) +} +` + ); + await styles; + + expect(styles.ast['test.css'].selectors).toMatchInlineSnapshot(` + Array [ + Object { + "declaration": 1, + "hash": ".-lottie-player svg path[fill=\\"rgb(255,255,255)\\"]1noj2ak-1etm6d20", + "media": Array [], + "parents": Array [ + "-lottie-player", + ], + "pieces": Array [ + "-lottie-player", + ], + "postfix": "svg path[fill=\\"rgb(255,255,255)\\"]", + "selector": ".-lottie-player svg path[fill=\\"rgb(255,255,255)\\"]", + }, + ] + `); + + const extracted = getCriticalStyles( + '
', + styles + ); + + expect(extracted).toMatchInlineSnapshot(` + "" + `); + }); +}); diff --git a/src/parser/toAst.ts b/src/parser/toAst.ts index e0eec82..7950186 100644 --- a/src/parser/toAst.ts +++ b/src/parser/toAst.ts @@ -3,6 +3,7 @@ import * as crc32 from 'crc-32'; import * as postcss from 'postcss'; import { AtRule, Rule } from 'postcss'; +import { splitSelector } from '../utils/split-selectors'; import { AtRules, SingleStyleAst, StyleBodies, StyleBody, StyleSelector } from './ast'; import { createRange, localRangeMax, localRangeMin, rangesIntervalEqual } from './ranges'; import { extractParents, mapSelector } from './utils'; @@ -91,7 +92,7 @@ export const buildAst = (CSS: string, file = ''): SingleStyleAst => { return; } - const ruleSelectors = rule.selector.split(','); + const ruleSelectors = splitSelector(rule.selector); ruleSelectors .map((sel) => sel.trim()) diff --git a/src/utils/__tests__/class-extraction.ts b/src/utils/__tests__/class-extraction.spec.ts similarity index 100% rename from src/utils/__tests__/class-extraction.ts rename to src/utils/__tests__/class-extraction.spec.ts diff --git a/src/utils/__tests__/split-selectors.spec.ts b/src/utils/__tests__/split-selectors.spec.ts new file mode 100644 index 0000000..a37d66d --- /dev/null +++ b/src/utils/__tests__/split-selectors.spec.ts @@ -0,0 +1,14 @@ +import { splitSelector } from '../split-selectors'; + +describe('split selectors', () => { + it('simple', () => { + expect(splitSelector('.a')).toEqual(['.a']); + expect(splitSelector('.a,.b')).toEqual(['.a', '.b']); + expect(splitSelector('.a:before,:after')).toEqual(['.a:before', ':after']); + }); + + it('complex', () => { + expect(splitSelector('a ~ span,b')).toEqual(['a ~ span', 'b']); + expect(splitSelector("a#item p[alt^='test'],.body.test")).toEqual(["a#item p[alt^='test']", '.body.test']); + }); +}); diff --git a/src/utils/split-selectors.ts b/src/utils/split-selectors.ts new file mode 100644 index 0000000..a665738 --- /dev/null +++ b/src/utils/split-selectors.ts @@ -0,0 +1,92 @@ +/** + * @fileOverview inspired by https://github.com/perry-mitchell/css-selector-splitter/tree/master + */ + +const BLOCKS: Record = { + '(': ')', + '[': ']', +}; + +const QUOTES: Record = { + '"': '"', + "'": "'", +}; + +const FINALIZERS: Record = { + ')': '(', + ']': '[', +}; + +const SPLIT_ON: Record = { + ',': ',', +}; + +export const splitSelector = (selector: string): string[] => { + const selectors: string[] = []; + + const stack: string[] = []; + const joiners: string[] = []; + let currentSelector = ''; + + for (let i = 0; i < selector.length; i += 1) { + const char = selector[i]; + + if (BLOCKS[char] || QUOTES[char]) { + if (stack.length === 0) { + stack.push(char); + } else { + const lastBrace = stack[stack.length - 1]; + + if (QUOTES[lastBrace]) { + // within quotes + if (char === lastBrace) { + // closing quote + stack.pop(); + } + } else { + // inside brackets or square brackets + stack.push(char); + } + } + + currentSelector += char; + } else if (FINALIZERS.hasOwnProperty(char)) { + const lastBrace = stack[stack.length - 1]; + const matchingOpener = FINALIZERS[char]; + + if (lastBrace === matchingOpener) { + stack.pop(); + } + + currentSelector += char; + } else if (SPLIT_ON[char]) { + if (!stack.length) { + // we're not inside another block, so we can split using the comma/splitter + const lastJoiner = joiners[joiners.length - 1]; + + if (lastJoiner === ' ' && currentSelector.length <= 0) { + // we just split by a space, but there seems to be another split character, so use + // this new one instead of the previous space + joiners[joiners.length - 1] = char; + } else if (currentSelector.length <= 0) { + // skip this character, as it's just padding + } else { + // split by this character + const newLength = selectors.push(currentSelector); + joiners[newLength - 1] = char; + currentSelector = ''; + } + } else { + // we're inside another block, so ignore the comma/splitter + currentSelector += char; + } + } else { + // just add this character + currentSelector += char; + } + } + + selectors.push(currentSelector); + + return selectors.map((selector) => selector.trim()).filter((cssSelector) => cssSelector.length > 0); +};