| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- /**
- * @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 cwd = process.cwd();
- const path = require( 'path' );
- const fs = require( 'fs' );
- const chalk = require( 'chalk' );
- const glob = require( 'glob' );
- const mkdirp = require( 'mkdirp' );
- const postcss = require( 'postcss' );
- const webpack = require( 'webpack' );
- const Table = require( 'cli-table' );
- const { tools, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
- const { version } = require( '../../package.json' );
- const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
- const CONTENT_STYLES_GUIDE_PATH = path.join( __dirname, '..', '..', 'docs', 'builds', 'guides', 'integration', 'content-styles.md' );
- const CONTENT_STYLES_DETAILS_PATH = path.join( __dirname, 'content-styles-details.json' );
- 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 contentStylesDetails = require( CONTENT_STYLES_DETAILS_PATH );
- // An array of objects with plugins used to generate the current version of the content styles.
- let foundModules;
- const contentRules = {
- selector: [],
- variables: [],
- atRules: {}
- };
- const packagesPath = path.join( cwd, 'packages' );
- const shouldCommitChanges = process.argv.includes( '--commit' );
- logProcess( 'Gathering all CKEditor 5 modules...' );
- getCkeditor5ModulePaths()
- .then( files => {
- console.log( `Found ${ files.length } files.` );
- logProcess( 'Filtering CKEditor 5 plugins...' );
- let promise = Promise.resolve();
- const ckeditor5Modules = [];
- for ( const modulePath of files ) {
- promise = promise.then( () => {
- return checkWhetherIsCKEditor5Plugin( modulePath )
- .then( isModule => {
- if ( isModule ) {
- ckeditor5Modules.push( path.join( cwd, modulePath ) );
- }
- } );
- } );
- }
- return promise.then( () => ckeditor5Modules );
- } )
- .then( ckeditor5Modules => {
- console.log( `Found ${ ckeditor5Modules.length } plugins.` );
- logProcess( 'Generating source file...' );
- return mkdirp( DESTINATION_DIRECTORY ).then( () => generateCKEditor5Source( ckeditor5Modules ) );
- } )
- .then( ckeditor5Modules => {
- foundModules = ckeditor5Modules;
- logProcess( 'Building the editor...' );
- const webpackConfig = getWebpackConfig();
- return runWebpack( webpackConfig );
- } )
- .then( () => {
- logProcess( 'Preparing the content styles file...' );
- // 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' ) }` );
- logProcess( 'Looking for new plugins...' );
- const newPlugins = findNewPlugins( foundModules, contentStylesDetails.plugins );
- if ( newPlugins.length ) {
- console.log( 'Found new plugins.' );
- displayNewPluginsTable( newPlugins );
- } else {
- console.log( 'Previous and current versions of the content styles stylesheet were generated with the same set of plugins.' );
- }
- if ( !shouldCommitChanges ) {
- logProcess( 'Done.' );
- return Promise.resolve();
- }
- if ( newPlugins.length ) {
- logProcess( 'Updating the content styles details file...' );
- tools.updateJSONFile( CONTENT_STYLES_DETAILS_PATH, json => {
- const newPluginsObject = {};
- for ( const data of foundModules ) {
- const modulePath = normalizePath( data.modulePath.replace( cwd + path.sep, '' ) );
- newPluginsObject[ modulePath ] = data.pluginName;
- }
- json.plugins = newPluginsObject;
- return json;
- } );
- }
- logProcess( 'Updating the content styles guide...' );
- const promises = [
- readFile( CONTENT_STYLES_GUIDE_PATH ),
- readFile( path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) )
- ];
- return Promise.all( promises )
- .then( ( [ guideContent, newContentStyles ] ) => {
- guideContent = guideContent.replace( /```css([^`]+)```/, '```css\n' + newContentStyles + '\n```' );
- return writeFile( CONTENT_STYLES_GUIDE_PATH, guideContent );
- } )
- .then( () => {
- logProcess( 'Saving and committing...' );
- const contentStyleGuide = CONTENT_STYLES_GUIDE_PATH.replace( cwd + path.sep, '' );
- const contentStyleDetails = CONTENT_STYLES_DETAILS_PATH.replace( cwd + path.sep, '' );
- // Commit the documentation.
- if ( exec( `git diff --name-only ${ contentStyleGuide } ${ contentStyleDetails }` ).trim().length ) {
- exec( `git add ${ contentStyleGuide } ${ contentStyleDetails }` );
- exec( 'git commit -m "Docs (ckeditor5): Updated the content styles stylesheet."' );
- console.log( 'Successfully updated the content styles guide.' );
- } else {
- console.log( 'Nothing to commit. The content styles guide is up to date.' );
- }
- logProcess( 'Done.' );
- } );
- } )
- .catch( err => {
- console.log( err );
- } );
- /**
- * Resolves the promise with an array of paths to CKEditor 5 modules.
- *
- * @returns {Promise.<Array>}
- */
- function getCkeditor5ModulePaths() {
- return new Promise( ( resolve, reject ) => {
- glob( 'packages/*/src/**/*.js', ( err, files ) => {
- if ( err ) {
- return reject( err );
- }
- return resolve( files );
- } );
- } );
- }
- /**
- * Resolves the promise with a boolean value that indicates whether the module under `modulePath` is the CKEditor 5 plugin.
- *
- * @param modulePath
- * @returns {Promise.<Boolean>}
- */
- function checkWhetherIsCKEditor5Plugin( modulePath ) {
- return readFile( path.join( cwd, modulePath ) )
- .then( content => {
- const pluginName = path.basename( modulePath, '.js' );
- if ( content.match( new RegExp( `export default class ${ pluginName } extends Plugin`, 'i' ) ) ) {
- return Promise.resolve( true );
- }
- return Promise.resolve( false );
- } );
- }
- /**
- * Generates a source file that will be used to build the editor.
- *
- * @param {Array.<String>} ckeditor5Modules Paths to CKEditor 5 modules.
- * @returns {Promise>}
- */
- function generateCKEditor5Source( ckeditor5Modules ) {
- ckeditor5Modules = ckeditor5Modules.map( modulePath => {
- const pluginName = capitalize( path.basename( modulePath, '.js' ) );
- return { modulePath, pluginName };
- } );
- const sourceFileContent = [
- '/**',
- ` * @license Copyright (c) 2003-${ new Date().getFullYear() }, CKSource - Frederico Knabben. All rights reserved.`,
- ' * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license',
- ' */',
- '',
- '// The editor creator to use.',
- 'import ClassicEditorBase from \'@ckeditor/ckeditor5-editor-classic/src/classiceditor\';',
- ''
- ];
- for ( const { modulePath, pluginName } of ckeditor5Modules ) {
- sourceFileContent.push( `import ${ pluginName } from '${ modulePath }';` );
- }
- sourceFileContent.push( '' );
- sourceFileContent.push( 'export default class ClassicEditor extends ClassicEditorBase {}' );
- sourceFileContent.push( '' );
- sourceFileContent.push( '// Plugins to include in the build.' );
- sourceFileContent.push( 'ClassicEditor.builtinPlugins = [' );
- for ( const { pluginName } of ckeditor5Modules ) {
- sourceFileContent.push( '\t' + pluginName + ',' );
- }
- sourceFileContent.push( '];' );
- return writeFile( path.join( DESTINATION_DIRECTORY, 'source.js' ), sourceFileContent.join( '\n' ) )
- .then( () => ckeditor5Modules );
- function capitalize( value ) {
- return value.charAt( 0 ).toUpperCase() + value.slice( 1 );
- }
- }
- /**
- * Prepares the configuration for webpack.
- *
- * @returns {Object}
- */
- function getWebpackConfig() {
- const postCssConfig = styles.getPostCssConfig( {
- themeImporter: {
- themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
- },
- minify: false
- } );
- postCssConfig.plugins.push( postCssContentStylesPlugin( contentRules ) );
- return {
- mode: 'development',
- devtool: 'source-map',
- entry: {
- ckeditor5: path.join( DESTINATION_DIRECTORY, 'source.js' )
- },
- output: {
- path: DESTINATION_DIRECTORY,
- filename: '[name].js'
- },
- resolve: {
- modules: getModuleResolvePaths()
- },
- resolveLoader: {
- modules: getModuleResolvePaths()
- },
- module: {
- rules: [
- {
- test: /\.svg$/,
- use: [ 'raw-loader' ]
- },
- {
- test: /\.css$/,
- use: [
- 'style-loader',
- {
- loader: 'postcss-loader',
- options: postCssConfig
- }
- ]
- }
- ]
- }
- };
- }
- /**
- * Returns the PostCSS plugin that allows intercepting CSS definition used in the editor's build.
- *
- * @param {Object} contentRules
- * @param {Array.<String>} contentRules.variables Variables defined as `:root`.
- * @param {Object} contentRules.atRules Definitions of behaves.
- * @param {Array.<String>} contentRules.selector CSS definitions for all selectors.
- * @returns {Function}
- */
- function postCssContentStylesPlugin( contentRules ) {
- return postcss.plugin( 'list-content-styles', function() {
- const selectorStyles = contentRules.selector;
- const variables = contentRules.variables;
- return root => {
- root.walkRules( rule => {
- for ( const selector of rule.selectors ) {
- const data = {
- file: root.source.input.file,
- css: rule.toString()
- };
- if ( selector.match( ':root' ) ) {
- addDefinition( variables, data );
- }
- if ( selector.match( '.ck-content' ) ) {
- if ( rule.parent.name && rule.parent.params ) {
- const atRule = getAtRuleArray( contentRules.atRules, rule.parent.name, rule.parent.params );
- addDefinition( atRule, data );
- } else {
- addDefinition( selectorStyles, data );
- }
- }
- }
- } );
- };
- } );
- /**
- * @param {Object} collection
- * @param {String} name Name of an `at-rule`.
- * @param {String} params Parameters that describes the `at-rule`.
- * @returns {Array}
- */
- function getAtRuleArray( collection, name, params ) {
- const definition = `${ name } ${ params }`;
- if ( !collection[ definition ] ) {
- collection[ definition ] = [];
- }
- return collection[ definition ];
- }
- /**
- * Checks whether specified definition is duplicated in the colletion.
- *
- * @param {Array.<StyleStructure>} collection
- * @param {StyleStructure} def
- * @returns {Boolean}
- */
- function isDuplicatedDefinition( collection, def ) {
- for ( const item of collection ) {
- if ( item.file === def.file && item.css === def.css ) {
- return true;
- }
- }
- return false;
- }
- /**
- * Adds definition to the collection if it does not exist in the collection.
- *
- * @param {Array.<StyleStructure>} collection
- * @param {StyleStructure} def
- */
- function addDefinition( collection, def ) {
- if ( !isDuplicatedDefinition( collection, def ) ) {
- collection.push( def );
- }
- }
- }
- /**
- * @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'
- ];
- }
- /**
- * Resolves the promise with the content of the file saved under the `filePath` location.
- *
- * @param {String} filePath The path to fhe file.
- * @returns {Promise.<String>}
- */
- function readFile( filePath ) {
- return new Promise( ( resolve, reject ) => {
- fs.readFile( filePath, 'utf-8', ( err, content ) => {
- if ( err ) {
- return reject( err );
- }
- return resolve( content );
- } );
- } );
- }
- /**
- * Saves the `data` value to the file saved under the `filePath` location.
- *
- * @param {String} filePath The path to fhe file.
- * @param {String} data The content to save.
- * @returns {Promise.<String>}
- */
- function writeFile( filePath, data ) {
- return new Promise( ( resolve, reject ) => {
- fs.writeFile( filePath, 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' );
- }
- /**
- * Returns an object that contains objects with new plugins.
- *
- * @param {Array.<Object>} currentPlugins
- * @param {Array.<Object>} previousPlugins
- * @returns {{Array.<Object>}}
- */
- function findNewPlugins( currentPlugins, previousPlugins ) {
- const newPlugins = [];
- for ( const data of currentPlugins ) {
- // Use relative paths.
- const modulePath = normalizePath( data.modulePath.replace( cwd + path.sep, '' ) );
- if ( !previousPlugins[ modulePath ] ) {
- newPlugins.push( data );
- }
- }
- return newPlugins;
- }
- /**
- * Displays a table with new plugins.
- *
- * @param {Array.<Object>} newPlugins
- */
- function displayNewPluginsTable( newPlugins ) {
- const table = new Table( {
- head: [ 'Plugin name', 'Module path' ],
- style: { compact: true }
- } );
- for ( const data of newPlugins ) {
- const modulePath = normalizePath( data.modulePath.replace( cwd + path.sep, '' ) );
- table.push( [ data.pluginName, modulePath ] );
- }
- console.log( table.toString() );
- }
- function normalizePath( modulePath ) {
- return modulePath.split( path.sep ).join( path.posix.sep );
- }
- function exec( command ) {
- return tools.shExec( command, { verbosity: 'error' } );
- }
- function logProcess( message ) {
- console.log( '\n📍 ' + chalk.cyan( message ) );
- }
- /**
- * @typedef {Object} StyleStructure
- * @property {String} file An absolute path to the file where a definition is defined.
- * @property {String} css Definition.
- */
|