Skip to content

Commit 5cda75f

Browse files
authored
Add collectStylesheets that allows to pick stylesheets from provided urls. (#17752)
Feature (utils): Add a `collectStylesheets` helper function to retrieve stylesheets from the provided URLs.
1 parent a8f3527 commit 5cda75f

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4+
*/
5+
6+
/**
7+
* @module utils/collectstylesheets
8+
*/
9+
10+
/**
11+
* A helper function for getting concatenated CSS rules from external stylesheets.
12+
*
13+
* @param stylesheets An array of stylesheet paths delivered by the user through the plugin configuration.
14+
*/
15+
export default async function collectStylesheets( stylesheets?: Array<string> ): Promise<string> {
16+
if ( !stylesheets ) {
17+
return '';
18+
}
19+
20+
const results = await Promise.all(
21+
stylesheets.map( async stylesheet => {
22+
if ( stylesheet === 'EDITOR_STYLES' ) {
23+
return getEditorStyles();
24+
}
25+
26+
const response = await window.fetch( stylesheet );
27+
28+
return response.text();
29+
} )
30+
);
31+
32+
return results.join( ' ' ).trim();
33+
}
34+
35+
/**
36+
* A helper function for getting the basic editor content styles for the `.ck-content` class
37+
* and all CSS variables defined in the document.
38+
*/
39+
function getEditorStyles(): string {
40+
const editorStyles = [];
41+
const editorCSSVariables = [];
42+
43+
for ( const styleSheet of Array.from( document.styleSheets ) ) {
44+
const ownerNode = styleSheet.ownerNode as Element;
45+
46+
if ( ownerNode.hasAttribute( 'data-cke' ) ) {
47+
for ( const rule of Array.from( styleSheet.cssRules ) ) {
48+
if ( rule.cssText.indexOf( '.ck-content' ) !== -1 ) {
49+
editorStyles.push( rule.cssText );
50+
} else if ( rule.cssText.indexOf( ':root' ) !== -1 ) {
51+
editorCSSVariables.push( rule.cssText );
52+
}
53+
}
54+
}
55+
}
56+
57+
if ( !editorStyles.length ) {
58+
console.warn(
59+
'The editor stylesheet could not be found in the document. ' +
60+
'Check your webpack config - style-loader should use data-cke=true attribute for the editor stylesheet.'
61+
);
62+
}
63+
64+
// We want to trim the returned value in case of `[ "", "", ... ]`.
65+
return [ ...editorCSSVariables, ...editorStyles ].join( ' ' ).trim();
66+
}

packages/ckeditor5-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export { default as delay, type DelayedFunc } from './delay.js';
9494
export { default as wait } from './wait.js';
9595
export { default as parseBase64EncodedObject } from './parsebase64encodedobject.js';
9696
export { default as crc32, type CRCData } from './crc32.js';
97+
export { default as collectStylesheets } from './collectstylesheets.js';
9798

9899
export * from './unicode.js';
99100

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4+
*/
5+
6+
/* global document, window, console, Response */
7+
8+
import collectStylesheets from '../src/collectstylesheets.js';
9+
10+
describe( 'collectStylesheets', () => {
11+
let styleSheetsMock;
12+
13+
beforeEach( () => {
14+
styleSheetsMock = [
15+
{
16+
ownerNode: {
17+
hasAttribute: name => name === 'data-cke'
18+
},
19+
cssRules: [
20+
{ cssText: ':root { --variable1: white; }' },
21+
{ cssText: '.ck-content { color: black }' },
22+
{ cssText: '.some-styles { color: red }' }
23+
]
24+
},
25+
{
26+
ownerNode: {
27+
hasAttribute: name => name === 'data-cke'
28+
},
29+
cssRules: [
30+
{ cssText: ':root { --variable2: blue; }' },
31+
{ cssText: '.ck-content { background: white }' }
32+
]
33+
},
34+
{
35+
ownerNode: {
36+
hasAttribute: () => false
37+
},
38+
cssRules: [
39+
{ cssText: 'h2 { color: black }' }
40+
]
41+
}
42+
];
43+
44+
sinon.stub( document, 'styleSheets' ).get( () => styleSheetsMock );
45+
} );
46+
47+
afterEach( () => {
48+
sinon.restore();
49+
} );
50+
51+
it( 'should not return any styles if no paths to stylesheets provided', async () => {
52+
expect( await collectStylesheets( undefined ) ).to.equal( '' );
53+
} );
54+
55+
it( 'should log into the console when ".ck-content" styles are missing', async () => {
56+
styleSheetsMock = [ {
57+
ownerNode: {
58+
hasAttribute: name => name === 'data-cke'
59+
},
60+
cssRules: [
61+
{ cssText: ':root { --variable: white; }' }
62+
]
63+
} ];
64+
65+
const consoleSpy = sinon.stub( console, 'warn' );
66+
67+
await collectStylesheets( [ 'EDITOR_STYLES' ] );
68+
69+
sinon.assert.calledOnce( consoleSpy );
70+
} );
71+
72+
it( 'should get ".ck-content" styles when "EDITOR_STYLES" token is provided', async () => {
73+
const consoleSpy = sinon.stub( console, 'warn' );
74+
75+
const styles = await collectStylesheets( [ './foo.css', 'EDITOR_STYLES' ] );
76+
77+
sinon.assert.notCalled( consoleSpy );
78+
79+
expect( styles.length > 0 ).to.be.true;
80+
expect( styles.indexOf( '.ck-content' ) !== -1 ).to.be.true;
81+
} );
82+
83+
it( 'should get styles from multiple stylesheets with data-cke attribute', async () => {
84+
const styles = await collectStylesheets( [ 'EDITOR_STYLES' ] );
85+
86+
expect( styles ).to.include( ':root { --variable1: white; }' );
87+
expect( styles ).to.include( ':root { --variable2: blue; }' );
88+
expect( styles ).to.include( '.ck-content { color: black }' );
89+
expect( styles ).to.include( '.ck-content { background: white }' );
90+
} );
91+
92+
it( 'should collect all :root styles from stylesheets with data-cke attribute', async () => {
93+
const styles = await collectStylesheets( [ 'EDITOR_STYLES' ] );
94+
95+
expect( styles ).to.include( '--variable1: white' );
96+
expect( styles ).to.include( '--variable2: blue' );
97+
} );
98+
99+
it( 'should fetch stylesheets from the provided paths and return concat result', async () => {
100+
sinon
101+
.stub( window, 'fetch' )
102+
.onFirstCall().resolves( new Response( '.foo { color: green; }' ) )
103+
.onSecondCall().resolves( new Response( '.bar { color: red; }' ) );
104+
105+
const styles = await collectStylesheets( [ './foo.css', './bar.css' ] );
106+
107+
expect( styles ).to.equal( '.foo { color: green; } .bar { color: red; }' );
108+
} );
109+
} );

0 commit comments

Comments
 (0)