build-content-styles.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* eslint-env node */
  6. const path = require( 'path' );
  7. const fs = require( 'fs' );
  8. const webpack = require( 'webpack' );
  9. const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
  10. const { version } = require( '../../package.json' );
  11. const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
  12. const DOCUMENTATION_URL = 'https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html';
  13. const VARIABLE_DEFINITION_REGEXP = /(--[\w-]+):\s+(.*);/g;
  14. const VARIABLE_USAGE_REGEXP = /var\((--[\w-]+)\)/g;
  15. const contentRules = {
  16. selector: [],
  17. variables: []
  18. };
  19. const webpackConfig = getWebpackConfig();
  20. const packagesPath = path.join( process.cwd(), 'packages' );
  21. runWebpack( webpackConfig )
  22. .then( () => {
  23. // All variables are placed inside the `:root` selector. Let's extract their names and values as a map.
  24. const cssVariables = new Map( contentRules.variables
  25. .map( rule => {
  26. // Let's extract all of them as an array of pairs: [ name, value ].
  27. const allRules = [];
  28. let match;
  29. while ( ( match = VARIABLE_DEFINITION_REGEXP.exec( rule.css ) ) ) {
  30. allRules.push( [ match[ 1 ], match[ 2 ] ] );
  31. }
  32. return allRules;
  33. } )
  34. .reduce( ( previousValue, currentValue ) => {
  35. // And simplify nested arrays as a flattened array.
  36. previousValue.push( ...currentValue );
  37. return previousValue;
  38. }, [] ) );
  39. // CSS variables that are used by the `.ck-content` selector.
  40. const usedVariables = new Set();
  41. const selectorCss = contentRules.selector
  42. .map( rule => {
  43. // Removes all comments from the rule definition.
  44. const cssAsArray = rule.css.replace( /\/\*[^*]+\*\//g, '' ).split( '\n' );
  45. // We want to fix invalid indentations. We need to find a number of how many indentations we want to remove.
  46. // Because the last line ends the block, we can use this value.
  47. const lastLineIndent = cssAsArray[ cssAsArray.length - 1 ].length - 1;
  48. const css = cssAsArray
  49. .filter( line => line.trim().length > 0 )
  50. .map( ( line, index ) => {
  51. // Do not touch the first line. It is always correct.
  52. if ( index === 0 ) {
  53. return line;
  54. }
  55. return line.slice( lastLineIndent );
  56. } )
  57. .join( '\n' );
  58. return `/* ${ rule.file.replace( packagesPath + path.sep, '' ) } */\n${ css }`;
  59. } )
  60. .filter( rule => {
  61. // 1st: path to the CSS file, 2nd: selector definition - start block, 3rd: end block
  62. // If the rule contains only 3 lines, it means that it does not define any rules.
  63. return rule.split( '\n' ).length > 3;
  64. } )
  65. .join( '\n' );
  66. // Find all CSS variables inside the `.ck-content` selector.
  67. let match;
  68. while ( ( match = VARIABLE_USAGE_REGEXP.exec( selectorCss ) ) ) {
  69. usedVariables.add( match[ 1 ] );
  70. }
  71. // We need to also look at whether any of the used variables requires the value of other variables.
  72. let clearRun = false;
  73. // We need to process all variables as long as the entire collection won't be changed.
  74. while ( !clearRun ) {
  75. clearRun = true;
  76. // For every used variable...
  77. for ( const variable of usedVariables ) {
  78. const value = cssVariables.get( variable );
  79. let match;
  80. // ...find its value and check whether it requires another variable.
  81. while ( ( match = VARIABLE_USAGE_REGEXP.exec( value ) ) ) {
  82. // If so, mark the entire `while()` block as it should be checked once again.
  83. // Also, add the new variable to the used variables collection.
  84. if ( !usedVariables.has( match[ 1 ] ) ) {
  85. clearRun = false;
  86. usedVariables.add( match[ 1 ] );
  87. }
  88. }
  89. }
  90. }
  91. // Build the final content of the CSS file.
  92. let data = [
  93. '/*',
  94. ` * CKEditor 5 (v${ version }) content styles.`,
  95. ` * Generated on ${ new Date().toUTCString() }.`,
  96. ` * For more information, check out ${ DOCUMENTATION_URL }`,
  97. ' */\n\n',
  98. ].join( '\n' );
  99. data += ':root {\n';
  100. for ( const variable of [ ...usedVariables ].sort() ) {
  101. data += `\t${ variable }: ${ cssVariables.get( variable ) };\n`;
  102. }
  103. data += '}\n\n';
  104. data += selectorCss;
  105. return writeFile( path.join( DESTINATION_DIRECTORY, 'content-styles.css' ), data );
  106. } )
  107. .then( () => {
  108. console.log( `Content styles have been extracted to ${ path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) }` );
  109. } )
  110. .catch( err => {
  111. console.log( err );
  112. } );
  113. /**
  114. * Prepares the configuration for webpack.
  115. *
  116. * @returns {Object}
  117. */
  118. function getWebpackConfig() {
  119. const postCssConfig = styles.getPostCssConfig( {
  120. themeImporter: {
  121. themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
  122. },
  123. minify: false
  124. } );
  125. const contentStylesPlugin = require( './content-styles/list-content-styles' )( { contentRules } );
  126. postCssConfig.plugins.push( contentStylesPlugin );
  127. return {
  128. mode: 'development',
  129. devtool: 'source-map',
  130. entry: {
  131. ckeditor5: path.join( __dirname, 'content-styles', 'ckeditor.js' )
  132. },
  133. output: {
  134. path: DESTINATION_DIRECTORY,
  135. filename: '[name].js'
  136. },
  137. // Configure the paths so building CKEditor 5 snippets work even if the script
  138. // is triggered from a directory outside `ckeditor5` (e.g. in a multi-project case).
  139. resolve: {
  140. modules: getModuleResolvePaths()
  141. },
  142. resolveLoader: {
  143. modules: getModuleResolvePaths()
  144. },
  145. module: {
  146. rules: [
  147. {
  148. test: /\.svg$/,
  149. use: [ 'raw-loader' ]
  150. },
  151. {
  152. test: /\.css$/,
  153. use: [
  154. 'style-loader',
  155. {
  156. loader: 'postcss-loader',
  157. options: postCssConfig
  158. }
  159. ]
  160. }
  161. ]
  162. }
  163. };
  164. }
  165. /**
  166. * @param {Object} webpackConfig
  167. * @returns {Promise}
  168. */
  169. function runWebpack( webpackConfig ) {
  170. return new Promise( ( resolve, reject ) => {
  171. webpack( webpackConfig, ( err, stats ) => {
  172. if ( err ) {
  173. reject( err );
  174. } else if ( stats.hasErrors() ) {
  175. reject( new Error( stats.toString() ) );
  176. } else {
  177. resolve();
  178. }
  179. } );
  180. } );
  181. }
  182. /**
  183. * @returns {Array.<String>}
  184. */
  185. function getModuleResolvePaths() {
  186. return [
  187. path.resolve( __dirname, '..', '..', 'node_modules' ),
  188. 'node_modules'
  189. ];
  190. }
  191. function writeFile( file, data ) {
  192. return new Promise( ( resolve, reject ) => {
  193. fs.writeFile( file, data, err => {
  194. if ( err ) {
  195. return reject( err );
  196. }
  197. return resolve();
  198. } );
  199. } );
  200. }