diff --git a/.gitattributes b/.gitattributes index 894d27d..d231fc0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,7 @@ /package-lock.json export-ignore /.gitignore export-ignore /.gitattributes export-ignore +/coverage/ export-ignore /rollup.config.mjs export-ignore /tsconfig.json export-ignore # exclude all files in test/ from stats @@ -11,6 +12,7 @@ /docs/** linguist-vendored /tools/** linguist-vendored /dist/** linguist-vendored +/coverage/** linguist-vendored # # do not replace lf by crlf *.css text diff --git a/.npmignore b/.npmignore index badbf02..c44a2fa 100644 --- a/.npmignore +++ b/.npmignore @@ -6,4 +6,6 @@ /src /.idea /package-lock.json -/node_modules \ No newline at end of file +/node_modules +/coverage +/.github \ No newline at end of file diff --git a/README.md b/README.md index 3f27da3..04d1994 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions) + # css-parser CSS parser for node and the browser diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 35b4c75..a1a74a6 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -2241,6 +2241,53 @@ const configuration = getConfig(); const combinators = ['+', '>', '~']; + const notEndingWith = ['(', '['].concat(combinators); + function wrapNodes(previous, node, match, ast, i, nodeIndex) { + // @ts-ignore + let pSel = match.selector1.reduce(reducer, []).join(','); + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); + // @ts-ignore + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; + // @ts-ignore + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } + } + else { + // @ts-ignore + wrapper.chi.push(previous, node); + } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; + } function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { @@ -2277,62 +2324,21 @@ if (node.typ == 'Rule') { reduceRuleSelector(node); let wrapper; + let match; // @ts-ignore if (options.nestingRules) { - // if (node.sel == '.card>hr') { - // - // console.error({idem: previous == node, previous, node}); - // } // @ts-ignore - if (previous != null) { + if (previous != null && previous.typ == 'Rule') { + reduceRuleSelector(previous); // @ts-ignore - if (node.optimized != null && - // @ts-ignore - previous.optimized != null && - // @ts-ignore - node.optimized.optimized.length > 0 && - // @ts-ignore - previous.optimized.optimized.length > 0 && + match = matchSelectors(previous.raw, node.raw, ast.typ); + // @ts-ignore + if (match != null) { // @ts-ignore - node.optimized.optimized[0] == previous.optimized.optimized[0]) { + wrapper = wrapNodes(previous, node, match, ast, i, nodeIndex); + nodeIndex = i - 1; // @ts-ignore - if (hasOnlyDeclarations(previous)) { - // @ts-ignore - let pSel = reduceRawTokens(previous.optimized.selector); - if (pSel === '') { - // @ts-ignore - pSel = previous.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - let nSel = reduceRawTokens(node.optimized.selector); - if (nSel === '') { - // @ts-ignore - nSel = node.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0].slice()]] - }); - // @ts-ignore - previous.sel = pSel === '' ? '&' : pSel; // reduceRawTokens(previous.optimized.selector); - // @ts-ignore - previous.raw = pSel === '' ? [['&']] : previous.optimized.selector.slice(); - // @ts-ignore - node.sel = nSel === '' ? '&' : nSel; // reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = nSel === '' ? [['&']] : node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(previous, node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - } + previous = ast.chi[nodeIndex]; } } // @ts-ignore @@ -2342,49 +2348,26 @@ // @ts-ignore const nextNode = ast.chi[i]; // @ts-ignore - if (nextNode.typ != 'Rule' || nextNode.raw == null) { + if (nextNode.typ != 'Rule') { + // i--; + // previous = wrapper; + // nodeIndex = i; break; } reduceRuleSelector(nextNode); // @ts-ignore - if (nextNode.raw.length != 1 || !eq(wrapper.raw[0], nextNode.raw[0].slice(0, wrapper.raw[0].length))) { - break; - } - // @ts-ignore - nextNode.raw[0].splice(0, wrapper.raw[0].length); - // @ts-ignore - trimRawToken(nextNode.raw[0]); + match = matchSelectors(wrapper.raw, nextNode.raw, ast.typ); // @ts-ignore - if (nextNode.raw[0].length == 0 || - // @ts-ignore - (nextNode.raw.length == 1 && nextNode.raw[0] == '&')) { - if (hasOnlyDeclarations(wrapper)) { - // @ts-ignore - wrapper.chi.push(...nextNode.chi); - // @ts-ignore - ast.chi.splice(i, 1); - continue; - } - else { - // @ts-ignore - nextNode.raw[0].push('&'); - } + if (match == null) { + break; } // @ts-ignore - nextNode.sel = nextNode.raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); - // @ts-ignore - wrapper.chi.push(nextNode); - // @ts-ignore - ast.chi.splice(i, 1); - node = wrapper; + wrapper = wrapNodes(wrapper, nextNode, match, ast, i, nodeIndex); } - deduplicate(wrapper, options, recursive); - nodeIndex = i; + nodeIndex = --i; // @ts-ignore - previous = ast.chi[i]; + previous = ast.chi[nodeIndex]; + deduplicate(wrapper, options, recursive); continue; } // @ts-ignore @@ -2403,7 +2386,7 @@ value: [[node.optimized.optimized[0]]] }); // @ts-ignore - node.sel = reduceRawTokens(node.optimized.selector); + node.sel = node.optimized.selector.reduce(reducer, []).join(','); // @ts-ignore node.raw = node.optimized.selector.slice(); // @ts-ignore @@ -2413,6 +2396,34 @@ node = wrapper; } } + // @ts-ignore + else if (node.optimized?.match) { + let wrap = true; + // @ts-ignore + const selector = node.optimized.selector.reduce((acc, curr) => { + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else { + if (ast.typ != 'Rule' && combinators.includes(curr[1])) { + wrap = false; + } + else { + curr.splice(0, 1); + } + } + } + else if (combinators.includes(curr[0])) { + curr.unshift('&'); + } + // @ts-ignore + acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + return acc; + }, []); + // @ts-ignore + node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + } } // @ts-ignore if (previous != null && 'chi' in previous && ('chi' in node)) { @@ -2441,6 +2452,7 @@ ast.chi.splice(nodeIndex, 1); // @ts-ignore if (hasDeclaration(node)) { + // @ts-ignore deduplicateRule(node); } else { @@ -2485,6 +2497,7 @@ if (recursive && previous != node) { // @ts-ignore if (hasDeclaration(previous)) { + // @ts-ignore deduplicateRule(previous); } else { @@ -2582,31 +2595,40 @@ ast.chi = children.concat(ast.chi?.slice(k)); return ast; } - function reduceRawTokens(raw) { - return raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); - } - function trimRawToken(raw) { - while (raw.length > 0) { - if (raw[0] == ' ') { - raw.shift(); - continue; - } - break; - } - } function splitRule(buffer) { - const result = []; + const result = [[]]; let str = ''; for (let i = 0; i < buffer.length; i++) { let chr = buffer.charAt(i); + if (isWhiteSpace(chr.charCodeAt(0))) { + let k = i; + while (k + 1 < buffer.length) { + if (isWhiteSpace(buffer[k + 1].charCodeAt(0))) { + k++; + continue; + } + break; + } + if (str !== '') { + // @ts-ignore + result.at(-1).push(str); + str = ''; + } + // @ts-ignore + if (result.at(-1).length > 0) { + // @ts-ignore + result.at(-1).push(' '); + } + i = k; + continue; + } if (chr == ',') { if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); str = ''; } + result.push([]); continue; } str += chr; @@ -2656,41 +2678,45 @@ } } if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); } return result; } function reduceRuleSelector(node) { + if (node.raw == null) { + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: splitRule(node.sel) }); + } // @ts-ignore - if (node.raw != null) { - // @ts-ignore - let optimized = reduceSelector(node.raw.reduce((acc, curr) => { - acc.push(curr.slice()); - return acc; - }, [])); - if (optimized != null) { - Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); - } - if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { - const raw = [ - [ - optimized.optimized[0], ':is(' - ].concat(optimized.selector.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, [])).concat(')') - ]; - const sel = raw[0].join(''); - if (sel.length < node.sel.length) { - node.sel = sel; - // node.raw = raw; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - } + // if (node.raw != null) { + // @ts-ignore + let optimized = reduceSelector(node.raw.reduce((acc, curr) => { + acc.push(curr.slice()); + return acc; + }, [])); + if (optimized != null) { + Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); + } + if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { + const raw = [ + [ + optimized.optimized[0], ':is(' + ].concat(optimized.selector.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, [])).concat(')') + ]; + const sel = raw[0].join(''); + if (sel.length < node.sel.length) { + node.sel = sel; + // node.raw = raw; + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); } } + // } } function diff(n1, n2, options = {}) { let node1 = n1; @@ -2711,25 +2737,25 @@ // @ts-ignore const raw1 = node1.raw; // @ts-ignore - const optimized1 = node1.optimized; + // const optimized1 = node1.optimized; // @ts-ignore const raw2 = node2.raw; // @ts-ignore - const optimized2 = node2.optimized; + // const optimized2 = node2.optimized; node1 = { ...node1, chi: node1.chi.slice() }; node2 = { ...node2, chi: node2.chi.slice() }; if (raw1 != null) { Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); } - if (optimized1 != null) { - Object.defineProperty(node1, 'optimized', { enumerable: false, writable: true, value: optimized1 }); - } + // if (optimized1 != null) { + // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); + // } if (raw2 != null) { Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); } - if (optimized2 != null) { - Object.defineProperty(node2, 'optimized', { enumerable: false, writable: true, value: optimized2 }); - } + // if (optimized2 != null) { + // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); + // } const intersect = []; while (i--) { if (node1.chi[i].typ == 'Comment') { @@ -2757,7 +2783,7 @@ const result = (intersect.length == 0 ? null : { ...node1, // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(), + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), chi: intersect.reverse() }); if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { @@ -2766,6 +2792,167 @@ } return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; } + function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; + } function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -2789,49 +2976,75 @@ } optimized.push(item); } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } selector.forEach((selector) => selector.splice(0, optimized.length)); // combinator if (combinators.includes(optimized.at(-1))) { const combinator = optimized.pop(); selector.forEach(selector => selector.unshift(combinator)); } - if (optimized.at(-1) == ' ') { - optimized.pop(); - } let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } if (optimized.length == 0 || - optimized[0].charAt(0) == '&' || - selector.length == 1) { + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { return { match: false, optimized, - selector, - reducible: selector.length > 1 && selector.every((selector) => !['>', '+', '~'].includes(selector[0])) + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) }; } return { match: true, optimized, selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } // @ts-ignore - if (curr.length > 0 && curr[0] == ' ') { - curr.shift(); + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); } if (curr.length == 0) { curr.push('&'); + hasCompound = false; } if (reducible) { const chr = curr[0].charAt(0); // @ts-ignore reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); } - acc.push(curr); + acc.push(hasCompound ? ['&'].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } - function reducer(acc, curr) { + function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } acc.push(curr.join('')); return acc; } @@ -3573,13 +3786,13 @@ buffer = value; break; case '>': - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } if (buffer !== '') { pushToken(getType(buffer)); buffer = ''; } + if (tokens[tokens.length - 1]?.typ == 'Whitespace') { + tokens.pop(); + } pushToken({ typ: 'Gt' }); consumeWhiteSpace(); break; @@ -4018,7 +4231,7 @@ yield { node, parent, root }; if ('chi' in node) { for (const child of node.chi) { - yield* doWalk(child, node, (root == null ? node : root)); + yield* doWalk(child, node, (root ?? node)); } } } diff --git a/dist/index.cjs b/dist/index.cjs index afe6ade..034515e 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -2239,6 +2239,53 @@ class PropertyList { const configuration = getConfig(); const combinators = ['+', '>', '~']; +const notEndingWith = ['(', '['].concat(combinators); +function wrapNodes(previous, node, match, ast, i, nodeIndex) { + // @ts-ignore + let pSel = match.selector1.reduce(reducer, []).join(','); + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); + // @ts-ignore + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; + // @ts-ignore + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } + } + else { + // @ts-ignore + wrapper.chi.push(previous, node); + } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; +} function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { @@ -2275,62 +2322,21 @@ function deduplicate(ast, options = {}, recursive = false) { if (node.typ == 'Rule') { reduceRuleSelector(node); let wrapper; + let match; // @ts-ignore if (options.nestingRules) { - // if (node.sel == '.card>hr') { - // - // console.error({idem: previous == node, previous, node}); - // } // @ts-ignore - if (previous != null) { + if (previous != null && previous.typ == 'Rule') { + reduceRuleSelector(previous); // @ts-ignore - if (node.optimized != null && - // @ts-ignore - previous.optimized != null && - // @ts-ignore - node.optimized.optimized.length > 0 && - // @ts-ignore - previous.optimized.optimized.length > 0 && + match = matchSelectors(previous.raw, node.raw, ast.typ); + // @ts-ignore + if (match != null) { // @ts-ignore - node.optimized.optimized[0] == previous.optimized.optimized[0]) { + wrapper = wrapNodes(previous, node, match, ast, i, nodeIndex); + nodeIndex = i - 1; // @ts-ignore - if (hasOnlyDeclarations(previous)) { - // @ts-ignore - let pSel = reduceRawTokens(previous.optimized.selector); - if (pSel === '') { - // @ts-ignore - pSel = previous.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - let nSel = reduceRawTokens(node.optimized.selector); - if (nSel === '') { - // @ts-ignore - nSel = node.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0].slice()]] - }); - // @ts-ignore - previous.sel = pSel === '' ? '&' : pSel; // reduceRawTokens(previous.optimized.selector); - // @ts-ignore - previous.raw = pSel === '' ? [['&']] : previous.optimized.selector.slice(); - // @ts-ignore - node.sel = nSel === '' ? '&' : nSel; // reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = nSel === '' ? [['&']] : node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(previous, node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - } + previous = ast.chi[nodeIndex]; } } // @ts-ignore @@ -2340,49 +2346,26 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore const nextNode = ast.chi[i]; // @ts-ignore - if (nextNode.typ != 'Rule' || nextNode.raw == null) { + if (nextNode.typ != 'Rule') { + // i--; + // previous = wrapper; + // nodeIndex = i; break; } reduceRuleSelector(nextNode); // @ts-ignore - if (nextNode.raw.length != 1 || !eq(wrapper.raw[0], nextNode.raw[0].slice(0, wrapper.raw[0].length))) { - break; - } - // @ts-ignore - nextNode.raw[0].splice(0, wrapper.raw[0].length); - // @ts-ignore - trimRawToken(nextNode.raw[0]); + match = matchSelectors(wrapper.raw, nextNode.raw, ast.typ); // @ts-ignore - if (nextNode.raw[0].length == 0 || - // @ts-ignore - (nextNode.raw.length == 1 && nextNode.raw[0] == '&')) { - if (hasOnlyDeclarations(wrapper)) { - // @ts-ignore - wrapper.chi.push(...nextNode.chi); - // @ts-ignore - ast.chi.splice(i, 1); - continue; - } - else { - // @ts-ignore - nextNode.raw[0].push('&'); - } + if (match == null) { + break; } // @ts-ignore - nextNode.sel = nextNode.raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); - // @ts-ignore - wrapper.chi.push(nextNode); - // @ts-ignore - ast.chi.splice(i, 1); - node = wrapper; + wrapper = wrapNodes(wrapper, nextNode, match, ast, i, nodeIndex); } - deduplicate(wrapper, options, recursive); - nodeIndex = i; + nodeIndex = --i; // @ts-ignore - previous = ast.chi[i]; + previous = ast.chi[nodeIndex]; + deduplicate(wrapper, options, recursive); continue; } // @ts-ignore @@ -2401,7 +2384,7 @@ function deduplicate(ast, options = {}, recursive = false) { value: [[node.optimized.optimized[0]]] }); // @ts-ignore - node.sel = reduceRawTokens(node.optimized.selector); + node.sel = node.optimized.selector.reduce(reducer, []).join(','); // @ts-ignore node.raw = node.optimized.selector.slice(); // @ts-ignore @@ -2411,6 +2394,34 @@ function deduplicate(ast, options = {}, recursive = false) { node = wrapper; } } + // @ts-ignore + else if (node.optimized?.match) { + let wrap = true; + // @ts-ignore + const selector = node.optimized.selector.reduce((acc, curr) => { + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else { + if (ast.typ != 'Rule' && combinators.includes(curr[1])) { + wrap = false; + } + else { + curr.splice(0, 1); + } + } + } + else if (combinators.includes(curr[0])) { + curr.unshift('&'); + } + // @ts-ignore + acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + return acc; + }, []); + // @ts-ignore + node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + } } // @ts-ignore if (previous != null && 'chi' in previous && ('chi' in node)) { @@ -2439,6 +2450,7 @@ function deduplicate(ast, options = {}, recursive = false) { ast.chi.splice(nodeIndex, 1); // @ts-ignore if (hasDeclaration(node)) { + // @ts-ignore deduplicateRule(node); } else { @@ -2483,6 +2495,7 @@ function deduplicate(ast, options = {}, recursive = false) { if (recursive && previous != node) { // @ts-ignore if (hasDeclaration(previous)) { + // @ts-ignore deduplicateRule(previous); } else { @@ -2580,31 +2593,40 @@ function deduplicateRule(ast) { ast.chi = children.concat(ast.chi?.slice(k)); return ast; } -function reduceRawTokens(raw) { - return raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); -} -function trimRawToken(raw) { - while (raw.length > 0) { - if (raw[0] == ' ') { - raw.shift(); - continue; - } - break; - } -} function splitRule(buffer) { - const result = []; + const result = [[]]; let str = ''; for (let i = 0; i < buffer.length; i++) { let chr = buffer.charAt(i); + if (isWhiteSpace(chr.charCodeAt(0))) { + let k = i; + while (k + 1 < buffer.length) { + if (isWhiteSpace(buffer[k + 1].charCodeAt(0))) { + k++; + continue; + } + break; + } + if (str !== '') { + // @ts-ignore + result.at(-1).push(str); + str = ''; + } + // @ts-ignore + if (result.at(-1).length > 0) { + // @ts-ignore + result.at(-1).push(' '); + } + i = k; + continue; + } if (chr == ',') { if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); str = ''; } + result.push([]); continue; } str += chr; @@ -2654,41 +2676,45 @@ function splitRule(buffer) { } } if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); } return result; } function reduceRuleSelector(node) { + if (node.raw == null) { + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: splitRule(node.sel) }); + } // @ts-ignore - if (node.raw != null) { - // @ts-ignore - let optimized = reduceSelector(node.raw.reduce((acc, curr) => { - acc.push(curr.slice()); - return acc; - }, [])); - if (optimized != null) { - Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); - } - if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { - const raw = [ - [ - optimized.optimized[0], ':is(' - ].concat(optimized.selector.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, [])).concat(')') - ]; - const sel = raw[0].join(''); - if (sel.length < node.sel.length) { - node.sel = sel; - // node.raw = raw; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - } + // if (node.raw != null) { + // @ts-ignore + let optimized = reduceSelector(node.raw.reduce((acc, curr) => { + acc.push(curr.slice()); + return acc; + }, [])); + if (optimized != null) { + Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); + } + if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { + const raw = [ + [ + optimized.optimized[0], ':is(' + ].concat(optimized.selector.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, [])).concat(')') + ]; + const sel = raw[0].join(''); + if (sel.length < node.sel.length) { + node.sel = sel; + // node.raw = raw; + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); } } + // } } function diff(n1, n2, options = {}) { let node1 = n1; @@ -2709,25 +2735,25 @@ function diff(n1, n2, options = {}) { // @ts-ignore const raw1 = node1.raw; // @ts-ignore - const optimized1 = node1.optimized; + // const optimized1 = node1.optimized; // @ts-ignore const raw2 = node2.raw; // @ts-ignore - const optimized2 = node2.optimized; + // const optimized2 = node2.optimized; node1 = { ...node1, chi: node1.chi.slice() }; node2 = { ...node2, chi: node2.chi.slice() }; if (raw1 != null) { Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); } - if (optimized1 != null) { - Object.defineProperty(node1, 'optimized', { enumerable: false, writable: true, value: optimized1 }); - } + // if (optimized1 != null) { + // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); + // } if (raw2 != null) { Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); } - if (optimized2 != null) { - Object.defineProperty(node2, 'optimized', { enumerable: false, writable: true, value: optimized2 }); - } + // if (optimized2 != null) { + // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); + // } const intersect = []; while (i--) { if (node1.chi[i].typ == 'Comment') { @@ -2755,7 +2781,7 @@ function diff(n1, n2, options = {}) { const result = (intersect.length == 0 ? null : { ...node1, // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(), + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), chi: intersect.reverse() }); if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { @@ -2764,6 +2790,167 @@ function diff(n1, n2, options = {}) { } return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; } +function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; +} function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -2787,49 +2974,75 @@ function reduceSelector(selector) { } optimized.push(item); } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } selector.forEach((selector) => selector.splice(0, optimized.length)); // combinator if (combinators.includes(optimized.at(-1))) { const combinator = optimized.pop(); selector.forEach(selector => selector.unshift(combinator)); } - if (optimized.at(-1) == ' ') { - optimized.pop(); - } let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } if (optimized.length == 0 || - optimized[0].charAt(0) == '&' || - selector.length == 1) { + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { return { match: false, optimized, - selector, - reducible: selector.length > 1 && selector.every((selector) => !['>', '+', '~'].includes(selector[0])) + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) }; } return { match: true, optimized, selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } // @ts-ignore - if (curr.length > 0 && curr[0] == ' ') { - curr.shift(); + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); } if (curr.length == 0) { curr.push('&'); + hasCompound = false; } if (reducible) { const chr = curr[0].charAt(0); // @ts-ignore reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); } - acc.push(curr); + acc.push(hasCompound ? ['&'].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } -function reducer(acc, curr) { +function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } acc.push(curr.join('')); return acc; } @@ -3571,13 +3784,13 @@ async function parse$1(iterator, opt = {}) { buffer = value; break; case '>': - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } if (buffer !== '') { pushToken(getType(buffer)); buffer = ''; } + if (tokens[tokens.length - 1]?.typ == 'Whitespace') { + tokens.pop(); + } pushToken({ typ: 'Gt' }); consumeWhiteSpace(); break; @@ -4016,7 +4229,7 @@ function* doWalk(node, parent, root) { yield { node, parent, root }; if ('chi' in node) { for (const child of node.chi) { - yield* doWalk(child, node, (root == null ? node : root)); + yield* doWalk(child, node, (root ?? node)); } } } diff --git a/dist/lib/parser/deduplicate.js b/dist/lib/parser/deduplicate.js index 0449579..6e3ad93 100644 --- a/dist/lib/parser/deduplicate.js +++ b/dist/lib/parser/deduplicate.js @@ -1,4 +1,4 @@ -import { isIdentStart } from './utils/syntax.js'; +import { isIdentStart, isWhiteSpace } from './utils/syntax.js'; import { getConfig } from './utils/config.js'; import { PropertyList } from './declaration/list.js'; import { eq } from './utils/eq.js'; @@ -6,6 +6,53 @@ import { render } from '../renderer/render.js'; const configuration = getConfig(); const combinators = ['+', '>', '~']; +const notEndingWith = ['(', '['].concat(combinators); +function wrapNodes(previous, node, match, ast, i, nodeIndex) { + // @ts-ignore + let pSel = match.selector1.reduce(reducer, []).join(','); + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); + // @ts-ignore + const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; + // @ts-ignore + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); + if (pSel == '&' || pSel === '') { + // @ts-ignore + wrapper.chi.push(...previous.chi); + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + // @ts-ignore + wrapper.chi.push(...node.chi); + } + else { + // @ts-ignore + wrapper.chi.push(node); + } + } + else { + // @ts-ignore + wrapper.chi.push(previous, node); + } + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + reduceRuleSelector(wrapper); + return wrapper; +} function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { @@ -42,62 +89,21 @@ function deduplicate(ast, options = {}, recursive = false) { if (node.typ == 'Rule') { reduceRuleSelector(node); let wrapper; + let match; // @ts-ignore if (options.nestingRules) { - // if (node.sel == '.card>hr') { - // - // console.error({idem: previous == node, previous, node}); - // } // @ts-ignore - if (previous != null) { + if (previous != null && previous.typ == 'Rule') { + reduceRuleSelector(previous); // @ts-ignore - if (node.optimized != null && - // @ts-ignore - previous.optimized != null && - // @ts-ignore - node.optimized.optimized.length > 0 && - // @ts-ignore - previous.optimized.optimized.length > 0 && + match = matchSelectors(previous.raw, node.raw, ast.typ); + // @ts-ignore + if (match != null) { // @ts-ignore - node.optimized.optimized[0] == previous.optimized.optimized[0]) { + wrapper = wrapNodes(previous, node, match, ast, i, nodeIndex); + nodeIndex = i - 1; // @ts-ignore - if (hasOnlyDeclarations(previous)) { - // @ts-ignore - let pSel = reduceRawTokens(previous.optimized.selector); - if (pSel === '') { - // @ts-ignore - pSel = previous.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - let nSel = reduceRawTokens(node.optimized.selector); - if (nSel === '') { - // @ts-ignore - nSel = node.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0].slice()]] - }); - // @ts-ignore - previous.sel = pSel === '' ? '&' : pSel; // reduceRawTokens(previous.optimized.selector); - // @ts-ignore - previous.raw = pSel === '' ? [['&']] : previous.optimized.selector.slice(); - // @ts-ignore - node.sel = nSel === '' ? '&' : nSel; // reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = nSel === '' ? [['&']] : node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(previous, node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - } + previous = ast.chi[nodeIndex]; } } // @ts-ignore @@ -107,49 +113,26 @@ function deduplicate(ast, options = {}, recursive = false) { // @ts-ignore const nextNode = ast.chi[i]; // @ts-ignore - if (nextNode.typ != 'Rule' || nextNode.raw == null) { + if (nextNode.typ != 'Rule') { + // i--; + // previous = wrapper; + // nodeIndex = i; break; } reduceRuleSelector(nextNode); // @ts-ignore - if (nextNode.raw.length != 1 || !eq(wrapper.raw[0], nextNode.raw[0].slice(0, wrapper.raw[0].length))) { - break; - } - // @ts-ignore - nextNode.raw[0].splice(0, wrapper.raw[0].length); + match = matchSelectors(wrapper.raw, nextNode.raw, ast.typ); // @ts-ignore - trimRawToken(nextNode.raw[0]); - // @ts-ignore - if (nextNode.raw[0].length == 0 || - // @ts-ignore - (nextNode.raw.length == 1 && nextNode.raw[0] == '&')) { - if (hasOnlyDeclarations(wrapper)) { - // @ts-ignore - wrapper.chi.push(...nextNode.chi); - // @ts-ignore - ast.chi.splice(i, 1); - continue; - } - else { - // @ts-ignore - nextNode.raw[0].push('&'); - } + if (match == null) { + break; } // @ts-ignore - nextNode.sel = nextNode.raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); - // @ts-ignore - wrapper.chi.push(nextNode); - // @ts-ignore - ast.chi.splice(i, 1); - node = wrapper; + wrapper = wrapNodes(wrapper, nextNode, match, ast, i, nodeIndex); } - deduplicate(wrapper, options, recursive); - nodeIndex = i; + nodeIndex = --i; // @ts-ignore - previous = ast.chi[i]; + previous = ast.chi[nodeIndex]; + deduplicate(wrapper, options, recursive); continue; } // @ts-ignore @@ -168,7 +151,7 @@ function deduplicate(ast, options = {}, recursive = false) { value: [[node.optimized.optimized[0]]] }); // @ts-ignore - node.sel = reduceRawTokens(node.optimized.selector); + node.sel = node.optimized.selector.reduce(reducer, []).join(','); // @ts-ignore node.raw = node.optimized.selector.slice(); // @ts-ignore @@ -178,6 +161,34 @@ function deduplicate(ast, options = {}, recursive = false) { node = wrapper; } } + // @ts-ignore + else if (node.optimized?.match) { + let wrap = true; + // @ts-ignore + const selector = node.optimized.selector.reduce((acc, curr) => { + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else { + if (ast.typ != 'Rule' && combinators.includes(curr[1])) { + wrap = false; + } + else { + curr.splice(0, 1); + } + } + } + else if (combinators.includes(curr[0])) { + curr.unshift('&'); + } + // @ts-ignore + acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + return acc; + }, []); + // @ts-ignore + node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + } } // @ts-ignore if (previous != null && 'chi' in previous && ('chi' in node)) { @@ -206,6 +217,7 @@ function deduplicate(ast, options = {}, recursive = false) { ast.chi.splice(nodeIndex, 1); // @ts-ignore if (hasDeclaration(node)) { + // @ts-ignore deduplicateRule(node); } else { @@ -250,6 +262,7 @@ function deduplicate(ast, options = {}, recursive = false) { if (recursive && previous != node) { // @ts-ignore if (hasDeclaration(previous)) { + // @ts-ignore deduplicateRule(previous); } else { @@ -347,31 +360,40 @@ function deduplicateRule(ast) { ast.chi = children.concat(ast.chi?.slice(k)); return ast; } -function reduceRawTokens(raw) { - return raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); -} -function trimRawToken(raw) { - while (raw.length > 0) { - if (raw[0] == ' ') { - raw.shift(); - continue; - } - break; - } -} function splitRule(buffer) { - const result = []; + const result = [[]]; let str = ''; for (let i = 0; i < buffer.length; i++) { let chr = buffer.charAt(i); + if (isWhiteSpace(chr.charCodeAt(0))) { + let k = i; + while (k + 1 < buffer.length) { + if (isWhiteSpace(buffer[k + 1].charCodeAt(0))) { + k++; + continue; + } + break; + } + if (str !== '') { + // @ts-ignore + result.at(-1).push(str); + str = ''; + } + // @ts-ignore + if (result.at(-1).length > 0) { + // @ts-ignore + result.at(-1).push(' '); + } + i = k; + continue; + } if (chr == ',') { if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); str = ''; } + result.push([]); continue; } str += chr; @@ -421,41 +443,45 @@ function splitRule(buffer) { } } if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); } return result; } function reduceRuleSelector(node) { + if (node.raw == null) { + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: splitRule(node.sel) }); + } // @ts-ignore - if (node.raw != null) { - // @ts-ignore - let optimized = reduceSelector(node.raw.reduce((acc, curr) => { - acc.push(curr.slice()); - return acc; - }, [])); - if (optimized != null) { - Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); - } - if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { - const raw = [ - [ - optimized.optimized[0], ':is(' - ].concat(optimized.selector.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, [])).concat(')') - ]; - const sel = raw[0].join(''); - if (sel.length < node.sel.length) { - node.sel = sel; - // node.raw = raw; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - } + // if (node.raw != null) { + // @ts-ignore + let optimized = reduceSelector(node.raw.reduce((acc, curr) => { + acc.push(curr.slice()); + return acc; + }, [])); + if (optimized != null) { + Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); + } + if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { + const raw = [ + [ + optimized.optimized[0], ':is(' + ].concat(optimized.selector.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, [])).concat(')') + ]; + const sel = raw[0].join(''); + if (sel.length < node.sel.length) { + node.sel = sel; + // node.raw = raw; + Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); } } + // } } function diff(n1, n2, options = {}) { let node1 = n1; @@ -476,25 +502,25 @@ function diff(n1, n2, options = {}) { // @ts-ignore const raw1 = node1.raw; // @ts-ignore - const optimized1 = node1.optimized; + // const optimized1 = node1.optimized; // @ts-ignore const raw2 = node2.raw; // @ts-ignore - const optimized2 = node2.optimized; + // const optimized2 = node2.optimized; node1 = { ...node1, chi: node1.chi.slice() }; node2 = { ...node2, chi: node2.chi.slice() }; if (raw1 != null) { Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); } - if (optimized1 != null) { - Object.defineProperty(node1, 'optimized', { enumerable: false, writable: true, value: optimized1 }); - } + // if (optimized1 != null) { + // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); + // } if (raw2 != null) { Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); } - if (optimized2 != null) { - Object.defineProperty(node2, 'optimized', { enumerable: false, writable: true, value: optimized2 }); - } + // if (optimized2 != null) { + // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); + // } const intersect = []; while (i--) { if (node1.chi[i].typ == 'Comment') { @@ -522,7 +548,7 @@ function diff(n1, n2, options = {}) { const result = (intersect.length == 0 ? null : { ...node1, // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(), + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), chi: intersect.reverse() }); if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { @@ -531,6 +557,167 @@ function diff(n1, n2, options = {}) { } return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; } +function matchSelectors(selector1, selector2, parentType) { + let match = [[]]; + const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); + let i = 0; + let k; + let l; + let token; + let matching = true; + let matchFunction = 0; + let inAttr = 0; + for (; i < j; i++) { + k = 0; + token = selector1[0][i]; + for (; k < selector1.length; k++) { + if (selector1[k][i] != token) { + matching = false; + break; + } + } + if (matching) { + l = 0; + for (; l < selector2.length; l++) { + if (selector2[l][i] != token) { + matching = false; + break; + } + } + } + if (!matching) { + break; + } + if (token == ',') { + match.push([]); + } + else { + if (token.endsWith('(')) { + matchFunction++; + } + if (token.endsWith('[')) { + inAttr++; + } + else if (token == ')') { + matchFunction--; + } + else if (token == ']') { + inAttr--; + } + match.at(-1).push(token); + } + } + // invalid function + if (matchFunction != 0 || inAttr != 0) { + return null; + } + if (parentType != 'Rule') { + for (const part of match) { + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + return null; + } + } + } + if (match.length > 1) { + console.error(`unsupported multilevel matching`); + console.error({ match, selector1, selector2 }); + return null; + } + for (const part of match) { + while (part.length > 0) { + const token = part.at(-1); + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + part.pop(); + continue; + } + break; + } + } + if (match.every(t => t.length == 0)) { + return null; + } + if (eq([['&']], match)) { + return null; + } + function reduce(acc, curr) { + if (acc === null) { + return null; + } + let hasCompoundSelector = true; + curr = curr.slice(match[0].length); + while (curr.length > 0) { + if (curr[0] == ' ') { + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + break; + } + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + return null; + } + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + return null; + } + if (hasCompoundSelector && curr.length > 0) { + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + if (curr[0] == ':is(') { + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index) => { + if (index == 0) { + inFunction++; + canReduce = curr[1] == '&'; + } + else if (token.endsWith('(')) { + if (inFunction == 0) { + canReduce = false; + } + inFunction++; + } + else if (token == ')') { + inFunction--; + } + else if (token == ',') { + if (!canReduce) { + canReduce = curr[index + 1] == '&'; + } + acc.push([]); + } + else + acc.at(-1)?.push(token); + return acc; + }, [[]]); + if (inFunction > 0) { + canReduce = false; + } + if (canReduce) { + curr = isCompound.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, []); + } + } + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); + return acc; + } + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + }; +} function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -554,49 +741,75 @@ function reduceSelector(selector) { } optimized.push(item); } + while (optimized.length > 0) { + const last = optimized.at(-1); + if ((last == ' ' || combinators.includes(last))) { + optimized.pop(); + continue; + } + break; + } selector.forEach((selector) => selector.splice(0, optimized.length)); // combinator if (combinators.includes(optimized.at(-1))) { const combinator = optimized.pop(); selector.forEach(selector => selector.unshift(combinator)); } - if (optimized.at(-1) == ' ') { - optimized.pop(); - } let reducible = optimized.length == 1; + if (optimized[0] == '&' && optimized[1] == ' ') { + optimized.splice(0, 2); + } if (optimized.length == 0 || - optimized[0].charAt(0) == '&' || - selector.length == 1) { + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { return { match: false, optimized, - selector, - reducible: selector.length > 1 && selector.every((selector) => !['>', '+', '~'].includes(selector[0])) + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) }; } return { match: true, optimized, selector: selector.reduce((acc, curr) => { + let hasCompound = true; + if (hasCompound && curr.length > 0) { + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } // @ts-ignore - if (curr.length > 0 && curr[0] == ' ') { - curr.shift(); + if (hasCompound && curr[0] == ' ') { + hasCompound = false; + curr.unshift('&'); } if (curr.length == 0) { curr.push('&'); + hasCompound = false; } if (reducible) { const chr = curr[0].charAt(0); // @ts-ignore reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); } - acc.push(curr); + acc.push(hasCompound ? ['&'].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } -function reducer(acc, curr) { +function reducer(acc, curr, index, array) { + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + curr = curr.slice(1, -1); + } + if (curr[0] == '&') { + if (curr[1] == ' ') { + curr.splice(0, 2); + } + else if (combinators.includes(curr[1])) { + curr.splice(0, 1); + } + } acc.push(curr.join('')); return acc; } diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index e5c1777..3a4f960 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -740,13 +740,13 @@ async function parse(iterator, opt = {}) { buffer = value; break; case '>': - if (tokens[tokens.length - 1]?.typ == 'Whitespace') { - tokens.pop(); - } if (buffer !== '') { pushToken(getType(buffer)); buffer = ''; } + if (tokens[tokens.length - 1]?.typ == 'Whitespace') { + tokens.pop(); + } pushToken({ typ: 'Gt' }); consumeWhiteSpace(); break; diff --git a/dist/lib/walker/walk.js b/dist/lib/walker/walk.js index 847f5a1..eb8c216 100644 --- a/dist/lib/walker/walk.js +++ b/dist/lib/walker/walk.js @@ -6,7 +6,7 @@ function* doWalk(node, parent, root) { yield { node, parent, root }; if ('chi' in node) { for (const child of node.chi) { - yield* doWalk(child, node, (root == null ? node : root)); + yield* doWalk(child, node, (root ?? node)); } } } diff --git a/package.json b/package.json index b27e4d7..fea45d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "0.0.1-alpha4", + "version": "0.0.1-alpha5", "exports": { ".": "./dist/index.js", "./web": "./dist/web/index.js", @@ -45,7 +45,6 @@ "@web/test-runner": "^0.16.1", "@webref/css": "^6.5.9", "c8": "^8.0.1", - "glob": "^10.3.0", "mocha": "^10.2.0", "rollup": "^3.20.1", "rollup-plugin-dts": "^5.3.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index fc857c2..6354e57 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,27 +1,14 @@ import dts from 'rollup-plugin-dts'; import typescript from "@rollup/plugin-typescript"; import nodeResolve from "@rollup/plugin-node-resolve"; -import {glob} from "glob"; import terser from "@rollup/plugin-terser"; import json from "@rollup/plugin-json"; import commonjs from "@rollup/plugin-commonjs"; -export default [...await glob(['./test/specs/**/*.spec.ts', './test/specs/**/*.web-spec.ts']).then(files => files.map(input => { - return { - input, - plugins: [nodeResolve(), commonjs({transformMixedEsModules:true}), json(), typescript()], - output: - { - banner: `/* generate from ${input} */`, - file: `${input.replace(/\.spec\.ts$/, '.test.js').replace(/\.web-spec\.ts$/, '.web.js').replace('/specs/', '/spec-runners/')}`, - format: 'es' - } - } -})) -].concat([ +export default [ { input: 'src/index.ts', - plugins: [nodeResolve(), commonjs({transformMixedEsModules:true}), json(), typescript()], + plugins: [nodeResolve(), commonjs({transformMixedEsModules: true}), json(), typescript()], output: [ { // file: './dist/index.mjs', @@ -38,7 +25,7 @@ export default [...await glob(['./test/specs/**/*.spec.ts', './test/specs/**/*.w }, { input: 'src/web/index.ts', - plugins: [nodeResolve(), commonjs({transformMixedEsModules:true}), json(), typescript()], + plugins: [nodeResolve(), commonjs({transformMixedEsModules: true}), json(), typescript()], output: [ { // file: './dist/index.mjs', @@ -62,4 +49,4 @@ export default [...await glob(['./test/specs/**/*.spec.ts', './test/specs/**/*.w format: 'es' } } -]) \ No newline at end of file +]; \ No newline at end of file diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index dbd3c10..0a0c0ea 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -79,6 +79,14 @@ export interface ParseTokenOptions extends ParserOptions { parseColor?: boolean; } + +export interface MatchedSelector { + match: string[][]; + selector1: string[][]; + selector2: string[][]; + eq: boolean +} + export interface Position { ind: number; diff --git a/src/lib/parser/deduplicate.ts b/src/lib/parser/deduplicate.ts index ad5edc3..12aca7c 100644 --- a/src/lib/parser/deduplicate.ts +++ b/src/lib/parser/deduplicate.ts @@ -1,5 +1,14 @@ -import {getConfig, isIdentStart} from "./utils"; -import {AstAtRule, AstDeclaration, AstNode, AstRule, OptimizedSelector, ParserOptions} from "../../@types"; +import {getConfig, isIdentStart, isWhiteSpace} from "./utils"; +import { + AstAtRule, + AstDeclaration, + AstNode, + AstRule, AstRuleStyleSheet, + MatchedSelector, + NodeType, + OptimizedSelector, + ParserOptions +} from "../../@types"; import {PropertyList} from "./declaration"; import {eq} from "./utils/eq"; import {render} from "../renderer"; @@ -7,14 +16,73 @@ import {render} from "../renderer"; const configuration = getConfig(); const combinators = ['+', '>', '~']; +const notEndingWith = ['(', '['].concat(combinators); + +function wrapNodes(previous: AstRule, node: AstRule, match: MatchedSelector, ast: AstNode, i: number, nodeIndex: number): AstRule { + + // @ts-ignore + let pSel = match.selector1.reduce(reducer, []).join(','); + + // @ts-ignore + let nSel = match.selector2.reduce(reducer, []).join(','); + +// @ts-ignore + const wrapper = {...previous, chi: [], sel: match.match.reduce(reducer, []).join(',')}; + + // @ts-ignore + Object.defineProperty(wrapper, 'raw', { + enumerable: false, + writable: true, + // @ts-ignore + value: match.match.map(t => t.slice()) + }); + + if (pSel == '&' || pSel === '') { + + // @ts-ignore + wrapper.chi.push(...previous.chi); + + // @ts-ignore + if ((nSel == '&' || nSel === '') && hasOnlyDeclarations(previous)) { + + // @ts-ignore + wrapper.chi.push(...node.chi); + } else { + + // @ts-ignore + wrapper.chi.push(node); + } + } else { + + // @ts-ignore + wrapper.chi.push(previous, node); + } + + // @ts-ignore + ast.chi.splice(i, 1, wrapper); + // @ts-ignore + ast.chi.splice(nodeIndex, 1); + // @ts-ignore + previous.sel = pSel; + // @ts-ignore + previous.raw = match.selector1; + // @ts-ignore + node.sel = nSel; + // @ts-ignore + node.raw = match.selector2; + + + reduceRuleSelector(wrapper); + return wrapper; +} export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive: boolean = false): AstNode { // @ts-ignore if (('chi' in ast) && ast.chi?.length > 0) { - let i = 0; - let previous; - let node; - let nodeIndex; + let i: number = 0; + let previous: AstNode; + let node: AstNode; + let nodeIndex: number; // @ts-ignore for (; i < ast.chi.length; i++) { // @ts-ignore @@ -23,6 +91,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive } // @ts-ignore node = ast.chi[i]; + // @ts-ignore if (previous == node) { // console.error('idem!'); @@ -31,6 +100,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive i--; continue; } + if (node.typ == 'AtRule' && (node).nam == 'font-face') { continue; } @@ -42,117 +112,69 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive } // @ts-ignore if (node.typ == 'Rule') { + reduceRuleSelector(node); - let wrapper; + let wrapper: AstRule; + let match; + // @ts-ignore if (options.nestingRules) { - // if (node.sel == '.card>hr') { - // - // console.error({idem: previous == node, previous, node}); - // } + // @ts-ignore - if (previous != null) { + if (previous != null && previous.typ == 'Rule') { + + reduceRuleSelector(previous); + // @ts-ignore - if (node.optimized != null && - // @ts-ignore - previous.optimized != null && - // @ts-ignore - node.optimized.optimized.length > 0 && - // @ts-ignore - previous.optimized.optimized.length > 0 && + match = matchSelectors(previous.raw, node.raw, ast.typ); + + // @ts-ignore + if (match != null) { + // @ts-ignore - node.optimized.optimized[0] == previous.optimized.optimized[0]) { + wrapper = wrapNodes(previous, node, match, ast, i, nodeIndex); + nodeIndex = i - 1; // @ts-ignore - if (hasOnlyDeclarations(previous)) { - // @ts-ignore - let pSel = reduceRawTokens(previous.optimized.selector); - if (pSel === '') { - // @ts-ignore - pSel = previous.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - let nSel = reduceRawTokens(node.optimized.selector); - if (nSel === '') { - // @ts-ignore - nSel = node.optimized.optimized.slice(1).join('').trim(); - } - // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; - // @ts-ignore - Object.defineProperty(wrapper, 'raw', { - enumerable: false, - writable: true, - // @ts-ignore - value: [[node.optimized.optimized[0].slice()]] - }); - // @ts-ignore - previous.sel = pSel === '' ? '&' : pSel; // reduceRawTokens(previous.optimized.selector); - // @ts-ignore - previous.raw = pSel === '' ? [['&']] : previous.optimized.selector.slice(); - // @ts-ignore - node.sel = nSel === '' ? '&' : nSel; // reduceRawTokens(node.optimized.selector); - // @ts-ignore - node.raw = nSel === '' ? [['&']] : node.optimized.selector.slice(); - // @ts-ignore - wrapper.chi.push(previous, node); - // @ts-ignore - ast.chi.splice(i, 1, wrapper); - // @ts-ignore - ast.chi.splice(nodeIndex, 1); - } + previous = ast.chi[nodeIndex]; } } + // @ts-ignore if (wrapper != null) { // @ts-ignore while (i < ast.chi.length) { + // @ts-ignore - const nextNode = ast.chi[i]; + const nextNode = ast.chi[i]; + // @ts-ignore - if (nextNode.typ != 'Rule' || nextNode.raw == null) { + if (nextNode.typ != 'Rule') { + // i--; + // previous = wrapper; + // nodeIndex = i; + break; } + reduceRuleSelector(nextNode); + // @ts-ignore - if (nextNode.raw.length != 1 || !eq(wrapper.raw[0], nextNode.raw[0].slice(0, wrapper.raw[0].length))) { - break; - } - // @ts-ignore - nextNode.raw[0].splice(0, wrapper.raw[0].length); - // @ts-ignore - trimRawToken(nextNode.raw[0]); + match = matchSelectors(wrapper.raw, nextNode.raw, ast.typ); + // @ts-ignore - if (nextNode.raw[0].length == 0 || - // @ts-ignore - (nextNode.raw.length == 1 && nextNode.raw[0] == '&')) { - if (hasOnlyDeclarations(wrapper)) { - // @ts-ignore - wrapper.chi.push(...(nextNode).chi); - // @ts-ignore - ast.chi.splice(i, 1); - continue; - } - else { - // @ts-ignore - nextNode.raw[0].push('&'); - } + if (match == null) { + + break; } - // @ts-ignore - nextNode.sel = nextNode.raw.reduce((acc, curr) => { - acc.push(curr.join('')); - return acc; - }, []).join(','); // @ts-ignore - wrapper.chi.push(nextNode); - // @ts-ignore - ast.chi.splice(i, 1); - node = wrapper; + wrapper = wrapNodes(wrapper, nextNode, match, ast, i, nodeIndex); } - deduplicate(wrapper, options, recursive); - nodeIndex = i; + + nodeIndex = --i; // @ts-ignore - previous = ast.chi[i]; + previous = ast.chi[nodeIndex]; + deduplicate(wrapper, options, recursive); continue; } // @ts-ignore @@ -162,7 +184,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive // @ts-ignore node.optimized.selector.length > 1) { // @ts-ignore - wrapper = { ...node, chi: [], sel: node.optimized.optimized[0] }; + wrapper = {...node, chi: [], sel: node.optimized.optimized[0]}; // @ts-ignore Object.defineProperty(wrapper, 'raw', { enumerable: false, @@ -170,8 +192,9 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive // @ts-ignore value: [[node.optimized.optimized[0]]] }); + // @ts-ignore - node.sel = reduceRawTokens(node.optimized.selector); + node.sel = node.optimized.selector.reduce(reducer, []).join(','); // @ts-ignore node.raw = node.optimized.selector.slice(); // @ts-ignore @@ -181,6 +204,50 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive node = wrapper; } } + + // @ts-ignore + else if (node.optimized?.match) { + + let wrap: boolean = true; + // @ts-ignore + const selector: string[] = node.optimized.selector.reduce((acc, curr) => { + + if (curr[0] == '&') { + + if (curr[1] == ' ') { + + curr.splice(0, 2); + } + + else { + + if (ast.typ != 'Rule' && combinators.includes(curr[1])) { + + wrap = false; + } + + else { + + curr.splice(0, 1); + } + } + } + + else if (combinators.includes(curr[0])) { + + curr.unshift('&'); + } + + // @ts-ignore + acc.push(curr.map(t => t.replaceAll('&', node.optimized.optimized[0])).join('')); + + return acc + + }, []); + + // @ts-ignore + node.sel = (wrap ? node.optimized.optimized[0] : '') + `:is(${selector.join(',')})`; + } } // @ts-ignore if (previous != null && 'chi' in previous && ('chi' in node)) { @@ -209,17 +276,16 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive ast.chi.splice(nodeIndex, 1); // @ts-ignore if (hasDeclaration(node)) { + // @ts-ignore deduplicateRule(node); - } - else { + } else { deduplicate(node, options, recursive); } i--; previous = node; nodeIndex = i; continue; - } - else if (node.typ == 'Rule' && previous?.typ == 'Rule') { + } else if (node.typ == 'Rule' && previous?.typ == 'Rule') { const intersect = diff(previous, node, options); if (intersect != null) { if (intersect.node1.chi.length == 0) { @@ -227,8 +293,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive ast.chi.splice(i--, 1); // @ts-ignore node = ast.chi[i]; - } - else { + } else { // @ts-ignore ast.chi.splice(i, 1, intersect.node1); node = intersect.node1; @@ -237,8 +302,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive // @ts-ignore ast.chi.splice(nodeIndex, 1, intersect.result); previous = intersect.result; - } - else { + } else { // @ts-ignore ast.chi.splice(nodeIndex, 1, intersect.result, intersect.node2); previous = intersect.result; @@ -253,9 +317,9 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive if (recursive && previous != node) { // @ts-ignore if (hasDeclaration(previous)) { + // @ts-ignore deduplicateRule(previous); - } - else { + } else { deduplicate(previous, options, recursive); } } @@ -268,8 +332,7 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive // @ts-ignore if (node.chi.some(n => n.typ == 'Declaration')) { deduplicateRule(node); - } - else { + } else { // @ts-ignore if (!(node.typ == 'AtRule' && (node).nam != 'font-face')) { deduplicate(node, options, recursive); @@ -279,29 +342,40 @@ export function deduplicate(ast: AstNode, options: ParserOptions = {}, recursive } return ast; } + function hasOnlyDeclarations(node: AstRule): boolean { let k: number = node.chi.length; while (k--) { + if (node.chi[k].typ == 'Comment') { + continue; } + return node.chi[k].typ == 'Declaration'; } + return true; } + export function hasDeclaration(node: AstRule): boolean { + // @ts-ignore for (let i = 0; i < node.chi?.length; i++) { + // @ts-ignore if (node.chi[i].typ == 'Comment') { + continue; } // @ts-ignore return node.chi[i].typ == 'Declaration'; } + return true; } + export function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { // @ts-ignore if (!('chi' in ast) || ast.chi?.length <= 1) { @@ -319,8 +393,7 @@ export function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { // @ts-ignore map.set(node, node); continue; - } - else if (node.typ != 'Declaration') { + } else if (node.typ != 'Declaration') { break; } if ((node).nam in configuration.map || @@ -331,18 +404,18 @@ export function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { map.set(shorthand, new PropertyList()); } map.get(shorthand).add(node); - } - else { + } else { map.set((node).nam, node); } } + const children = []; + for (let child of map.values()) { if (child instanceof PropertyList) { // @ts-ignore children.push(...child); - } - else { + } else { // @ts-ignore children.push(child); } @@ -351,6 +424,7 @@ export function deduplicateRule(ast: AstRule | AstAtRule): AstRule | AstAtRule { ast.chi = children.concat(ast.chi?.slice(k)); return ast; } + function reduceRawTokens(raw: string[][]) { return raw.reduce((acc, curr) => { @@ -358,6 +432,7 @@ function reduceRawTokens(raw: string[][]) { return acc; }, []).join(','); } + function trimRawToken(raw: string[]) { while (raw.length > 0) { if (raw[0] == ' ') { @@ -367,21 +442,57 @@ function trimRawToken(raw: string[]) { break; } } -function splitRule(buffer: string): string[] { - const result: string[] = []; +function splitRule(buffer: string): string[][] { + + const result: string[][] = [[]]; let str: string = ''; for (let i = 0; i < buffer.length; i++) { let chr: string = buffer.charAt(i); + if (isWhiteSpace(chr.charCodeAt(0))) { + + let k = i; + + while (k + 1 < buffer.length) { + + if (isWhiteSpace(buffer[k + 1].charCodeAt(0))) { + + k++; + continue; + } + + break; + } + + if (str !== '') { + + // @ts-ignore + result.at(-1).push(str); + str = ''; + } + + // @ts-ignore + if (result.at(-1).length > 0) { + + // @ts-ignore + result.at(-1).push(' '); + } + + i = k; + continue; + } + if (chr == ',') { if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); str = ''; } + result.push([]); continue; } @@ -420,8 +531,7 @@ function splitRule(buffer: string): string[] { str += chr; if (chr == open) { inParens++; - } - else if (chr == close) { + } else if (chr == close) { inParens--; } if (inParens == 0) { @@ -431,44 +541,56 @@ function splitRule(buffer: string): string[] { i = k; } } + if (str !== '') { - result.push(str); + // @ts-ignore + result.at(-1).push(str); } + return result; } + function reduceRuleSelector(node: AstRule) { + + if (node.raw == null) { + + Object.defineProperty(node, 'raw', {enumerable: false, writable: true, value: splitRule(node.sel)}) + } + // @ts-ignore - if (node.raw != null) { - // @ts-ignore - let optimized: OptimizedSelector = reduceSelector(node.raw.reduce((acc, curr) => { - acc.push(curr.slice()); - return acc; - }, [])); + // if (node.raw != null) { + // @ts-ignore + let optimized: OptimizedSelector = reduceSelector(node.raw.reduce((acc, curr) => { + acc.push(curr.slice()); + return acc; + }, [])); - if (optimized != null) { - Object.defineProperty(node, 'optimized', { enumerable: false, writable: true, value: optimized }); - } - if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { - const raw = [ - [ - optimized.optimized[0], ':is(' - ].concat(optimized.selector.reduce((acc, curr) => { - if (acc.length > 0) { - acc.push(','); - } - acc.push(...curr); - return acc; - }, [])).concat(')') - ]; - const sel = raw[0].join(''); - if (sel.length < node.sel.length) { - node.sel = sel; - // node.raw = raw; - Object.defineProperty(node, 'raw', { enumerable: false, writable: true, value: raw }); - } + if (optimized != null) { + Object.defineProperty(node, 'optimized', {enumerable: false, writable: true, value: optimized}); + } + if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { + + const raw = [ + [ + optimized.optimized[0], ':is(' + ].concat(optimized.selector.reduce((acc, curr) => { + if (acc.length > 0) { + acc.push(','); + } + acc.push(...curr); + return acc; + }, [])).concat(')') + ]; + const sel = raw[0].join(''); + if (sel.length < node.sel.length) { + node.sel = sel; + // node.raw = raw; + Object.defineProperty(node, 'raw', {enumerable: false, writable: true, value: raw}); } } + // } } + function diff(n1: AstRule, n2: AstRule, options: ParserOptions = {}) { let node1 = n1; let node2 = n2; @@ -488,25 +610,25 @@ function diff(n1: AstRule, n2: AstRule, options: ParserOptions = {}) { // @ts-ignore const raw1 = node1.raw; // @ts-ignore - const optimized1 = node1.optimized; + // const optimized1 = node1.optimized; // @ts-ignore const raw2 = node2.raw; // @ts-ignore - const optimized2 = node2.optimized; - node1 = { ...node1, chi: node1.chi.slice() }; - node2 = { ...node2, chi: node2.chi.slice() }; + // const optimized2 = node2.optimized; + node1 = {...node1, chi: node1.chi.slice()}; + node2 = {...node2, chi: node2.chi.slice()}; if (raw1 != null) { - Object.defineProperty(node1, 'raw', { enumerable: false, writable: true, value: raw1 }); - } - if (optimized1 != null) { - Object.defineProperty(node1, 'optimized', { enumerable: false, writable: true, value: optimized1 }); + Object.defineProperty(node1, 'raw', {enumerable: false, writable: true, value: raw1}); } + // if (optimized1 != null) { + // Object.defineProperty(node1, 'optimized', {enumerable: false, writable: true, value: optimized1}); + // } if (raw2 != null) { - Object.defineProperty(node2, 'raw', { enumerable: false, writable: true, value: raw2 }); - } - if (optimized2 != null) { - Object.defineProperty(node2, 'optimized', { enumerable: false, writable: true, value: optimized2 }); + Object.defineProperty(node2, 'raw', {enumerable: false, writable: true, value: raw2}); } + // if (optimized2 != null) { + // Object.defineProperty(node2, 'optimized', {enumerable: false, writable: true, value: optimized2}); + // } const intersect = []; while (i--) { if (node1.chi[i].typ == 'Comment') { @@ -534,19 +656,260 @@ function diff(n1: AstRule, n2: AstRule, options: ParserOptions = {}) { const result = (intersect.length == 0 ? null : { ...node1, // @ts-ignore - sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(), + sel: [...new Set([...(n1?.raw?.reduce(reducer, []) || splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) || splitRule(n2.sel))])].join(','), chi: intersect.reverse() }); if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + render(curr, options).code.length, 0)) { // @ts-ignore return null; } - return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2 }; + return {result, node1: exchanged ? node2 : node1, node2: exchanged ? node2 : node2}; +} + +function matchSelectors(selector1: string[][], selector2: string[][], parentType: NodeType): null | MatchedSelector { + + let match: string[][] = [[]]; + const j = Math.min( + selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), + selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0) + ); + + let i: number = 0; + let k: number; + let l: number; + let token: string; + let matching: boolean = true; + let matchFunction: number = 0; + let inAttr: number = 0; + + for (; i < j; i++) { + + k = 0; + token = selector1[0][i]; + + for (; k < selector1.length; k++) { + + if (selector1[k][i] != token) { + + matching = false; + break; + } + } + + if (matching) { + + l = 0; + for (; l < selector2.length; l++) { + + if (selector2[l][i] != token) { + + matching = false; + break; + } + } + } + + if (!matching) { + + break; + } + + if (token == ',') { + + match.push([]); + } else { + + if (token.endsWith('(')) { + + matchFunction++; + } + + if (token.endsWith('[')) { + + inAttr++; + } else if (token == ')') { + + matchFunction--; + } else if (token == ']') { + + inAttr--; + } + + (match.at(-1)).push(token); + } + } + + // invalid function + if (matchFunction != 0 || inAttr != 0) { + + return null; + } + + if (parentType != 'Rule') { + + for (const part of match) { + + if (part.length > 0 && combinators.includes(part[0].charAt(0))) { + + return null; + } + } + } + if (match.length > 1) { + + console.error(`unsupported multilevel matching`); + console.error({match, selector1, selector2}); + return null; + } + + for (const part of match) { + + while (part.length > 0) { + + const token = part.at(-1); + + if (token == ' ' || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { + + part.pop(); + continue; + } + + break; + } + } + + if (match.every(t => t.length == 0)) { + + return null; + } + + if (eq([['&']], match)) { + + return null; + } + + function reduce(acc: string[][], curr: string[]) { + + if (acc === null) { + + return null; + } + + let hasCompoundSelector = true; + + curr = curr.slice(match[0].length); + + while (curr.length > 0) { + + if (curr[0] == ' ') { + + hasCompoundSelector = false; + curr.unshift('&'); + continue; + } + + break; + } + + // invalid function match + if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { + + return null; + } + + if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { + + return null; + } + + if (hasCompoundSelector && curr.length > 0) { + + hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + + if (curr[0] == ':is(') { + + let inFunction = 0; + let canReduce = true; + const isCompound = curr.reduce((acc, token, index: number) => { + + if (index == 0) { + + inFunction++; + canReduce = curr[1] == '&'; + } else if (token.endsWith('(')) { + + if (inFunction == 0) { + + canReduce = false; + } + + inFunction++; + } else if (token == ')') { + + inFunction--; + } else if (token == ',') { + + if (!canReduce) { + + canReduce = curr[index + 1] == '&'; + } + + acc.push([]); + } else acc.at(-1)?.push(token); + + return acc; + + }, [[]]); + + if (inFunction > 0) { + + canReduce = false; + } + + if (canReduce) { + + curr = isCompound.reduce((acc, curr) => { + + if (acc.length > 0) { + + acc.push(','); + } + + acc.push(...curr); + + return acc + }, []); + } + } + + // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' + + acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)) + + return acc; + } + + + // @ts-ignore + selector1 = selector1.reduce(reduce, []); + // @ts-ignore + selector2 = selector2.reduce(reduce, []); + + return selector1 == null || selector2 == null ? null : { + eq: eq(selector1, selector2), + match, + selector1, + selector2 + } } + export function reduceSelector(selector: string[][]) { + if (selector.length == 0) { return null; } + const optimized: string[] = []; const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : (curr.length == 0 ? acc : Math.min(acc, curr.length)), 0); let i = 0; @@ -564,51 +927,107 @@ export function reduceSelector(selector: string[][]) { if (!match) { break; } + optimized.push(item); } + + while (optimized.length > 0) { + + const last = optimized.at(-1); + + if ((last == ' ' || combinators.includes(last))) { + + optimized.pop(); + continue; + } + + break; + } + selector.forEach((selector) => selector.splice(0, optimized.length)); + // combinator if (combinators.includes(optimized.at(-1))) { const combinator = optimized.pop(); selector.forEach(selector => selector.unshift(combinator)); } - if (optimized.at(-1) == ' ') { - optimized.pop(); - } + let reducible = optimized.length == 1; + + if (optimized[0] == '&' && optimized[1] == ' ') { + + optimized.splice(0, 2); + } + if (optimized.length == 0 || - optimized[0].charAt(0) == '&' || - selector.length == 1) { + (optimized[0].charAt(0) == '&' || + selector.length == 1)) { return { match: false, optimized, - selector, - reducible: selector.length > 1 && selector.every((selector) => !['>', '+', '~'].includes(selector[0])) + selector: selector.map(selector => selector[0] == '&' && selector[1] == ' ' ? selector.slice(2) : selector), + reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])) }; } + return { match: true, optimized, selector: selector.reduce((acc, curr) => { + + let hasCompound = true; + + if (hasCompound && curr.length > 0) { + + hasCompound = !['&'].concat(combinators).includes(curr[0].charAt(0)); + } + // @ts-ignore - if (curr.length > 0 && curr[0] == ' ') { - curr.shift(); + if (hasCompound && curr[0] == ' ') { + + hasCompound = false; + curr.unshift('&'); } + if (curr.length == 0) { curr.push('&'); + hasCompound = false; } + if (reducible) { const chr = curr[0].charAt(0); // @ts-ignore reducible = chr == '.' || chr == ':' || isIdentStart(chr.codePointAt(0)); } - acc.push(curr); + + acc.push(hasCompound ? ['&'].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } -function reducer(acc: string[], curr: string[]) { + +function reducer(acc: string[], curr: string[], index: number, array: string[][]) { + + // trim :is() + if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { + + curr = curr.slice(1, -1); + } + + if (curr[0] == '&') { + + if (curr[1] == ' ') { + + curr.splice(0, 2); + } + + else if (combinators.includes(curr[1])) { + + curr.splice(0, 1); + } + } + acc.push(curr.join('')); return acc; } diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index b9aed28..51e8c45 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -203,6 +203,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< } } async function parseNode(tokens: Token[]) { + let i: number; let loc: Location; @@ -888,15 +889,16 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< case '>': + if (buffer !== '') { + pushToken(getType(buffer)); + buffer = ''; + } + if (tokens[tokens.length - 1]?.typ == 'Whitespace') { tokens.pop(); } - if (buffer !== '') { - pushToken(getType(buffer)); - buffer = ''; - } pushToken({ typ: 'Gt' }); consumeWhiteSpace(); break; @@ -1060,6 +1062,7 @@ export async function parse(iterator: string, opt: ParserOptions = {}): Promise< pushToken(getBlockType(value)); let node = null; if (value == '{' || value == ';') { + node = await parseNode(tokens); if (node != null) { stack.push(node); @@ -1154,6 +1157,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt tokens[i + 1].typ = 'Pseudo-class'; } if (typ == 'Func' || typ == 'Iden') { + tokens.splice(i, 1); i--; continue; @@ -1217,6 +1221,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt (tokens[k + 1]).val = ':' + (tokens[k + 1]).val; } if (typ == 'Func' || typ == 'Iden') { + tokens.splice(k, 1); k--; continue; @@ -1233,6 +1238,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt break; } } + // @ts-ignore t.chi = tokens.splice(i + 1, k - i); // @ts-ignore @@ -1262,11 +1268,13 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt if (t.chi[m].typ == 'Literal') { // @ts-ignore if (t.chi[m + 1]?.typ == 'Whitespace') { + // @ts-ignore t.chi.splice(m + 1, 1); } // @ts-ignore if (t.chi[m - 1]?.typ == 'Whitespace') { + // @ts-ignore t.chi.splice(m - 1, 1); m--; @@ -1307,6 +1315,7 @@ function parseTokens(tokens: Token[], nodeType: NodeType, options: ParseTokenOpt (i == 0 && (tokens[i + 1]?.typ == 'Comma' || tokens.length == i + 1)) || (tokens[i - 1]?.typ == 'Comma' && (tokens[i + 1]?.typ == 'Comma' || tokens.length == i + 1))) { + tokens.splice(i, 1, ...t.chi); i = Math.max(0, i - t.chi.length); } diff --git a/test/specs/block.spec.js b/test/specs/block.spec.js index bc4f800..ae81135 100644 --- a/test/specs/block.spec.js +++ b/test/specs/block.spec.js @@ -7,18 +7,23 @@ import { readFileSync } from 'fs'; const dir = dirname(new URL(import.meta.url).pathname) + '/../files'; describe('parse block', function () { + it('parse file', function () { return readFile(`${dir}/css/smalli.css`, { encoding: 'utf-8' }).then(file => transform(file).then(result => f(result.ast).deep.equals(JSON.parse((readFileSync(dir + '/json/smalli.json')).toString())))); }); + it('parse file #2', function () { return readFile(`${dir}/css/small.css`, { encoding: 'utf-8' }).then(file => transform(file).then(result => f(result.ast).deep.equals(JSON.parse((readFileSync(dir + '/json/small.json')).toString())))); }); + it('parse file #3', function () { readFile(`${dir}/css/invalid-1.css`, { encoding: 'utf-8' }).then(file => transform(file).then(result => f(result.ast).deep.equals(JSON.parse((readFileSync(dir + '/json/invalid-1.json')).toString())))); }); + it('parse file #4', function () { return readFile(`${dir}/css/invalid-2.css`, { encoding: 'utf-8' }).then(file => transform(file).then(result => f(result.ast).deep.equals(JSON.parse((readFileSync(dir + '/json/invalid-2.json')).toString())))); }); + it('similar rules #5', function () { const file = ` .clear { @@ -35,6 +40,7 @@ describe('parse block', function () { compress: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); + it('similar rules #5', function () { const file = ` .clear { @@ -51,6 +57,7 @@ describe('parse block', function () { compress: true }).then(result => f(result.code).equals(`.clear,.clearfix:before{width:0;height:0}`)); }); + it('duplicated selector components #6', function () { const file = ` @@ -64,6 +71,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`.test input[type=text],a{border-color:gold red}`)); }); + it('merge selectors #7', function () { const file = ` @@ -106,6 +114,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}`)); }); + it('merge selectors #8', function () { const file = ` @@ -119,6 +128,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`.nav-pills:is(.nav-link.active,.show>.nav-link){color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}`)); }); + it('merge selectors #9', function () { const file = ` @@ -131,6 +141,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`.card{--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)))}`)); }); + it('merge selectors #10', function () { const file = ` @@ -145,6 +156,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}`)); }); + it('merge selectors #11', function () { const file = ` @@ -158,6 +170,7 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`:root:is(.fa-flip-both,.fa-flip-horizontal,.fa-flip-vertical,.fa-rotate-180,.fa-rotate-270,.fa-rotate-90){-webkit-filter:none;filter:none}`)); }); + it('merge selectors #12', function () { const file = ` .bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))} @@ -167,4 +180,22 @@ border-left-color: red; compress: true }).then(result => f(result.code).equals(`.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}`)); }); + + it('merge selectors #13', function () { + const file = ` + +abbr[title], abbr[data-original-title], abbr>[data-original-title] { + text-decoration: underline dotted; + -webkit-text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none +} + +`; + return transform(file, { + compress: true + }).then(result => f(result.code).equals(`abbr:is([title],[data-original-title],abbr>[data-original-title]){text-decoration:underline dotted;-webkit-text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}`)); + }); }); diff --git a/test/specs/import.spec.js b/test/specs/import.spec.js index c144440..6264a93 100644 --- a/test/specs/import.spec.js +++ b/test/specs/import.spec.js @@ -20,6 +20,6 @@ describe('process import', function () { return transform(atRule, { compress: true, resolveImport: true - }).then((result) => f(result.code).equals(`p{color:#8133cc26}abbr[title],abbr[data-original-title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}`)); + }).then((result) => f(result.code).equals(`p{color:#8133cc26}abbr:is([title],[data-original-title]){text-decoration:underline dotted;-webkit-text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}`)); }); }); diff --git a/test/specs/nesting.spec.js b/test/specs/nesting.spec.js new file mode 100644 index 0000000..9a6f598 --- /dev/null +++ b/test/specs/nesting.spec.js @@ -0,0 +1,366 @@ +/* generate from test/specs/nesting.spec.ts */ +import {expect as f} from '../../node_modules/@esm-bundle/chai/esm/chai.js'; +import {transform} from '../../dist/node/index.js'; + +describe('CSS Nesting', function () { + it('nesting #1', function () { + const nesting1 = ` +.nesting { + color: hotpink; +} + +.nesting > .is { + color: rebeccapurple; +} + +.nesting > .is > .awesome { + color: deeppink; +} +`; + return transform(nesting1, { + compress: true, + nestingRules: true + }).then((result) => f(result.code).equals(`.nesting{color:hotpink;>.is{color:#639;>.awesome{color:#ff1493}}}`)); + }); + + it('nesting #2', function () { + const nesting2 = ` +.nav-link:focus, .nav-link:hover { + color: var(--bs-nav-link-hover-color) +} + +`; + return transform(nesting2, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}}`)); + }); + it('nesting #3', function () { + const nesting3 = ` +.nav-link:focus, .nav-link:hover { + color: var(--bs-nav-link-hover-color) +} + +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25) +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}}`)); + }); + it('nesting #4', function () { + const nesting3 = ` + +.nav-link:focus, .nav-link:hover { + color: var(--bs-nav-link-hover-color) +} + +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25) +} + +.nav-link.disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}&.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}}`)); + }); + it('nesting #5', function () { + const nesting3 = ` + +.nav-link:focus, .nav-link:hover { + color: var(--bs-nav-link-hover-color) +} + +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25) +} + +.nav-link.disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default +} + +.nav-link:disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default +} +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.nav-link{&:focus,&:hover{color:var(--bs-nav-link-hover-color)}&:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}&.disabled,&:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}}`)); + }); + it('nesting #6', function () { + const nesting3 = ` + +.form-floating > .form-control, .form-floating > .form-control-plaintext, .form-floating > .form-select { + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); +} +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.form-floating{>.form-control,>.form-control-plaintext,>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2))}}`)); + }); + it('nesting #7', function () { + const nesting3 = ` + +.form-floating { + position: relative +} + +.form-floating > .form-control, .form-floating > .form-control-plaintext, .form-floating > .form-select { + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + min-height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + line-height: 1.25; + z-index: 2; + white-space: nowrap; +} + +.form-floating > label { + position: absolute; + top: 0; + left: 0; + z-index: 2; + height: 100%; + padding: 1rem .75rem; + overflow: hidden; + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--bs-border-width) solid transparent; + transform-origin: 0 0; + transition: opacity .1s ease-in-out, transform .1s ease-in-out +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.form-floating{position:relative;>.form-control,>.form-control-plaintext,>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25;z-index:2;white-space:nowrap}>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid #0000;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}}`)); + }); + + it('nesting #8', function () { + const nesting3 = ` + +.card { + --bs-card-spacer-y: 1rem; + --bs-card-spacer-x: 1rem; + --bs-card-title-spacer-y: 0.5rem; + --bs-card-title-color: ; + --bs-card-subtitle-color: ; + --bs-card-border-width: var(--bs-border-width); + --bs-card-border-color: var(--bs-border-color-translucent); + --bs-card-border-radius: var(--bs-border-radius); + --bs-card-box-shadow: ; + --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-card-cap-padding-y: 0.5rem; + --bs-card-cap-padding-x: 1rem; + --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); + --bs-card-cap-color: ; + --bs-card-height: ; + --bs-card-color: ; + --bs-card-bg: var(--bs-body-bg); + --bs-card-img-overlay-padding: 1rem; + --bs-card-group-margin: 0.75rem; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + height: var(--bs-card-height); + color: var(--bs-body-color); + word-wrap: break-word; + background-color: var(--bs-card-bg); + background-clip: border-box; + border: var(--bs-card-border-width) solid var(--bs-card-border-color); + border-radius: var(--bs-card-border-radius) +} + +.card > hr { + margin-right: 0; + margin-left: 0 +} +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:.5rem;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb),.03);--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);>hr{margin-right:0;margin-left:0}}`)); + }); + it('nesting #9', function () { + const nesting3 = ` +.tab-content{>.tab-pane{display:none}>.active{display:block}}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb),.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb),.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb),.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-padding-y:.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-nav-link-padding-x:.5rem;--bs-navbar-toggler-padding-y:.25rem;--bs-navbar-toggler-padding-x:.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb),.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:.25rem;--bs-navbar-toggler-transition:box-shadow .15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);>.container,>.container-fluid,>.container-lg,>.container-md,>.container-sm,>.container-xl,>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.tab-content{>.tab-pane{display:none}>.active{display:block}}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb),.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb),.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb),.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-padding-y:.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb),1);--bs-navbar-nav-link-padding-x:.5rem;--bs-navbar-toggler-padding-y:.25rem;--bs-navbar-toggler-padding-x:.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb),.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:.25rem;--bs-navbar-toggler-transition:box-shadow .15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);>.container,>.container-fluid,>.container-lg,>.container-md,>.container-sm,>.container-xl,>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}}`)); + }); + + it('nesting #10', function () { + const nesting3 = ` + + +.navbar-text { + padding-top: .5rem; + padding-bottom: .5rem; + color: var(--bs-navbar-color) +} + +.navbar-text a, .navbar-text a:focus, .navbar-text a:hover { + color: var(--bs-navbar-active-color) +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color);a{&,&:focus,&:hover{color:var(--bs-navbar-active-color)}}}`)); + }); + + it('nesting #11', function () { + const nesting3 = ` + +.table-bordered > :not(caption) > * { + border-width: var(--bs-border-width) 0 +} + +.table-bordered > :not(caption) > * > * { + border-width: 0 var(--bs-border-width) +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0;>*{border-width:0 var(--bs-border-width)}}`)); + }); + + + it('nesting #12', function () { + const nesting3 = ` + +.dropdown-item { + display: block; + width: 100%; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + clear: both; + font-weight: 400; + color: var(--bs-dropdown-link-color); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; + border-radius: var(--bs-dropdown-item-border-radius, 0) +} + +.dropdown-item:focus, .dropdown-item:hover { + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg) +} + +.dropdown-item.active, .dropdown-item:active { + color: var(--bs-dropdown-link-active-color); + text-decoration: none; + background-color: var(--bs-dropdown-link-active-bg) +} + +.dropdown-item.disabled, .dropdown-item:disabled { + color: var(--bs-dropdown-link-disabled-color); + pointer-events: none; + background-color: transparent +} +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:#0000;border:0;border-radius:var(--bs-dropdown-item-border-radius,0);&:focus,&:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}&.active,&:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}&.disabled,&:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:#0000}}`)); + }); + + it('nesting #13', function () { + const nesting3 = ` + +[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark] .carousel .carousel-control-prev-icon, [data-bs-theme=dark].carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon { + filter: invert(1) grayscale(100) +} + +[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] { + background-color: #000 +} + +[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption { + color: #000 +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`[data-bs-theme=dark]{.carousel .carousel-control-next-icon,.carousel .carousel-control-prev-icon,&.carousel .carousel-control-next-icon,&.carousel .carousel-control-prev-icon{filter:invert(1)grayscale(100)}.carousel .carousel-indicators [data-bs-target],&.carousel .carousel-indicators [data-bs-target]{background-color:#000}.carousel .carousel-caption,&.carousel .carousel-caption{color:#000}}`)); + }); + + it('nesting #14', function () { + const nesting3 = ` + +.demo.lg.triangle { + opacity: .25; + filter: blur(25px); + } + .demo.lg.circle { + opacity: .25; + filter: blur(25px); + } +} +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.demo.lg{&.triangle,&.circle{opacity:.25;filter:blur(25px)}}`)); + }); + + it('nesting #15', function () { + const nesting3 = ` + +.nav-pills .nav-link.active, .nav-pills .show>.nav-link { + color: var(--bs-nav-pills-link-active-color); + background-color: var(--bs-nav-pills-link-active-bg) +} + +`; + return transform(nesting3, { + compress: true, + nestingRules: true, + resolveImport: true + }).then((result) => f(result.code).equals(`.nav-pills{.nav-link.active,.show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}}`)); + }); +});