From 1a727c765f62a0876c3598f1b8975919c4307a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 18 Jul 2024 14:52:46 +0200 Subject: [PATCH 1/9] Preserve all selectors in CSS files during split into editor and content stylesheets. --- .../ckeditor5-dev-build-tools/package.json | 5 +- .../src/plugins/splitCss.ts | 282 +++++------------- .../splitCss/fixtures/keyframes/file1.css | 12 + .../splitCss/fixtures/keyframes/file2.css | 17 ++ .../splitCss/fixtures/keyframes/input.ts | 9 + .../splitCss/fixtures/media-query/file1.css | 5 + .../splitCss/fixtures/media-query/file2.css | 12 + .../splitCss/fixtures/media-query/input.ts | 9 + .../fixtures/more-than-one-selector/file1.css | 5 + .../fixtures/more-than-one-selector/input.ts | 8 + .../fixtures/nested-css-variables/file1.css | 9 + .../tests/plugins/splitCss/splitCss.test.ts | 212 +++++++++---- 12 files changed, 322 insertions(+), 263 deletions(-) create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file1.css create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file2.css create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/input.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file1.css create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file2.css create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/input.ts create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/file1.css create mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/input.ts diff --git a/packages/ckeditor5-dev-build-tools/package.json b/packages/ckeditor5-dev-build-tools/package.json index 6cf0fb174..3a841d4dd 100644 --- a/packages/ckeditor5-dev-build-tools/package.json +++ b/packages/ckeditor5-dev-build-tools/package.json @@ -27,6 +27,7 @@ "ckeditor5-dev-build-tools": "bin/build-project.js" }, "dependencies": { + "@fullhuman/postcss-purgecss": "^6.0.0", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", @@ -36,8 +37,8 @@ "@rollup/pluginutils": "^5.1.0", "@swc/core": "^1.4.8", "chalk": "^5.3.0", - "css": "^3.0.0", - "cssnano": "^6.0.3", + "cssnano": "^7.0.4", + "cssnano-preset-lite": "^4.0.1", "estree-walker": "^3.0.3", "glob": "^10.3.10", "lodash-es": "^4.17.21", diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 7f490537c..97c4ee403 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -4,11 +4,11 @@ */ import { createFilter } from '@rollup/pluginutils'; -import { parse, type Rule, type Declaration, type Stylesheet } from 'css'; import type { Plugin, OutputBundle, NormalizedOutputOptions, EmittedAsset } from 'rollup'; import type { Processor } from 'postcss'; import cssnano from 'cssnano'; -import { removeNewline } from '../utils'; +import litePreset from 'cssnano-preset-lite'; +import { PurgeCSS } from 'purgecss' export interface RollupSplitCssOptions { @@ -30,10 +30,35 @@ export interface RollupSplitCssOptions { */ const filter = createFilter( [ '**/*.css' ] ); -/** - * Regular expression to match CSS variables. - */ -const VARIABLE_DEFINITION_REGEXP = /--([\w-]+)/gm; +const commonPurgeCssOptions = { + fontFace: true, + keyframes: true, + variables: true, +} + +const purgeCssOptionsForContent = { + ...commonPurgeCssOptions, + content: [], + safelist: { deep: [/^ck-content/] }, + blocklist: [] +} + +const purgeCssOptionsForEditor = { + ...commonPurgeCssOptions, + // Pseudo class`:where` is preserved only if the appropriate html structure matches the CSS selector. + // It's a temporary solution to avoid removing selectors for Show blocks styles where `:where` occurs. + // See: https://github.com/FullHuman/purgecss/issues/978 + content: [ { + raw: '
' + + '
', + extension: 'html' + } ], + safelist: { + deep: [ /ck(?!-content)/, /^(?!.*ck)/ ], + }, + // Option to preserve all CSS selectors that starts with `[dir=ltr/rtl]` attribute. + dynamicAttributes: [ 'dir' ] +} export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { const options: Required = Object.assign( { @@ -53,25 +78,32 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { async generateBundle( output: NormalizedOutputOptions, bundle: OutputBundle ) { // Get stylesheet from output bundle. - const cssStylesheet = getCssStylesheet( bundle ); + const cssStylesheetFromBundle = getCssStylesheet( bundle ); - // Parse `CSS` string and returns an `AST` object. - const parsedCss = parse( cssStylesheet ); + // Some of CSS variables are used with spaces after/before brackets: + // var( --var-name ) + // PurgeCss parser currently doesn't respect this syntax and removes this variable from definitions. + // See: https://github.com/FullHuman/purgecss/issues/1264 + // + // Till it's not solved we need to remove spaces from variables. + const cssStylesheet = removeWhitespaceFromVars( cssStylesheetFromBundle! ); // Generate split stylesheets for editor, content, and one that contains them all. - const { editorStylesContent, editingViewStylesContent } = getSplittedStyleSheets( parsedCss ); + const { editorStylesContent, editingViewStylesContent } = await getSplittedStyleSheets( cssStylesheet, options.baseFileName ); + const contentStylesheet = await normalizeStylesheet( editorStylesContent! ); + const editingViewStylesheet = await normalizeStylesheet( editingViewStylesContent! ); - // Emit those styles ito files. + // Emit those styles into files. this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-editor.css`, - source: await unifyFileContentOutput( editorStylesContent, options.minimize ) + source: await unifyFileContentOutput( editingViewStylesheet, options.minimize ) } ); this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-content.css`, - source: await unifyFileContentOutput( editingViewStylesContent, options.minimize ) + source: await unifyFileContentOutput( contentStylesheet, options.minimize ) } ); } }; @@ -95,190 +127,31 @@ function getCssStylesheet( bundle: OutputBundle ) { /** * Returns split stylesheets for editor, content, and one that contains all styles. */ -function getSplittedStyleSheets( parsedCss: Stylesheet ): Record< string, string> { - const rules: Array = parsedCss.stylesheet!.rules; - - const { rootDefinitions, dividedStylesheets } = getDividedStyleSheetsDependingOnItsPurpose( rules ); - - if ( rootDefinitions.length ) { - const { - rootDeclarationForEditorStyles, - rootDeclarationForEditingViewStyles - } = filterCssVariablesBasedOnUsage( rootDefinitions, dividedStylesheets ); - - dividedStylesheets.editorStylesContent = rootDeclarationForEditorStyles + dividedStylesheets.editorStylesContent; - dividedStylesheets.editingViewStylesContent = rootDeclarationForEditingViewStyles + dividedStylesheets.editingViewStylesContent; - } - - return dividedStylesheets; -} - -/** - * Returns `:root` definitions and divided Stylesheets. - * @param rules List of `CSS` StyleSheet rules. - */ -function getDividedStyleSheetsDependingOnItsPurpose( rules: Array ) { - const rootDefinitionsList: Array = []; - - let editorStylesContent = ''; - let editingViewStylesContent = ''; - - rules.forEach( rule => { - if ( rule.type !== 'rule' ) { - return; - } - const objectWithDividedStyles = divideRuleStylesBetweenStylesheets( rule ); - - editorStylesContent += objectWithDividedStyles.editorStyles; - editingViewStylesContent += objectWithDividedStyles.editingViewStyles; - - if ( objectWithDividedStyles.rootDefinitions.length ) { - rootDefinitionsList.push( ...objectWithDividedStyles.rootDefinitions ); - } +async function getSplittedStyleSheets( cssStylesheet: string, filename: string ): Promise> { + const purgeCSSResultForContent = await new PurgeCSS().purge( { + ...purgeCssOptionsForContent, + css: [ + { + raw: cssStylesheet + }, + `${ filename }-content.css`, + ] } ); - const rootDefinitions = rootDefinitionsList.join( '' ); - - return { - rootDefinitions, - dividedStylesheets: { - editorStylesContent, - editingViewStylesContent - } - }; -} - -/** - * Extracts all `CSS` variables from passed `styles`. - */ -function getCssVariables( - styles: string -): Set { - return new Set( styles.match( VARIABLE_DEFINITION_REGEXP ) ); -} - -/** - * Recursively gets all used CSS variables from the `allDeclarations` list. - * This is required, because CSS variables can contain other CSS variables. - */ -function recursivelyGetCssVariables( - variables: Set, - allDeclarations: Array -): Array { - return allDeclarations - // Filter out declarations of unused CSS variables. - .filter( ( declaration: Declaration ) => variables.has( declaration.property! ) ) - // Because CSS variables can themselves contain other CSS variables, we need to recursively get all of them. - .reduce( ( acc: Array, declaration: Declaration ) => { - const nestedVariables = recursivelyGetCssVariables( - getCssVariables( declaration.value! ), - allDeclarations - ); - - acc.push( declaration, ...nestedVariables ); - - return acc; - }, [] ) - // Flatten the array of arrays. - .flat( 20 ); // `20` instead of `Infinity` so that TypeScript doesn't complain. -} - -/** - * Returns `:root` declarations for editor and content stylesheets. - */ -function filterCssVariablesBasedOnUsage( - rootDefinitions: string, - dividedStylesheets: { [key: string]: string } -): Record { - const rootDeclarationForEditorStyles = createRootDeclarationOfUsedVariables( - rootDefinitions, - getCssVariables( dividedStylesheets.editorStylesContent! ) - ); - - const rootDeclarationForEditingViewStyles = createRootDeclarationOfUsedVariables( - rootDefinitions, - getCssVariables( dividedStylesheets.editingViewStylesContent! ) - ); + const purgeCSSResultForEditingView = await new PurgeCSS().purge({ + ...purgeCssOptionsForEditor, + css: [ + { + raw: cssStylesheet + }, + `${ filename }-editor.css`, + ] + } ); return { - rootDeclarationForEditorStyles, - rootDeclarationForEditingViewStyles - }; -} - -/** - * Returns filtered `:root` declaration based on list of used `CSS` variables in stylesheet content. - */ -function createRootDeclarationOfUsedVariables( - rootDefinition: string, - usedVariables: Set -): string { - if ( rootDefinition.length === 0 || usedVariables.size === 0 ) { - return ''; + editorStylesContent: purgeCSSResultForContent[ 0 ]!.css, + editingViewStylesContent: purgeCSSResultForEditingView[ 0 ]!.css } - - const rootDeclarationWithSelector = wrapDefinitionsIntoSelector( ':root', rootDefinition ); - const parsedRootDeclaration = parse( rootDeclarationWithSelector ); - const firstRule = parsedRootDeclaration.stylesheet!.rules[ 0 ] as Rule; - const allDeclarations = firstRule.declarations as Array; - - const variables: string = recursivelyGetCssVariables( usedVariables, allDeclarations ) - // Convert declarations to CSS string. - .map( ( declaration: Declaration ) => `${ declaration.property }: ${ declaration.value };` ) - // Remove duplicates. - .filter( ( item, pos, self ) => self.indexOf( item ) == pos ) - // Join declarations into a single string. - .join( '\n' ); - - return wrapDefinitionsIntoSelector( ':root', variables ); -} - -/** - * Decides to which stylesheet should passed `rule` be placed or it should be in `:root` definition. - */ -function divideRuleStylesBetweenStylesheets( rule: Rule ) { - const selector = rule.selectors![ 0 ]!; - const rootDefinitions = []; - - let editorStyles = ''; - let editingViewStyles = ''; - - const ruleDeclarations = getRuleDeclarations( rule.declarations! ); - const ruleDeclarationsWithSelector = wrapDefinitionsIntoSelector( selector, ruleDeclarations ); - const isRootSelector = selector.includes( ':root' ); - const isStartingWithContentSelector = selector.startsWith( '.ck-content' ); - - // `:root` selector need to be in each file at the top. - if ( isRootSelector ) { - rootDefinitions.push( ruleDeclarations ); - } else { - // Dividing styles depending on purpose - if ( isStartingWithContentSelector ) { - editingViewStyles += ruleDeclarationsWithSelector; - } else { - editorStyles += ruleDeclarationsWithSelector; - } - } - - return { - rootDefinitions, - editorStyles, - editingViewStyles - }; -} - -/** - * Returns all `rule` declarations as a concatenated string. - */ -function getRuleDeclarations( declarations: Array ): string { - return declarations.reduce( ( acc, currentDeclaration ) => { - if ( currentDeclaration.type !== 'declaration' ) { - return acc; - } - - const property = `${ currentDeclaration.property }: ${ currentDeclaration.value };\n`; - return acc + property; - }, '' ); } /** @@ -297,16 +170,21 @@ async function unifyFileContentOutput( content: string = '', minimize: boolean ) } /** - * Wraps `declarations` list into passed `selector`; + * Returns normalized stylesheet content. */ -function wrapDefinitionsIntoSelector( selector: string, definitions: string ): string { - // When definition contains `data:image` it should be in one line following the specification. - // Currently used tool responsible for parsing definitions tries to split each definition into new line. - // When `data:image` contains `SVG` with style attribute which contains CSS definitions it splits it into new lines, - // which breaks the CSS. - if ( definitions.includes( 'data:image' ) ) { - definitions = removeNewline( definitions ); - } +async function normalizeStylesheet( content: string ): Promise { + const normalizeContent = await cssnano( { preset: litePreset( { + normalizeWhitespace: false, + } ) } ).process( content!, { from: undefined } ); + + return normalizeContent.css; +} + +/* + * Removes whitespace between brackets from CSS variables. + */ +function removeWhitespaceFromVars( content: string ): string { + const regex = /(?<=var\()\s+|\s+(?=\))/g; - return `${ selector } {\n${ definitions }\n}\n`; + return content.replace( regex, '' ); } diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file1.css new file mode 100644 index 000000000..bc077ec61 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file1.css @@ -0,0 +1,12 @@ +.animation { + /* This is a comment */ + animation: fadeIn 1s; +} +@keyframes fadeIn { + from { + /* This is a comment */ + opacity: 0; + } + /* This is a comment */ + to { opacity: 1; } +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file2.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file2.css new file mode 100644 index 000000000..476d684e4 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/file2.css @@ -0,0 +1,17 @@ +@media (forced-colors: none) { + /* This is a comment */ + .ck-content.animation-in-media-query { + /* This is a comment */ + animation: ck-animation 1s ease-out; + } +} + +@keyframes ck-animation { + 0% { + background-color: white; + } + + 100% { + background-color: black; + } +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/input.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/input.ts new file mode 100644 index 000000000..bc29c5c6a --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/keyframes/input.ts @@ -0,0 +1,9 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import './file1.css'; +import './file2.css'; + +export const test = 123; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file1.css new file mode 100644 index 000000000..ebcb481a7 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file1.css @@ -0,0 +1,5 @@ +@media (prefers-reduced-motion: reduce) { + .ck-image-upload-complete-icon { + animation-duration: 0ms; + } +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file2.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file2.css new file mode 100644 index 000000000..2617eb7db --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/file2.css @@ -0,0 +1,12 @@ +@media print { + .ck-content .page-break { + padding: 0; + } +} + +@media screen and (max-width: 600px) { + /* This is a comment */ + .ck-content { + width: 100%; + } +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/input.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/input.ts new file mode 100644 index 000000000..bc29c5c6a --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/media-query/input.ts @@ -0,0 +1,9 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import './file1.css'; +import './file2.css'; + +export const test = 123; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/file1.css new file mode 100644 index 000000000..5da26c182 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/file1.css @@ -0,0 +1,5 @@ +.ck, +.second-selector, +.third-selector p { + color: red; +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/input.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/input.ts new file mode 100644 index 000000000..ca5155836 --- /dev/null +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/more-than-one-selector/input.ts @@ -0,0 +1,8 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import './file1.css'; + +export const test = 123; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css index 4310d2d44..becc2ef8f 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css @@ -9,8 +9,17 @@ --ck-unused-primary: green; --ck-unused-secondary: red; --ck-unused-variable-2: var(--ck-unused-primary, var(--ck-unused-secondary)); + + /* Test CSS variables with calc() function. */ + --ck-variable-4: 1px; + --ck-variable-5: 1em; + --ck-calc-variables: calc( var(--ck-variable-4) + var(--ck-variable-5)); } .ck { margin: var(--ck-spacing-unit); } + +.ck { + transform: translateX( var( --ck-calc-variables )); +} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts index 842d5582f..a14b1192d 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts @@ -54,13 +54,13 @@ test( 'should import a single `CSS` file', async () => { ); const expectedResult = removeWhitespace( - `body { - color: '#000'; + `body{ + color:'#000'; } ` ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '' ); + verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); test( 'should import multiple `CSS` files', async () => { @@ -70,16 +70,16 @@ test( 'should import multiple `CSS` files', async () => { ); const expectedResult = removeWhitespace( - `body { - color: '#000'; + `body{ + color:'#000'; } - div { - display: grid; + div{ + display:grid; } ` ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '' ); + verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); test( 'should import `CSS` file only once (without duplication)', async () => { @@ -89,14 +89,14 @@ test( 'should import `CSS` file only once (without duplication)', async () => { ); const expectedEditorResult = removeWhitespace( - `.ck-feature { - display: grid; + `.ck-feature{ + display:grid; } ` ); const expectedContentResult = removeWhitespace( - `.ck-content.ck-feature { - display: block; + `.ck-content.ck-feature{ + display:block; } ` ); @@ -111,13 +111,13 @@ test( 'should ignore `CSS` comments', async () => { ); const expectedResult = removeWhitespace( - `body { - color: '#000'; + `body{ + color:'#000'; } ` ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '' ); + verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); test( 'should combine `:root` declarations from multiple entries into one', async () => { @@ -127,20 +127,22 @@ test( 'should combine `:root` declarations from multiple entries into one', asyn ); const expectedResult = removeWhitespace( - `:root { - --variable1: blue; - --variable2: red; + `:root{ + --variable1:blue; + } + h1{ + color:var(--variable1); } - h1 { - color: var(--variable1); + :root{ + --variable2:red; } - p { - color: var(--variable2); + p{ + color:var(--variable2); } ` ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '' ); + verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); test( 'should filter `:root` declaration based on `CSS` variables usage', async () => { @@ -150,24 +152,24 @@ test( 'should filter `:root` declaration based on `CSS` variables usage', async ); const expectedEditorResult = removeWhitespace( - `:root { - --variable1: blue; - --variable2: red; + `:root{ + --variable1:blue; + --variable2:red; } - .ck-feature { - color: var(--variable1); - background-color: var(--variable2); + .ck-feature{ + color:var(--variable1); + background-color:var(--variable2); } ` ); const expectedContentResult = removeWhitespace( - `:root { - --variable3: red; - --variable4: pink; + `:root{ + --variable3:red; + --variable4:pink; } - .ck-content.ck-feature { - color: var(--variable3); - background-color: var(--variable4); + .ck-content.ck-feature{ + color:var(--variable3); + background-color:var(--variable4); } ` ); @@ -182,20 +184,20 @@ test( 'should omit `:root` declaration when it\'s not exist', async () => { ); const expectedEditorResult = removeWhitespace( - `:root { - --variable1: blue; - --variable2: red; + `:root{ + --variable1:blue; + --variable2:red; } - .ck-feature { - color: var(--variable1); - background-color: var(--variable2); + .ck-feature{ + color:var(--variable1); + background-color:var(--variable2); } ` ); const expectedContentResult = removeWhitespace( - `.ck-content.ck-feature { - color: red; - background-color: blue; + `.ck-content.ck-feature{ + color:red; + background-color:blue; } ` ); @@ -210,14 +212,14 @@ test( 'should divide classes into files based on its purpose', async () => { ); const expectedEditorResult = removeWhitespace( - `.ck-feature { - display: grid; + `.ck-feature{ + display:grid; } ` ); const expectedContentResult = removeWhitespace( - `.ck-content.ck-feature { - display: block; + `.ck-content.ck-feature{ + display:block; } ` ); @@ -254,16 +256,16 @@ test( 'should correctly parse the `data:image` style definition (should do not a ); const expectedResult = removeWhitespace( - '.ck {\n' + - 'background-image: url("data:image/svg+xml;utf8,' + + '.ck{\n' + + 'background-image:url("data:image/svg+xml;utf8,' + '' + - 'FIGCAPTION");background-position: calc(100% - 1px) 1px;\n}\n' ); + 'FIGCAPTION");\nbackground-position:calc(100% - 1px) 1px;\n}\n' ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '' ); + verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); test( 'should keep CSS variables used by other CSS variables', async () => { @@ -273,16 +275,108 @@ test( 'should keep CSS variables used by other CSS variables', async () => { ); const expectedResult = removeWhitespace( - `:root { - --ck-spacing-unit: var(--ck-variable-1); - --ck-variable-1: var(--ck-variable-2); - --ck-variable-2: var(--ck-nonexistent-variable, var(--ck-variable-3)); - --ck-variable-3: 0.6em; + `:root{ + --ck-spacing-unit:var(--ck-variable-1); + --ck-variable-1:var(--ck-variable-2); + --ck-variable-2:var(--ck-nonexistent-variable, var(--ck-variable-3)); + --ck-variable-3:0.6em; + --ck-variable-4:1px; + --ck-variable-5:1em; + --ck-calc-variables:calc( var(--ck-variable-4) + var(--ck-variable-5)); + } + .ck{ + margin:var(--ck-spacing-unit); } - .ck { - margin: var(--ck-spacing-unit); + .ck{ + transform:translateX( var(--ck-calc-variables)); } ` ); verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); } ); + +test( 'should preserve all selectors', async () => { + const output = await generateBundle( + './fixtures/more-than-one-selector/input.ts', + { baseFileName: 'styles' } + ); + + const expectedResult = removeWhitespace( + `.ck, + .second-selector, + .third-selector p{ + color:red; + } + ` ); + + verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); +} ); + +test( 'should preserve all `@media` queries and split it correctly', async () => { + const output = await generateBundle( + './fixtures/media-query/input.ts', + { baseFileName: 'styles' } + ); + + const expectedEditorResult = removeWhitespace( + `@media (prefers-reduced-motion: reduce){ + .ck-image-upload-complete-icon{ + animation-duration:0ms; + } + } + ` ); + + const expectedContentResult = removeWhitespace( + `@media print{ + .ck-content .page-break{ + padding:0; + } + } + @media screen and (max-width: 600px){ + .ck-content{ + width:100%; + } + } + ` ); + + verifyDividedStyleSheet( output, 'styles-editor.css', expectedEditorResult ); + verifyDividedStyleSheet( output, 'styles-content.css', expectedContentResult ); +} ); + +test( 'should preserve all `@keyframes` queries and split it correctly', async () => { + const output = await generateBundle( + './fixtures/keyframes/input.ts', + { baseFileName: 'styles' } + ); + + const expectedEditorResult = removeWhitespace( + `.animation{ + animation:fadeIn 1s; + } + @keyframes fadeIn{ + from{ + opacity:0; + } + to{ opacity:1; } + } + ` ); + + const expectedContentResult = removeWhitespace( + `@media (forced-colors: none){ + .ck-content.animation-in-media-query{ + animation:ck-animation 1s ease-out; + } + } + @keyframes ck-animation{ + 0%{ + background-color:white; + } + 100%{ + background-color:black; + } + } + ` ); + + verifyDividedStyleSheet( output, 'styles-editor.css', expectedEditorResult ); + verifyDividedStyleSheet( output, 'styles-content.css', expectedContentResult ); +} ); From 374f836f4461970d95e2246c69a6e17d351e5443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 18 Jul 2024 15:05:11 +0200 Subject: [PATCH 2/9] Make linter happy. --- .../src/plugins/splitCss.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 97c4ee403..0d78be1f3 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -8,7 +8,7 @@ import type { Plugin, OutputBundle, NormalizedOutputOptions, EmittedAsset } from import type { Processor } from 'postcss'; import cssnano from 'cssnano'; import litePreset from 'cssnano-preset-lite'; -import { PurgeCSS } from 'purgecss' +import { PurgeCSS } from 'purgecss'; export interface RollupSplitCssOptions { @@ -33,32 +33,32 @@ const filter = createFilter( [ '**/*.css' ] ); const commonPurgeCssOptions = { fontFace: true, keyframes: true, - variables: true, -} + variables: true +}; const purgeCssOptionsForContent = { ...commonPurgeCssOptions, content: [], - safelist: { deep: [/^ck-content/] }, + safelist: { deep: [ /^ck-content/ ] }, blocklist: [] -} +}; const purgeCssOptionsForEditor = { ...commonPurgeCssOptions, // Pseudo class`:where` is preserved only if the appropriate html structure matches the CSS selector. // It's a temporary solution to avoid removing selectors for Show blocks styles where `:where` occurs. // See: https://github.com/FullHuman/purgecss/issues/978 - content: [ { + content: [ { raw: '
' + '
', - extension: 'html' - } ], - safelist: { - deep: [ /ck(?!-content)/, /^(?!.*ck)/ ], - }, + extension: 'html' + } ], + safelist: { + deep: [ /ck(?!-content)/, /^(?!.*ck)/ ] + }, // Option to preserve all CSS selectors that starts with `[dir=ltr/rtl]` attribute. - dynamicAttributes: [ 'dir' ] -} + dynamicAttributes: [ 'dir' ] +}; export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { const options: Required = Object.assign( { @@ -134,24 +134,24 @@ async function getSplittedStyleSheets( cssStylesheet: string, filename: string ) { raw: cssStylesheet }, - `${ filename }-content.css`, + `${ filename }-content.css` ] } ); - const purgeCSSResultForEditingView = await new PurgeCSS().purge({ + const purgeCSSResultForEditingView = await new PurgeCSS().purge( { ...purgeCssOptionsForEditor, css: [ { raw: cssStylesheet }, - `${ filename }-editor.css`, + `${ filename }-editor.css` ] } ); return { editorStylesContent: purgeCSSResultForContent[ 0 ]!.css, editingViewStylesContent: purgeCSSResultForEditingView[ 0 ]!.css - } + }; } /** @@ -174,7 +174,7 @@ async function unifyFileContentOutput( content: string = '', minimize: boolean ) */ async function normalizeStylesheet( content: string ): Promise { const normalizeContent = await cssnano( { preset: litePreset( { - normalizeWhitespace: false, + normalizeWhitespace: false } ) } ).process( content!, { from: undefined } ); return normalizeContent.css; From 96d3bb7b7b48d4cb0effce6ad7b1650d5e9789a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 18 Jul 2024 16:00:56 +0200 Subject: [PATCH 3/9] Code review fixes. --- .../src/plugins/splitCss.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 0d78be1f3..5f0526775 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -47,10 +47,24 @@ const purgeCssOptionsForEditor = { ...commonPurgeCssOptions, // Pseudo class`:where` is preserved only if the appropriate html structure matches the CSS selector. // It's a temporary solution to avoid removing selectors for Show blocks styles where `:where` occurs. + // For example this structure will be omitted without the HTML content: + // + // ```css + // .ck.ck-editor__editable.ck-editor__editable_inline.ck-show-blocks:not(.ck-widget) + // :where(figure.image, figure.table) figcaption { /* ... */ } + // ``` + // // See: https://github.com/FullHuman/purgecss/issues/978 content: [ { - raw: '
' + - '
', + raw: ` + +
+
+
+
+
+ + `, extension: 'html' } ], safelist: { @@ -97,13 +111,13 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-editor.css`, - source: await unifyFileContentOutput( editingViewStylesheet, options.minimize ) + source: options.minimize ? await minifyContent( editingViewStylesheet ) : editingViewStylesheet } ); this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-content.css`, - source: await unifyFileContentOutput( contentStylesheet, options.minimize ) + source: options.minimize ? await minifyContent( contentStylesheet ) : contentStylesheet } ); } }; @@ -158,11 +172,7 @@ async function getSplittedStyleSheets( cssStylesheet: string, filename: string ) * @param content is a `CSS` content. * @param minimize When set to `true` it will minify the content. */ -async function unifyFileContentOutput( content: string = '', minimize: boolean ): Promise { - if ( !minimize ) { - return content; - } - +async function minifyContent( content: string = '' ): Promise { const minifier = cssnano() as Processor; const minifiedResult = await minifier.process( content, { from: undefined } ); @@ -170,7 +180,7 @@ async function unifyFileContentOutput( content: string = '', minimize: boolean ) } /** - * Returns normalized stylesheet content. + * Safe and minimum CSS stylesheet transformation with just removing line breaks, comments and empty rules. */ async function normalizeStylesheet( content: string ): Promise { const normalizeContent = await cssnano( { preset: litePreset( { From b74e31c682b89fed06394c9aeabd105df2612a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 18 Jul 2024 16:04:16 +0200 Subject: [PATCH 4/9] Changed function description. --- packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 5f0526775..c3ff86e61 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -169,12 +169,11 @@ async function getSplittedStyleSheets( cssStylesheet: string, filename: string ) } /** - * @param content is a `CSS` content. - * @param minimize When set to `true` it will minify the content. + * Returns minified stylesheet content. */ -async function minifyContent( content: string = '' ): Promise { +async function minifyContent( stylesheetContent: string = '' ): Promise { const minifier = cssnano() as Processor; - const minifiedResult = await minifier.process( content, { from: undefined } ); + const minifiedResult = await minifier.process( stylesheetContent, { from: undefined } ); return minifiedResult.css; } From 2e6a2c63b2570ecc93a59d6d386fb61ccf331909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Thu, 18 Jul 2024 16:13:45 +0200 Subject: [PATCH 5/9] Update function description. --- packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index c3ff86e61..5c8db3ef1 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -179,7 +179,7 @@ async function minifyContent( stylesheetContent: string = '' ): Promise } /** - * Safe and minimum CSS stylesheet transformation with just removing line breaks, comments and empty rules. + * Safe and minimum CSS stylesheet transformation with removing comments and empty rules. */ async function normalizeStylesheet( content: string ): Promise { const normalizeContent = await cssnano( { preset: litePreset( { From 216403db3d3e84e4a17363247737717f0999f3fe Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Fri, 19 Jul 2024 09:34:51 +0200 Subject: [PATCH 6/9] Small refactor of the `splitCSS` plugin --- .../src/plugins/splitCss.ts | 99 +++++++------------ 1 file changed, 36 insertions(+), 63 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 5c8db3ef1..0e1c69462 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -8,7 +8,7 @@ import type { Plugin, OutputBundle, NormalizedOutputOptions, EmittedAsset } from import type { Processor } from 'postcss'; import cssnano from 'cssnano'; import litePreset from 'cssnano-preset-lite'; -import { PurgeCSS } from 'purgecss'; +import { PurgeCSS, type UserDefinedOptions } from 'purgecss'; export interface RollupSplitCssOptions { @@ -25,26 +25,25 @@ export interface RollupSplitCssOptions { minimize?: boolean; } -/** - * Filter files only with `css` extension. - */ const filter = createFilter( [ '**/*.css' ] ); -const commonPurgeCssOptions = { +const REGEX_FOR_REMOVING_VAR_WHITESPACE = /(?<=var\()\s+|\s+(?=\))/g; + +const COMMON_PURGE_OPTIONS = { fontFace: true, keyframes: true, variables: true }; -const purgeCssOptionsForContent = { - ...commonPurgeCssOptions, +const CONTENT_PURGE_OPTIONS = { + ...COMMON_PURGE_OPTIONS, content: [], safelist: { deep: [ /^ck-content/ ] }, blocklist: [] }; -const purgeCssOptionsForEditor = { - ...commonPurgeCssOptions, +const EDITOR_PURGE_OPTIONS = { + ...COMMON_PURGE_OPTIONS, // Pseudo class`:where` is preserved only if the appropriate html structure matches the CSS selector. // It's a temporary solution to avoid removing selectors for Show blocks styles where `:where` occurs. // For example this structure will be omitted without the HTML content: @@ -82,7 +81,7 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { return { name: 'cke5-split-css', - transform( code, id ) { + transform( code: string, id: string ): string | undefined { if ( !filter( id ) ) { return; } @@ -90,9 +89,9 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { return ''; }, - async generateBundle( output: NormalizedOutputOptions, bundle: OutputBundle ) { + async generateBundle( output: NormalizedOutputOptions, bundle: OutputBundle ): Promise { // Get stylesheet from output bundle. - const cssStylesheetFromBundle = getCssStylesheet( bundle ); + const css = getCssStylesheet( bundle ); // Some of CSS variables are used with spaces after/before brackets: // var( --var-name ) @@ -100,31 +99,30 @@ export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { // See: https://github.com/FullHuman/purgecss/issues/1264 // // Till it's not solved we need to remove spaces from variables. - const cssStylesheet = removeWhitespaceFromVars( cssStylesheetFromBundle! ); + const normalizedCss = css.replace( REGEX_FOR_REMOVING_VAR_WHITESPACE, '' ); - // Generate split stylesheets for editor, content, and one that contains them all. - const { editorStylesContent, editingViewStylesContent } = await getSplittedStyleSheets( cssStylesheet, options.baseFileName ); - const contentStylesheet = await normalizeStylesheet( editorStylesContent! ); - const editingViewStylesheet = await normalizeStylesheet( editingViewStylesContent! ); + // Generate stylesheets for editor and content. + const editorStyles = await getStyles( normalizedCss, EDITOR_PURGE_OPTIONS ); + const contentStyles = await getStyles( normalizedCss, CONTENT_PURGE_OPTIONS ); // Emit those styles into files. this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-editor.css`, - source: options.minimize ? await minifyContent( editingViewStylesheet ) : editingViewStylesheet + source: options.minimize ? await minifyContent( editorStyles ) : editorStyles } ); this.emitFile( { type: 'asset', fileName: `${ options.baseFileName }-content.css`, - source: options.minimize ? await minifyContent( contentStylesheet ) : contentStylesheet + source: options.minimize ? await minifyContent( contentStyles ) : contentStyles } ); } }; } /** - * @param bundle provides the full list of files being written or generated along with their details. + * Returns CSS stylesheet from the output bundle. */ function getCssStylesheet( bundle: OutputBundle ) { const cssStylesheetChunk = Object @@ -139,61 +137,36 @@ function getCssStylesheet( bundle: OutputBundle ) { } /** - * Returns split stylesheets for editor, content, and one that contains all styles. + * Returns stylesheets content after removing comments and unused or empty CSS rules. */ -async function getSplittedStyleSheets( cssStylesheet: string, filename: string ): Promise> { - const purgeCSSResultForContent = await new PurgeCSS().purge( { - ...purgeCssOptionsForContent, - css: [ - { - raw: cssStylesheet - }, - `${ filename }-content.css` - ] +async function getStyles( styles: string, purgeConfig: Omit ): Promise { + const result = await new PurgeCSS().purge( { + ...purgeConfig, + css: [ { raw: styles } ] } ); - const purgeCSSResultForEditingView = await new PurgeCSS().purge( { - ...purgeCssOptionsForEditor, - css: [ - { - raw: cssStylesheet - }, - `${ filename }-editor.css` - ] - } ); - - return { - editorStylesContent: purgeCSSResultForContent[ 0 ]!.css, - editingViewStylesContent: purgeCSSResultForEditingView[ 0 ]!.css - }; -} - -/** - * Returns minified stylesheet content. - */ -async function minifyContent( stylesheetContent: string = '' ): Promise { - const minifier = cssnano() as Processor; - const minifiedResult = await minifier.process( stylesheetContent, { from: undefined } ); - - return minifiedResult.css; + return cleanContent( result[ 0 ]!.css ); } /** * Safe and minimum CSS stylesheet transformation with removing comments and empty rules. */ -async function normalizeStylesheet( content: string ): Promise { - const normalizeContent = await cssnano( { preset: litePreset( { - normalizeWhitespace: false - } ) } ).process( content!, { from: undefined } ); +async function cleanContent( content: string ): Promise { + const normalizeContent = await cssnano( { + preset: litePreset( { + normalizeWhitespace: false + } ) + } ).process( content!, { from: undefined } ); return normalizeContent.css; } -/* - * Removes whitespace between brackets from CSS variables. +/** + * Returns minified stylesheet content. */ -function removeWhitespaceFromVars( content: string ): string { - const regex = /(?<=var\()\s+|\s+(?=\))/g; +async function minifyContent( stylesheetContent: string = '' ): Promise { + const minifier = cssnano() as Processor; + const minifiedResult = await minifier.process( stylesheetContent, { from: undefined } ); - return content.replace( regex, '' ); + return minifiedResult.css; } From 3992f11f59a2cec1362997e76f63894d77da4877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Fri, 19 Jul 2024 10:49:10 +0200 Subject: [PATCH 7/9] Set fontFace, keyframes and variables to false, to preserve them across splitted stylesheets. --- .../ckeditor5-dev-build-tools/src/config.ts | 2 +- .../src/plugins/splitCss.ts | 9 ++- .../combine-root-definitions/file1.css | 7 --- .../combine-root-definitions/file2.css | 7 --- .../combine-root-definitions/input.ts | 9 --- .../fixtures/nested-css-variables/file1.css | 5 -- .../fixtures/omit-root-definitions/file1.css | 9 +-- .../fixtures/omit-root-definitions/file2.css | 2 + .../tests/plugins/splitCss/splitCss.test.ts | 61 ++++++++----------- 9 files changed, 37 insertions(+), 74 deletions(-) delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file1.css delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file2.css delete mode 100644 packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/input.ts diff --git a/packages/ckeditor5-dev-build-tools/src/config.ts b/packages/ckeditor5-dev-build-tools/src/config.ts index b954b1526..28c159789 100644 --- a/packages/ckeditor5-dev-build-tools/src/config.ts +++ b/packages/ckeditor5-dev-build-tools/src/config.ts @@ -145,7 +145,7 @@ export async function getRollupConfig( options: BuildOptions ) { } ), /** - * Allows using imports, mixins and nesting in CSS and exctacts output CSS to a separate file. + * Allows using imports, mixins and nesting in CSS and extracts output CSS to a separate file. */ styles( { mode: [ 'extract', cssFileName ], diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 0e1c69462..2e1a1585a 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -29,10 +29,13 @@ const filter = createFilter( [ '**/*.css' ] ); const REGEX_FOR_REMOVING_VAR_WHITESPACE = /(?<=var\()\s+|\s+(?=\))/g; +// We must preserve all variables, keyframes and font faces. +// For example this is caused by case when some of them can be defined in the `ckeditor5` +// but used in `ckeditor5-premium-features` stylesheet and vice versa. const COMMON_PURGE_OPTIONS = { - fontFace: true, - keyframes: true, - variables: true + fontFace: false, + keyframes: false, + variables: false }; const CONTENT_PURGE_OPTIONS = { diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file1.css deleted file mode 100644 index 642b19fc2..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file1.css +++ /dev/null @@ -1,7 +0,0 @@ -:root { - --variable1: blue; -} - -h1 { - color: var(--variable1); -} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file2.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file2.css deleted file mode 100644 index 310b27b71..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/file2.css +++ /dev/null @@ -1,7 +0,0 @@ -:root { - --variable2: red; -} - -p { - color: var(--variable2); -} diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/input.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/input.ts deleted file mode 100644 index bc29c5c6a..000000000 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/combine-root-definitions/input.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import './file1.css'; -import './file2.css'; - -export const test = 123; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css index becc2ef8f..7e9a8c2f5 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/nested-css-variables/file1.css @@ -5,11 +5,6 @@ --ck-variable-2: var(--ck-nonexistent-variable, var(--ck-variable-3)); --ck-variable-3: 0.6em; - /* Test whether the unused variables are removed. */ - --ck-unused-primary: green; - --ck-unused-secondary: red; - --ck-unused-variable-2: var(--ck-unused-primary, var(--ck-unused-secondary)); - /* Test CSS variables with calc() function. */ --ck-variable-4: 1px; --ck-variable-5: 1em; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file1.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file1.css index a2c370a1a..addc266dc 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file1.css +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file1.css @@ -1,12 +1,9 @@ :root { - --variable1: blue; - --variable2: red; - --variable3: red; - --variable4: pink; + /* empty */ } .ck-feature { - color: var(--variable1); - background-color: var(--variable2); + color: red; + background-color: blue; } diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file2.css b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file2.css index 0c6d23883..440be94b5 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file2.css +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/fixtures/omit-root-definitions/file2.css @@ -1,3 +1,5 @@ +:root { /* empty */ } + .ck-content.ck-feature { color: red; background-color: blue; diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts index a14b1192d..5a449c1fa 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts @@ -120,32 +120,7 @@ test( 'should ignore `CSS` comments', async () => { verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); -test( 'should combine `:root` declarations from multiple entries into one', async () => { - const output = await generateBundle( - './fixtures/combine-root-definitions/input.ts', - { baseFileName: 'styles' } - ); - - const expectedResult = removeWhitespace( - `:root{ - --variable1:blue; - } - h1{ - color:var(--variable1); - } - :root{ - --variable2:red; - } - p{ - color:var(--variable2); - } - ` ); - - verifyDividedStyleSheet( output, 'styles-editor.css', expectedResult ); - verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); -} ); - -test( 'should filter `:root` declaration based on `CSS` variables usage', async () => { +test( 'should not filter `:root` declaration based on `CSS` variables usage', async () => { const output = await generateBundle( './fixtures/filter-root-definitions/input.ts', { baseFileName: 'styles' } @@ -155,6 +130,8 @@ test( 'should filter `:root` declaration based on `CSS` variables usage', async `:root{ --variable1:blue; --variable2:red; + --variable3:red; + --variable4:pink; } .ck-feature{ color:var(--variable1); @@ -164,6 +141,8 @@ test( 'should filter `:root` declaration based on `CSS` variables usage', async const expectedContentResult = removeWhitespace( `:root{ + --variable1:blue; + --variable2:red; --variable3:red; --variable4:pink; } @@ -177,20 +156,16 @@ test( 'should filter `:root` declaration based on `CSS` variables usage', async verifyDividedStyleSheet( output, 'styles-content.css', expectedContentResult ); } ); -test( 'should omit `:root` declaration when it\'s not exist', async () => { +test( 'should omit `:root` declaration when it\'s empty', async () => { const output = await generateBundle( './fixtures/omit-root-definitions/input.ts', { baseFileName: 'styles' } ); const expectedEditorResult = removeWhitespace( - `:root{ - --variable1:blue; - --variable2:red; - } - .ck-feature{ - color:var(--variable1); - background-color:var(--variable2); + `.ck-feature{ + color:red; + background-color:blue; } ` ); @@ -343,7 +318,7 @@ test( 'should preserve all `@media` queries and split it correctly', async () => verifyDividedStyleSheet( output, 'styles-content.css', expectedContentResult ); } ); -test( 'should preserve all `@keyframes` queries and split it correctly', async () => { +test( 'should preserve all `@keyframes` queries across splitted stylesheet files', async () => { const output = await generateBundle( './fixtures/keyframes/input.ts', { baseFileName: 'styles' } @@ -359,10 +334,24 @@ test( 'should preserve all `@keyframes` queries and split it correctly', async ( } to{ opacity:1; } } + @keyframes ck-animation{ + 0%{ + background-color:white; + } + 100%{ + background-color:black; + } + } ` ); const expectedContentResult = removeWhitespace( - `@media (forced-colors: none){ + `@keyframes fadeIn{ + from{ + opacity:0; + } + to{ opacity:1; } + } + @media (forced-colors: none){ .ck-content.animation-in-media-query{ animation:ck-animation 1s ease-out; } From 6eb70f04797dadd50ae3bfced08ecb64e9a74bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Fri, 19 Jul 2024 10:52:11 +0200 Subject: [PATCH 8/9] Fix comment. --- packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 2e1a1585a..32a23a186 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -29,7 +29,7 @@ const filter = createFilter( [ '**/*.css' ] ); const REGEX_FOR_REMOVING_VAR_WHITESPACE = /(?<=var\()\s+|\s+(?=\))/g; -// We must preserve all variables, keyframes and font faces. +// We must preserve all variables, keyframes and font faces in splitted stylesheets. // For example this is caused by case when some of them can be defined in the `ckeditor5` // but used in `ckeditor5-premium-features` stylesheet and vice versa. const COMMON_PURGE_OPTIONS = { From d6449dc89f07fc231fe5260d3ce15a9b35955369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szcz=C4=99=C5=9Bniak?= Date: Fri, 19 Jul 2024 12:30:58 +0200 Subject: [PATCH 9/9] Filter variables, keyframes and fontface only for content stylesheet. --- .../src/plugins/splitCss.ts | 24 +++++++++---------- .../tests/plugins/splitCss/splitCss.test.ts | 14 +++-------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts index 32a23a186..a6714ba1d 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/splitCss.ts @@ -29,24 +29,16 @@ const filter = createFilter( [ '**/*.css' ] ); const REGEX_FOR_REMOVING_VAR_WHITESPACE = /(?<=var\()\s+|\s+(?=\))/g; -// We must preserve all variables, keyframes and font faces in splitted stylesheets. -// For example this is caused by case when some of them can be defined in the `ckeditor5` -// but used in `ckeditor5-premium-features` stylesheet and vice versa. -const COMMON_PURGE_OPTIONS = { - fontFace: false, - keyframes: false, - variables: false -}; - const CONTENT_PURGE_OPTIONS = { - ...COMMON_PURGE_OPTIONS, content: [], safelist: { deep: [ /^ck-content/ ] }, - blocklist: [] + blocklist: [], + fontFace: true, + keyframes: true, + variables: true }; const EDITOR_PURGE_OPTIONS = { - ...COMMON_PURGE_OPTIONS, // Pseudo class`:where` is preserved only if the appropriate html structure matches the CSS selector. // It's a temporary solution to avoid removing selectors for Show blocks styles where `:where` occurs. // For example this structure will be omitted without the HTML content: @@ -73,7 +65,13 @@ const EDITOR_PURGE_OPTIONS = { deep: [ /ck(?!-content)/, /^(?!.*ck)/ ] }, // Option to preserve all CSS selectors that starts with `[dir=ltr/rtl]` attribute. - dynamicAttributes: [ 'dir' ] + dynamicAttributes: [ 'dir' ], + // We must preserve all variables, keyframes and font faces in splitted stylesheets. + // For example this is caused by case when some of them can be defined in the `ckeditor5` + // but used in `ckeditor5-premium-features` stylesheet and vice versa. + fontFace: false, + keyframes: false, + variables: false }; export function splitCss( pluginOptions: RollupSplitCssOptions ): Plugin { diff --git a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts index 5a449c1fa..879843584 100644 --- a/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/plugins/splitCss/splitCss.test.ts @@ -120,7 +120,7 @@ test( 'should ignore `CSS` comments', async () => { verifyDividedStyleSheet( output, 'styles-content.css', '\n' ); } ); -test( 'should not filter `:root` declaration based on `CSS` variables usage', async () => { +test( 'should filter `:root` declaration based on `CSS` variables usage only for `content` stylesheet', async () => { const output = await generateBundle( './fixtures/filter-root-definitions/input.ts', { baseFileName: 'styles' } @@ -141,8 +141,6 @@ test( 'should not filter `:root` declaration based on `CSS` variables usage', as const expectedContentResult = removeWhitespace( `:root{ - --variable1:blue; - --variable2:red; --variable3:red; --variable4:pink; } @@ -318,7 +316,7 @@ test( 'should preserve all `@media` queries and split it correctly', async () => verifyDividedStyleSheet( output, 'styles-content.css', expectedContentResult ); } ); -test( 'should preserve all `@keyframes` queries across splitted stylesheet files', async () => { +test( 'should filter `@keyframes` queries based on class names usage only for `content` stylesheet', async () => { const output = await generateBundle( './fixtures/keyframes/input.ts', { baseFileName: 'styles' } @@ -345,13 +343,7 @@ test( 'should preserve all `@keyframes` queries across splitted stylesheet files ` ); const expectedContentResult = removeWhitespace( - `@keyframes fadeIn{ - from{ - opacity:0; - } - to{ opacity:1; } - } - @media (forced-colors: none){ + `@media (forced-colors: none){ .ck-content.animation-in-media-query{ animation:ck-animation 1s ease-out; }