| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /* eslint-env node */
- const path = require( 'path' );
- const fs = require( 'fs' );
- const webpack = require( 'webpack' );
- const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
- const { version } = require( '../../package.json' );
- const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
- const DOCUMENTATION_URL = 'https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html';
- const VARIABLE_DEFINITION_REGEXP = /(--[\w-]+):\s+(.*);/g;
- const VARIABLE_USAGE_REGEXP = /var\((--[\w-]+)\)/g;
- const contentRules = {
- selector: [],
- variables: [],
- atRules: {}
- };
- const webpackConfig = getWebpackConfig();
- const packagesPath = path.join( process.cwd(), 'packages' );
- runWebpack( webpackConfig )
- .then( () => {
- // All variables are placed inside the `:root` selector. Let's extract their names and values as a map.
- const cssVariables = new Map( contentRules.variables
- .map( rule => {
- // Let's extract all of them as an array of pairs: [ name, value ].
- const allRules = [];
- let match;
- while ( ( match = VARIABLE_DEFINITION_REGEXP.exec( rule.css ) ) ) {
- allRules.push( [ match[ 1 ], match[ 2 ] ] );
- }
- return allRules;
- } )
- .reduce( ( previousValue, currentValue ) => {
- // And simplify nested arrays as a flattened array.
- previousValue.push( ...currentValue );
- return previousValue;
- }, [] ) );
- // CSS variables that are used by the `.ck-content` selector.
- const usedVariables = new Set();
- // `.ck-content` selectors.
- const selectorCss = transformCssRules( contentRules.selector );
- // Find all CSS variables inside the `.ck-content` selector.
- let match;
- while ( ( match = VARIABLE_USAGE_REGEXP.exec( selectorCss ) ) ) {
- usedVariables.add( match[ 1 ] );
- }
- // We need to also look at whether any of the used variables requires the value of other variables.
- let clearRun = false;
- // We need to process all variables as long as the entire collection won't be changed.
- while ( !clearRun ) {
- clearRun = true;
- // For every used variable...
- for ( const variable of usedVariables ) {
- const value = cssVariables.get( variable );
- let match;
- // ...find its value and check whether it requires another variable.
- while ( ( match = VARIABLE_USAGE_REGEXP.exec( value ) ) ) {
- // If so, mark the entire `while()` block as it should be checked once again.
- // Also, add the new variable to the used variables collection.
- if ( !usedVariables.has( match[ 1 ] ) ) {
- clearRun = false;
- usedVariables.add( match[ 1 ] );
- }
- }
- }
- }
- const atRulesDefinitions = [];
- // Additional at-rules.
- for ( const atRuleName of Object.keys( contentRules.atRules ) ) {
- const rules = transformCssRules( contentRules.atRules[ atRuleName ] )
- .split( '\n' )
- .map( line => `\t${ line }` )
- .join( '\n' );
- atRulesDefinitions.push( `@${ atRuleName } {\n${ rules }\n}` );
- }
- // Build the final content of the CSS file.
- let data = [
- '/*',
- ` * CKEditor 5 (v${ version }) content styles.`,
- ` * Generated on ${ new Date().toUTCString() }.`,
- ` * For more information, check out ${ DOCUMENTATION_URL }`,
- ' */\n\n'
- ].join( '\n' );
- data += ':root {\n';
- for ( const variable of [ ...usedVariables ].sort() ) {
- data += `\t${ variable }: ${ cssVariables.get( variable ) };\n`;
- }
- data += '}\n\n';
- data += selectorCss;
- data += '\n';
- data += atRulesDefinitions.join( '\n' );
- return writeFile( path.join( DESTINATION_DIRECTORY, 'content-styles.css' ), data );
- } )
- .then( () => {
- console.log( `Content styles have been extracted to ${ path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) }` );
- } )
- .catch( err => {
- console.log( err );
- } );
- /**
- * Prepares the configuration for webpack.
- *
- * @returns {Object}
- */
- function getWebpackConfig() {
- const postCssConfig = styles.getPostCssConfig( {
- themeImporter: {
- themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
- },
- minify: false
- } );
- const contentStylesPlugin = require( './content-styles/list-content-styles' )( { contentRules } );
- postCssConfig.plugins.push( contentStylesPlugin );
- return {
- mode: 'development',
- devtool: 'source-map',
- entry: {
- ckeditor5: path.join( __dirname, 'content-styles', 'ckeditor.js' )
- },
- output: {
- path: DESTINATION_DIRECTORY,
- filename: '[name].js'
- },
- // Configure the paths so building CKEditor 5 snippets work even if the script
- // is triggered from a directory outside `ckeditor5` (e.g. in a multi-project case).
- resolve: {
- modules: getModuleResolvePaths()
- },
- resolveLoader: {
- modules: getModuleResolvePaths()
- },
- module: {
- rules: [
- {
- test: /\.svg$/,
- use: [ 'raw-loader' ]
- },
- {
- test: /\.css$/,
- use: [
- 'style-loader',
- {
- loader: 'postcss-loader',
- options: postCssConfig
- }
- ]
- }
- ]
- }
- };
- }
- /**
- * @param {Object} webpackConfig
- * @returns {Promise}
- */
- function runWebpack( webpackConfig ) {
- return new Promise( ( resolve, reject ) => {
- webpack( webpackConfig, ( err, stats ) => {
- if ( err ) {
- reject( err );
- } else if ( stats.hasErrors() ) {
- reject( new Error( stats.toString() ) );
- } else {
- resolve();
- }
- } );
- } );
- }
- /**
- * @returns {Array.<String>}
- */
- function getModuleResolvePaths() {
- return [
- path.resolve( __dirname, '..', '..', 'node_modules' ),
- 'node_modules'
- ];
- }
- function writeFile( file, data ) {
- return new Promise( ( resolve, reject ) => {
- fs.writeFile( file, data, err => {
- if ( err ) {
- return reject( err );
- }
- return resolve();
- } );
- } );
- }
- /**
- * @param {Array} rules
- * @returns {String}
- */
- function transformCssRules( rules ) {
- return rules
- .map( rule => {
- // Removes all comments from the rule definition.
- const cssAsArray = rule.css.replace( /\/\*[^*]+\*\//g, '' ).split( '\n' );
- // We want to fix invalid indentations. We need to find a number of how many indentations we want to remove.
- // Because the last line ends the block, we can use this value.
- const lastLineIndent = cssAsArray[ cssAsArray.length - 1 ].length - 1;
- const css = cssAsArray
- .filter( line => line.trim().length > 0 )
- .map( ( line, index ) => {
- // Do not touch the first line. It is always correct.
- if ( index === 0 ) {
- return line;
- }
- const newLine = line.slice( lastLineIndent );
- // If a line is not a CSS definition, do not touch it.
- if ( !newLine.match( /[A-Z-_0-9]+:/i ) ) {
- return newLine;
- }
- // The line is a CSS definition – let's check whether it ends with a semicolon.
- if ( newLine.endsWith( ';' ) ) {
- return newLine;
- }
- return newLine + ';';
- } )
- .join( '\n' );
- return `/* ${ rule.file.replace( packagesPath + path.sep, '' ) } */\n${ css }`;
- } )
- .filter( rule => {
- // 1st: path to the CSS file, 2nd: selector definition - start block, 3rd: end block
- // If the rule contains only 3 lines, it means that it does not define any rules.
- return rule.split( '\n' ).length > 3;
- } )
- .join( '\n' );
- }
|